From 07fe1a62fbc433f46d6314905c3ba0182271ec14 Mon Sep 17 00:00:00 2001 From: David Al-Kanani Date: Fri, 13 Sep 2024 11:48:03 +0100 Subject: [PATCH] implement import as (#827) * implement import as * update documentation with import alias details * add extra validation tests for import as --- docs/rune-modelling-component.md | 11 +++ rosetta-lang/model/Rosetta.xcore | 1 + .../java/com/regnosys/rosetta/Rosetta.xtext | 2 +- .../scoping/AliasAwareImportNormalizer.java | 64 ++++++++++++++ .../scoping/RosettaScopeProvider.xtend | 49 +++++++++-- .../validation/RosettaSimpleValidator.xtend | 21 +++-- .../rosetta/tests/RosettaParsingTest.xtend | 37 ++++++++ .../validation/RosettaValidatorTest.xtend | 85 +++++++++++++++++++ 8 files changed, 255 insertions(+), 15 deletions(-) create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/AliasAwareImportNormalizer.java diff --git a/docs/rune-modelling-component.md b/docs/rune-modelling-component.md index 400deeb1c..afce518a1 100644 --- a/docs/rune-modelling-component.md +++ b/docs/rune-modelling-component.md @@ -1664,6 +1664,17 @@ import cdm.base.staticdata.asset.credit.* In the example above all model components contained within the layers of the `cdm.base` namespace are imported. +Another method of referencing a namespace is by using an `import ... as ...` alias. + +``` Haskell +import cdm.base.staticdata.party.* as cdmParty + +type MyContainer: + party cdmParty.Party (1..1) +``` + +In the above example you can see that all types under the namespace `cdm.base.staticdata.party` have been given an alias `cdmParty`. To reference those types elsewhere in the Rune syntax you must prefix the type with that alias as in the above example. Note that only namespaces that import the entire namespace content using the `*` syntax can be aliased. + ## Mapping Component ### Purpose diff --git a/rosetta-lang/model/Rosetta.xcore b/rosetta-lang/model/Rosetta.xcore index 3e56629c9..234a6c55c 100644 --- a/rosetta-lang/model/Rosetta.xcore +++ b/rosetta-lang/model/Rosetta.xcore @@ -23,6 +23,7 @@ class RosettaModel extends RosettaDefinable { class Import { String importedNamespace + String namespaceAlias } /********************************************************************** diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext index 7978ea5f9..143337cbf 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -26,7 +26,7 @@ QualifiedName: ; Import: - 'import' importedNamespace=QualifiedNameWithWildcard; + 'import' importedNamespace=QualifiedNameWithWildcard ('as' namespaceAlias=ValidID)?; QualifiedNameWithWildcard: QualifiedName ('.' '*')?; diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/AliasAwareImportNormalizer.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/AliasAwareImportNormalizer.java new file mode 100644 index 000000000..41d30fe93 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/AliasAwareImportNormalizer.java @@ -0,0 +1,64 @@ +package com.regnosys.rosetta.scoping; + +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.scoping.impl.ImportNormalizer; + +public class AliasAwareImportNormalizer extends ImportNormalizer { + private final ImportNormalizer aliasNormalizer; + + public AliasAwareImportNormalizer(QualifiedName importedNamespace, QualifiedName namespaceAlias, boolean wildCard, + boolean ignoreCase) { + super(importedNamespace, wildCard, ignoreCase); + this.aliasNormalizer = new ImportNormalizer(namespaceAlias, wildCard, ignoreCase); + } + + @Override + public QualifiedName deresolve(QualifiedName fullyQualifiedName) { + QualifiedName deresolved = super.deresolve(fullyQualifiedName); + if (deresolved != null) { + return aliasNormalizer.resolve(deresolved); + } + return null; + } + + @Override + public QualifiedName resolve(QualifiedName relativeName) { + QualifiedName deresolved = aliasNormalizer.deresolve(relativeName); + if (deresolved != null) { + return super.resolve(deresolved); + } + return null; + } + + @Override + public String toString() { + return getImportedNamespacePrefix().toString() + (hasWildCard() ? ".*" : "") + + "as " + aliasNormalizer.getImportedNamespacePrefix(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = super.hashCode(); + result = prime * result + aliasNormalizer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (super.equals(obj) == false) { + return false; + } + if (obj instanceof AliasAwareImportNormalizer) { + AliasAwareImportNormalizer other = (AliasAwareImportNormalizer) obj; + return other.aliasNormalizer.equals(aliasNormalizer); + } + return false; + } + +} \ No newline at end of file diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend index 97dcf2d80..013cb8525 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/scoping/RosettaScopeProvider.xtend @@ -57,6 +57,8 @@ import com.regnosys.rosetta.rosetta.expression.ConstructorKeyValuePair import com.regnosys.rosetta.rosetta.expression.RosettaDeepFeatureCall import com.regnosys.rosetta.types.RDataType import com.regnosys.rosetta.utils.DeepFeatureCallUtil +import org.eclipse.xtext.scoping.impl.ImportNormalizer +import org.eclipse.xtext.util.Strings import com.regnosys.rosetta.rosetta.simple.Annotated import com.regnosys.rosetta.types.RObjectFactory import com.regnosys.rosetta.RosettaEcoreUtil @@ -253,15 +255,52 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { override protected internalGetImportedNamespaceResolvers(EObject context, boolean ignoreCase) { return if (context instanceof RosettaModel) { - val imports = super.internalGetImportedNamespaceResolvers(context, ignoreCase) - imports.add( - doCreateImportNormalizer(getQualifiedNameConverter.toQualifiedName(context.name), true, ignoreCase) - ) + val List imports = newArrayList() + context.imports.forEach[ + val resolver = createImportedNamespaceResolver(importedNamespace, namespaceAlias, ignoreCase) + if (resolver !== null) { + imports.add(resolver) + } + ] + //This import allows two models with the same namespace to reference each other + imports.add(doCreateImportNormalizer(getQualifiedNameConverter.toQualifiedName(context.name), true, ignoreCase)) return imports } else emptyList } + private def ImportNormalizer createImportedNamespaceResolver(String namespace, String namespaceAlias, + boolean ignoreCase) { + if (Strings.isEmpty(namespace)) { + return null; + } + + val importedNamespace = qualifiedNameConverter.toQualifiedName(namespace) + if (importedNamespace === null || importedNamespace.isEmpty()) { + return null; + } + val qualifiedAlias = namespaceAlias === null ? null : qualifiedNameConverter.toQualifiedName(namespaceAlias) + + val hasWildCard = ignoreCase ? + importedNamespace.getLastSegment().equalsIgnoreCase(getWildCard()) : + importedNamespace.getLastSegment().equals(getWildCard()); + + if (hasWildCard) { + if (importedNamespace.getSegmentCount() <= 1) + return null; + return doCreateImportNormalizer(importedNamespace.skipLast(1), qualifiedAlias, true, ignoreCase); + } else { + return doCreateImportNormalizer(importedNamespace, qualifiedAlias, false, ignoreCase); + } + } + + private def ImportNormalizer doCreateImportNormalizer(QualifiedName importedNamespace, QualifiedName namespaceAlias, boolean wildcard, boolean ignoreCase) { + if (namespaceAlias === null) { + return doCreateImportNormalizer(importedNamespace, wildcard, ignoreCase); + } + return new AliasAwareImportNormalizer(importedNamespace, namespaceAlias, wildcard, ignoreCase); + } + private def IScope defaultScope(EObject object, EReference reference) { filteredScope(super.getScope(object, reference), [it.EClass !== FUNCTION_DISPATCH]) } @@ -343,7 +382,7 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { emptyList } } - + private def IScope createDeepFeatureScope(RType receiverType) { if (receiverType instanceof RDataType) { return Scopes.scopeFor(receiverType.findDeepFeatures.filter[EObject !== null].map[EObject]) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index 57a40be0f..215238141 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -322,7 +322,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { final extension RObjectFactory objectFactory new(RObjectFactory objectFactory) { this.objectFactory = objectFactory - } + } final Map ruleMap = newHashMap; final Map errorMap = newHashMap; @@ -510,7 +510,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { val parentAttrType = parentAttr.RType if (childAttrType != parentAttrType) { error('''Overriding attribute '«name»' with type «childAttrType» must match the type of the attribute it overrides («parentAttrType»)''', - childAttr.EObject, ROSETTA_NAMED__NAME, DUPLICATE_ATTRIBUTE) + childAttr.EObject, ROSETTA_NAMED__NAME, DUPLICATE_ATTRIBUTE) } ] ] @@ -1397,13 +1397,16 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { for (ns : model.imports) { if (ns.importedNamespace !== null) { val qn = QualifiedName.create(ns.importedNamespace.split('\\.')) - val isWildcard = qn.lastSegment.equals('*'); - - - val isUsed = if (isWildcard) { - usedNames.stream.anyMatch[startsWith(qn.skipLast(1)) && segmentCount === qn.segmentCount] - } else { - usedNames.contains(qn) + val isWildcard = qn.lastSegment.equals('*'); + if (!isWildcard && ns.namespaceAlias !== null) { + error('''"as" statement can only be used with wildcard imports''', ns, IMPORT__NAMESPACE_ALIAS); + } + + + val isUsed = if (isWildcard) { + usedNames.stream.anyMatch[startsWith(qn.skipLast(1)) && segmentCount === qn.segmentCount] + } else { + usedNames.contains(qn) } if (!isUsed) { warning('''Unused import «ns.importedNamespace»''', ns, IMPORT__IMPORTED_NAMESPACE, UNUSED_IMPORT) diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend index 03204d613..43a201593 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend @@ -33,6 +33,43 @@ class RosettaParsingTest { @Inject extension ModelHelper modelHelper @Inject extension ValidationTestHelper @Inject extension ExpressionParser + + @Test + def void testTwoModelsSameNamespaceReferencesEachOther() { + val model1 = ''' + namespace test + type A: + id string (1..1) + ''' + + val model2 = ''' + namespace test + type B: + a A (1..1) + ''' + + #[model1, model2].parseRosettaWithNoIssues + } + + @Test + def void testCanUseAliasesWhenImporting() { + val model1 = ''' + namespace foo.bar + + type A: + id string (1..1) + ''' + + val model2 = ''' + namespace test + import foo.bar.* as someAlias + + type B: + a someAlias.A (1..1) + ''' + + #[model1, model2].parseRosettaWithNoIssues + } @Test def void testFullyQualifiedNamesCanBeUsedInExpression() { diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend index e51dccbbb..aa63d62df 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend @@ -30,6 +30,91 @@ class RosettaValidatorTest implements RosettaIssueCodes { @Inject extension ValidationTestHelper @Inject extension ModelHelper @Inject extension ExpressionParser + + @Test + def void testCanUseMixOfImportAliasAnFullyQualified() { + val model1 = ''' + namespace foo.bar + + type A: + id string (1..1) + + type D: + id string (1..1) + ''' + + val model2 = ''' + namespace test + + import foo.bar.* as someAlias + + type B: + a someAlias.A (1..1) + d foo.bar.D (1..1) + ''' + + #[model1, model2].parseRosettaWithNoIssues + } + + @Test + def void testCanUseMixOfImportAliasAndNoAlias() { + val model1 = ''' + namespace foo.bar + + type A: + id string (1..1) + ''' + + val model2 = ''' + namespace test + + import foo.bar.* as someAlias + + + type D: + id string (1..1) + + type B: + a someAlias.A (1..1) + d D (1..1) + ''' + + #[model1, model2].parseRosettaWithNoIssues + } + + @Test + def void testCanUseImportAlisesWhenWildcardPresent() { + val model1 = ''' + namespace foo.bar + + type A: + id string (1..1) + ''' + + val model2 = ''' + namespace test + + import foo.bar.* as someAlias + + + + type B: + a someAlias.A (1..1) + ''' + + #[model1, model2].parseRosettaWithNoIssues + } + + @Test + def void testCannotUseImportAliasesWithoutWildcard() { + val model = ''' + import foo.bar.Test as someAlias + '''.parseRosetta + + model.assertError(IMPORT, null, + '"as" statement can only be used with wildcard import' + ) + } @Test def void testCannotAccessUncommonMetaFeatureOfDeepFeatureCall() {