From b0676e5cfd103240beffe562582b342be53e3084 Mon Sep 17 00:00:00 2001 From: David Al-Kanani Date: Fri, 13 Sep 2024 15:01:41 +0100 Subject: [PATCH] 819 switch operation (#829) * save * build switch without enum completeness awareness * implement switch with enum or literal condition only * fix validation * add validation test for mismatched switch conditions * validate missing enum values * add validation test for missing enum values with default present * fix test * fix switch generate * update documentation with switch and update operator precendance * update docs with review comments * Rename CaseStatement to SwitchCase * inline default switch case statement * renames SwitchOperation.values to SwitchOperation.cases * Handle switch statement with only default and no cases * fix typo * fix spacing * fix spacing * fix typo * neaten up recursion logic * take switch return type from context * Show missing enum values on validation * Prevent multi cardinality inputs * Only allow basic and enum type switch inputs * Improve invalid switch argument message * Rename switch conditions to guards * Test for handling multi cardinality results * fix formatting * fix whitespace diffs --- docs/rune-modelling-component.md | 39 ++- .../ide/contentassist/ContentAssistTest.xtend | 4 + .../AbstractRosettaLanguageServerTest.xtend | 43 ++-- rosetta-lang/model/RosettaExpression.xcore | 14 ++ .../java/com/regnosys/rosetta/Rosetta.xtext | 11 +- .../java/expression/ExpressionGenerator.xtend | 121 +++++++--- .../interpreter/RosettaInterpreter.java | 7 + .../scoping/RosettaScopeProvider.xtend | 44 +++- .../rosetta/types/CardinalityProvider.xtend | 13 + .../rosetta/types/RosettaTypeProvider.xtend | 12 +- .../utils/RosettaExpressionSwitch.java | 6 +- .../validation/RosettaSimpleValidator.xtend | 62 ++++- .../java/function/FunctionGeneratorTest.xtend | 174 ++++++++++++++ .../validation/RosettaValidatorTest.xtend | 225 ++++++++++++++++++ 14 files changed, 697 insertions(+), 78 deletions(-) diff --git a/docs/rune-modelling-component.md b/docs/rune-modelling-component.md index afce518a1..3c43475b8 100644 --- a/docs/rune-modelling-component.md +++ b/docs/rune-modelling-component.md @@ -912,6 +912,31 @@ Rune supports basic arithmetic operators The `default` operator takes two values of matching type. If the value to the left of the default is empty then the value of to the right of the default will be returned. Note that the type and cardinality of both sides of the operator must match for the syntax to be valid. +### Switch Operator + +The `switch` operator takes as its left hand input an argument on which to perform case analysis. The right side of the operator takes a set of case statements which define a return value for the expression when matching that case to the input. + +```Haskell + "valueB" switch + "valueA" then "resultA" + "valueB" then "resultB" + default "resultC" +``` + +The `switch` operator can also operate over enumerations and in the case where all enumeration values are not provided as case statements then a syntax validation error will occur until either all enumeration values or a default value is provided. + +```Haskell + "aCondition" switch + "aCondition" then SomeEnum -> A, + "bCondition" then SomeEnum -> B, + "cCondition" then SomeEnum -> C, + default SomeEnum -> D +``` + +{{< notice info "Note" >}} +The `default` case is optional, in case when there is no match then the `empty` value is returned. +{{< /notice >}} + #### Operator Precedence Expressions are evaluated in Rune in the following order, from first to last - see [Operator Precedence](https://en.wikipedia.org/wiki/Order_of_operations)). @@ -919,13 +944,13 @@ Expressions are evaluated in Rune in the following order, from first to last - s 1. RosettaPathExpressions - e.g. `Lineage -> executionReference` 1. Brackets - e.g. `(1+2)` 1. if-then-else - e.g. `if (1=2) then 3` -1. only-element - e.g. `Lineage -> executionReference only-element` -1. count - e.g. `Lineage -> executionReference count` -1. Multiplicative operators `*`, `/` - e.g. `3*4` -1. Additive operators `+`, `-` - e.g. `3-4` -1. Comparison operators `>=`, `<=`, `>`, `<` - e.g. `3>4` -1. Existence operators `exists`,`is absent`, `contains`, `disjoint` - e.g. `Lineage -> executionReference exists` -1. Default operator `a default b` +1. Constructor expressions - e.g `MyType {attr1: "value 1"}` +1. Unary operators `->`, `->>`, `exists`, `is absent`, `only-element`, `flatten`, `distinct`, `reverse`, `first`, `last`, `sum`, `one-of`, `choice`, `to-string`, `to-number`, `to-int`, `to-time`, `to-enum`, `to-date`, `to-date-time`, `to-zoned-date-time`, `switch`, `sort`, `min`, `max`, `reduce`, `filter`, `map`, `extract` +1. Binary operators `contains`, `disjoint`, `default`, `join` +1. Multiplicative operators `*`, `/` +1. Additive operators `+`, `-` +1. Comparison operators `>=`, `<=`, `>`, `<` +1. Equality operators `=`, `<>` 1. and - e.g. `5>6 and true` 1. or - e.g. `5>6 or true` diff --git a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/contentassist/ContentAssistTest.xtend b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/contentassist/ContentAssistTest.xtend index dcf11ee88..5467dd7aa 100644 --- a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/contentassist/ContentAssistTest.xtend +++ b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/contentassist/ContentAssistTest.xtend @@ -59,6 +59,7 @@ class ContentAssistTest extends AbstractRosettaLanguageServerTest { single -> single [[9, 14] .. [9, 14]] sort -> sort [[9, 14] .. [9, 14]] sum -> sum [[9, 14] .. [9, 14]] + switch -> switch [[9, 14] .. [9, 14]] then -> then [[9, 14] .. [9, 14]] to-date -> to-date [[9, 14] .. [9, 14]] to-date-time -> to-date-time [[9, 14] .. [9, 14]] @@ -154,6 +155,7 @@ class ContentAssistTest extends AbstractRosettaLanguageServerTest { single -> single [[7, 27] .. [7, 27]] sort -> sort [[7, 27] .. [7, 27]] sum -> sum [[7, 27] .. [7, 27]] + switch -> switch [[7, 27] .. [7, 27]] synonym -> synonym [[7, 27] .. [7, 27]] then -> then [[7, 27] .. [7, 27]] to-date -> to-date [[7, 27] .. [7, 27]] @@ -231,6 +233,7 @@ class ContentAssistTest extends AbstractRosettaLanguageServerTest { single -> single [[6, 25] .. [6, 25]] sort -> sort [[6, 25] .. [6, 25]] sum -> sum [[6, 25] .. [6, 25]] + switch -> switch [[6, 25] .. [6, 25]] then -> then [[6, 25] .. [6, 25]] to-date -> to-date [[6, 25] .. [6, 25]] to-date-time -> to-date-time [[6, 25] .. [6, 25]] @@ -425,6 +428,7 @@ class ContentAssistTest extends AbstractRosettaLanguageServerTest { single -> single [[19, 2] .. [19, 2]] sort -> sort [[19, 2] .. [19, 2]] sum -> sum [[19, 2] .. [19, 2]] + switch -> switch [[19, 2] .. [19, 2]] to-date -> to-date [[19, 2] .. [19, 2]] to-date-time -> to-date-time [[19, 2] .. [19, 2]] to-enum -> to-enum [[19, 2] .. [19, 2]] diff --git a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend index 481f446ee..5a33929ae 100644 --- a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend +++ b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend @@ -1,35 +1,30 @@ package com.regnosys.rosetta.ide.tests -import org.eclipse.xtext.testing.AbstractLanguageServerTest -import org.eclipse.xtend.lib.annotations.Accessors -import org.eclipse.xtext.testing.TextDocumentPositionConfiguration -import org.eclipse.lsp4j.InlayHint +import com.google.inject.Module +import com.regnosys.rosetta.RosettaStandaloneSetup +import com.regnosys.rosetta.builtin.RosettaBuiltinsService +import com.regnosys.rosetta.ide.semantictokens.SemanticToken +import com.regnosys.rosetta.ide.server.RosettaLanguageServerImpl +import com.regnosys.rosetta.ide.server.RosettaServerModule +import com.regnosys.rosetta.ide.util.RangeUtils +import java.nio.charset.StandardCharsets +import java.util.HashMap import java.util.List +import java.util.stream.Collectors +import javax.inject.Inject +import org.eclipse.lsp4j.DiagnosticSeverity +import org.eclipse.lsp4j.InlayHint import org.eclipse.lsp4j.InlayHintParams -import org.eclipse.lsp4j.TextDocumentIdentifier -import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.SemanticTokensParams -import javax.inject.Inject -import com.regnosys.rosetta.ide.semantictokens.SemanticToken -import com.regnosys.rosetta.ide.server.RosettaLanguageServerImpl -import org.eclipse.xtext.testing.TextDocumentConfiguration +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.xtend.lib.annotations.Accessors +import org.eclipse.xtext.testing.AbstractLanguageServerTest import org.eclipse.xtext.testing.FileInfo -import java.nio.charset.StandardCharsets -import com.regnosys.rosetta.ide.util.RangeUtils -import java.util.stream.Collectors +import org.eclipse.xtext.testing.TextDocumentConfiguration +import org.eclipse.xtext.testing.TextDocumentPositionConfiguration import org.junit.jupiter.api.Assertions -import com.regnosys.rosetta.builtin.RosettaBuiltinsService -import com.regnosys.rosetta.ide.server.RosettaServerModule -import com.google.inject.Module -import org.eclipse.lsp4j.DiagnosticSeverity -import com.regnosys.rosetta.RosettaStandaloneSetup -import java.util.HashMap -import org.eclipse.emf.ecore.EPackage -import org.eclipse.emf.ecore.EValidator -import com.regnosys.rosetta.rosetta.RosettaPackage -import com.regnosys.rosetta.rosetta.simple.SimplePackage -import com.regnosys.rosetta.rosetta.expression.ExpressionPackage /** * TODO: contribute to Xtext. diff --git a/rosetta-lang/model/RosettaExpression.xcore b/rosetta-lang/model/RosettaExpression.xcore index c84d5fb3c..e2382566b 100644 --- a/rosetta-lang/model/RosettaExpression.xcore +++ b/rosetta-lang/model/RosettaExpression.xcore @@ -16,6 +16,7 @@ import com.regnosys.rosetta.rosetta.RosettaMapTestExpression import com.regnosys.rosetta.rosetta.RosettaTyped import com.regnosys.rosetta.rosetta.simple.Attribute import org.eclipse.emf.common.util.BasicEList +import com.regnosys.rosetta.rosetta.RosettaEnumValue interface RosettaExpression { @@ -406,3 +407,16 @@ class MinOperation extends ComparingFunctionalOperation, ListOperation { class MaxOperation extends ComparingFunctionalOperation, ListOperation { } + +class SwitchOperation extends RosettaUnaryOperation { + contains SwitchCase[] cases opposite switchOperation + contains RosettaExpression ^default +} + +class SwitchCase { + container SwitchOperation switchOperation opposite cases + contains RosettaLiteral literalGuard + refers RosettaEnumValue enumGuard + contains RosettaExpression expression +} + 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 143337cbf..b1eaa372b 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -630,15 +630,19 @@ RosettaCalcBinary returns RosettaExpression: ) ; - /** - * List operations + * Unary operations */ enum ExistsModifier: SINGLE='single' | MULTIPLE='multiple' ; +SwitchCase: + (literalGuard=RosettaLiteral | enumGuard=[RosettaEnumValue|ValidID]) 'then' expression=RosettaCalcExpression + ; + + UnaryOperation returns RosettaExpression: RosettaCalcPrimary ( @@ -665,6 +669,7 @@ UnaryOperation returns RosettaExpression: |({ToDateOperation.argument=current} operator='to-date') |({ToDateTimeOperation.argument=current} operator='to-date-time') |({ToZonedDateTimeOperation.argument=current} operator='to-zoned-date-time') + |({SwitchOperation.argument=current} operator='switch' ((cases+=SwitchCase =>(',' cases+=SwitchCase)* =>(',' 'default' default=RosettaCalcExpression)?) | ('default' default=RosettaCalcExpression))) ) | =>( ({SortOperation.argument=current} operator='sort') @@ -701,6 +706,7 @@ UnaryOperation returns RosettaExpression: |({ToDateOperation} operator='to-date') |({ToDateTimeOperation} operator='to-date-time') |({ToZonedDateTimeOperation} operator='to-zoned-date-time') + |({SwitchOperation} operator='switch' ((cases+=SwitchCase =>(',' cases+=SwitchCase)* =>(',' 'default' default=RosettaCalcExpression)?) | ('default' default=RosettaCalcExpression))) ) | ( ({SortOperation} operator='sort') @@ -738,6 +744,7 @@ UnaryOperation returns RosettaExpression: |({ToDateOperation.argument=current} operator='to-date') |({ToDateTimeOperation.argument=current} operator='to-date-time') |({ToZonedDateTimeOperation.argument=current} operator='to-zoned-date-time') + |({SwitchOperation.argument=current} operator='switch' ((cases+=SwitchCase =>(',' cases+=SwitchCase)* =>(',' 'default' default=RosettaCalcExpression)?) | ('default' default=RosettaCalcExpression))) ) | =>( ({SortOperation.argument=current} operator='sort') 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..908951d24 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 @@ -1,9 +1,17 @@ package com.regnosys.rosetta.generator.java.expression +import com.regnosys.rosetta.RosettaEcoreUtil import com.regnosys.rosetta.generator.java.JavaIdentifierRepresentationService import com.regnosys.rosetta.generator.java.JavaScope +import com.regnosys.rosetta.generator.java.statement.builder.JavaConditionalExpression +import com.regnosys.rosetta.generator.java.statement.builder.JavaExpression +import com.regnosys.rosetta.generator.java.statement.builder.JavaIfThenElseBuilder +import com.regnosys.rosetta.generator.java.statement.builder.JavaStatementBuilder +import com.regnosys.rosetta.generator.java.statement.builder.JavaVariable import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator +import com.regnosys.rosetta.generator.java.types.JavaTypeUtil import com.regnosys.rosetta.generator.java.util.ImportManagerExtension +import com.regnosys.rosetta.generator.java.util.RecordJavaUtil import com.regnosys.rosetta.generator.util.RosettaFunctionExtensions import com.regnosys.rosetta.rosetta.RosettaCallableWithArgs import com.regnosys.rosetta.rosetta.RosettaEnumValue @@ -12,12 +20,14 @@ import com.regnosys.rosetta.rosetta.RosettaExternalFunction import com.regnosys.rosetta.rosetta.RosettaFeature import com.regnosys.rosetta.rosetta.RosettaMetaType import com.regnosys.rosetta.rosetta.RosettaRecordFeature +import com.regnosys.rosetta.rosetta.RosettaRule import com.regnosys.rosetta.rosetta.expression.ArithmeticOperation import com.regnosys.rosetta.rosetta.expression.AsKeyOperation import com.regnosys.rosetta.rosetta.expression.CardinalityModifier import com.regnosys.rosetta.rosetta.expression.ChoiceOperation import com.regnosys.rosetta.rosetta.expression.ClosureParameter import com.regnosys.rosetta.rosetta.expression.ComparisonOperation +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.ExistsModifier @@ -41,8 +51,10 @@ import com.regnosys.rosetta.rosetta.expression.RosettaAbsentExpression import com.regnosys.rosetta.rosetta.expression.RosettaBinaryOperation 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 @@ -59,23 +71,32 @@ 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.SwitchOperation 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.Attribute import com.regnosys.rosetta.rosetta.simple.Data import com.regnosys.rosetta.rosetta.simple.Function import com.regnosys.rosetta.rosetta.simple.ShortcutDeclaration import com.regnosys.rosetta.types.CardinalityProvider +import com.regnosys.rosetta.types.RAttribute import com.regnosys.rosetta.types.RDataType import com.regnosys.rosetta.types.REnumType +import com.regnosys.rosetta.types.RFunction +import com.regnosys.rosetta.types.RObjectFactory +import com.regnosys.rosetta.types.RShortcut import com.regnosys.rosetta.types.RType import com.regnosys.rosetta.types.RosettaOperators import com.regnosys.rosetta.types.RosettaTypeProvider import com.regnosys.rosetta.types.TypeSystem +import com.regnosys.rosetta.types.builtin.RRecordType import com.regnosys.rosetta.utils.ExpressionHelper import com.regnosys.rosetta.utils.ImplicitVariableUtil import com.regnosys.rosetta.utils.RosettaExpressionSwitch @@ -84,51 +105,32 @@ import com.rosetta.model.lib.expression.ExpressionOperators import com.rosetta.model.lib.expression.MapperMaths import com.rosetta.model.lib.mapper.MapperC import com.rosetta.model.lib.mapper.MapperS +import com.rosetta.model.lib.records.Date import com.rosetta.model.lib.validation.ChoiceRuleValidationMethod +import com.rosetta.util.types.JavaGenericTypeDeclaration +import com.rosetta.util.types.JavaPrimitiveType +import com.rosetta.util.types.JavaType import java.math.BigDecimal +import java.math.BigInteger +import java.time.LocalDateTime import java.time.LocalTime +import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException import java.util.Arrays +import java.util.Collection import java.util.List +import java.util.Objects import java.util.Optional +import java.util.stream.Collectors +import javax.inject.Inject import org.apache.commons.text.StringEscapeUtils 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 com.regnosys.rosetta.types.RObjectFactory -import javax.inject.Inject -import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression -import com.regnosys.rosetta.generator.java.util.RecordJavaUtil -import com.regnosys.rosetta.types.builtin.RRecordType -import java.util.stream.Collectors -import com.regnosys.rosetta.rosetta.RosettaRule -import com.rosetta.util.types.JavaType -import com.rosetta.util.types.JavaPrimitiveType -import com.regnosys.rosetta.types.RShortcut -import com.regnosys.rosetta.types.RFunction -import com.regnosys.rosetta.generator.java.types.JavaTypeUtil -import java.math.BigInteger -import com.regnosys.rosetta.generator.java.statement.builder.JavaStatementBuilder -import com.regnosys.rosetta.generator.java.statement.builder.JavaExpression -import com.regnosys.rosetta.generator.java.statement.builder.JavaVariable -import com.regnosys.rosetta.generator.java.statement.builder.JavaIfThenElseBuilder -import com.rosetta.util.types.JavaGenericTypeDeclaration -import com.regnosys.rosetta.rosetta.expression.ToDateOperation -import com.regnosys.rosetta.generator.java.expression.ExpressionGenerator.Context -import com.regnosys.rosetta.rosetta.expression.ToDateTimeOperation -import com.regnosys.rosetta.rosetta.expression.ToZonedDateTimeOperation -import com.rosetta.model.lib.records.Date -import java.time.LocalDateTime -import java.time.ZonedDateTime -import com.regnosys.rosetta.rosetta.expression.RosettaDeepFeatureCall -import com.regnosys.rosetta.rosetta.expression.DefaultOperation -import com.regnosys.rosetta.generator.java.statement.builder.JavaConditionalExpression -import com.regnosys.rosetta.types.RAttribute -import java.util.Collection -import com.regnosys.rosetta.RosettaEcoreUtil +import com.regnosys.rosetta.rosetta.expression.SwitchCase class ExpressionGenerator extends RosettaExpressionSwitch { @@ -153,7 +155,7 @@ class ExpressionGenerator extends RosettaExpressionSwitch { static Logger LOGGER = LoggerFactory.getLogger(CardinalityProvider) @@ -532,4 +533,16 @@ class CardinalityProvider extends RosettaExpressionSwitch { override protected caseToZonedDateTimeOperation(ToZonedDateTimeOperation expr, Boolean breakOnClosureParameter) { false } + + override protected caseSwitchOperation(SwitchOperation expr, Boolean breakOnClosureParameter) { + if (expr.^default.isMulti) { + return true + } + for (switchCase : expr.cases) { + if (switchCase.expression.isMulti) { + return true + } + } + false + } } \ No newline at end of file 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..99cc41b9d 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 @@ -78,6 +78,7 @@ import com.regnosys.rosetta.rosetta.expression.RosettaDeepFeatureCall import com.regnosys.rosetta.rosetta.expression.DefaultOperation import com.regnosys.rosetta.cache.IRequestScopedCache import com.regnosys.rosetta.rosetta.TypeParameter +import com.regnosys.rosetta.rosetta.expression.SwitchOperation import com.regnosys.rosetta.rosetta.simple.AssignPathRoot import com.regnosys.rosetta.rosetta.RosettaCallableWithArgs import com.regnosys.rosetta.RosettaEcoreUtil @@ -95,7 +96,7 @@ class RosettaTypeProvider extends RosettaExpressionSwitch findFeaturesOfImplicitVariable(EObject context) { return extensions.allFeatures(typeOfImplicitVariable(context), context) } - private def RType safeRType(RosettaSymbol symbol, EObject context, Map cycleTracker) { + private def RType safeRType(RosettaSymbol symbol, EObject context,Map cycleTracker) { if (!extensions.isResolved(symbol)) { return NOTHING } @@ -525,4 +525,10 @@ class RosettaTypeProvider extends RosettaExpressionSwitch context) { + expr.cases + .map[it.expression.RType] + .join + } + } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaExpressionSwitch.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaExpressionSwitch.java index b2b7e0e18..ced1f4b7d 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaExpressionSwitch.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/RosettaExpressionSwitch.java @@ -63,6 +63,7 @@ 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.SwitchOperation; import com.regnosys.rosetta.rosetta.expression.ThenOperation; import com.regnosys.rosetta.rosetta.expression.ToDateOperation; import com.regnosys.rosetta.rosetta.expression.ToDateTimeOperation; @@ -234,7 +235,9 @@ protected Return doSwitch(RosettaUnaryOperation expr, Context context) { return caseToDateTimeOperation((ToDateTimeOperation)expr, context); } else if (expr instanceof ToZonedDateTimeOperation) { return caseToZonedDateTimeOperation((ToZonedDateTimeOperation)expr, context); - } else if (expr instanceof RosettaFunctionalOperation) { + } else if (expr instanceof SwitchOperation) { + return caseSwitchOperation((SwitchOperation)expr, context); + } else if (expr instanceof RosettaFunctionalOperation) { return doSwitch((RosettaFunctionalOperation)expr, context); } throw errorMissedCase(expr); @@ -319,6 +322,7 @@ private UnsupportedOperationException errorMissedCase(RosettaExpression expr) { protected abstract Return caseToDateOperation(ToDateOperation expr, Context context); protected abstract Return caseToDateTimeOperation(ToDateTimeOperation expr, Context context); protected abstract Return caseToZonedDateTimeOperation(ToZonedDateTimeOperation expr, Context context); + protected abstract Return caseSwitchOperation(SwitchOperation expr, Context context); protected abstract Return caseFilterOperation(FilterOperation expr, Context context); protected abstract Return caseMapOperation(MapOperation expr, Context context); diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index 215238141..129bed1b8 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -2,6 +2,7 @@ package com.regnosys.rosetta.validation import com.google.common.collect.HashMultimap import com.google.common.collect.LinkedHashMultimap +import com.regnosys.rosetta.RosettaEcoreUtil import com.regnosys.rosetta.generator.util.RosettaFunctionExtensions import com.regnosys.rosetta.rosetta.ExternalAnnotationSource import com.regnosys.rosetta.rosetta.ParametrizedRosettaType @@ -56,6 +57,8 @@ import com.regnosys.rosetta.rosetta.expression.RosettaOperation import com.regnosys.rosetta.rosetta.expression.RosettaSymbolReference import com.regnosys.rosetta.rosetta.expression.RosettaUnaryOperation import com.regnosys.rosetta.rosetta.expression.SumOperation +import com.regnosys.rosetta.rosetta.expression.SwitchCase +import com.regnosys.rosetta.rosetta.expression.SwitchOperation import com.regnosys.rosetta.rosetta.expression.ThenOperation import com.regnosys.rosetta.rosetta.expression.ToStringOperation import com.regnosys.rosetta.rosetta.expression.UnaryFunctionalOperation @@ -73,13 +76,16 @@ import com.regnosys.rosetta.rosetta.simple.ShortcutDeclaration import com.regnosys.rosetta.scoping.RosettaScopeProvider import com.regnosys.rosetta.services.RosettaGrammarAccess import com.regnosys.rosetta.types.CardinalityProvider +import com.regnosys.rosetta.types.RAttribute import com.regnosys.rosetta.types.RDataType import com.regnosys.rosetta.types.REnumType import com.regnosys.rosetta.types.RErrorType +import com.regnosys.rosetta.types.RObjectFactory import com.regnosys.rosetta.types.RType import com.regnosys.rosetta.types.RosettaExpectedTypeProvider import com.regnosys.rosetta.types.RosettaTypeProvider import com.regnosys.rosetta.types.TypeSystem +import com.regnosys.rosetta.types.TypeValidationUtil import com.regnosys.rosetta.types.builtin.RBasicType import com.regnosys.rosetta.types.builtin.RBuiltinTypeService import com.regnosys.rosetta.types.builtin.RRecordType @@ -111,12 +117,9 @@ import org.eclipse.xtext.validation.Check import static com.regnosys.rosetta.rosetta.RosettaPackage.Literals.* import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.* import static com.regnosys.rosetta.rosetta.simple.SimplePackage.Literals.* +import static com.regnosys.rosetta.validation.RosettaIssueCodes.* -import static extension com.regnosys.rosetta.validation.RosettaIssueCodes.* import static extension org.eclipse.emf.ecore.util.EcoreUtil.* -import com.regnosys.rosetta.types.RObjectFactory -import com.regnosys.rosetta.types.RAttribute -import com.regnosys.rosetta.RosettaEcoreUtil // TODO: split expression validator // TODO: type check type call arguments @@ -137,8 +140,59 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { @Inject extension RBuiltinTypeService @Inject extension TypeSystem @Inject extension RosettaGrammarAccess + @Inject extension TypeValidationUtil @Inject extension RObjectFactory objectFactory + @Check + def void switchInputsMustBeSingleCardinality(SwitchOperation op) { + if (op.argument.multi) { + error("Input to switch must be single cardinality", op.argument, null) + } + } + + + @Check + def void switchStatementMustProvideCaseForAllEnumValues(SwitchOperation op) { + val argumentType = op.argument.RType + if (op.^default === null && argumentType instanceof REnumType) { + val enumConditions = op.cases.map[it.enumGuard].toSet + + val enumeration = (argumentType as REnumType).EObject + val missingEnumValues = newArrayList + for (enumValue : enumeration.enumValues) { + if (!enumConditions.contains(enumValue)) { + missingEnumValues.add(enumValue) + } + } + if (!missingEnumValues.empty) { + error('''Missing the following enumeration values from switch: «missingEnumValues.map[it.name].join(", ")» . Either provide all or use default.''', op, null) + } + } + + } + + @Check + def void switchArgumentTypeMatchesCaseStatementTypes(SwitchOperation op) { + val argumentRType = op.argument.RType + for (SwitchCase caseStatement : op.cases) { + if (caseStatement.literalGuard !== null) { + val conditionType = caseStatement.literalGuard.RType + if (!conditionType.isSubtypeOf(argumentRType)) { + error('''Mismatched condition type: «argumentRType.notASubtypeMessage(conditionType)»''', caseStatement.literalGuard ?: caseStatement.enumGuard, null) + } + } + + } + } + + @Check + def void switchArgumentsAreCorrectTypes(SwitchOperation op) { + val inputType = op.argument.RType.stripFromTypeAliases + if (!(inputType instanceof RBasicType) && !(inputType instanceof REnumType)) { + error('''Type `«inputType.name»` is not a valid switch argument type, supported argument types are basic types and enumerations''', op.argument, null) + } + } + @Check def void ruleMustHaveInputTypeDeclared(RosettaRule rule) { if (rule.input === null) { 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..d7f8893bd 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,180 @@ class FunctionGeneratorTest { @Inject extension ModelHelper @Inject extension ValidationTestHelper + @Test + def void switchCaseCanReturnMultiCardinalityResult() { + val code = ''' + func SomeFunc: + + output: + result int (1..*) + + alias inString: "b" + + set result: inString switch + "a" then [1, 2, 3], + "b" then 9, + default 10 + '''.generateCode + + val classes = code.compileToClasses + + val someFunc = classes.createFunc("SomeFunc") + val result = someFunc.invokeFunc(List) + assertEquals(#[9], result) + } + + + @Test + def void switchOperationWithOnlyDefaultCaseReturnsCorrectResult() { + val code = ''' + enum SomeEnum: + A + B + C + D + + func SomeFunc: + + output: + result SomeEnum (1..1) + + alias inString: "anything" + + set result: inString switch + default SomeEnum -> B + '''.generateCode + + val classes = code.compileToClasses + + val someFunc = classes.createFunc("SomeFunc") + val result = someFunc.invokeFunc(Enum) + assertEquals("B", result.toString) + } + + @Test + def void switchOperationWithNoMatchesReturnsDefaultWithImplicitEnums() { + val code = ''' + enum SomeEnum: + A + B + C + D + + func SomeFunc: + + output: + result SomeEnum (1..1) + + alias inString: "noMatch" + + + set result: inString switch + "aCondition" then SomeEnum -> A, + "bCondition" then SomeEnum -> B, + "cCondition" then SomeEnum -> C, + default SomeEnum -> D + '''.generateCode + + val classes = code.compileToClasses + + val someFunc = classes.createFunc("SomeFunc") + val result = someFunc.invokeFunc(Enum) + assertEquals("D", result.toString) + } + + @Test + def void switchOperationWithNoMatchesReturnsDefault() { + val code = ''' + enum SomeEnum: + A + B + C + D + + func SomeFunc: + + output: + result SomeEnum (1..1) + + alias inString: "noMatch" + + + set result: inString switch + "aCondition" then SomeEnum -> A, + "bCondition" then SomeEnum -> B, + "cCondition" then SomeEnum -> C, + default SomeEnum -> D + '''.generateCode + + val classes = code.compileToClasses + + val someFunc = classes.createFunc("SomeFunc") + val result = someFunc.invokeFunc(Enum) + assertEquals("D", result.toString) + } + + @Test + def void switchOperationMatchingOnString() { + val code = ''' + enum SomeEnum: + A + B + C + D + + func SomeFunc: + + output: + result SomeEnum (1..1) + + alias inString: "bCondition" + + + set result: inString switch + "aCondition" then SomeEnum -> A, + "bCondition" then SomeEnum -> B, + "cCondition" then SomeEnum -> C, + "dCondition" then SomeEnum -> D + '''.generateCode + + val classes = code.compileToClasses + + val someFunc = classes.createFunc("SomeFunc") + val result = someFunc.invokeFunc(Enum) + assertEquals("B", result.toString) + } + + @Test + def void switchOperationMatchingOnEnum() { + val code = ''' + enum SomeEnum: + A + B + C + D + + func SomeFunc: + + output: + result string (1..1) + + alias inEnum: SomeEnum -> B + + + set result: inEnum switch + A then "aValue", + B then "bValue", + C then "cValue", + D then "dValue" + '''.generateCode + + val classes = code.compileToClasses + + val someFunc = classes.createFunc("SomeFunc") + assertEquals("bValue", someFunc.invokeFunc(String)) + } + @Test def void assignToMultiMetaFeature() { val code = ''' 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 aa63d62df..7dd0dd7f4 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 @@ -116,6 +116,231 @@ class RosettaValidatorTest implements RosettaIssueCodes { ) } + @Test + def void testSwitchIinputRecordTypesAreNotValid() { + val model = ''' + namespace test + + typeAlias myDate: date + + func SomeFunc: + inputs: + someDate myDate (1..1) + + output: + result string (1..1) + + set result: someDate switch + default "someResult" + ''' + + model + .parseRosetta + .assertError(ROSETTA_EXPRESSION, null, "Type `date` is not a valid switch argument type, supported argument types are basic types and enumerations") + } + + @Test + def void testSwitchWithMultiCardinalityInputIsInvalid() { + val model = ''' + namespace test + + enum SomeEnum: + A + B + C + D + + func SomeFunc: + inputs: + inEnum SomeEnum (1..*) + output: + result string (1..1) + + set result: inEnum switch + A then "aValue", + B then "bValue", + C then "cValue", + default "someOtherValue" + ''' + + model + .parseRosetta + .assertError(ROSETTA_EXPRESSION, null, "Input to switch must be single cardinality") + + } + + @Test + def void testValidSwitchSyntaxEnumIsValidWhenMissingEnumValuesWithDefault() { + val model = ''' + namespace test + + enum SomeEnum: + A + B + C + D + + func SomeFunc: + inputs: + inEnum SomeEnum (1..1) + output: + result string (1..1) + + set result: inEnum switch + A then "aValue", + B then "bValue", + C then "cValue", + default "someOtherValue" + ''' + + model.parseRosettaWithNoIssues + } + + @Test + def void testValidSwitchSyntaxEnumFailsValitionWhenMissingEnumValues() { + val model = ''' + namespace test + + enum SomeEnum: + A + B + C + D + + func SomeFunc: + inputs: + inEnum SomeEnum (1..1) + output: + result string (1..1) + + set result: inEnum switch + A then "aValue", + B then "bValue", + C then "cValue" + ''' + + model.parseRosetta + .assertError(SWITCH_OPERATION, null, "Missing the following enumeration values from switch: D . Either provide all or use default.") + } + + @Test + def void testSwitchArgumentMatchesCaseStatmentTypes() { + val model = ''' + namespace test + + enum SomeEnum: + A + B + C + D + + func SomeFunc: + inputs: + inEnum SomeEnum (1..1) + output: + result string (1..1) + + set result: inEnum switch + A then "aValue", + 10 then "bValue", + C then "cValue", + default "defaultValue" + ''' + + model.parseRosetta + .assertError(ROSETTA_EXPRESSION, null, '''Mismatched condition type: Expected type `SomeEnum`, but got `int` instead.''') + } + + @Test + def void testDataTypesAreInvalidSwitchInputs() { + val model = ''' + namespace test + + type Foo: + fooField string (1..1) + + func SomeFunc: + inputs: + inFoo Foo (1..1) + output: + result string (1..1) + + set result: inFoo switch + inFoo then "aValue" + ''' + + model.parseRosetta + .assertError(ROSETTA_EXPRESSION, null, "Type `Foo` is not a valid switch argument type, supported argument types are basic types and enumerations") + } + + @Test + def void testValidSwitchSyntaxWithDefault() { + val context =''' + enum SomeEnum: + A + B + C + D + '''.parseRosettaWithNoIssues + + val expression = ''' + inEnum switch + A then "aValue", + B then "bValue", + C then "cValue", + default "defaultValue" + ''' + + expression.parseExpression(#[context], #["inEnum SomeEnum (1..1)"]) + .assertNoIssues + } + + @Test + def void testValidSwitchSyntaxEnum() { + val model = ''' + namespace test + + enum SomeEnum: + A + B + C + D + + func SomeFunc: + inputs: + inEnum SomeEnum (1..1) + output: + result string (1..1) + + set result: inEnum switch + A then "aValue", + B then "bValue", + C then "cValue", + D then "dValue" + ''' + + model.parseRosettaWithNoIssues + } + + @Test + def void testValidSwitchSyntaxString() { + val model = ''' + namespace test + + func SomeFunc: + inputs: + someInput string (1..1) + output: + result string (1..1) + + set result: someInput switch + "A" then "aValue", + "B" then "bValue" + ''' + + model.parseRosettaWithNoIssues + } + @Test def void testCannotAccessUncommonMetaFeatureOfDeepFeatureCall() { val model = '''