diff --git a/docs/rune-modelling-component.md b/docs/rune-modelling-component.md index 400deeb1c..83e0ec6a9 100644 --- a/docs/rune-modelling-component.md +++ b/docs/rune-modelling-component.md @@ -282,6 +282,26 @@ enum DayCountFractionEnum: _30_360 displayName "30/360" ``` +#### Enum extensions + +An enumeration can extend one or more other enumerations, which includes all of their values. + +```Haskell +enum ISOCurrencyCodeEnum: + AFN + EUR + GBP + USD + +enum CurrencyCodeEnum extends ISOCurrencyCodeEnum: + CNH + JEP + KID + VAL +``` + +In the example above, all values defined in `ISOCurrencyCodeEnum` are also included in `CurrencyCodeEnum`. + #### External Reference Data In some cases, a model may rely on an enumeration whose values are already defined as a static dataset in some other data model, schema or technical specification. To avoid duplicating that information and risk it becoming stale, it is possible to annotate such enumeration with the source of the reference data, using the [document reference](#document-reference) mechanism. This ensures that the enumeration information in the model is kept up-to-date with information at the source. @@ -679,6 +699,24 @@ E.g. : DayOfWeekEnum -> SAT ``` +In case of enum extensions, the name of the enum type can be either that of the actual enum or that of the parent. Consider the following scenario: + +```Haskell +enum ISOCurrencyCodeEnum: + AFN + EUR + GBP + USD + +enum CurrencyCodeEnum extends ISOCurrencyCodeEnum: + CNH + JEP + KID + VAL +``` + +In this example, both `ISOCurrencyCodeEnum -> EUR` and `CurrencyCodeEnum -> EUR` refer to the same value, and are considered equal. + #### List Constant Constants can also be declared as lists: @@ -1128,21 +1166,34 @@ Rune provides five conversion operators: `to-enum`, `to-string`, `to-number`, `t Given the following enum ``` -enum Foo: +enum FooEnum: VALUE1 VALUE2 displayName "Value 2" ``` -- `Foo -> VALUE1 to-string` will result in the string `"VALUE1"`, -- `Foo -> VALUE2 to-string` will result in the string `"Value 2"`, (note that the display name is used if present) -- `"VALUE1" to-enum Foo` will result in the enum value `Foo -> VALUE1`, -- `"Value 2" to-enum Foo` will result in the enum value `Foo -> VALUE2`, (again, the display name is used if present) +- `FooEnum -> VALUE1 to-string` will result in the string `"VALUE1"`, +- `FooEnum -> VALUE2 to-string` will result in the string `"Value 2"`, (note that the display name is used if present) +- `"VALUE1" to-enum FooEnum` will result in the enum value `FooEnum -> VALUE1`, +- `"Value 2" to-enum FooEnum` will result in the enum value `FooEnum -> VALUE2`, (again, the display name is used if present) - `"-3.14" to-number` will result in the number -3.14, - `"17:05:33" to-time` will result in a value representing the local time 17 hours, 5 minutes and 33 seconds. If the conversion fails, the result is an empty value. For example, -- `"VALUE2" to-enum Foo` results in an empty value, (because `Foo -> VALUE2` has a display name "Value 2", this conversion fails) +- `"VALUE2" to-enum FooEnum` results in an empty value, (because `FooEnum -> VALUE2` has a display name "Value 2", this conversion fails) - `"3.14" to-int` results in an empty value. +In case of extended enums, the `to-enum` operation can also be used to convert an enum value to a parent enum. Consider the following enum: +``` +enum FooEnum: + VALUE1 + +enum BarEnum extends FooEnum: + VALUE2 +``` +- `BarEnum -> VALUE1 to-enum FooEnum` will result in the enum value `FooEnum -> VALUE1`, +- `BarEnum -> VALUE2 to-enum FooEnum` results in an empty value, since `VALUE2` does not exist in `FooEnum`. + +Note that conversion in the other direction - from `FooEnum` to `BarEnum` - is disallowed, since a value of type `FooEnum` can be used directly in any location where a value of type `BarEnum` is expected. + ### Keyword clashes If a model name, such as an enum value or attribute name, clashes with a Rune DSL keyword then the name must be escaped by prefixing with the `^` operator. The generated code (e.g. Java) and serialised format (e.g. JSON) will not include the `^` prefix. diff --git a/rosetta-lang/model/Rosetta.xcore b/rosetta-lang/model/Rosetta.xcore index 3e56629c9..0ca095665 100644 --- a/rosetta-lang/model/Rosetta.xcore +++ b/rosetta-lang/model/Rosetta.xcore @@ -6,6 +6,8 @@ package com.regnosys.rosetta.rosetta import com.google.common.collect.Iterables import java.util.stream.Collectors +import com.regnosys.rosetta.rosetta.simple.Annotated +import com.regnosys.rosetta.rosetta.simple.RootElement import com.regnosys.rosetta.rosetta.simple.References import com.regnosys.rosetta.rosetta.simple.Data import com.regnosys.rosetta.rosetta.simple.Attribute @@ -39,10 +41,6 @@ interface RosettaNamed { interface RosettaTyped { contains TypeCall typeCall - - derived boolean isTypeInferred get { - return typeCall === null - } } interface RosettaFeature extends RosettaNamed { @@ -131,13 +129,13 @@ class RosettaMetaType extends RosettaRootElement, RosettaTypedFeature, RosettaTy } -class RosettaEnumeration extends RosettaRootElement, RosettaType, RosettaDefinable, References, RosettaSymbol { - refers RosettaEnumeration superType +class RosettaEnumeration extends RootElement, RosettaType, RosettaDefinable, References, RosettaSymbol { + refers RosettaEnumeration[] parentEnums contains RosettaSynonym[] synonyms contains RosettaEnumValue[] enumValues opposite enumeration } -class RosettaEnumValue extends RosettaSymbol, RosettaDefinable, RosettaFeature, References { +class RosettaEnumValue extends RosettaSymbol, RosettaDefinable, RosettaFeature, References, Annotated { String display contains RosettaEnumSynonym[] enumSynonyms container RosettaEnumeration enumeration opposite enumValues 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..8dadffdaa 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -93,14 +93,13 @@ Attribute: ; Enumeration returns RosettaEnumeration: - 'enum' RosettaNamed ('extends' superType=[RosettaEnumeration|QualifiedName])? ':' RosettaDefinable? - References* - (synonyms += RosettaSynonym)* + 'enum' RosettaNamed ('extends' parentEnums+=[RosettaEnumeration|QualifiedName] (',' parentEnums+=[RosettaEnumeration|QualifiedName])*)? ':' RosettaDefinable? + (References|Annotations|synonyms+=RosettaSynonym)* enumValues += RosettaEnumValue* ; Function: - 'func' + 'func' ( RosettaNamed | ({FunctionDispatch} RosettaNamed '(' attribute=[Attribute|ValidID] ':' value=EnumValueReference')') @@ -282,8 +281,8 @@ RosettaMetaType: ; RosettaEnumValue: - RosettaNamed ('displayName' display=STRING)? RosettaDefinable? References* - (enumSynonyms += RosettaEnumSynonym)* + RosettaNamed ('displayName' display=STRING)? RosettaDefinable? + (References|Annotations|enumSynonyms+=RosettaEnumSynonym)* ; RosettaCardinality: diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend index c1332dd81..c16158acb 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaEcoreUtil.xtend @@ -1,7 +1,6 @@ package com.regnosys.rosetta import com.google.common.base.CaseFormat -import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.regnosys.rosetta.rosetta.RosettaFeature import com.regnosys.rosetta.rosetta.RosettaRecordType import com.regnosys.rosetta.rosetta.RosettaSynonym @@ -33,6 +32,7 @@ import com.regnosys.rosetta.scoping.RosettaScopeProvider import com.regnosys.rosetta.rosetta.simple.SimpleFactory import com.regnosys.rosetta.types.RObjectFactory import java.util.LinkedHashSet +import com.regnosys.rosetta.rosetta.RosettaEnumeration @Singleton // see `metaFieldsCache` class RosettaEcoreUtil { @@ -52,7 +52,7 @@ class RosettaEcoreUtil { RDataType: t.allNonOverridenAttributes.map[EObject] REnumType: - t.EObject.allEnumValues + t.allEnumValues RRecordType: { if (resourceSet !== null) { builtins.toRosettaType(t, RosettaRecordType, resourceSet).features @@ -92,19 +92,17 @@ class RosettaEcoreUtil { return result.values(); } - @Deprecated // TODO: move to REnumType, similar to RDataType + @Deprecated // Use REnumType#getAllParents instead def Set getAllSuperEnumerations(RosettaEnumeration e) { doGetSuperEnumerations(e, newLinkedHashSet) } - - @Deprecated private def Set doGetSuperEnumerations(RosettaEnumeration e, Set seenEnums) { - if(e !== null && seenEnums.add(e)) - doGetSuperEnumerations(e.superType, seenEnums) + if(seenEnums.add(e)) + e.parentEnums.forEach[doGetSuperEnumerations(it, seenEnums)] return seenEnums } - @Deprecated // TODO: move to REnumType, similar to RDataType + @Deprecated // Use REnumType#getAllEnumValues instead def getAllEnumValues(RosettaEnumeration e) { e.allSuperEnumerations.map[enumValues].flatten } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend index ba88d4854..d5c893acb 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/RosettaGenerator.xtend @@ -226,7 +226,7 @@ class RosettaGenerator implements IGenerator2 { ] } RosettaEnumeration: { - enumGenerator.generate(packages, fsa, elem, version) + enumGenerator.generate(packages, fsa, elem.buildREnumType, version) } } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend index 0d5c7d427..02c72a0af 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumGenerator.xtend @@ -4,10 +4,8 @@ import com.regnosys.rosetta.generator.java.JavaScope import com.regnosys.rosetta.generator.java.RosettaJavaPackages.RootPackage import com.regnosys.rosetta.generator.java.util.ImportManagerExtension import com.regnosys.rosetta.rosetta.RosettaEnumValue -import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.rosetta.model.lib.annotations.RosettaEnum import com.rosetta.model.lib.annotations.RosettaSynonym -import java.util.ArrayList import java.util.Collections import java.util.Map import java.util.concurrent.ConcurrentHashMap @@ -15,45 +13,43 @@ import javax.inject.Inject import org.eclipse.xtend2.lib.StringConcatenationClient import org.eclipse.xtext.generator.IFileSystemAccess2 -import static com.regnosys.rosetta.generator.java.enums.EnumHelper.* import static com.regnosys.rosetta.generator.java.util.ModelGeneratorUtil.* +import com.regnosys.rosetta.types.REnumType +import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator +import com.regnosys.rosetta.generator.java.types.RJavaEnum +import java.util.List +import java.util.Set +import org.apache.commons.text.StringEscapeUtils class EnumGenerator { @Inject extension ImportManagerExtension + @Inject extension JavaTypeTranslator - def generate(RootPackage root, IFileSystemAccess2 fsa, RosettaEnumeration enumeration, String version) { + def generate(RootPackage root, IFileSystemAccess2 fsa, REnumType enumeration, String version) { fsa.generateFile(root.withForwardSlashes + '/' + enumeration.name + '.java', enumeration.toJava(root, version)) } - - private def allEnumsValues(RosettaEnumeration enumeration) { - val enumValues = new ArrayList - var e = enumeration; - while (e !== null) { - e.enumValues.forEach[enumValues.add(it)] - e = e.superType - } - return enumValues; - } - - private def String toJava(RosettaEnumeration e, RootPackage root, String version) { + private def String toJava(REnumType e, RootPackage root, String version) { val scope = new JavaScope(root) + val javaEnum = e.toJavaReferenceType as RJavaEnum + val StringConcatenationClient classBody = ''' - «javadoc(e, version)» - @«RosettaEnum»("«e.name»") - public enum «e.name» { + «javadoc(e.EObject, version)» + @«RosettaEnum»(value="«e.name»"«IF !javaEnum.parents.empty», parents={«FOR p : javaEnum.parents SEPARATOR ", "»«p».class«ENDFOR»}«ENDIF») + public enum «javaEnum» { - «FOR value: allEnumsValues(e) SEPARATOR ',\n' AFTER ';'» - «javadoc(value)» - «value.contributeAnnotations» - @«com.rosetta.model.lib.annotations.RosettaEnumValue»(value = "«value.name»"«IF value.display !== null», displayName = "«value.display»"«ENDIF») «convertValuesWithDisplay(value)» + «FOR value: javaEnum.enumValues SEPARATOR ',\n' AFTER ';'» + «javadoc(value.EObject)» + «value.EObject.contributeAnnotations» + @«com.rosetta.model.lib.annotations.RosettaEnumValue»(value = "«value.rosettaName»"«IF value.displayName !== null», displayName = "«value.displayName»"«ENDIF») + «value.name»("«value.rosettaName»", «IF value.displayName !== null»"«StringEscapeUtils.escapeJava(value.displayName)»"«ELSE»null«ENDIF») «ENDFOR» - private static «Map»<«String», «e.name»> values; + private static «Map»<«String», «javaEnum»> values; static { - «Map»<«String», «e.name»> map = new «ConcurrentHashMap»<>(); - for («e.name» instance : «e.name».values()) { + «Map»<«String», «javaEnum»> map = new «ConcurrentHashMap»<>(); + for («javaEnum» instance : «javaEnum».values()) { map.put(instance.toDisplayString(), instance); } values = «Collections».unmodifiableMap(map); @@ -61,23 +57,45 @@ class EnumGenerator { private final «String» rosettaName; private final «String» displayName; - - «e.name»(«String» rosettaName) { - this(rosettaName, null); - } - «e.name»(«String» rosettaName, «String» displayName) { + «javaEnum»(«String» rosettaName, «String» displayName) { this.rosettaName = rosettaName; this.displayName = displayName; } - public static «e.name» fromDisplayName(String name) { - «e.name» value = values.get(name); + public static «javaEnum» fromDisplayName(String name) { + «javaEnum» value = values.get(name); if (value == null) { throw new «IllegalArgumentException»("No enum constant with display name \"" + name + "\"."); } return value; } + «val visitedAncestors = javaEnum.parents.toSet» + «FOR p : javaEnum.parents» + + «val fromScope = scope.methodScope("from" + p.simpleName)» + «val fromParam = fromScope.createUniqueIdentifier(p.simpleName.toFirstLower)» + public static «javaEnum» from«p.simpleName»(«p» «fromParam») { + switch («fromParam») { + «FOR v : p.enumValues» + case «v.name»: return «v.name»; + «ENDFOR» + } + return null; + } + + «val toScope = scope.methodScope("to" + p.simpleName)» + «val toParam = toScope.createUniqueIdentifier(javaEnum.simpleName.toFirstLower)» + public static «p» to«p.simpleName»(«javaEnum» «toParam») { + switch («toParam») { + «FOR v : p.enumValues» + case «v.name»: return «p».«v.name»; + «ENDFOR» + } + return null; + } + «ancestorConversions(javaEnum, p, p.parents, visitedAncestors, scope)» + «ENDFOR» @Override public «String» toString() { @@ -93,6 +111,28 @@ class EnumGenerator { buildClass(root, classBody, scope) } + private def StringConcatenationClient ancestorConversions(RJavaEnum javaEnum, RJavaEnum currentParent, List ancestors, Set visitedAncestors, JavaScope scope) { + ''' + «FOR a : ancestors» + «IF visitedAncestors.add(a)» + + «val fromScope = scope.methodScope("from" + a.simpleName)» + «val fromParam = fromScope.createUniqueIdentifier(a.simpleName.toFirstLower)» + public static «javaEnum» from«a.simpleName»(«a» «fromParam») { + return from«currentParent.simpleName»(«currentParent».from«a.simpleName»(«fromParam»)); + } + + «val toScope = scope.methodScope("to" + a.simpleName)» + «val toParam = toScope.createUniqueIdentifier(javaEnum.simpleName.toFirstLower)» + public static «a» to«a.simpleName»(«javaEnum» «toParam») { + return «currentParent».to«a.simpleName»(to«currentParent.simpleName»(«toParam»)); + } + «ancestorConversions(javaEnum, currentParent, a.parents, visitedAncestors, scope)» + «ENDIF» + «ENDFOR» + ''' + } + private def StringConcatenationClient contributeAnnotations(RosettaEnumValue e) ''' «FOR synonym : e.enumSynonyms» diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend index 828623c72..71cca8762 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/enums/EnumHelper.xtend @@ -8,11 +8,7 @@ import java.util.stream.Collectors class EnumHelper { - def static convertValuesWithDisplay(RosettaEnumValue enumValue) { - formatEnumName(enumValue.name) + '''("«enumValue.name»"«IF enumValue.display !== null», "«enumValue.display»"«ENDIF»)''' - } - - def static convertValues(RosettaEnumValue enumValue) { + def static convertValue(RosettaEnumValue enumValue) { formatEnumName(enumValue.name) } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend index bdfcc23de..f3b5c95b1 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/expression/ExpressionGenerator.xtend @@ -97,7 +97,7 @@ import org.eclipse.emf.ecore.EObject import org.eclipse.xtend2.lib.StringConcatenationClient import org.eclipse.xtext.EcoreUtil2 -import static extension com.regnosys.rosetta.generator.java.enums.EnumHelper.convertValues +import static extension com.regnosys.rosetta.generator.java.enums.EnumHelper.convertValue import com.regnosys.rosetta.types.RObjectFactory import javax.inject.Inject import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression @@ -271,9 +271,9 @@ class ExpressionGenerator extends RosettaExpressionSwitch { + private static Logger LOGGER = LoggerFactory.getLogger(JavaTypeTranslator.class); + @Inject public JavaTypeTranslator(RBuiltinTypeService builtins) { super(builtins); @@ -404,4 +410,10 @@ protected JavaClass caseDateTimeType(RDateTimeType type, Void con protected JavaClass caseZonedDateTimeType(RZonedDateTimeType type, Void context) { return typeUtil.ZONED_DATE_TIME; } + @Override + protected JavaClass caseUnionType(RUnionType type, Void context) { + // As union types are purely internal to the Rune type system, this should never happen. + LOGGER.error("Trying to convert " + RUnionType.class.getSimpleName() + " `" + type + "` to a Java type"); + return typeUtil.OBJECT; + } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java index 8c5438d81..9af65b5c6 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnum.java @@ -16,9 +16,15 @@ package com.regnosys.rosetta.generator.java.types; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import com.regnosys.rosetta.generator.java.enums.EnumHelper; +import com.regnosys.rosetta.rosetta.RosettaEnumValue; import com.regnosys.rosetta.types.REnumType; import com.rosetta.util.DottedPath; import com.rosetta.util.types.JavaClass; @@ -28,10 +34,38 @@ public class RJavaEnum extends JavaClass { private final REnumType enumeration; + + private List parents = null; + private List enumValues = null; public RJavaEnum(REnumType enumeration) { this.enumeration = enumeration; } + + public List getParents() { + if (parents == null) { + parents = enumeration.getParents().stream().map(p -> new RJavaEnum(p)).collect(Collectors.toList()); + } + return parents; + } + + public List getEnumValues() { + if (enumValues == null) { + enumValues = new ArrayList<>(); + Set visited = new HashSet<>(); + for (RJavaEnum p : getParents()) { + for (RJavaEnumValue v : p.getEnumValues()) { + if (visited.add(v.getEObject())) { + enumValues.add(new RJavaEnumValue(this, v.getName(), v.getEObject(), v)); + } + } + } + for (RosettaEnumValue v : enumeration.getOwnEnumValues()) { + enumValues.add(new RJavaEnumValue(this, EnumHelper.convertValue(v), v, null)); + } + } + return enumValues; + } @Override public boolean isSubtypeOf(JavaType other) { @@ -71,7 +105,7 @@ public List> getInterfaces() { @Override public boolean extendsDeclaration(JavaTypeDeclaration other) { - return false; + return other.equals(JavaClass.OBJECT); } @Override diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java new file mode 100644 index 000000000..ab80c4133 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/RJavaEnumValue.java @@ -0,0 +1,43 @@ +package com.regnosys.rosetta.generator.java.types; + +import com.regnosys.rosetta.rosetta.RosettaEnumValue; + +public class RJavaEnumValue { + private final RJavaEnum enumeration; + + private final String name; + private final RosettaEnumValue value; + + private final RJavaEnumValue parentValue; + + public RJavaEnumValue(RJavaEnum enumeration, String name, RosettaEnumValue value, RJavaEnumValue parentValue) { + this.enumeration = enumeration; + this.name = name; + this.value = value; + this.parentValue = parentValue; + } + + public RJavaEnum getEnumeration() { + return enumeration; + } + + public RosettaEnumValue getEObject() { + return value; + } + + public String getName() { + return name; + } + + public String getRosettaName() { + return value.getName(); + } + + public String getDisplayName() { + return value.getDisplay(); + } + + public RJavaEnumValue getParentValue() { + return parentValue; + } +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java index 28fb210aa..c4c47e4f8 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/interpreter/RosettaValueFactory.java @@ -30,6 +30,7 @@ import com.regnosys.rosetta.types.REnumType; import com.regnosys.rosetta.types.RErrorType; import com.regnosys.rosetta.types.RType; +import com.regnosys.rosetta.types.RUnionType; import com.regnosys.rosetta.types.builtin.RBasicType; import com.regnosys.rosetta.types.builtin.RBuiltinTypeService; import com.regnosys.rosetta.types.builtin.RDateTimeType; @@ -137,4 +138,9 @@ protected RosettaValue caseDateTimeType(RDateTimeType type, List context) { protected RosettaValue caseZonedDateTimeType(RZonedDateTimeType type, List context) { return new RosettaZonedDateTimeValue(castList(context, ZonedDateTime.class)); } + + @Override + protected RosettaValue caseUnionType(RUnionType type, List context) { + throw new UnsupportedOperationException("Cannot create a value of a union type"); + } } 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..8041a5a41 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 @@ -60,6 +60,7 @@ import com.regnosys.rosetta.utils.DeepFeatureCallUtil import com.regnosys.rosetta.rosetta.simple.Annotated import com.regnosys.rosetta.types.RObjectFactory import com.regnosys.rosetta.RosettaEcoreUtil +import com.regnosys.rosetta.types.REnumType /** * This class contains custom scoping description. @@ -198,7 +199,7 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { } case ROSETTA_ENUM_VALUE_REFERENCE__VALUE: { if (context instanceof RosettaEnumValueReference) { - return Scopes.scopeFor(context.enumeration.allEnumValues) + return Scopes.scopeFor(context.enumeration.buildREnumType.allEnumValues) } return IScope.NULLSCOPE } @@ -214,7 +215,7 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { if (context instanceof RosettaExternalEnumValue) { val enumRef = (context.eContainer as RosettaExternalEnum).typeRef if (enumRef instanceof RosettaEnumeration) - return Scopes.scopeFor(enumRef.allEnumValues) + return Scopes.scopeFor(enumRef.buildREnumType.allEnumValues) } return IScope.NULLSCOPE } @@ -311,6 +312,12 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { } private def IScope createExtendedFeatureScope(EObject receiver, RType receiverType) { + if (receiverType instanceof REnumType) { + if (!(receiver instanceof RosettaSymbolReference) || !((receiver as RosettaSymbolReference).symbol instanceof RosettaEnumeration)) { + return IScope.NULLSCOPE + } + } + val List allPosibilities = newArrayList allPosibilities.addAll( receiverType.allFeatures(receiver) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java new file mode 100644 index 000000000..499a10d75 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/ExpectedTypeProvider.java @@ -0,0 +1,603 @@ +package com.regnosys.rosetta.types; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.regnosys.rosetta.rosetta.simple.SimplePackage.Literals.*; +import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.*; + +import com.google.inject.ImplementedBy; +import com.regnosys.rosetta.cache.IRequestScopedCache; +import com.regnosys.rosetta.rosetta.RosettaRule; +import com.regnosys.rosetta.rosetta.RosettaSymbol; +import com.regnosys.rosetta.rosetta.expression.ArithmeticOperation; +import com.regnosys.rosetta.rosetta.expression.AsKeyOperation; +import com.regnosys.rosetta.rosetta.expression.ChoiceOperation; +import com.regnosys.rosetta.rosetta.expression.ComparingFunctionalOperation; +import com.regnosys.rosetta.rosetta.expression.ComparisonOperation; +import com.regnosys.rosetta.rosetta.expression.ConstructorKeyValuePair; +import com.regnosys.rosetta.rosetta.expression.DefaultOperation; +import com.regnosys.rosetta.rosetta.expression.DistinctOperation; +import com.regnosys.rosetta.rosetta.expression.EqualityOperation; +import com.regnosys.rosetta.rosetta.expression.FilterOperation; +import com.regnosys.rosetta.rosetta.expression.FirstOperation; +import com.regnosys.rosetta.rosetta.expression.FlattenOperation; +import com.regnosys.rosetta.rosetta.expression.InlineFunction; +import com.regnosys.rosetta.rosetta.expression.JoinOperation; +import com.regnosys.rosetta.rosetta.expression.LastOperation; +import com.regnosys.rosetta.rosetta.expression.ListLiteral; +import com.regnosys.rosetta.rosetta.expression.LogicalOperation; +import com.regnosys.rosetta.rosetta.expression.MapOperation; +import com.regnosys.rosetta.rosetta.expression.MaxOperation; +import com.regnosys.rosetta.rosetta.expression.MinOperation; +import com.regnosys.rosetta.rosetta.expression.OneOfOperation; +import com.regnosys.rosetta.rosetta.expression.ReduceOperation; +import com.regnosys.rosetta.rosetta.expression.ReverseOperation; +import com.regnosys.rosetta.rosetta.expression.RosettaAbsentExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaBooleanLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaContainsExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaCountOperation; +import com.regnosys.rosetta.rosetta.expression.RosettaDeepFeatureCall; +import com.regnosys.rosetta.rosetta.expression.RosettaDisjointExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaExistsExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaFeatureCall; +import com.regnosys.rosetta.rosetta.expression.RosettaFunctionalOperation; +import com.regnosys.rosetta.rosetta.expression.RosettaImplicitVariable; +import com.regnosys.rosetta.rosetta.expression.RosettaIntLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaNumberLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaOnlyElement; +import com.regnosys.rosetta.rosetta.expression.RosettaOnlyExistsExpression; +import com.regnosys.rosetta.rosetta.expression.RosettaStringLiteral; +import com.regnosys.rosetta.rosetta.expression.RosettaSymbolReference; +import com.regnosys.rosetta.rosetta.expression.RosettaUnaryOperation; +import com.regnosys.rosetta.rosetta.expression.SortOperation; +import com.regnosys.rosetta.rosetta.expression.SumOperation; +import com.regnosys.rosetta.rosetta.expression.ThenOperation; +import com.regnosys.rosetta.rosetta.expression.ToDateOperation; +import com.regnosys.rosetta.rosetta.expression.ToDateTimeOperation; +import com.regnosys.rosetta.rosetta.expression.ToEnumOperation; +import com.regnosys.rosetta.rosetta.expression.ToIntOperation; +import com.regnosys.rosetta.rosetta.expression.ToNumberOperation; +import com.regnosys.rosetta.rosetta.expression.ToStringOperation; +import com.regnosys.rosetta.rosetta.expression.ToTimeOperation; +import com.regnosys.rosetta.rosetta.expression.ToZonedDateTimeOperation; +import com.regnosys.rosetta.rosetta.simple.Function; +import com.regnosys.rosetta.rosetta.simple.Operation; +import com.regnosys.rosetta.rosetta.simple.Segment; +import com.regnosys.rosetta.types.builtin.RBuiltinTypeService; +import com.regnosys.rosetta.utils.RosettaExpressionSwitch; + +import java.util.List; +import java.util.Objects; + +import javax.inject.Inject; + + +@ImplementedBy(ExpectedTypeProvider.Impl.class) +public interface ExpectedTypeProvider { + static final int INSIGNIFICANT_INDEX = -1; + + RType getExpectedTypeFromContainer(EObject owner); + default RType getExpectedType(EObject owner, EReference reference) { + return getExpectedType(owner, reference, INSIGNIFICANT_INDEX); + } + RType getExpectedType(EObject owner, EReference reference, int index); + + public static class Impl implements ExpectedTypeProvider { + private static Logger LOGGER = LoggerFactory.getLogger(Impl.class); + + private final RBuiltinTypeService builtins; + private final RosettaTypeProvider typeProvider; + private final TypeSystem typeSystem; + private final ExpectedTypeSwitch expressionSwitch; + private final IRequestScopedCache cache; + + @Inject + public Impl(RBuiltinTypeService builtins, RosettaTypeProvider typeProvider, TypeSystem typeSystem, IRequestScopedCache cache) { + this.builtins = builtins; + this.typeProvider = typeProvider; + this.typeSystem = typeSystem; + this.cache = cache; + this.expressionSwitch = new ExpectedTypeSwitch(); + } + + @Override + public RType getExpectedTypeFromContainer(EObject owner) { + EObject container = owner.eContainer(); + EReference reference = owner.eContainmentFeature(); + if (container == null || reference == null) { + return null; + } + if (reference.isMany()) { + int index = ((List)container.eGet(reference)).indexOf(owner); + return getExpectedType(container, reference, index); + } + return getExpectedType(container, reference); + } + @Override + public RType getExpectedType(EObject owner, EReference reference, int index) { + return cache.get(new CacheKey(owner, reference, index), () -> { + if (OPERATION__EXPRESSION.equals(reference) && owner instanceof Operation) { + Operation op = (Operation)owner; + if(op.getPath() == null) { + return typeProvider.getRTypeOfSymbol(op.getAssignRoot()); + } + List path = op.pathAsSegmentList(); + return typeProvider.getRTypeOfSymbol(path.get(path.size() - 1).getAttribute()); + } else if (CONSTRUCTOR_KEY_VALUE_PAIR__VALUE.equals(reference) && owner instanceof ConstructorKeyValuePair) { + ConstructorKeyValuePair pair = (ConstructorKeyValuePair) owner; + return typeProvider.getRTypeOfFeature(pair.getKey(), null); + } else if (owner instanceof RosettaExpression) { + return this.expressionSwitch.doSwitch((RosettaExpression) owner, reference, index); + } else if (INLINE_FUNCTION__BODY.equals(reference)) { + EObject operation = owner.eContainer(); + if (operation instanceof ReduceOperation) { + return getExpectedTypeFromContainer(operation); + } else if (operation instanceof FilterOperation) { + return builtins.BOOLEAN; + } else if (operation instanceof MapOperation) { + return getExpectedTypeFromContainer(operation); + } else if (operation instanceof ThenOperation) { + return getExpectedTypeFromContainer(operation); + } else if (operation instanceof ComparingFunctionalOperation) { + return builtins.BOOLEAN; + } else { + LOGGER.debug("Unexpected functional operation of type " + operation.getClass().getCanonicalName()); + } + } + return null; + }); + } + + private static class CacheKey { + private final EObject owner; + private final EReference reference; + private final int index; + public CacheKey(EObject owner, EReference reference, int index) { + this.owner = owner; + this.reference = reference; + this.index = index; + } + + @Override + public int hashCode() { + return Objects.hash(CacheKey.class, index, owner, reference); + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CacheKey other = (CacheKey) obj; + return index == other.index && Objects.equals(owner, other.owner) + && Objects.equals(reference, other.reference); + } + } + private static class Context { + public final EReference reference; + public final int index; + + public Context(EReference reference, int index) { + this.reference = reference; + this.index = index; + } + } + private class ExpectedTypeSwitch extends RosettaExpressionSwitch { + public RType doSwitch(RosettaExpression expr, EReference reference, int index) { + return doSwitch(expr, new Context(reference, index)); + } + + private boolean leavesItemTypeUnchanged(RosettaExpression expr) { + if (expr instanceof RosettaImplicitVariable) { + return true; + } else if ( + expr instanceof AsKeyOperation + || expr instanceof FilterOperation + || expr instanceof FlattenOperation + || expr instanceof DistinctOperation + || expr instanceof FirstOperation + || expr instanceof LastOperation + || expr instanceof MaxOperation + || expr instanceof MinOperation + || expr instanceof ReverseOperation + || expr instanceof SortOperation + || expr instanceof RosettaOnlyElement + ) { + return leavesItemTypeUnchanged(((RosettaUnaryOperation) expr).getArgument()); + } else if (expr instanceof MapOperation || expr instanceof ThenOperation) { + RosettaFunctionalOperation f = (RosettaFunctionalOperation) expr; + return leavesItemTypeUnchanged(f.getFunction().getBody()); + } + return false; + } + + @Override + protected RType caseConstructorExpression(RosettaConstructorExpression expr, Context context) { + return null; + } + + @Override + protected RType caseListLiteral(ListLiteral expr, Context context) { + if (LIST_LITERAL__ELEMENTS.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseConditionalExpression(RosettaConditionalExpression expr, Context context) { + if (ROSETTA_CONDITIONAL_EXPRESSION__IF.equals(context.reference)) { + return builtins.BOOLEAN; + } else if (ROSETTA_CONDITIONAL_EXPRESSION__IFTHEN.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } else if (ROSETTA_CONDITIONAL_EXPRESSION__ELSETHEN.equals(context.reference)) { + RType expectedType = getExpectedTypeFromContainer(expr); + if (expectedType != null) { + return expectedType; + } + return typeProvider.getRType(expr.getIfthen()); + } + return null; + } + + @Override + protected RType caseFeatureCall(RosettaFeatureCall expr, Context context) { + return null; + } + + @Override + protected RType caseDeepFeatureCall(RosettaDeepFeatureCall expr, Context context) { + return null; + } + + @Override + protected RType caseBooleanLiteral(RosettaBooleanLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseIntLiteral(RosettaIntLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseNumberLiteral(RosettaNumberLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseStringLiteral(RosettaStringLiteral expr, Context context) { + return null; + } + + @Override + protected RType caseOnlyExists(RosettaOnlyExistsExpression expr, Context context) { + return null; + } + + @Override + protected RType caseImplicitVariable(RosettaImplicitVariable expr, Context context) { + return null; + } + + @Override + protected RType caseSymbolReference(RosettaSymbolReference expr, Context context) { + if (ROSETTA_SYMBOL_REFERENCE__RAW_ARGS.equals(context.reference)) { + RosettaSymbol symbol = expr.getSymbol(); + if (symbol instanceof Function) { + Function fun = (Function)symbol; + return typeProvider.getRTypeOfSymbol(fun.getInputs().get(context.index)); + } else if (symbol instanceof RosettaRule) { + RosettaRule rule = (RosettaRule)symbol; + return typeSystem.typeCallToRType(rule.getInput()); + } + } + return null; + } + + @Override + protected RType caseAddOperation(ArithmeticOperation expr, Context context) { + return null; + } + + @Override + protected RType caseSubtractOperation(ArithmeticOperation expr, Context context) { + return null; + } + + @Override + protected RType caseMultiplyOperation(ArithmeticOperation expr, Context context) { + return builtins.UNCONSTRAINED_NUMBER; + } + + @Override + protected RType caseDivideOperation(ArithmeticOperation expr, Context context) { + return builtins.UNCONSTRAINED_NUMBER; + } + + @Override + protected RType caseJoinOperation(JoinOperation expr, Context context) { + return builtins.UNCONSTRAINED_STRING; + } + + @Override + protected RType caseAndOperation(LogicalOperation expr, Context context) { + return builtins.BOOLEAN; + } + + @Override + protected RType caseOrOperation(LogicalOperation expr, Context context) { + return builtins.BOOLEAN; + } + + @Override + protected RType caseLessThanOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseLessThanOrEqualOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseGreaterThanOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseGreaterThanOrEqualOperation(ComparisonOperation expr, Context context) { + return null; + } + + @Override + protected RType caseEqualsOperation(EqualityOperation expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseNotEqualsOperation(EqualityOperation expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseContainsOperation(RosettaContainsExpression expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseDisjointOperation(RosettaDisjointExpression expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseDefaultOperation(DefaultOperation expr, Context context) { + if (ROSETTA_BINARY_OPERATION__RIGHT.equals(context.reference)) { + return typeProvider.getRType(expr.getLeft()); + } + return null; + } + + @Override + protected RType caseAsKeyOperation(AsKeyOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseChoiceOperation(ChoiceOperation expr, Context context) { + return null; + } + + @Override + protected RType caseOneOfOperation(OneOfOperation expr, Context context) { + return null; + } + + @Override + protected RType caseAbsentOperation(RosettaAbsentExpression expr, Context context) { + return null; + } + + @Override + protected RType caseCountOperation(RosettaCountOperation expr, Context context) { + return null; + } + + @Override + protected RType caseExistsOperation(RosettaExistsExpression expr, Context context) { + return null; + } + + @Override + protected RType caseDistinctOperation(DistinctOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseFirstOperation(FirstOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseFlattenOperation(FlattenOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseLastOperation(LastOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseReverseOperation(ReverseOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseOnlyElementOperation(RosettaOnlyElement expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseSumOperation(SumOperation expr, Context context) { + return null; + } + + @Override + protected RType caseToStringOperation(ToStringOperation expr, Context context) { + return null; + } + + @Override + protected RType caseToNumberOperation(ToNumberOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToIntOperation(ToIntOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToTimeOperation(ToTimeOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToEnumOperation(ToEnumOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToDateOperation(ToDateOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToDateTimeOperation(ToDateTimeOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseToZonedDateTimeOperation(ToZonedDateTimeOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return builtins.UNCONSTRAINED_STRING; + } + return null; + } + + @Override + protected RType caseFilterOperation(FilterOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseMapOperation(MapOperation expr, Context context) { + InlineFunction f = expr.getFunction(); + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference) && f != null && leavesItemTypeUnchanged(f.getBody())) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseMaxOperation(MaxOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseMinOperation(MinOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseReduceOperation(ReduceOperation expr, Context context) { + return null; + } + + @Override + protected RType caseSortOperation(SortOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference)) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + + @Override + protected RType caseThenOperation(ThenOperation expr, Context context) { + if (ROSETTA_UNARY_OPERATION__ARGUMENT.equals(context.reference) && leavesItemTypeUnchanged(expr.getFunction().getBody())) { + return getExpectedTypeFromContainer(expr); + } + return null; + } + } + } +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java index adf67176c..e7b3f612d 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/REnumType.java @@ -16,8 +16,14 @@ package com.regnosys.rosetta.types; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import com.regnosys.rosetta.rosetta.RosettaEnumValue; import com.regnosys.rosetta.rosetta.RosettaEnumeration; import com.regnosys.rosetta.utils.ModelIdProvider; import com.rosetta.model.lib.ModelSymbolId; @@ -26,14 +32,17 @@ public class REnumType extends RAnnotateType implements RObject { private final RosettaEnumeration enumeration; private ModelSymbolId symbolId = null; + private List parents = null; private final ModelIdProvider modelIdProvider; + private final RObjectFactory objectFactory; - public REnumType(final RosettaEnumeration enumeration, final ModelIdProvider modelIdProvider) { + public REnumType(final RosettaEnumeration enumeration, final ModelIdProvider modelIdProvider, final RObjectFactory objectFactory) { super(); this.enumeration = enumeration; this.modelIdProvider = modelIdProvider; + this.objectFactory = objectFactory; } @Override @@ -48,6 +57,51 @@ public ModelSymbolId getSymbolId() { public RosettaEnumeration getEObject() { return this.enumeration; } + + public List getParents() { + if (parents == null) { + parents = enumeration.getParentEnums().stream().map(e -> objectFactory.buildREnumType(e)).collect(Collectors.toList()); + } + return parents; + } + /** + * Get a list of all parents of this enum type, including itself. + * + * The list is ordered from the most top-level enumeration to the least (i.e., itself). In case of multiple parents, + * the order is left-to-right depth-first. + */ + public List getAllParents() { + LinkedHashSet reversedResult = new LinkedHashSet<>(); + doGetAllParents(this, reversedResult); + List result = reversedResult.stream().collect(Collectors.toCollection(ArrayList::new)); + Collections.reverse(result); + return result; + } + private void doGetAllParents(REnumType current, LinkedHashSet parents) { + if (parents.add(current)) { + current.getParents().forEach(p -> doGetAllParents(p, parents)); + } + } + + /** + * Get a list of the enum values defined in this enumeration. This does not include enum values of any parents. + */ + public List getOwnEnumValues() { + return enumeration.getEnumValues(); + } + + /** + * Get a list of all enum values of this enumeration, including all enum values of its parents. + * + * The list starts with the enum values of the top-most enumeration, and ends with the enum values of itself. In case of multiple parents, + * the order is left-to-right depth-first. + */ + public List getAllEnumValues() { + return getAllParents() + .stream() + .flatMap(p -> p.getOwnEnumValues().stream()) + .collect(Collectors.toList()); + } @Override public int hashCode() { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java index 0aafc507e..170aec83c 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RObjectFactory.java @@ -213,6 +213,6 @@ public RDataType buildRDataType(Data data, List additionalAttributes return new RDataType(data, modelIdProvider, this, additionalAttributes); } public REnumType buildREnumType(RosettaEnumeration enumeration) { - return new REnumType(enumeration, modelIdProvider); + return new REnumType(enumeration, modelIdProvider, this); } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java new file mode 100644 index 000000000..29b1362ea --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RUnionType.java @@ -0,0 +1,60 @@ +package com.regnosys.rosetta.types; + +import java.util.List; +import java.util.Objects; + +import com.rosetta.model.lib.ModelSymbolId; + +/** + * An `RType` representing an unknown join of multiple types. + * A Rune expression should never have this type - it is purely internal to Rune's type system, + * so code generators can ignore it. + */ +public class RUnionType extends RType { + private final List types; + + public RUnionType(List types) { + this.types = types; + } + public RUnionType(RType... types) { + this(List.of(types)); + } + + public List getTypes() { + return types; + } + + @Override + public ModelSymbolId getSymbolId() { + return null; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("one of "); + for (int i = 0; i mayBeEmpty(RDataType d) keepTypeAliasIfPossible(RType t1, RType t2, BiFunction combineUnderlyingTypes): RType @@ -66,8 +65,8 @@ auxiliary ancestors(Data t) { } auxiliary ancestorEnums(RosettaEnumeration t) { getAll(t, - RosettaPackage::eINSTANCE.rosettaEnumeration_SuperType, - RosettaPackage::eINSTANCE.rosettaEnumeration_SuperType, + RosettaPackage::eINSTANCE.rosettaEnumeration_ParentEnums, + RosettaPackage::eINSTANCE.rosettaEnumeration_ParentEnums, typeof(RosettaEnumeration) ) } @@ -90,13 +89,6 @@ auxiliary listJoin(RListType t1, RListType t2) { val sup = join(t1.itemType, t2.itemType); return createListType(sup, union(t1.constraint, t2.constraint)) } -auxiliary allEnumValues(RosettaEnumeration e) { - if (e.superType === null) { - return e.enumValues; - } else { - return allEnumValues(e.superType) + e.enumValues; - } -} auxiliary mayBeEmpty(RDataType t) { t.allAttributes.forall[ cardinality.minBound === 0 @@ -144,7 +136,7 @@ auxiliary allFeatures(RType t, ResourceSet resourceSet) { RDataType: t.allNonOverridenAttributes.map[EObject] REnumType: - t.EObject.allEnumValues + t.allEnumValues RRecordType: { if (resourceSet !== null) { builtinTypes.toRosettaType(t, RosettaRecordType, resourceSet).features diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend deleted file mode 100644 index ce1a86c12..000000000 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaExpectedTypeProvider.xtend +++ /dev/null @@ -1,47 +0,0 @@ -package com.regnosys.rosetta.types - -import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression -import com.regnosys.rosetta.rosetta.RosettaExternalFunction -import com.regnosys.rosetta.rosetta.simple.Operation -import org.eclipse.emf.ecore.EObject -import org.eclipse.emf.ecore.EReference - -import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.* -import static com.regnosys.rosetta.rosetta.simple.SimplePackage.Literals.* -import com.regnosys.rosetta.rosetta.expression.RosettaSymbolReference -import com.regnosys.rosetta.types.builtin.RBuiltinTypeService -import javax.inject.Inject - -class RosettaExpectedTypeProvider { - - @Inject extension RosettaTypeProvider - @Inject extension RBuiltinTypeService - @Inject extension TypeSystem - - def RType getExpectedType(EObject owner, EReference reference, int idx) { - switch owner { - RosettaConditionalExpression case reference == ROSETTA_CONDITIONAL_EXPRESSION__IF: - BOOLEAN - RosettaSymbolReference case reference == ROSETTA_SYMBOL_REFERENCE__RAW_ARGS: { - if(idx >= 0 && owner.symbol instanceof RosettaExternalFunction) { - val fun = (owner.symbol as RosettaExternalFunction) - if(idx >= fun.parameters.size) { - null // add error type? - } else { - val targetParam = fun.parameters.get(idx) - targetParam.typeCall.typeCallToRType - } - } - } - Operation case reference == OPERATION__EXPRESSION: { - if(owner.path === null) - owner.assignRoot.RTypeOfSymbol - else owner.pathAsSegmentList.last?.attribute?.RTypeOfSymbol - } - } - } - - def RType getExpectedType(EObject owner, EReference reference) { - owner.getExpectedType(reference, -1) - } -} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics index 4504432ff..5f896c8cc 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeChecking.xsemantics @@ -385,50 +385,6 @@ checkrule CheckIfConditionalExpression for from { looseListSubtypeCheck(e.^if, singleBoolean) } -checkrule CheckBodyConditionalExpression for - RosettaConditionalExpression e -from { - var RListType tthen - var RListType telse - empty |- e.ifthen : tthen or tthen = null - empty |- e.elsethen : telse or telse = null - if (tthen !== null && telse !== null) { - val joined = listJoin(tthen, telse) - - if ({empty |- ANY <: joined.itemType}) { - fail error "Types `" + tthen.itemType.name + "` and `" + telse.itemType.name + "` do not have a common supertype." - } - } -} - -checkrule CheckListLiteral for - ListLiteral e -from { - val telems = newArrayList - if (e.elements.forall[ - var RListType telem - empty |- it : telem or telem = null - if (telem !== null) { - telems.add(telem) - } - telem !== null - ]) { - telems.fold(emptyNothing, [ RListType acc, RListType telem | - if (acc === null) { - null - } else { - val sup = join(telem.itemType, acc.itemType); - if ({empty |- ANY <: sup}) { - null - } else { - createListType(sup, telem.constraint + acc.constraint) - } - } - ]) !== null - or - fail error "Elements do not have a common supertype: " + telems.join(', ')["`" + it.itemType.name + "`"] + "." - } -} checkrule CheckRosettaSymbolReference for RosettaSymbolReference e from { val f = e.symbol diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend index 7af8c1966..5dd71f1d1 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RosettaTypeProvider.xtend @@ -81,6 +81,7 @@ import com.regnosys.rosetta.rosetta.TypeParameter import com.regnosys.rosetta.rosetta.simple.AssignPathRoot import com.regnosys.rosetta.rosetta.RosettaCallableWithArgs import com.regnosys.rosetta.RosettaEcoreUtil +import org.eclipse.xtend2.lib.StringConcatenationClient class RosettaTypeProvider extends RosettaExpressionSwitch> { public static String EXPRESSION_RTYPE_CACHE_KEY = RosettaTypeProvider.canonicalName + ".EXPRESSION_RTYPE" @@ -95,6 +96,7 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { val left = expr.left var leftType = left.safeRType(cycleTracker) - if (leftType === null || leftType instanceof RErrorType) { - return leftType + if (leftType instanceof RErrorType) { + return NOTHING } val right = expr.right var rightType = right.safeRType(cycleTracker) - if (rightType === null || rightType instanceof RErrorType) { - return rightType + if (rightType instanceof RErrorType) { + return NOTHING } expr.operator.resultType(leftType, rightType) } @@ -284,14 +286,21 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { val ifT = expr.ifthen.safeRType(cycleTracker) val elseT = expr.elsethen.safeRType(cycleTracker) - if (ifT === null || ifT instanceof RErrorType) { - elseT - } else if (elseT === null || elseT instanceof RErrorType) { - ifT + if (ifT instanceof RErrorType) { + NOTHING + } else if (elseT instanceof RErrorType) { + NOTHING } else { val joined = join(ifT, elseT) if (joined == ANY) { - new RErrorType("Can not infer common type for '" + ifT.name + "' and " + elseT.name + "'.") + new RErrorType('''Types `«ifT»` and `«elseT»` do not have a common supertype.''') + } else if (joined instanceof RUnionType) { + val expected = expr.expectedTypeFromContainer + if (expected !== null && joined.isSubtypeOf(expected)) { + expected + } else { + new RErrorType('''Cannot infer common supertype of `«ifT»` and `«elseT»`.''') + } } else { joined } @@ -397,8 +406,17 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { val types = expr.elements.map[RType].filter[it !== null] val joined = types.join + val unique = newLinkedHashSet(types) + val StringConcatenationClient failedList = '''«FOR t: unique.take(unique.size-1) SEPARATOR ", "»`«t»`«ENDFOR» and `«unique.last»`''' if (joined == ANY) { - new RErrorType(types.groupBy[name].keySet.join(', ')) + new RErrorType('''Types «failedList» do not have a common supertype.''') + } else if (joined instanceof RUnionType) { + val expected = expr.expectedTypeFromContainer + if (expected !== null && joined.isSubtypeOf(expected)) { + expected + } else { + new RErrorType('''Cannot infer common supertype of «failedList».''') + } } else { joined } @@ -425,6 +443,9 @@ class RosettaTypeProvider extends RosettaExpressionSwitch cycleTracker) { + if (expr.value === null) { // In case of a parse error + return NOTHING + } constrainedNumber(expr.value.toPlainString.replaceAll("\\.|\\-", "").length, Math.max(0, expr.value.scale), expr.value, expr.value) } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java index 2b65c3285..d4548b0e2 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/SubtypeRelation.java @@ -31,6 +31,12 @@ public boolean isSubtypeOf(RType t1, RType t2) { return false; } return isSubtypeOf(st, t2); + } else if (t1 instanceof RUnionType) { + return ((RUnionType)t1).getTypes().stream().allMatch(t -> isSubtypeOf(t, t2)); + } else if (t2 instanceof RUnionType) { + return ((RUnionType)t2).getTypes().stream().anyMatch(t -> isSubtypeOf(t1, t)); + } else if (t2 instanceof REnumType) { + return ((REnumType)t2).getParents().stream().anyMatch(p -> isSubtypeOf(t1, p)); } else if (t1 instanceof RAliasType) { return isSubtypeOf(((RAliasType)t1).getRefersTo(), t2); } else if (t2 instanceof RAliasType) { @@ -50,6 +56,14 @@ public RType join(RType t1, RType t2) { return join((RStringType)t1, (RStringType)t2); } else if (t1 instanceof RDataType && t2 instanceof RDataType) { return join((RDataType)t1, (RDataType)t2); + } else if (t1 instanceof REnumType && t2 instanceof REnumType) { + return join((REnumType)t1, (REnumType)t2); + } else if (t1 instanceof RUnionType && t2 instanceof RUnionType) { + return join((RUnionType)t1, (RUnionType)t2); + } else if (t1 instanceof RUnionType) { + return join((RUnionType)t1, t2); + } else if (t2 instanceof RUnionType) { + return join(t1, (RUnionType)t2); } else if (t1 instanceof RAliasType && t2 instanceof RAliasType) { return join((RAliasType)t1, (RAliasType)t2); } else if (t1 instanceof RAliasType) { @@ -73,6 +87,53 @@ public RType join(RDataType t1, RDataType t2) { return joinByTraversingAncestorsAndAliases(t1, t2); } } + public RType join(REnumType t1, REnumType t2) { + if (isSubtypeOf(t1, t2)) { + return t2; + } else if (isSubtypeOf(t2, t1)) { + return t1; + } + return new RUnionType(t1, t2); + } + public RType join(RUnionType t1, RUnionType t2) { + ArrayList allTypes = new ArrayList<>(t1.getTypes().size() + t2.getTypes().size()); + allTypes.addAll(t1.getTypes()); + allTypes.addAll(t2.getTypes()); + return joinWithUnion(allTypes); + } + public RType join(RUnionType t1, RType t2) { + ArrayList allTypes = new ArrayList<>(t1.getTypes().size() + 1); + allTypes.addAll(t1.getTypes()); + allTypes.add(t2); + return joinWithUnion(allTypes); + } + public RType join(RType t1, RUnionType t2) { + ArrayList allTypes = new ArrayList<>(t2.getTypes().size() + 1); + allTypes.add(t1); + allTypes.addAll(t2.getTypes()); + return joinWithUnion(allTypes); + } + private RType joinWithUnion(ArrayList types) { + // Trim types which are the subtype of any other type in the union + for (int i=types.size()-1; i>=0; i--) { + RType toCheck = types.get(i); + for (int j=0; j types) { RType acc = builtins.NOTHING; for (RType t: types) { - acc = join(acc, t); + acc = subtypeRelation.join(acc, t); if (acc.equals(builtins.ANY)) { return acc; } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java index f1f83d4fc..3135eba8a 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaTypeSwitch.java @@ -23,6 +23,7 @@ import com.regnosys.rosetta.types.RErrorType; import com.regnosys.rosetta.types.RParametrizedType; import com.regnosys.rosetta.types.RType; +import com.regnosys.rosetta.types.RUnionType; import com.regnosys.rosetta.types.builtin.RBasicType; import com.regnosys.rosetta.types.builtin.RBuiltinTypeService; import com.regnosys.rosetta.types.builtin.RDateTimeType; @@ -53,6 +54,8 @@ protected Return doSwitch(RType type, Context context) { return doSwitch((RParametrizedType)type, context); } else if (type instanceof RRecordType) { return doSwitch((RRecordType)type, context); + } else if (type instanceof RUnionType) { + return caseUnionType((RUnionType)type, context); } throw errorMissedCase(type); } @@ -108,6 +111,8 @@ protected Return doSwitch(RRecordType type, Context context) { protected abstract Return caseAliasType(RAliasType type, Context context); + protected abstract Return caseUnionType(RUnionType type, Context context); + protected abstract Return caseNumberType(RNumberType type, Context context); protected abstract Return caseStringType(RStringType type, Context context); protected abstract Return caseBooleanType(RBasicType type, Context context); diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java new file mode 100644 index 000000000..1dfc8efde --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/EnumValidator.java @@ -0,0 +1,86 @@ +package com.regnosys.rosetta.validation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.xtext.validation.Check; + +import com.regnosys.rosetta.rosetta.RosettaEnumValue; +import com.regnosys.rosetta.rosetta.RosettaEnumeration; +import com.regnosys.rosetta.types.REnumType; +import com.regnosys.rosetta.types.RObjectFactory; + +import static com.regnosys.rosetta.rosetta.RosettaPackage.Literals.*; + +public class EnumValidator extends AbstractDeclarativeRosettaValidator { + @Inject + private RObjectFactory rObjectFactory; + + @Check + public void checkCyclicExtensions(RosettaEnumeration enumeration) { + for (int i=0; i path = new ArrayList<>(); + path.add(enumeration); + Set visited = new HashSet<>(); + visited.add(enumeration); + if (hasCyclicExtension(p, path, visited)) { + String pathString = path.stream().map(e -> e.getName()).collect(Collectors.joining(" extends ")); + error("Cyclic extension: " + pathString, enumeration, ROSETTA_ENUMERATION__PARENT_ENUMS, i); + } + } + } + + private boolean hasCyclicExtension(RosettaEnumeration current, List path, Set visited) { + path.add(current); + if (visited.add(current)) { + for (RosettaEnumeration p : current.getParentEnums()) { + if (hasCyclicExtension(p, path, visited)) { + return true; + } + } + } else { + if (path.get(0).equals(path.get(path.size() - 1))) { + return true; + } + } + path.remove(path.size() - 1); + return false; + } + + @Check + public void checkEnumValuesAreUnique(RosettaEnumeration enumeration) { + REnumType t = rObjectFactory.buildREnumType(enumeration); + Set usedNames = new HashSet<>(); + t.getParents().forEach(p -> p.getAllEnumValues().forEach(v -> usedNames.add(v.getName()))); + for (RosettaEnumValue value: t.getOwnEnumValues()) { + if (!usedNames.add(value.getName())) { + error("Duplicate enum value '" + value.getName() + "'", value, ROSETTA_NAMED__NAME); + } + } + } + + @Check + public void checkEnumExtensionsDoNotHaveOverlappingEnumValueNames(RosettaEnumeration enumeration) { + REnumType t = rObjectFactory.buildREnumType(enumeration); + Map usedNames = new HashMap<>(); + for (int i=0; i + if (crossRefs !== null) { + for (ref : crossRefs) { + if (ref.many) { + val annotatedList = (object.eGet(ref) as List).filter(Annotated) + for (var i=0; i).forEach [ it, i | - val expectedType = owner.getExpectedType(ref, i) - checkType(expectedType, it, owner, ref, i) - ] - } else { - val expectedType = owner.getExpectedType(ref) - checkType(expectedType, referenceValue as RosettaExpression, owner, ref, INSIGNIFICANT_INDEX) - } - ] + def void checkConditionalExpression(RosettaConditionalExpression expr) { + checkType(BOOLEAN, expr.^if, expr, ROSETTA_CONDITIONAL_EXPRESSION__IF, INSIGNIFICANT_INDEX) } private def checkType(RType expectedType, RosettaExpression expression, EObject owner, EReference ref, int index) { @@ -530,20 +545,6 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { private def cardRepr(RAttribute attr) '''«attr.cardinality.minBound»..«attr.cardinality.max.map[toString].orElse('*')»''' - @Check - def checkEnumValuesAreUnique(RosettaEnumeration enumeration) { - val name2attr = HashMultimap.create - enumeration.allEnumValues.forEach [ - name2attr.put(name, it) - ] - for (value : enumeration.enumValues) { - val valuesByName = name2attr.get(value.name) - if (valuesByName.size > 1) { - error('''Duplicate enum value '«value.name»'«»''', value, ROSETTA_NAMED__NAME, DUPLICATE_ENUM_VALUE) - } - } - } - @Check def checkFeatureNamesAreUnique(RosettaFeatureOwner ele) { ele.features.groupBy[name].forEach [ k, v | @@ -697,6 +698,17 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { ROSETTA_SYMBOL_REFERENCE__RAW_ARGS, 0) } } + } else if (callable instanceof RosettaExternalFunction) { + element.args.indexed.forEach [ indexed | + val callerArg = indexed.value + val callerIdx = indexed.key + val param = callable.parameters.get(callerIdx) + checkType(param.typeCall.typeCallToRType, callerArg, element, ROSETTA_SYMBOL_REFERENCE__RAW_ARGS, callerIdx) + if(cardinality.isMulti(callerArg)) { + error('''Expecting single cardinality for parameter '«param.name»'.''', element, + ROSETTA_SYMBOL_REFERENCE__RAW_ARGS, callerIdx) + } + ] } } } else { @@ -936,7 +948,7 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { if (structured.nullOrEmpty) return val mostUsedEnum = structured.max[$0.value.size <=> $1.value.size].key - val toImplement = mostUsedEnum.allEnumValues.map[name].toSet + val toImplement = mostUsedEnum.buildREnumType.allEnumValues.map[name].toSet enumsUsed.get(mostUsedEnum).forEach [ toImplement.remove(it.key) ] @@ -1069,7 +1081,14 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { if (cardinality.isMulti(arg)) { error('''The argument of «ele.operator» should be of singular cardinality.''', ele, ROSETTA_UNARY_OPERATION__ARGUMENT) } - if (!arg.RType.isSubtypeOf(UNCONSTRAINED_STRING)) { + val argType = arg.RType.stripFromTypeAliases + if (ele instanceof ToEnumOperation && argType instanceof REnumType) { + // `to-enum` can also be used to convert an enum value to a parent enum + val resultType = (ele as ToEnumOperation).enumeration.buildREnumType + if (!resultType.isSubtypeOf(argType)) { + error('''«argType» does not extend «resultType».''', ele, TO_ENUM_OPERATION__ENUMERATION) + } + } else if (!argType.isSubtypeOf(UNCONSTRAINED_STRING)) { error('''The argument of «ele.operator» should be a string.''', ele, ROSETTA_UNARY_OPERATION__ARGUMENT) } } @@ -1473,9 +1492,11 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { error('''Assign expression contains a list of lists, use flatten to create a list.''', o, OPERATION__EXPRESSION) } - val isList = cardinality.isSymbolMulti(o.path !== null + val attr = o.path !== null ? o.pathAsSegmentList.last.attribute - : o.assignRoot) + : o.assignRoot + checkType(attr.RTypeOfSymbol, expr, o, OPERATION__EXPRESSION, INSIGNIFICANT_INDEX) + val isList = cardinality.isSymbolMulti(attr) if (o.add && !isList) { error('''Add must be used with a list.''', o, OPERATION__ASSIGN_ROOT) } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend index 39de06559..7c11b7b84 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaValidator.xtend @@ -10,7 +10,7 @@ import org.eclipse.xtext.validation.ComposedChecks * * See https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#validation */ -@ComposedChecks(validators = #[RosettaSimpleValidator, StandaloneRosettaTypingValidator]) +@ComposedChecks(validators = #[RosettaSimpleValidator, StandaloneRosettaTypingValidator, EnumValidator]) class RosettaValidator extends AbstractRosettaValidator { diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java index a95a024eb..9daea28cb 100644 --- a/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RosettaEnum.java @@ -19,10 +19,11 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; @Documented @Retention(RetentionPolicy.RUNTIME) public @interface RosettaEnum { - String value() default ""; - + String value() default ""; + Class>[] parents() default {}; } diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend index 4cad2e05b..ea429b4d1 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/function/FunctionGeneratorTest.xtend @@ -42,6 +42,83 @@ class FunctionGeneratorTest { @Inject extension ModelHelper @Inject extension ValidationTestHelper + @Test + def void testConvertEnumValueToParentEnum() { + val code = ''' + enum A: + VALUE_A + + enum B extends A: + VALUE_B + + func Test: + inputs: + b B (1..1) + output: + result A (1..1) + + set result: + b to-enum A + '''.generateCode + + val classes = code.compileToClasses + + val aClass = classes.get("com.rosetta.test.model.A") + val valueAOfA = aClass.enumConstants.findFirst[c| c.toString == "VALUE_A"] + val bClass = classes.get("com.rosetta.test.model.B") + val valueAOfB = bClass.enumConstants.findFirst[c| c.toString == "VALUE_A"] + val valueBOfB = bClass.enumConstants.findFirst[c| c.toString == "VALUE_B"] + + val test = classes.createFunc("Test") + assertEquals(valueAOfA, test.invokeFunc(aClass, #[valueAOfB])) + assertNull(test.invokeFunc(aClass, #[valueBOfB])) + assertNull(test.invokeFunc(aClass, #[null])) + } + + @Test + def void testExtendedEnumTypeCoercion() { + val code = ''' + enum A: + VALUE_A + + enum B extends A: + VALUE_B + + enum C extends A: + VALUE_C + + enum D extends B, C: + VALUE_D + + func TestCoercion: + inputs: + b B (1..1) + c C (1..1) + output: + result D (1..1) + + set result: + if True + then b + else if True + then c + else A -> VALUE_A + '''.generateCode + + val classes = code.compileToClasses + + val bClass = classes.get("com.rosetta.test.model.B") + val valueB = bClass.enumConstants.findFirst[c| c.toString == "VALUE_B"] + val cClass = classes.get("com.rosetta.test.model.C") + val valueC = cClass.enumConstants.findFirst[c| c.toString == "VALUE_C"] + + val dClass = classes.get("com.rosetta.test.model.D") + val valueBOfDClass = dClass.enumConstants.findFirst[c| c.toString == "VALUE_B"] + + val testCoercion = classes.createFunc("TestCoercion") + assertEquals(valueBOfDClass, testCoercion.invokeFunc(valueBOfDClass.class, #[valueB, valueC])) + } + @Test def void assignToMultiMetaFeature() { val code = ''' diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend index 61eb0133f..8180f22ba 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/EnumGeneratorTest.xtend @@ -68,8 +68,7 @@ class EnumGeneratorTest { val testEnumCode = code.get(rootPackage + ".TestEnumWithDisplay") assertThat(testEnumCode, - allOf(containsString('''TestEnumWithDisplay(String rosettaName)'''), - containsString('''TestEnumWithDisplay(String rosettaName, String displayName)'''), + allOf(containsString('''TestEnumWithDisplay(String rosettaName, String displayName)'''), containsString('''public String toDisplayString()'''))) code.compileToClasses @@ -99,17 +98,4 @@ class EnumGeneratorTest { assertThat(EnumHelper.formatEnumName("AggregateClient"), is("AGGREGATE_CLIENT")) assertThat(EnumHelper.formatEnumName("Currency1PerCurrency2"), is("CURRENCY_1_PER_CURRENCY_2")) } - - @Test - @Disabled - def void shouldAllowDeprectedAnnotationForEnum() { - val code = ''' - enum TestEnumDeprecated: - [deprecated] - one - two - '''.generateCode - - code.compileToClasses - } } diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend index f13473357..40f0ebefe 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/generator/java/object/RosettaExtensionsTest.xtend @@ -13,14 +13,12 @@ import org.junit.jupiter.api.^extension.ExtendWith import static org.junit.jupiter.api.Assertions.* import javax.inject.Inject import com.regnosys.rosetta.types.RObjectFactory -import com.regnosys.rosetta.RosettaEcoreUtil @ExtendWith(InjectionExtension) @InjectWith(RosettaInjectorProvider) class RosettaExtensionsTest { - @Inject extension ParseHelper - @Inject extension RosettaEcoreUtil + @Inject extension ParseHelper @Inject extension RObjectFactory @Test @@ -65,14 +63,15 @@ class RosettaExtensionsTest { enum Baz extends Bar: baz '''.parse - val foo = model.elements.filter(RosettaEnumeration).head() - val bar = model.elements.filter(RosettaEnumeration).get(1) - val baz = model.elements.filter(RosettaEnumeration).last() - assertEquals(#{foo, bar, baz}, baz.allSuperEnumerations) - assertEquals(#{foo, bar}, bar.allSuperEnumerations) - assertEquals(#{foo}, foo.allSuperEnumerations) - assertEquals(#['baz', 'bar', 'foo0', 'foo1'], baz.allEnumValues.map[name].toList) - assertEquals(#['bar', 'foo0', 'foo1'], bar.allEnumValues.map[name].toList) + val elems = model.elements.filter(RosettaEnumeration).map[buildREnumType] + val foo = elems.head() + val bar = elems.get(1) + val baz = elems.last() + assertEquals(#{foo, bar, baz}, baz.allParents.toSet) + assertEquals(#{foo, bar}, bar.allParents.toSet) + assertEquals(#{foo}, foo.allParents.toSet) + assertEquals(#['foo0', 'foo1', 'bar', 'baz'], baz.allEnumValues.map[name].toList) + assertEquals(#['foo0', 'foo1', 'bar'], bar.allEnumValues.map[name].toList) assertEquals(#['foo0', 'foo1'], foo.allEnumValues.map[name].toList) } } diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend index 08e8a7333..4f10e339d 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/tests/RosettaExpressionsTest.xtend @@ -5,7 +5,6 @@ package com.regnosys.rosetta.tests import com.regnosys.rosetta.tests.util.CodeGeneratorTestHelper import com.regnosys.rosetta.tests.util.ModelHelper -import com.regnosys.rosetta.validation.RosettaIssueCodes import org.eclipse.xtext.testing.InjectWith import org.eclipse.xtext.testing.extensions.InjectionExtension import org.eclipse.xtext.testing.validation.ValidationTestHelper @@ -100,7 +99,7 @@ class RosettaExpressionsTest { output: result boolean (1..1) set result: test -> one + test -> two = 42 - '''.parseRosetta.assertError(ROSETTA_BINARY_OPERATION, RosettaIssueCodes.TYPE_ERROR, "Incompatible types: cannot use operator '+' with date and date.") + '''.parseRosetta.assertError(ARITHMETIC_OPERATION, null, "Incompatible types: cannot use operator '+' with date and date.") } /** 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..af41803da 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 @@ -34,6 +34,42 @@ class RosettaParsingTest { @Inject extension ValidationTestHelper @Inject extension ExpressionParser + @Test + def void testCannotAccessEnumValueThroughAnotherEnumValue() { + val model = ''' + enum A: + VALUE_A + '''.parseRosettaWithNoIssues + + "A -> VALUE_A -> VALUE_A" + .parseExpression(#[model]) + .assertError(ROSETTA_FEATURE_CALL, Diagnostic.LINKING_DIAGNOSTIC, "Couldn't resolve reference to RosettaFeature 'VALUE_A'.") + } + + @Test + def void testAccessEnumValueOfMultiEnumExtension() { + val model = ''' + enum A: + VALUE_A + + enum B: + VALUE_B + + enum C extends A, B: + VALUE_C + '''.parseRosettaWithNoIssues + + "C -> VALUE_C" + .parseExpression(#[model]) + .assertNoIssues + "C -> VALUE_A" + .parseExpression(#[model]) + .assertNoIssues + "C -> VALUE_B" + .parseExpression(#[model]) + .assertNoIssues + } + @Test def void testFullyQualifiedNamesCanBeUsedInExpression() { val modelBar = ''' diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend index 0c2b221a3..852d04eb5 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/RosettaTypingTest.xtend @@ -10,10 +10,8 @@ import com.regnosys.rosetta.tests.util.ModelHelper import com.regnosys.rosetta.rosetta.simple.Function import com.regnosys.rosetta.rosetta.expression.RosettaConditionalExpression import com.regnosys.rosetta.rosetta.simple.Data -import com.regnosys.rosetta.rosetta.expression.ArithmeticOperation import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.regnosys.rosetta.rosetta.expression.LogicalOperation -import com.regnosys.rosetta.rosetta.expression.MapOperation import com.regnosys.rosetta.types.builtin.RBuiltinTypeService import java.util.Optional import java.math.BigDecimal @@ -55,7 +53,6 @@ class RosettaTypingTest { '"Some string"'.assertIsValidWithType(singleString(11, 11)) '3.14'.assertIsValidWithType(singleNumber(3, 2, "3.14", "3.14")) '1'.assertIsValidWithType(singleInt(1, "1", "1")) - 'empty'.assertIsValidWithType(emptyNothing) } @Test @@ -82,12 +79,6 @@ class RosettaTypingTest { ifthen.assertHasType(createListType(UNCONSTRAINED_INT, 2, 4)) elsethen.assertHasType(singleUnconstrainedNumber) ]]; - - model.elements.get(1) as Function => [operations.head.expression as MapOperation => [ - function.body as ArithmeticOperation => [ - left.assertHasType(singleInt(1, "1", "3")) - ] - ]]; } // TODO: test auxiliary functions @@ -158,31 +149,6 @@ class RosettaTypingTest { '1 = True' .parseExpression .assertError(null, "Types `int` and `boolean` are not comparable.") - 'empty = True' - .parseExpression - .assertError(null, "Cannot compare an empty value to a single value, as they cannot be of the same length. Perhaps you forgot to write `all` or `any` in front of the operator?") - '[1, 2] = [3, 4, 5]' - .parseExpression - .assertError(null, "Cannot compare a list with 2 items to a list with 3 items, as they cannot be of the same length.") - '[1, 2] <> [True, False, False]' - .parseExpression - .assertError(null, "Types `int` and `boolean` are not comparable.") - - '[1, 2] all = empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - 'empty any = empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - '[1, 2] all = [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - '5 any <> [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead. Perhaps you meant to swap the left and right operands?") - '[3.0] any <> 5' - .parseExpression - .assertError(null, "The cardinality operator `any` is redundant when comparing two single values.") } // TODO: test arithmetic and comparisons with dates/times/etc @@ -201,19 +167,10 @@ class RosettaTypingTest { '3.0 * 4'.assertIsValidWithType(singleNumber(Optional.empty, Optional.of(1), Optional.of(new BigDecimal("12")), Optional.of(new BigDecimal("12")), Optional.empty)) '3 / 4'.assertIsValidWithType(singleUnconstrainedNumber) - - // Test loosened version - '(if False then 2 else [3, 4]) + 5'.assertIsValidWithType(singleInt(Optional.empty, Optional.of(BigInteger.valueOf(7)), Optional.of(BigInteger.valueOf(9)))) } @Test def void testArithemticOperationTypeChecking() { - '[1, 2] + 3' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - 'empty - 3' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") '1.5 * False' .parseExpression .assertError(null, "Expected type `number`, but got `boolean` instead.") @@ -240,68 +197,15 @@ class RosettaTypingTest { @Test def void testComparisonOperationTypeChecking() { // TODO: support date, zonedDateTime and `time`? - '[1, 2] < 3' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - 'empty > 3' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") '1.5 <= False' .parseExpression .assertError(null, "Expected type `number`, but got `boolean` instead.") - - '[1, 2] all >= empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - 'empty any < empty' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - '[1, 2] all > [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - '5 any <= [1, 2]' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead. Perhaps you meant to swap the left and right operands?") + '5 all >= 1' .parseExpression .assertError(null, "The cardinality operator `all` is redundant when comparing two single values.") } - @Test - def void testConditionalExpressionTypeInference() { - 'if True then [1, 2] else [3.0, 4.0, 5.0, 6.0]'.assertIsValidWithType(createListType(constrainedNumber(2, 1, "1", "6"), 2, 4)); - } - - @Test - def void testConditionalExpressionTypeChecking() { - 'if [True, False] then 1 else 2' - .parseExpression - .assertError(null, "Expected a single value, but got a list with 2 items instead.") - 'if empty then 1 else 2' - .parseExpression - .assertError(null, "Expected a single value, but got an empty value instead.") - 'if True then 1 else False' - .parseExpression - .assertError(null, "Types `int` and `boolean` do not have a common supertype.") - 'if True then [1, 2, 3] else [False, True]' - .parseExpression - .assertError(null, "Types `int` and `boolean` do not have a common supertype.") - } - - @Test - def void testListLiteralTypeInference() { - '[]'.assertIsValidWithType(emptyNothing); - '[2, 4.5, 7, -3.14]'.assertIsValidWithType(createListType(constrainedNumber(3, 2, "-3.14", "7"), 4, 4)); - '[2, [1, 2], -3.14]'.assertIsValidWithType(createListType(constrainedNumber(3, 2, "-3.14", "2"), 4, 4)); - } - - @Test - def void testListLiteralTypeChecking() { - '[1, True]' - .parseExpression - .assertError(null, "Elements do not have a common supertype: `int`, `boolean`.") - } - @Test def void testFunctionCallTypeInference() { val model = ''' @@ -341,26 +245,10 @@ class RosettaTypingTest { output: result int (1..1) set result: SomeFunc(1, [False, True], True) - - func TestParamType: - output: result int (1..1) - set result: - SomeFunc(1, [2, 3]) - - func TestParamCardinality: - output: result int (1..1) - set result: - SomeFunc(1, [False, True, False, False, True]) '''.parseRosetta val expr1 = (model.elements.get(1) as Function).operations.head.expression; expr1.assertError(null, "Expected 2 arguments, but got 3 instead."); - - val expr2 = (model.elements.get(2) as Function).operations.head.expression; - expr2.assertError(null, "Expected type `boolean`, but got `int` instead."); - - val expr3 = (model.elements.get(3) as Function).operations.head.expression; - expr3.assertError(null, "Expected a list with 2 to 4 items, but got a list with 5 items instead."); } @Test @@ -461,15 +349,9 @@ class RosettaTypingTest { @Test def void testExistsTypeChecking() { - 'empty exists' - .parseExpression - .assertError(null, "Expected an optional value, but got an empty value instead.") '42 exists' .parseExpression .assertError(null, "Expected an optional value, but got a single value instead.") - '(if True then 42 else [1, 2, 3, 4, 5]) exists' - .parseExpression - .assertError(null, "Expected an optional value, but got a list with 1 to 5 items instead.") } @Test @@ -480,15 +362,9 @@ class RosettaTypingTest { @Test def void testAbsentTypeChecking() { - 'empty is absent' - .parseExpression - .assertError(null, "Expected an optional value, but got an empty value instead.") '42 is absent' .parseExpression .assertError(null, "Expected an optional value, but got a single value instead.") - '(if True then 42 else [1, 2, 3, 4, 5]) is absent' - .parseExpression - .assertError(null, "Expected an optional value, but got a list with 1 to 5 items instead.") } @Test @@ -592,27 +468,11 @@ class RosettaTypingTest { ] } - @Test - def void testOnlyElementTypeInference() { - '(if True then 0 else [1, 2]) only-element'.assertIsValidWithType(createListType(constrainedInt(1, "0", "2"), 0, 1)); - '(if True then empty else [True, False]) only-element'.assertIsValidWithType(createListType(BOOLEAN, 0, 1)); - '(if True then 0 else [1, 2, 3, 42.0]) only-element'.assertIsValidWithType(createListType(constrainedNumber(3, 1, "0", "42.0"), 0, 1)); - } - @Test def void testOnlyElementTypeChecking() { - 'empty only-element' - .parseExpression - .assertWarning(null, "Expected a list with 1 to 2 items, but got an empty value instead.") '42 only-element' .parseExpression .assertWarning(null, "Expected a list with 1 to 2 items, but got a single value instead.") - '[1, 2] only-element' - .parseExpression - .assertWarning(null, "Expected a list with 1 to 2 items, but got a list with 2 items instead.") - '(if True then empty else 42) only-element' - .parseExpression - .assertWarning(null, "Expected a list with 1 to 2 items, but got an optional value instead.") } @Test diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend new file mode 100644 index 000000000..fcdf032bb --- /dev/null +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/types/SubtypeRelationTest.xtend @@ -0,0 +1,72 @@ +package com.regnosys.rosetta.types + +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.junit.jupiter.api.^extension.ExtendWith +import com.regnosys.rosetta.rosetta.simple.Data +import com.regnosys.rosetta.tests.RosettaInjectorProvider +import org.eclipse.xtext.testing.InjectWith +import javax.inject.Inject +import com.regnosys.rosetta.tests.util.ModelHelper + +import static org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import com.regnosys.rosetta.rosetta.RosettaModel +import com.regnosys.rosetta.rosetta.RosettaNamed +import com.regnosys.rosetta.rosetta.RosettaEnumeration + +@ExtendWith(InjectionExtension) +@InjectWith(RosettaInjectorProvider) +class SubtypeRelationTest { + @Inject extension SubtypeRelation + @Inject extension ModelHelper + @Inject extension RObjectFactory + + private def Data getData(RosettaModel model, String name) { + return model.elements.filter(RosettaNamed).findFirst[it.name == name] as Data + } + private def RosettaEnumeration getEnum(RosettaModel model, String name) { + return model.elements.filter(RosettaNamed).findFirst[it.name == name] as RosettaEnumeration + } + + @Test + def testExtendedTypeIsSubtype() { + val model = ''' + type A: + type B extends A: + '''.parseRosettaWithNoIssues + + val a = model.getData('A').buildRDataType + val b = model.getData('B').buildRDataType + + assertTrue(b.isSubtypeOf(a)) + } + + @Test + def testJoinTypeHierarchy() { + val model = ''' + type A: + type B extends A: + type C extends A: + type D extends C: + '''.parseRosettaWithNoIssues + + val a = model.getData('A').buildRDataType + val b = model.getData('B').buildRDataType + val d = model.getData('D').buildRDataType + + assertEquals(a, join(b, d)) + } + + @Test + def testExtendedEnumIsSupertype() { + val model = ''' + enum A: + enum B extends A: + '''.parseRosettaWithNoIssues + + val a = model.getEnum('A').buildREnumType + val b = model.getEnum('B').buildREnumType + + assertTrue(a.isSubtypeOf(b)) + } +} diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend new file mode 100644 index 000000000..95825f4fa --- /dev/null +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/EnumValidatorTest.xtend @@ -0,0 +1,119 @@ +package com.regnosys.rosetta.validation + +import com.regnosys.rosetta.tests.util.ModelHelper +import org.eclipse.xtext.testing.InjectWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.eclipse.xtext.testing.validation.ValidationTestHelper +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.^extension.ExtendWith + +import static com.regnosys.rosetta.rosetta.RosettaPackage.Literals.* +import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.* +import javax.inject.Inject + +@ExtendWith(InjectionExtension) +@InjectWith(MyRosettaInjectorProvider) +class EnumValidatorTest implements RosettaIssueCodes { + + @Inject extension ValidationTestHelper + @Inject extension ModelHelper + + @Test + def void testDuplicateEnumValue() { + val model = ''' + enum Foo: + BAR + BAZ + BAR + '''.parseRosetta + model.assertError(ROSETTA_ENUM_VALUE, null, "Duplicate enum value 'BAR'") + } + + @Test + def void testCannotHaveEnumValuesWithSameNameAsParentValue() { + val model = ''' + enum A: + MY_VALUE + + enum B extends A: + MY_VALUE + '''.parseRosetta + + model + .assertError(ROSETTA_ENUM_VALUE, null, "Duplicate enum value 'MY_VALUE'") + } + + @Test + def void testCannotExtendEnumsWithEnumValuesWithSameName() { + val model = ''' + enum A: + MY_VALUE + + enum B: + MY_VALUE + + enum C extends A, B: + '''.parseRosetta + + model + .assertError(ROSETTA_ENUMERATION, null, "The value 'MY_VALUE' of enum B overlaps with the value 'MY_VALUE' of enum A") + } + + @Test + def void testParentEnumsCannotHaveACycle() { + val model = ''' + enum A extends D: + + enum B extends A: + + enum C extends A: + + enum D extends B, C: + '''.parseRosetta + + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: A extends D extends B extends A") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: B extends A extends D extends B") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: C extends A extends D extends C") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: D extends B extends A extends D") + model.assertError(ROSETTA_ENUMERATION, null, "Cyclic extension: D extends C extends A extends D") + } + + @Test + def void supportDeprecatedAnnotationOnEnum() { + val model = ''' + enum TestEnumDeprecated: + [deprecated] + ONE + TWO + + func Foo: + output: + result TestEnumDeprecated (1..1) + + set result: + TestEnumDeprecated -> ONE + '''.parseRosetta + + model.assertWarning(TYPE_CALL, null, "TestEnumDeprecated is deprecated") + model.assertWarning(ROSETTA_SYMBOL_REFERENCE, null, "TestEnumDeprecated is deprecated") + } + + @Test + def void supportDeprecatedAnnotationOnEnumValue() { + val model = ''' + enum TestEnumDeprecated: + ONE + [deprecated] + TWO + + func Foo: + output: + result TestEnumDeprecated (1..1) + + set result: + TestEnumDeprecated -> ONE + '''.parseRosetta + + model.assertWarning(ROSETTA_FEATURE_CALL, null, "ONE is deprecated") + } +} \ No newline at end of file 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..57489bad9 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 @@ -31,6 +31,21 @@ class RosettaValidatorTest implements RosettaIssueCodes { @Inject extension ModelHelper @Inject extension ExpressionParser + @Test + def void testCannotConvertEnumToNonParentEnum() { + val model = ''' + enum A: + VALUE_A + + enum B extends A: + VALUE_B + '''.parseRosettaWithNoIssues + + "a to-enum B" + .parseExpression(#[model], #["a A (1..1)"]) + .assertError(TO_ENUM_OPERATION, null, "A does not extend B.") + } + @Test def void testCannotAccessUncommonMetaFeatureOfDeepFeatureCall() { val model = ''' @@ -1291,7 +1306,7 @@ class RosettaValidatorTest implements RosettaIssueCodes { if id = True then id < 1 '''.parseRosetta - model.assertError(ROSETTA_CONDITIONAL_EXPRESSION, TYPE_ERROR, "Incompatible types: cannot use operator '<' with boolean and int.") + model.assertError(COMPARISON_OPERATION, null, "Incompatible types: cannot use operator '<' with boolean and int.") } @Test @@ -1480,15 +1495,6 @@ class RosettaValidatorTest implements RosettaIssueCodes { '''.parseRosetta model.assertError(ATTRIBUTE, DUPLICATE_ATTRIBUTE, "Overriding attribute 'i' with type string must match the type of the attribute it overrides (int)") } - - @Test - def void testDuplicateEnumLiteral() { - val model = ''' - enum Foo: - BAR BAZ BAR - '''.parseRosetta - model.assertError(ROSETTA_ENUM_VALUE, DUPLICATE_ENUM_VALUE, 'Duplicate enum value') - } @Test def void testDuplicateType() { @@ -2773,16 +2779,16 @@ class RosettaValidatorTest implements RosettaIssueCodes { type Foo: x1 boolean (1..1) x2 boolean (1..1) - x3 number (1..1) + x3 number (0..1) x4 number (1..1) x5 int (1..1) - x6 string (1..1) + x6 string (0..1) '''.parseRosetta - model.assertError(ROSETTA_BINARY_OPERATION, TYPE_ERROR, "Left hand side of 'and' expression must be boolean") + model.assertError(ROSETTA_BINARY_OPERATION, null, "Left hand side of 'and' expression must be boolean") } @Test - def void shouldNotGenerateTypeErrorForExpressionInBrackets3() { + def void shouldGenerateTypeErrorForExpressionInBrackets3() { val model = ''' func FuncFoo: inputs: @@ -2797,7 +2803,7 @@ class RosettaValidatorTest implements RosettaIssueCodes { x3 number (1..1) x4 number (1..1) '''.parseRosetta - model.assertError(ROSETTA_EXISTS_EXPRESSION, TYPE_ERROR, "Left hand side of 'and' expression must be boolean") + model.assertError(LOGICAL_OPERATION, null, "Left hand side of 'and' expression must be boolean") } @Test