Skip to content

Commit

Permalink
implement import as (finos#827)
Browse files Browse the repository at this point in the history
* implement import as

* update documentation with import alias details

* add extra validation tests for import as
  • Loading branch information
davidalk authored Sep 13, 2024
1 parent 5249565 commit 07fe1a6
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 15 deletions.
11 changes: 11 additions & 0 deletions docs/rune-modelling-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions rosetta-lang/model/Rosetta.xcore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class RosettaModel extends RosettaDefinable {

class Import {
String importedNamespace
String namespaceAlias
}

/**********************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ QualifiedName:
;

Import:
'import' importedNamespace=QualifiedNameWithWildcard;
'import' importedNamespace=QualifiedNameWithWildcard ('as' namespaceAlias=ValidID)?;

QualifiedNameWithWildcard:
QualifiedName ('.' '*')?;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ImportNormalizer> 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])
}
Expand Down Expand Up @@ -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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator {
final extension RObjectFactory objectFactory
new(RObjectFactory objectFactory) {
this.objectFactory = objectFactory
}
}

final Map<RAttribute, RosettaRule> ruleMap = newHashMap;
final Map<RosettaExternalRegularAttribute, String> errorMap = newHashMap;
Expand Down Expand Up @@ -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)
}
]
]
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 07fe1a6

Please sign in to comment.