Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum extensions #833

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
63 changes: 57 additions & 6 deletions docs/rune-modelling-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 5 additions & 7 deletions rosetta-lang/model/Rosetta.xcore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,10 +41,6 @@ interface RosettaNamed {

interface RosettaTyped {
contains TypeCall typeCall

derived boolean isTypeInferred get {
return typeCall === null
}
}

interface RosettaFeature extends RosettaNamed {
Expand Down Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext
Original file line number Diff line number Diff line change
Expand Up @@ -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')')
Expand Down Expand Up @@ -282,8 +281,8 @@ RosettaMetaType:
;

RosettaEnumValue:
RosettaNamed ('displayName' display=STRING)? RosettaDefinable? References*
(enumSynonyms += RosettaEnumSynonym)*
RosettaNamed ('displayName' display=STRING)? RosettaDefinable?
(References|Annotations|enumSynonyms+=RosettaEnumSynonym)*
;

RosettaCardinality:
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -92,19 +92,17 @@ class RosettaEcoreUtil {
return result.values();
}

@Deprecated // TODO: move to REnumType, similar to RDataType
@Deprecated // Use REnumType#getAllParents instead
def Set<RosettaEnumeration> getAllSuperEnumerations(RosettaEnumeration e) {
doGetSuperEnumerations(e, newLinkedHashSet)
}

@Deprecated
private def Set<RosettaEnumeration> doGetSuperEnumerations(RosettaEnumeration e, Set<RosettaEnumeration> 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class RosettaGenerator implements IGenerator2 {
]
}
RosettaEnumeration: {
enumGenerator.generate(packages, fsa, elem, version)
enumGenerator.generate(packages, fsa, elem.buildREnumType, version)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,98 @@ 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
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);
}

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() {
Expand All @@ -93,6 +111,28 @@ class EnumGenerator {
buildClass(root, classBody, scope)
}

private def StringConcatenationClient ancestorConversions(RJavaEnum javaEnum, RJavaEnum currentParent, List<RJavaEnum> ancestors, Set<RJavaEnum> 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»
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading