diff --git a/checkstyle.xml b/checkstyle.xml index 920eb4321..82acaf312 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -18,6 +18,8 @@ "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd"> + + diff --git a/docs/rune-java-documentation.md b/docs/rune-java-documentation.md index 32ff733c2..aad06bd4c 100644 --- a/docs/rune-java-documentation.md +++ b/docs/rune-java-documentation.md @@ -96,6 +96,7 @@ reporting rule VehicleClassificationType from VehicleOwnership: <"Classification In Java, the report is represented by the following class: ``` Java @RosettaReport(namespace="test.reg", body="EuropeanParliament", corpusList={"EmissionPerformanceStandardsEU"}) +@RuneLabelProvider(labelProvider=EuropeanParliamentEmissionPerformanceStandardsEULabelProvider.class) @ImplementedBy(EuropeanParliamentEmissionPerformanceStandardsEUReportFunction.EuropeanParliamentEmissionPerformanceStandardsEUReportFunctionDefault.class) public abstract class EuropeanParliamentEmissionPerformanceStandardsEUReportFunction implements ReportFunction { @Override diff --git a/pom.xml b/pom.xml index 2950601ad..cc002c685 100644 --- a/pom.xml +++ b/pom.xml @@ -139,12 +139,12 @@ rosetta-xcore-plugin-dependencies rosetta-runtime rosetta-lang + rosetta-testing + rosetta-integration-tests rosetta-ide rosetta-tools - rosetta-testing rosetta-maven-plugin rosetta-profiling - rosetta-integration-tests diff --git a/rosetta-ide/rosetta.tmLanguage.yaml b/rosetta-ide/rosetta.tmLanguage.yaml index 7bdaced2f..69ef3c550 100644 --- a/rosetta-ide/rosetta.tmLanguage.yaml +++ b/rosetta-ide/rosetta.tmLanguage.yaml @@ -594,6 +594,7 @@ repository: - include: '#synonymAnnotationBody' - include: '#referenceAnnotationBody' - include: '#ruleReferenceAnnotationBody' + - include: '#labelAnnotationBody' - include: '#customAnnotationBody' prefixAnnotationBody: @@ -715,6 +716,28 @@ repository: - name: entity.name.rule.rosetta match: '{{identifier}}' + labelAnnotationBody: + name: meta.annotated.label.rosetta + begin: '{{wordStart}}label{{wordEnd}}' + beginCaptures: + 0: { name: variable.annotation.label.rosetta } + end: (?=\])|{{sectionEnd}} + patterns: + - include: '#comment' + - name: meta.annotated.label.path.rosetta + begin: '(?<={{wordStart}}label{{wordEnd}})' + end: '{{wordStart}}as{{wordEnd}}' + endCaptures: + 0: { name: keyword.other.rosetta } + patterns: + - include: '#comment' + - include: '#implicitVariable' + - name: keyword.operator.rosetta + match: -> + - name: variable.other.member.rosetta + match: '{{identifier}}' + - include: '#string' + customAnnotationBody: name: meta.annotated.rosetta begin: '{{identifier}}' @@ -1138,8 +1161,7 @@ repository: patterns: - include: '#comment' - include: '#literal' - - name: constant.language.rosetta - match: '{{wordStart}}(item|it){{wordEnd}}' + - include: '#implicitVariable' - name: meta.if-then.rosetta begin: '{{wordStart}}if{{wordEnd}}' beginCaptures: @@ -1200,6 +1222,10 @@ repository: - name: variable.rosetta # Need semantic tokens to specify variable match: '{{identifier}}' + implicitVariable: + name: constant.language.rosetta + match: '{{wordStart}}item{{wordEnd}}' + constructorExpression: name: meta.constructor.rosetta begin: (\{) @@ -1296,6 +1322,7 @@ repository: - include: '#synonymAnnotationBody' - include: '#referenceAnnotationBody' - include: '#ruleReferenceAnnotationBody' + - include: '#labelAnnotationBody' - include: '#customAnnotationBody' - include: '#annotationsFollowedByExpression' - include: '#expression' 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 54c08731e..367f62aec 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 @@ -407,7 +407,6 @@ class ContentAssistTest extends AbstractRosettaLanguageServerTest { flatten -> flatten [[19, 2] .. [19, 2]] if -> if [[19, 2] .. [19, 2]] is -> is [[19, 2] .. [19, 2]] - it -> it [[19, 2] .. [19, 2]] item -> item [[19, 2] .. [19, 2]] join -> join [[19, 2] .. [19, 2]] last -> last [[19, 2] .. [19, 2]] diff --git a/rosetta-integration-tests/.checkstyle b/rosetta-integration-tests/.checkstyle new file mode 100644 index 000000000..5af5eb6e9 --- /dev/null +++ b/rosetta-integration-tests/.checkstyle @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/rosetta-integration-tests/.project b/rosetta-integration-tests/.project new file mode 100644 index 000000000..be553457a --- /dev/null +++ b/rosetta-integration-tests/.project @@ -0,0 +1,35 @@ + + + rosetta-integration-tests + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.xtext.ui.shared.xtextBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.xtext.ui.shared.xtextNature + org.eclipse.m2e.core.maven2Nature + net.sf.eclipsecs.core.CheckstyleNature + + diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/docs/DocumentationSamples.xtend b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/docs/DocumentationSamples.xtend index 613927bc4..f1f63877f 100644 --- a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/docs/DocumentationSamples.xtend +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/docs/DocumentationSamples.xtend @@ -127,6 +127,7 @@ class DocumentationSamples { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.reports.ReportFunction; import java.util.Optional; @@ -134,9 +135,11 @@ class DocumentationSamples { import test.reg.EuropeanParliamentReport; import test.reg.EuropeanParliamentReport.EuropeanParliamentReportBuilder; import test.reg.VehicleOwnership; + import test.reg.labels.EuropeanParliamentEmissionPerformanceStandardsEULabelProvider; @RosettaReport(namespace="test.reg", body="EuropeanParliament", corpusList={"EmissionPerformanceStandardsEU"}) + @RuneLabelProvider(labelProvider=EuropeanParliamentEmissionPerformanceStandardsEULabelProvider.class) @ImplementedBy(EuropeanParliamentEmissionPerformanceStandardsEUReportFunction.EuropeanParliamentEmissionPerformanceStandardsEUReportFunctionDefault.class) public abstract class EuropeanParliamentEmissionPerformanceStandardsEUReportFunction implements ReportFunction { diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend index 500983d3f..f7138510e 100644 --- a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend @@ -536,7 +536,7 @@ class RosettaFormattingTest { type AllocationOutcome: condition C1: if True - then True extract [it = False] = True + then True extract [item = False] = True condition C2: True ''', ''' @@ -548,7 +548,7 @@ class RosettaFormattingTest { if True then True extract [ - it = False + item = False ] = True condition C2: diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/condition/ConditionGeneratorTest.xtend b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/condition/ConditionGeneratorTest.xtend index 44ae1e368..25ab5309f 100644 --- a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/condition/ConditionGeneratorTest.xtend +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/condition/ConditionGeneratorTest.xtend @@ -80,7 +80,7 @@ class ConditionGeneratorTest { a int (0..1) condition C: - it -> a exists and [it, it] any = it + item -> a exists and [item, item] any = item '''.generateCode val classes = code.compileToClasses diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/function/LabelProviderGeneratorTest.java b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/function/LabelProviderGeneratorTest.java new file mode 100644 index 000000000..97cbea4a5 --- /dev/null +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/function/LabelProviderGeneratorTest.java @@ -0,0 +1,277 @@ +package com.regnosys.rosetta.generator.java.function; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.eclipse.xtext.xbase.testing.RegisteringFileSystemAccess; +import org.eclipse.xtext.xbase.testing.RegisteringFileSystemAccess.GeneratedFile; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import com.google.inject.Injector; +import com.regnosys.rosetta.tests.RosettaTestInjectorProvider; +import com.regnosys.rosetta.tests.testmodel.RosettaTestModel; +import com.regnosys.rosetta.tests.testmodel.RosettaTestModelService; +import com.regnosys.rosetta.tests.util.CodeGeneratorTestHelper; +import com.rosetta.model.lib.functions.LabelProvider; +import com.rosetta.model.lib.path.RosettaPath; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaTestInjectorProvider.class) +public class LabelProviderGeneratorTest { + @Inject + private RosettaTestModelService testModelService; + @Inject + private CodeGeneratorTestHelper generatorTestHelper; + @Inject + private Injector injector; + + @Inject + private LabelProviderGenerator labelProviderGenerator; + + private RegisteringFileSystemAccess fsa; + + @BeforeEach + void beforeEach() { + fsa = new RegisteringFileSystemAccess(); + fsa.setProjectName("test-project"); + fsa.setOutputPath("src-gen/main/java"); + } + + + private RosettaTestModel loadModel(String runeSourceCode) throws IOException { + return testModelService.toTestModel(runeSourceCode); + } + private void generateLabelProviderForFunction(RosettaTestModel model, String functionName) { + labelProviderGenerator.generateForFunctionIfApplicable(fsa, model.getFunction(functionName)); + } + private void generateLabelProviderForReport(RosettaTestModel model, String body, String... corpusList) { + labelProviderGenerator.generateForReport(fsa, model.getReport(body, corpusList)); + } + + private List getGeneratedFileNames() { + return fsa.getGeneratedFiles().stream().map(f -> f.getPath()).collect(Collectors.toList()); + } + private void assertNoGeneratedFiles() { + List fileNames = getGeneratedFileNames(); + Assertions.assertTrue( + fileNames.isEmpty(), + "The following files were generated:" + fileNames.stream().collect(Collectors.joining("\n", "\n", "\n")) + ); + } + private GeneratedFile getSingleGeneratedFile() { + List fileNames = getGeneratedFileNames(); + Assertions.assertEquals( + 1, + fileNames.size(), + "The following files were generated:" + fileNames.stream().collect(Collectors.joining("\n", "\n", "\n")) + ); + String actualGeneratedPath = fileNames.get(0); + return fsa.getGeneratedFiles().stream().filter(f -> f.getPath().equals(actualGeneratedPath)).findAny().orElseThrow(); + } + private LabelProvider getLabelProviderInstance() { + GeneratedFile file = getSingleGeneratedFile(); + Map> compiled = generatorTestHelper.compileToClasses(Map.of(file.getJavaClassName(), file.getContents().toString())); + Class clazz = compiled.get(file.getJavaClassName()).asSubclass(LabelProvider.class); + return injector.getInstance(clazz); + } + private void assertSingleGeneratedFile(String expectationFileName, String expectedGeneratedPath) throws IOException { + GeneratedFile file = getSingleGeneratedFile(); + Assertions.assertEquals(expectedGeneratedPath, file.getPath().replace("/test-project/src-gen/main/java", "")); + String actualSource = file.getContents().toString(); + String expectedSource = Resources.toString(getClass().getResource("/label-annotations/" + expectationFileName), Charsets.UTF_8); + Assertions.assertEquals(expectedSource, actualSource); + } + private void assertLabels(String... pathLabelExpectations) { + LabelProvider provider = getLabelProviderInstance(); + Assertions.assertAll( + Arrays.stream(pathLabelExpectations) + .map(expectation -> () -> { + String[] parts = expectation.split(":"); + String rawPath = parts[0]; + RosettaPath path = RosettaPath.valueOf(rawPath); + String expectedLabel = parts[1].equals("null") ? null : parts[1]; + String actualLabel = provider.getLabel(path); + Assertions.assertEquals(expectedLabel, actualLabel, "Expected label \"" + expectedLabel + "\", but got \"" + actualLabel + "\" for path `" + rawPath + "`"); + }) + ); + } + + + @Test + void testFunctionWithoutAnnotationDoesNotGenerateLabelProvider() throws IOException { + RosettaTestModel model = loadModel(""" + namespace test + + type Foo: + attr string (1..1) + [label as "My attribute"] + + annotation myAnn: + + func MyFunc: + [myAnn] + output: + foo Foo (1..1) + """); + + generateLabelProviderForFunction(model, "MyFunc"); + + assertNoGeneratedFiles(); + } + + @Test + void testFunctionWithIngestAnnotationGeneratesLabelProvider() throws IOException { + RosettaTestModel model = loadModel(""" + namespace test + + type Foo: + attr string (1..1) + [label as "My attribute"] + other int (1..1) + + func MyFunc: + [ingest JSON] + output: + foo Foo (1..1) + """); + + generateLabelProviderForFunction(model, "MyFunc"); + + assertSingleGeneratedFile("func-ingest/MyFuncLabelProvider.java", "/test/labels/MyFuncLabelProvider.java"); + assertLabels( + "attr:My attribute", + "other:null" + ); + } + + @Test + void testReportLabelOverridesRuleReferenceLabel() throws IOException { + RosettaTestModel model = loadModel(""" + namespace test + + body Authority Body + corpus Regulation "Description" Corpus + + report Body Corpus in T+1 + from int + when IsEligible + with type Foo + + eligibility rule IsEligible from int: + item + + type Foo: + attr string (1..1) + [ruleReference FooAttr] + [label as "My attribute"] + other int (1..1) + [ruleReference FooOther] + + + reporting rule FooAttr from int: + to-string + as "My attribute from rule" + + reporting rule FooOther from int: + item + as "Other from rule" + """); + + generateLabelProviderForReport(model, "Body", "Corpus"); + + assertSingleGeneratedFile("report-with-rule-references/BodyCorpusLabelProvider.java", "/test/labels/BodyCorpusLabelProvider.java"); + assertLabels( + "attr:My attribute", + "other:Other from rule" + ); + } + + @Test + void testComplexReportLabels() throws IOException { + RosettaTestModel model = loadModel(""" + namespace test + + body Authority Body + corpus Regulation "Description" Corpus + + report Body Corpus in T+1 + from int + when IsEligible + with type Foo + + eligibility rule IsEligible from int: + item + + type SuperFoo: + attr1 string (1..1) + [metadata scheme] + [label as "My Label"] + qux Qux (1..1) + [label Opt1 -> opt1Attr as "Super option 1 Attribute"] + + type Foo extends SuperFoo: + override attr1 string (1..1) + [metadata scheme] + [label as "My Overridden Label"] + override qux Qux (1..1) + [label item ->> id as "Deep path ID"] + attr2 string (1..1) + [label item as "Label with item"] + bar Bar (1..1) + [label barAttr as "Bar attribute using path"] + [label item -> nestedBarList -> nestedAttr as "Nested bar attribute $"] + + type Bar: + barAttr string (1..1) + [ruleReference BarAttr] + nestedBarList NestedBar (0..*) + + type NestedBar: + nestedAttr string (1..1) + + choice Qux: + Opt1 + Opt2 + + type Opt1: + id string (1..1) + opt1Attr int (1..1) + [label as "Option 1 Attribute"] + + type Opt2: + id string (1..1) + opt2Attribute int (1..1) + + + reporting rule BarAttr from int: + to-string + as "Bar attribute from rule" + """); + + generateLabelProviderForReport(model, "Body", "Corpus"); + + assertSingleGeneratedFile("report-with-complex-labels/BodyCorpusLabelProvider.java", "/test/labels/BodyCorpusLabelProvider.java"); + assertLabels( + "attr1:My Overridden Label", + "attr2:Label with item", + "bar.barAttr:Bar attribute using path", + "bar.nestedBarList(0).nestedAttr:Nested bar attribute $", + "bar.nestedBarList(1).nestedAttr:Nested bar attribute $", + "qux.Opt1.id:Deep path ID", + "qux.Opt1.opt1Attr:Super option 1 Attribute", + "qux.Opt2.id:Deep path ID" + ); + } +} diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/reports/TabulatorTest.xtend b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/reports/TabulatorTest.xtend index 186ca152f..6b2817de6 100644 --- a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/reports/TabulatorTest.xtend +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/reports/TabulatorTest.xtend @@ -122,7 +122,7 @@ class TabulatorTest { subsubbasic: 3 ''' assertFieldValuesEqual(expectedValues, flatReport) - } + } @Test diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/rule/RosettaRuleGeneratorTest.xtend b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/rule/RosettaRuleGeneratorTest.xtend index be53d19f9..f9d520d1a 100644 --- a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/rule/RosettaRuleGeneratorTest.xtend +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/generator/java/rule/RosettaRuleGeneratorTest.xtend @@ -144,16 +144,19 @@ class RosettaRuleGeneratorTest { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.test.model.Bar; import com.rosetta.test.model.BarReport; import com.rosetta.test.model.BarReport.BarReportBuilder; + import com.rosetta.test.model.labels.TEST_REGMiFIRLabelProvider; import java.util.Optional; import javax.inject.Inject; @RosettaReport(namespace="com.rosetta.test.model", body="TEST_REG", corpusList={"MiFIR"}) + @RuneLabelProvider(labelProvider=TEST_REGMiFIRLabelProvider.class) @ImplementedBy(TEST_REGMiFIRReportFunction.TEST_REGMiFIRReportFunctionDefault.class) public abstract class TEST_REGMiFIRReportFunction implements ReportFunction { @@ -290,17 +293,20 @@ class RosettaRuleGeneratorTest { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.mapper.MapperC; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.test.model.Bar; import com.rosetta.test.model.BarReport; import com.rosetta.test.model.BarReport.BarReportBuilder; + import com.rosetta.test.model.labels.TEST_REGMiFIRLabelProvider; import java.util.Optional; import javax.inject.Inject; @RosettaReport(namespace="com.rosetta.test.model", body="TEST_REG", corpusList={"MiFIR"}) + @RuneLabelProvider(labelProvider=TEST_REGMiFIRLabelProvider.class) @ImplementedBy(TEST_REGMiFIRReportFunction.TEST_REGMiFIRReportFunctionDefault.class) public abstract class TEST_REGMiFIRReportFunction implements ReportFunction { @@ -430,16 +436,19 @@ class RosettaRuleGeneratorTest { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.test.model.Bar; import com.rosetta.test.model.BarReport; import com.rosetta.test.model.BarReport.BarReportBuilder; + import com.rosetta.test.model.labels.TEST_REGMiFIRLabelProvider; import java.util.Optional; import javax.inject.Inject; @RosettaReport(namespace="com.rosetta.test.model", body="TEST_REG", corpusList={"MiFIR"}) + @RuneLabelProvider(labelProvider=TEST_REGMiFIRLabelProvider.class) @ImplementedBy(TEST_REGMiFIRReportFunction.TEST_REGMiFIRReportFunctionDefault.class) public abstract class TEST_REGMiFIRReportFunction implements ReportFunction { @@ -560,16 +569,19 @@ class RosettaRuleGeneratorTest { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.test.model.Bar; import com.rosetta.test.model.BarReport; import com.rosetta.test.model.BarReport.BarReportBuilder; + import com.rosetta.test.model.labels.TEST_REGMiFIRLabelProvider; import java.util.Optional; import javax.inject.Inject; @RosettaReport(namespace="com.rosetta.test.model", body="TEST_REG", corpusList={"MiFIR"}) + @RuneLabelProvider(labelProvider=TEST_REGMiFIRLabelProvider.class) @ImplementedBy(TEST_REGMiFIRReportFunction.TEST_REGMiFIRReportFunctionDefault.class) public abstract class TEST_REGMiFIRReportFunction implements ReportFunction { @@ -699,16 +711,19 @@ class RosettaRuleGeneratorTest { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.test.model.Bar; import com.rosetta.test.model.BarReport; import com.rosetta.test.model.BarReport.BarReportBuilder; + import com.rosetta.test.model.labels.TEST_REGMiFIRLabelProvider; import java.util.Optional; import javax.inject.Inject; @RosettaReport(namespace="com.rosetta.test.model", body="TEST_REG", corpusList={"MiFIR"}) + @RuneLabelProvider(labelProvider=TEST_REGMiFIRLabelProvider.class) @ImplementedBy(TEST_REGMiFIRReportFunction.TEST_REGMiFIRReportFunctionDefault.class) public abstract class TEST_REGMiFIRReportFunction implements ReportFunction { @@ -809,16 +824,19 @@ class RosettaRuleGeneratorTest { import com.google.inject.ImplementedBy; import com.rosetta.model.lib.annotations.RosettaReport; + import com.rosetta.model.lib.annotations.RuneLabelProvider; import com.rosetta.model.lib.functions.ModelObjectValidator; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.test.model.Bar; import com.rosetta.test.model.BarReport; import com.rosetta.test.model.BarReport.BarReportBuilder; + import com.rosetta.test.model.labels.TEST_REGMiFIRLabelProvider; import java.util.Optional; import javax.inject.Inject; @RosettaReport(namespace="com.rosetta.test.model", body="TEST_REG", corpusList={"MiFIR"}) + @RuneLabelProvider(labelProvider=TEST_REGMiFIRLabelProvider.class) @ImplementedBy(TEST_REGMiFIRReportFunction.TEST_REGMiFIRReportFunctionDefault.class) public abstract class TEST_REGMiFIRReportFunction implements ReportFunction { diff --git a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend index 6d52bc85a..54f8d3b22 100644 --- a/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend +++ b/rosetta-integration-tests/src/test/java/com/regnosys/rosetta/tests/RosettaParsingTest.xtend @@ -35,6 +35,39 @@ class RosettaParsingTest { @Inject extension ValidationTestHelper @Inject extension ExpressionParser + @Test + def void testLabelAnnotation() { + ''' + type Foo: + attr1 string (1..1) + [label as "My Attribute"] + attr2 string (1..1) + [label item as "My Attribute"] + bar Bar (1..1) + [label barAttr as "Bar Attribute"] + [label item -> nestedBar -> barAttr as "Nested Bar Attribute"] + qux Qux (1..1) + [label item ->> id as "Qux ID"] + + + type Bar: + barAttr string (1..1) + nestedBar Bar (0..1) + + choice Qux: + Opt1 + Opt2 + + type Opt1: + id string (1..1) + opt1Attr int (1..1) + + type Opt2: + id string (1..1) + opt2Attribute int (1..1) + '''.parseRosettaWithNoIssues + } + @Test def void canSwitchWithSingleCase() { val context = ''' @@ -50,6 +83,7 @@ class RosettaParsingTest { .assertNoIssues } + @Test def void testCannotOverrideAttributeOfItself() { ''' type Foo: @@ -519,7 +553,7 @@ class RosettaParsingTest { condition C: [deprecated] // the parser should parse this as an annotation, not a list - extract it -> a + extract item -> a then exists func F: diff --git a/rosetta-integration-tests/src/test/java/com/rosetta/model/lib/flatten/ModelObjectFlattenerTest.java b/rosetta-integration-tests/src/test/java/com/rosetta/model/lib/flatten/ModelObjectFlattenerTest.java new file mode 100644 index 000000000..76b674c09 --- /dev/null +++ b/rosetta-integration-tests/src/test/java/com/rosetta/model/lib/flatten/ModelObjectFlattenerTest.java @@ -0,0 +1,173 @@ +package com.rosetta.model.lib.flatten; + +import com.regnosys.rosetta.tests.RosettaTestInjectorProvider; +import com.regnosys.rosetta.tests.testmodel.JavaTestModel; +import com.regnosys.rosetta.tests.testmodel.RosettaTestModelService; +import com.rosetta.model.lib.RosettaModelObject; +import com.rosetta.model.lib.path.RosettaPathValue; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import javax.inject.Inject; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaTestInjectorProvider.class) +class ModelObjectFlattenerTest { + @Inject + private RosettaTestModelService modelService; + @Inject + private ModelObjectFlattener modelObjectFlattener; + + private void assertFlattenedValues(RosettaModelObject instance, String expected) { + List table = modelObjectFlattener.flatten(instance); + String actual = table.stream().map(pv -> pv.getPath() + ": " + pv.getValue()).collect(Collectors.joining("\n", "", "\n")); + Assertions.assertEquals(expected, actual); + } + + @Test + void flattenTest() throws IOException { + JavaTestModel model = modelService.toJavaTestModel(""" + namespace test + + type Root: + r1 string (0..1) + foo Foo (0..1) + bar Bar (0..*) + + type Foo: + f1 string (0..1) + bar Bar (0..1) + [metadata reference] + bars Bar (0..*) + [metadata reference] + + type Bar: + [metadata key] + b1 string (0..1) + [metadata scheme] + b2 string (0..*) + b3 string (0..*) + [metadata scheme] + """).compile(); + + RosettaModelObject instance = model.evaluateExpression(RosettaModelObject.class, """ + Root { + r1: "VALUE1", + foo: Foo { + f1: "VALUE2", + bar: Bar { + b1: "VALUE3", + b3: ["VALUE4"], + ... + }, + bars: [ + Bar { + b1: "VALUE5", + b2: ["VALUE6", "VALUE7"], + ... + }, + Bar { + b1: "VALUE8", + b2: ["VALUE9", "VALUE10"], + b3: ["VALUE11", "VALUE12"], + } + ] + }, + bar: [ + Bar { + b1: "VALUE13", + b2: ["VALUE14", "VALUE15"], + ... + }, + Bar { + b1: "VALUE16", + b2: ["VALUE17", "VALUE18"], + b3: ["VALUE19", "VALUE20"], + } + ] + } + """); + + assertFlattenedValues(instance, """ + r1: VALUE1 + foo.f1: VALUE2 + foo.bar.b1: VALUE3 + foo.bar.b3(0): VALUE4 + foo.bars(0).b1: VALUE5 + foo.bars(0).b2(0): VALUE6 + foo.bars(0).b2(1): VALUE7 + foo.bars(1).b1: VALUE8 + foo.bars(1).b2(0): VALUE9 + foo.bars(1).b2(1): VALUE10 + foo.bars(1).b3(0): VALUE11 + foo.bars(1).b3(1): VALUE12 + bar(0).b1: VALUE13 + bar(0).b2(0): VALUE14 + bar(0).b2(1): VALUE15 + bar(1).b1: VALUE16 + bar(1).b2(0): VALUE17 + bar(1).b2(1): VALUE18 + bar(1).b3(0): VALUE19 + bar(1).b3(1): VALUE20 + """); + } + + @Test + void flattenTestWithInheritance() throws IOException { + JavaTestModel model = modelService.toJavaTestModel(""" + namespace test + + type Foo1: + attr int (1..1) + numberAttr number (0..1) + parent Parent (1..1) + parentList Parent (0..10) + stringAttr string (1..1) + [metadata scheme] + + type Foo2 extends Foo1: + override numberAttr int(digits: 30, max: 100) (1..1) + override parent Child (1..1) + override parentList Child (1..1) + [metadata reference] + override stringAttr string(maxLength: 42) (1..1) + + type Foo3 extends Foo2: + override numberAttr int (1..1) + override parentList GrandChild (1..1) + + type Parent: + subAttr boolean (1..1) + + type Child extends Parent: + [metadata key] + + type GrandChild extends Child: + """).compile(); + + RosettaModelObject instance = model.evaluateExpression(RosettaModelObject.class, """ + Foo3 { + attr: 42, + numberAttr: 10, + parent: Child { subAttr: True }, + parentList: GrandChild { subAttr: False }, + stringAttr: "My string" + } + """); + + assertFlattenedValues(instance, """ + attr: 42 + numberAttr: 10 + parent.subAttr: true + parentList.subAttr: false + stringAttr: My string + """); + } +} \ No newline at end of file diff --git a/rosetta-integration-tests/src/test/resources/label-annotations/func-ingest/MyFuncLabelProvider.java b/rosetta-integration-tests/src/test/resources/label-annotations/func-ingest/MyFuncLabelProvider.java new file mode 100644 index 000000000..7ed61c332 --- /dev/null +++ b/rosetta-integration-tests/src/test/resources/label-annotations/func-ingest/MyFuncLabelProvider.java @@ -0,0 +1,23 @@ +package test.labels; + +import com.rosetta.model.lib.functions.LabelProvider; +import com.rosetta.model.lib.path.RosettaPath; +import java.util.HashMap; +import java.util.Map; + + +public class MyFuncLabelProvider implements LabelProvider { + private final Map labelMap; + + public MyFuncLabelProvider() { + labelMap = new HashMap<>(); + + labelMap.put(RosettaPath.valueOf("attr"), "My attribute"); + } + + @Override + public String getLabel(RosettaPath path) { + RosettaPath normalized = path.toIndexless(); + return labelMap.get(normalized); + } +} diff --git a/rosetta-integration-tests/src/test/resources/label-annotations/report-with-complex-labels/BodyCorpusLabelProvider.java b/rosetta-integration-tests/src/test/resources/label-annotations/report-with-complex-labels/BodyCorpusLabelProvider.java new file mode 100644 index 000000000..2990b1916 --- /dev/null +++ b/rosetta-integration-tests/src/test/resources/label-annotations/report-with-complex-labels/BodyCorpusLabelProvider.java @@ -0,0 +1,29 @@ +package test.labels; + +import com.rosetta.model.lib.functions.LabelProvider; +import com.rosetta.model.lib.path.RosettaPath; +import java.util.HashMap; +import java.util.Map; + + +public class BodyCorpusLabelProvider implements LabelProvider { + private final Map labelMap; + + public BodyCorpusLabelProvider() { + labelMap = new HashMap<>(); + + labelMap.put(RosettaPath.valueOf("attr1"), "My Overridden Label"); + labelMap.put(RosettaPath.valueOf("qux.Opt1.opt1Attr"), "Super option 1 Attribute"); + labelMap.put(RosettaPath.valueOf("qux.Opt1.id"), "Deep path ID"); + labelMap.put(RosettaPath.valueOf("qux.Opt2.id"), "Deep path ID"); + labelMap.put(RosettaPath.valueOf("attr2"), "Label with item"); + labelMap.put(RosettaPath.valueOf("bar.barAttr"), "Bar attribute using path"); + labelMap.put(RosettaPath.valueOf("bar.nestedBarList.nestedAttr"), "Nested bar attribute $"); + } + + @Override + public String getLabel(RosettaPath path) { + RosettaPath normalized = path.toIndexless(); + return labelMap.get(normalized); + } +} diff --git a/rosetta-integration-tests/src/test/resources/label-annotations/report-with-rule-references/BodyCorpusLabelProvider.java b/rosetta-integration-tests/src/test/resources/label-annotations/report-with-rule-references/BodyCorpusLabelProvider.java new file mode 100644 index 000000000..289e32b5f --- /dev/null +++ b/rosetta-integration-tests/src/test/resources/label-annotations/report-with-rule-references/BodyCorpusLabelProvider.java @@ -0,0 +1,24 @@ +package test.labels; + +import com.rosetta.model.lib.functions.LabelProvider; +import com.rosetta.model.lib.path.RosettaPath; +import java.util.HashMap; +import java.util.Map; + + +public class BodyCorpusLabelProvider implements LabelProvider { + private final Map labelMap; + + public BodyCorpusLabelProvider() { + labelMap = new HashMap<>(); + + labelMap.put(RosettaPath.valueOf("attr"), "My attribute"); + labelMap.put(RosettaPath.valueOf("other"), "Other from rule"); + } + + @Override + public String getLabel(RosettaPath path) { + RosettaPath normalized = path.toIndexless(); + return labelMap.get(normalized); + } +} diff --git a/rosetta-lang/model/RosettaExpression.xcore b/rosetta-lang/model/RosettaExpression.xcore index f6dfd3532..7fd5d3bf6 100644 --- a/rosetta-lang/model/RosettaExpression.xcore +++ b/rosetta-lang/model/RosettaExpression.xcore @@ -14,6 +14,7 @@ import com.regnosys.rosetta.rosetta.RosettaFeature import com.regnosys.rosetta.rosetta.RosettaCallableWithArgs import com.regnosys.rosetta.rosetta.RosettaMapTestExpression import com.regnosys.rosetta.rosetta.RosettaTyped +import com.regnosys.rosetta.rosetta.simple.AnnotationPathExpression import com.regnosys.rosetta.rosetta.simple.Attribute import com.regnosys.rosetta.rosetta.simple.ChoiceOption import org.eclipse.emf.common.util.BasicEList @@ -141,7 +142,7 @@ class RosettaSymbolReference extends RosettaReference { } } -class RosettaImplicitVariable extends RosettaReference, RosettaNamed { +class RosettaImplicitVariable extends RosettaReference, RosettaNamed, AnnotationPathExpression { } class RosettaFeatureCall extends RosettaExpression { diff --git a/rosetta-lang/model/RosettaSimple.xcore b/rosetta-lang/model/RosettaSimple.xcore index 558ee30fb..ddc1ba5eb 100644 --- a/rosetta-lang/model/RosettaSimple.xcore +++ b/rosetta-lang/model/RosettaSimple.xcore @@ -31,6 +31,7 @@ import com.regnosys.rosetta.rosetta.RosettaPackage.Literals abstract class RootElement extends RosettaRootElement, RosettaNamed, RosettaDefinable, Annotated { } +/* ANNOTATIONS */ class Annotation extends RootElement, RosettaNamed { String prefix contains Attribute[] attributes @@ -53,6 +54,28 @@ class AnnotationRef { contains AnnotationQualifier[] qualifiers } +interface BuiltinAnnotation extends RosettaNamed { +} +class LabelAnnotation extends BuiltinAnnotation { + contains AnnotationPathExpression path + String label +} + +interface AnnotationPathExpression { +} +class AnnotationPathAttributeReference extends AnnotationPathExpression { + refers Attribute attribute +} +class AnnotationPath extends AnnotationPathExpression { + contains AnnotationPathExpression receiver + refers Attribute attribute +} +class AnnotationDeepPath extends AnnotationPathExpression { + contains AnnotationPathExpression receiver + refers Attribute attribute +} +/* END ANNOTATIONS */ + interface AssignPathRoot extends RosettaSymbol { } @@ -61,6 +84,7 @@ class Attribute extends RosettaTypedFeature, RosettaDefinable, Annotated, Refere contains RosettaCardinality card contains RosettaSynonym[] synonyms contains RosettaRuleReference ruleReference + contains LabelAnnotation[] labels } diff --git a/rosetta-lang/pom.xml b/rosetta-lang/pom.xml index a444697c4..e4acf265c 100644 --- a/rosetta-lang/pom.xml +++ b/rosetta-lang/pom.xml @@ -126,6 +126,13 @@ **/* + + + ${basedir}/../rosetta-testing/src-gen/ + + **/* + + ${basedir}/../rosetta-ide/src-gen/ 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 23ccb7f8c..8d2e341ea 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -82,15 +82,13 @@ Choice: ChoiceOption: RosettaTyped RosettaDefinable? - (References|Annotations|Synonyms)* - RuleReference? + (References|Annotations|Synonyms|labels+=LabelAnnotation|RuleReference)* ; Attribute: override?='override'? RosettaNamed RosettaTyped card=RosettaCardinality RosettaDefinable? - (References|Annotations|Synonyms)* - RuleReference? + (References|Annotations|Synonyms|labels+=LabelAnnotation|RuleReference)* ; Enumeration returns RosettaEnumeration: @@ -474,7 +472,11 @@ RosettaSegment: RosettaReferenceOrFunctionCall returns RosettaExpression: {RosettaSymbolReference} symbol=[RosettaSymbol|QualifiedName] (explicitArguments?='(' (rawArgs+=RosettaCalcExpression (',' rawArgs+=RosettaCalcExpression)*)? ')')? - | {RosettaImplicitVariable} name=('item' | 'it') + | RosettaImplicitVariable +; + +RosettaImplicitVariable: + name='item' ; RosettaLiteral: @@ -804,7 +806,7 @@ RosettaOnlyExistsElement returns RosettaExpression: RosettaOnlyExistsElementRoot returns RosettaReference: {RosettaSymbolReference} symbol=[RosettaSymbol|QualifiedName] - | {RosettaImplicitVariable} name=('item' | 'it') + | RosettaImplicitVariable ; /***************************************** @@ -915,3 +917,23 @@ RosettaRule: ('as' identifier=STRING)?) ) ; + +/********************************** + * Labels + */ +LabelAnnotation: + '[' name='label' path=AnnotationPathExpression? 'as' label=STRING ']' +; +AnnotationPathExpression: + NestedAnnotationPath +; +NestedAnnotationPath returns AnnotationPathExpression: + PrimaryAnnotationPath ->( + ({AnnotationPath.receiver=current} '->' (=>attribute=[Attribute|ValidID])?) + |({AnnotationDeepPath.receiver=current} '->>' (=>attribute=[Attribute|ValidID])?) + )* +; +PrimaryAnnotationPath returns AnnotationPathExpression: + {AnnotationPathAttributeReference} attribute=[Attribute|ValidID] + | RosettaImplicitVariable +; 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 d5c893acb..72c7ba282 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 @@ -45,6 +45,7 @@ import com.regnosys.rosetta.rosetta.RosettaRootElement import com.regnosys.rosetta.rosetta.RosettaEnumeration import com.regnosys.rosetta.utils.ModelIdProvider import com.regnosys.rosetta.types.RObjectFactory +import com.regnosys.rosetta.generator.java.function.LabelProviderGenerator /** * Generates code from your model files on save. @@ -70,6 +71,7 @@ class RosettaGenerator implements IGenerator2 { @Inject FunctionGenerator funcGenerator @Inject ReportGenerator reportGenerator @Inject DeepPathUtilGenerator deepPathUtilGenerator + @Inject LabelProviderGenerator labelProviderGenerator @Inject DeepFeatureCallUtil deepFeatureCallUtil @@ -212,6 +214,7 @@ class RosettaGenerator implements IGenerator2 { funcGenerator.generate(packages, fsa, elem, version) } tabulatorGenerator.generateTabulatorForFunction(fsa, elem) + labelProviderGenerator.generateForFunctionIfApplicable(fsa, elem) } RosettaRule: { ruleGenerator.generate(packages, fsa, elem, version) @@ -219,6 +222,7 @@ class RosettaGenerator implements IGenerator2 { RosettaReport: { reportGenerator.generate(packages, fsa, elem, version) tabulatorGenerator.generateTabulatorForReport(fsa, elem) + labelProviderGenerator.generateForReport(fsa, elem) } RosettaExternalRuleSource: { elem.externalClasses.forEach [ externalClass | diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/RosettaJavaPackages.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/RosettaJavaPackages.java index f7ab9cd8c..2074383ba 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/RosettaJavaPackages.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/RosettaJavaPackages.java @@ -16,7 +16,6 @@ package com.regnosys.rosetta.generator.java; -import com.regnosys.rosetta.rosetta.RosettaModel; import com.regnosys.rosetta.scoping.RosettaScopeProvider; import com.rosetta.util.DottedPath; diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/FunctionGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/FunctionGenerator.xtend index 69e350b6a..2dace6a1f 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/FunctionGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/FunctionGenerator.xtend @@ -72,6 +72,7 @@ import static extension com.regnosys.rosetta.types.RMetaAnnotatedType.withNoMeta import com.regnosys.rosetta.generator.java.statement.builder.JavaVariable import com.regnosys.rosetta.generator.java.types.JavaPojoInterface import com.regnosys.rosetta.generator.java.types.RJavaWithMetaValue +import com.rosetta.model.lib.annotations.RuneLabelProvider class FunctionGenerator { @@ -90,6 +91,7 @@ class FunctionGenerator { @Inject extension JavaTypeUtil @Inject TypeCoercionService coercionService @Inject extension ModelIdProvider + @Inject LabelProviderGeneratorUtil labelProviderUtil def void generate(RootPackage root, IFileSystemAccess2 fsa, Function func, String version) { val fileName = root.functions.withForwardSlashes + '/' + func.name + '.java' @@ -108,14 +110,19 @@ class FunctionGenerator { overridesEvaluate = true functionInterfaces.add(getQualifyingFunctionInterface(rFunction.inputs)) } - rBuildClass(rFunction, false, functionInterfaces, emptyMap, overridesEvaluate, topScope) + val Map, StringConcatenationClient> annotations = newLinkedHashMap + if (labelProviderUtil.shouldGenerateLabelProvider(func)) { + val labelProviderClass = rFunction.toLabelProviderJavaClass + annotations.put(RuneLabelProvider, '''labelProvider=«labelProviderClass».class''' ) + } + rBuildClass(rFunction, false, functionInterfaces, annotations, overridesEvaluate, topScope) } val content = buildClass(root.functions, classBody, topScope) fsa.generateFile(fileName, content) } - def rBuildClass(RFunction rFunction, boolean isStatic, List functionInterfaces, Map, String> annotations, boolean overridesEvaluate, JavaScope topScope) { + def rBuildClass(RFunction rFunction, boolean isStatic, List functionInterfaces, Map, StringConcatenationClient> annotations, boolean overridesEvaluate, JavaScope topScope) { val dependencies = collectFunctionDependencies(rFunction) rFunction.classBody(isStatic, overridesEvaluate, dependencies, functionInterfaces, annotations, topScope) } @@ -147,7 +154,7 @@ class FunctionGenerator { boolean overridesEvaluate, List> dependencies, List functionInterfaces, - Map, String> annotations, + Map, StringConcatenationClient> annotations, JavaScope scope ) { val className = scope.createIdentifier(function, function.toFunctionJavaClass.simpleName) diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/LabelProviderGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/LabelProviderGenerator.xtend new file mode 100644 index 000000000..e4ab96353 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/LabelProviderGenerator.xtend @@ -0,0 +1,161 @@ +package com.regnosys.rosetta.generator.java.function + +import org.eclipse.xtext.generator.IFileSystemAccess2 +import com.regnosys.rosetta.rosetta.simple.Function +import com.regnosys.rosetta.rosetta.RosettaReport +import com.regnosys.rosetta.generator.java.JavaScope +import com.regnosys.rosetta.types.RObjectFactory +import javax.inject.Inject +import org.eclipse.xtend2.lib.StringConcatenationClient +import com.regnosys.rosetta.types.RFunction +import com.regnosys.rosetta.generator.java.util.ImportManagerExtension +import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator +import com.rosetta.util.types.JavaClass +import com.rosetta.model.lib.functions.LabelProvider +import com.rosetta.model.lib.path.RosettaPath +import java.util.Map +import com.regnosys.rosetta.types.RType +import com.regnosys.rosetta.types.RDataType +import com.rosetta.util.DottedPath +import com.regnosys.rosetta.rosetta.simple.LabelAnnotation +import com.regnosys.rosetta.rosetta.RosettaRule +import com.regnosys.rosetta.rosetta.simple.AnnotationPathAttributeReference +import com.regnosys.rosetta.rosetta.expression.RosettaImplicitVariable +import com.regnosys.rosetta.rosetta.simple.AnnotationPath +import com.regnosys.rosetta.rosetta.simple.AnnotationDeepPath +import java.util.List +import com.regnosys.rosetta.rosetta.simple.AnnotationPathExpression +import com.regnosys.rosetta.utils.DeepFeatureCallUtil +import com.regnosys.rosetta.types.RosettaTypeProvider +import com.regnosys.rosetta.types.RChoiceType +import java.util.HashMap +import com.regnosys.rosetta.generator.util.RosettaFunctionExtensions +import org.apache.commons.text.StringEscapeUtils + +class LabelProviderGenerator { + @Inject extension ImportManagerExtension + @Inject RObjectFactory rObjectFactory + @Inject RosettaTypeProvider typeProvider + @Inject JavaTypeTranslator typeTranslator + @Inject DeepFeatureCallUtil deepPathUtil + @Inject LabelProviderGeneratorUtil util + + def void generateForFunctionIfApplicable(IFileSystemAccess2 fsa, Function func) { + if (util.shouldGenerateLabelProvider(func)) { + val rFunction = rObjectFactory.buildRFunction(func) + generate(fsa, rFunction) + } + } + def void generateForReport(IFileSystemAccess2 fsa, RosettaReport report) { + val rFunction = rObjectFactory.buildRFunction(report) + generate(fsa, rFunction) + } + private def void generate(IFileSystemAccess2 fsa, RFunction f) { + val javaClass = typeTranslator.toLabelProviderJavaClass(f) + val fileName = javaClass.canonicalName.withForwardSlashes + '.java' + + val topScope = new JavaScope(javaClass.packageName) + val StringConcatenationClient classBody = classBody(f, javaClass, topScope) + + val content = buildClass(javaClass.packageName, classBody, topScope) + fsa.generateFile(fileName, content) + } + + private def StringConcatenationClient classBody( + RFunction function, + JavaClass javaClass, + JavaScope topScope + ) { + val className = topScope.createIdentifier(function, javaClass.simpleName) + + val labelMap = newLinkedHashMap + gatherLabels(function.output.RMetaAnnotatedType.RType, null, labelMap) + + ''' + public class «className» implements «LabelProvider» { + private final «Map»<«RosettaPath», «String»> labelMap; + + public «className»() { + labelMap = new «HashMap»<>(); + + «FOR path : labelMap.keySet» + labelMap.put(«RosettaPath».valueOf("«path.withDots»"), "«StringEscapeUtils.escapeJava(labelMap.get(path))»"); + «ENDFOR» + } + + @Override + public «String» getLabel(«RosettaPath» path) { + RosettaPath normalized = path.toIndexless(); + return labelMap.get(normalized); + } + } + ''' + } + + private def void gatherLabels(RType currentType, DottedPath currentPath, Map labels) { + val t = if (currentType instanceof RChoiceType) { + currentType.asRDataType + } else { + currentType + } + if (t instanceof RDataType) { + for (attr : t.allAttributes) { + val attrPath = currentPath === null ? DottedPath.of(attr.name) : currentPath.child(attr.name) + // 1. Register labels on the type of this attribute + var attrType = attr.RMetaAnnotatedType.RType + gatherLabels(attrType, attrPath, labels) + + // 2. Register legacy `as` annotations from rule references + if (attr.ruleReference !== null) { + registerLegacyRuleAsLabel(attr.ruleReference, attrPath, labels) + } + + // 3. Register label annotations + attr.labelAnnotations.forEach[ + registerLabelAnnotation(it, attrPath, labels) + ] + } + } + } + + private def void registerLabelAnnotation(LabelAnnotation ann, DottedPath attrPath, Map labels) { + evaluateAnnotationPathExpression(#[attrPath], ann.path) + .forEach[ + labels.put(it, ann.label) + ] + } + private def void registerLegacyRuleAsLabel(RosettaRule rule, DottedPath attrPath, Map labels) { + if (rule.identifier !== null) { + labels.put(attrPath, rule.identifier) + } + } + + private def List evaluateAnnotationPathExpression(List currentPaths, AnnotationPathExpression expr) { + if (expr === null) { + currentPaths + } else if (expr instanceof AnnotationPathAttributeReference) { + currentPaths.map[child(expr.attribute.name)] + } else if (expr instanceof RosettaImplicitVariable) { + currentPaths + } else if (expr instanceof AnnotationPath) { + currentPaths.evaluateAnnotationPathExpression(expr.receiver).map[child(expr.attribute.name)] + } else if (expr instanceof AnnotationDeepPath) { + val rawType = typeProvider.getRMetaAnnotatedType(expr.receiver).RType + val t = if (rawType instanceof RChoiceType) { + rawType.asRDataType + } else { + rawType + } + if (t instanceof RDataType) { + currentPaths.evaluateAnnotationPathExpression(expr.receiver).flatMap[p| + deepPathUtil.findDeepFeaturePaths(t, rObjectFactory.buildRAttribute(expr.attribute)) + .map[deepPath| + p.concat(DottedPath.of(deepPath.map[name])) + ] + ].toList + } else { + #[] + } + } + } +} \ No newline at end of file diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/LabelProviderGeneratorUtil.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/LabelProviderGeneratorUtil.java new file mode 100644 index 000000000..2f2fc9f7c --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/function/LabelProviderGeneratorUtil.java @@ -0,0 +1,15 @@ +package com.regnosys.rosetta.generator.java.function; + +import javax.inject.Inject; + +import com.regnosys.rosetta.generator.util.RosettaFunctionExtensions; +import com.regnosys.rosetta.rosetta.simple.Function; + +public class LabelProviderGeneratorUtil { + @Inject + private RosettaFunctionExtensions funcExtensions; + + public boolean shouldGenerateLabelProvider(Function function) { + return !funcExtensions.getTransformAnnotations(function).isEmpty(); + } +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/reports/ReportGenerator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/reports/ReportGenerator.xtend index 5cde95a2f..cee47ca0b 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/reports/ReportGenerator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/reports/ReportGenerator.xtend @@ -13,6 +13,9 @@ import org.eclipse.xtext.generator.IFileSystemAccess2 import com.regnosys.rosetta.rosetta.RosettaReport import com.fasterxml.jackson.core.type.TypeReference import com.regnosys.rosetta.utils.ModelIdProvider +import org.eclipse.xtend2.lib.StringConcatenationClient +import com.rosetta.model.lib.annotations.RuneLabelProvider +import java.util.Map class ReportGenerator { @Inject extension RObjectFactory @@ -27,8 +30,13 @@ class ReportGenerator { val clazz = rFunction.toFunctionJavaClass val topScope = new JavaScope(clazz.packageName) val baseInterface = JavaParameterizedType.from(new TypeReference>() {}, rFunction.inputs.head.toMetaJavaType, rFunction.output.toMetaJavaType) - val reportAnnotationArguments = '''namespace="«report.model.toDottedPath»", body="«report.regulatoryBody.body.name»", corpusList={«FOR corpus: report.regulatoryBody.corpusList SEPARATOR ", "»"«corpus.name»"«ENDFOR»}''' - val classBody = functionGenerator.rBuildClass(rFunction, false, #[baseInterface], #{com.rosetta.model.lib.annotations.RosettaReport -> reportAnnotationArguments}, true, topScope); + + val Map, StringConcatenationClient> annotations = newLinkedHashMap + annotations.put(com.rosetta.model.lib.annotations.RosettaReport, '''namespace="«report.model.toDottedPath»", body="«report.regulatoryBody.body.name»", corpusList={«FOR corpus: report.regulatoryBody.corpusList SEPARATOR ", "»"«corpus.name»"«ENDFOR»}''') + val labelProviderClass = rFunction.toLabelProviderJavaClass + annotations.put(RuneLabelProvider, '''labelProvider=«labelProviderClass».class''') + + val classBody = functionGenerator.rBuildClass(rFunction, false, #[baseInterface], annotations, true, topScope); val content = buildClass(clazz.packageName, classBody, topScope) fsa.generateFile(clazz.canonicalName.withForwardSlashes + ".java", content) } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/JavaTypeTranslator.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/JavaTypeTranslator.java index 918be7567..e866650d0 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/JavaTypeTranslator.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/generator/java/types/JavaTypeTranslator.java @@ -58,6 +58,7 @@ import com.rosetta.model.lib.ModelSymbolId; import com.rosetta.model.lib.RosettaModelObject; import com.rosetta.model.lib.RosettaModelObjectBuilder; +import com.rosetta.model.lib.functions.LabelProvider; import com.rosetta.model.lib.functions.RosettaFunction; import com.rosetta.model.lib.reports.ReportFunction; import com.rosetta.model.lib.reports.Tabulator; @@ -128,6 +129,11 @@ public JavaClass toFunctionJavaClass(RFunction func) throw new IllegalStateException("Unknown origin of RFunction: " + func.getOrigin()); } } + public GeneratedJavaClass toLabelProviderJavaClass(RFunction function) { + DottedPath packageName = function.getNamespace().child("labels"); + String simpleName = function.getAlphanumericName() + "LabelProvider"; + return new GeneratedJavaClass<>(packageName, simpleName, LabelProvider.class); + } public JavaClass toFunctionJavaClass(Function func) { return generatedJavaClassService.toJavaFunction(modelIdProvider.getSymbolId(func)); } 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 7f2511c88..011840a20 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 @@ -64,6 +64,8 @@ import static com.regnosys.rosetta.rosetta.simple.SimplePackage.Literals.* import static extension com.regnosys.rosetta.types.RMetaAnnotatedType.withNoMeta import com.regnosys.rosetta.rosetta.simple.Attribute +import com.regnosys.rosetta.rosetta.simple.AnnotationPath +import com.regnosys.rosetta.rosetta.simple.AnnotationDeepPath /** * This class contains custom scoping description. @@ -162,6 +164,26 @@ class RosettaScopeProvider extends ImportedNamespaceAwareLocalScopeProvider { return defaultScope(context, reference) } } + case ANNOTATION_PATH_ATTRIBUTE_REFERENCE__ATTRIBUTE: { + if (context instanceof Attribute) { + val t = typeProvider.getRTypeOfSymbol(context) + return Scopes.scopeFor(t.allFeatures(context)) + } + } + case ANNOTATION_PATH__ATTRIBUTE: { + if (context instanceof AnnotationPath) { + val t = typeProvider.getRMetaAnnotatedType(context.receiver) + return Scopes.scopeFor(t.allFeatures(context)) + } + return IScope.NULLSCOPE + } + case ANNOTATION_DEEP_PATH__ATTRIBUTE: { + if (context instanceof AnnotationDeepPath) { + val t = typeProvider.getRMetaAnnotatedType(context.receiver) + return createDeepFeatureScope(t.RType) + } + return IScope.NULLSCOPE + } case ROSETTA_SYMBOL_REFERENCE__SYMBOL: { if (context instanceof Operation) { val function = context.function diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RAttribute.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RAttribute.java index 806f9cb0a..7aeb1fc8c 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RAttribute.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/types/RAttribute.java @@ -23,6 +23,7 @@ import com.regnosys.rosetta.rosetta.RosettaDocReference; import com.regnosys.rosetta.rosetta.RosettaRule; import com.regnosys.rosetta.rosetta.simple.Attribute; +import com.regnosys.rosetta.rosetta.simple.LabelAnnotation; public class RAttribute implements RAssignedRoot { private final boolean isOverride; @@ -32,6 +33,7 @@ public class RAttribute implements RAssignedRoot { private final RMetaAnnotatedType rMetaAnnotatedType; private final RCardinality cardinality; private final RosettaRule ruleReference; + private final List labelAnnotations; private final Attribute origin; private final RObjectFactory rObjectFactory; @@ -39,7 +41,7 @@ public class RAttribute implements RAssignedRoot { public RAttribute(boolean isOverride, String name, String definition, List docReferences, RMetaAnnotatedType rMetaAnnotatedType, RCardinality cardinality, - RosettaRule ruleReference, Attribute origin, RObjectFactory rObjectFactory) { + RosettaRule ruleReference, List labelAnnotations, Attribute origin, RObjectFactory rObjectFactory) { this.isOverride = isOverride; this.name = name; this.definition = definition; @@ -47,6 +49,7 @@ public RAttribute(boolean isOverride, String name, String definition, List getLabelAnnotations() { + RAttribute p = getParentAttribute(); + List parentLabels; + if (p == null || (parentLabels = p.getLabelAnnotations()).isEmpty()) { + return labelAnnotations; + } + List labels = new ArrayList<>(docReferences.size() + parentLabels.size()); + labels.addAll(parentLabels); + labels.addAll(labelAnnotations); + return labels; + } + public RAttribute getParentAttribute() { if (parentAttribute == null && origin.isOverride()) { parentAttribute = rObjectFactory.buildRAttributeOfParent(origin); 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 6ec46e328..e16ec0140 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 @@ -75,7 +75,7 @@ public RFunction buildRFunction(Function function) { private RAttribute createArtificialAttribute(String name, RType type, boolean isMulti) { RMetaAnnotatedType rAnnotatedType = RMetaAnnotatedType.withNoMeta(type); - return new RAttribute(false, name, null, Collections.emptyList(), rAnnotatedType, isMulti ? RCardinality.UNBOUNDED : RCardinality.OPTIONAL, null, null, this); + return new RAttribute(false, name, null, Collections.emptyList(), rAnnotatedType, isMulti ? RCardinality.UNBOUNDED : RCardinality.OPTIONAL, null, Collections.emptyList(), null, this); } public RFunction buildRFunction(RosettaRule rule) { RType inputRType = typeSystem.typeCallToRType(rule.getInput()); @@ -177,7 +177,7 @@ public RAttribute buildRAttribute(Attribute attr) { RosettaRuleReference ruleRef = attr.getRuleReference(); return new RAttribute(attr.isOverride(), attr.getName(), attr.getDefinition(), attr.getReferences(), rAnnotatedType, - card, ruleRef != null ? ruleRef.getReportingRule() : null, attr, this); + card, ruleRef != null ? ruleRef.getReportingRule() : null, attr.getLabels(), attr, this); } public RAttribute buildRAttributeOfParent(Attribute attr) { Attribute parent = ecoreUtil.getParentAttribute(attr); 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 13b3b230b..c87fb04cc 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,8 +81,6 @@ import java.util.Optional import javax.inject.Inject import javax.inject.Provider import org.eclipse.emf.ecore.EObject -import org.eclipse.xtend2.lib.StringConcatenationClient -import org.eclipse.xtext.naming.IQualifiedNameProvider import com.regnosys.rosetta.rosetta.expression.SwitchCaseOrDefault import static extension com.regnosys.rosetta.types.RMetaAnnotatedType.withMeta import com.regnosys.rosetta.rosetta.RosettaParameter @@ -92,7 +90,11 @@ import com.regnosys.rosetta.utils.OptionalUtil import java.util.Map import static extension com.regnosys.rosetta.types.RMetaAnnotatedType.withNoMeta import com.regnosys.rosetta.rosetta.simple.Attribute - +import com.regnosys.rosetta.rosetta.simple.AnnotationPathExpression +import com.regnosys.rosetta.rosetta.simple.AnnotationPathAttributeReference +import com.regnosys.rosetta.rosetta.simple.AnnotationPath +import com.regnosys.rosetta.rosetta.simple.AnnotationDeepPath +import org.eclipse.xtext.EcoreUtil2 class RosettaTypeProvider extends RosettaExpressionSwitch> { public static String EXPRESSION_RTYPE_CACHE_KEY = RosettaTypeProvider.canonicalName + ".EXPRESSION_RTYPE" @@ -137,6 +139,14 @@ class RosettaTypeProvider extends RosettaExpressionSwitch findFeaturesOfImplicitVariable(EObject context) { return extensions.allFeatures(typeOfImplicitVariable(context), context) } + def RMetaAnnotatedType getRMetaAnnotatedType(AnnotationPathExpression it) { + switch it { + AnnotationPathAttributeReference: attribute.RTypeOfSymbol + RosettaImplicitVariable: EcoreUtil2.getContainerOfType(it, Attribute)?.RTypeOfSymbol ?: NOTHING_WITH_NO_META + AnnotationPath: attribute.RTypeOfSymbol + AnnotationDeepPath: attribute.RTypeOfSymbol + } + } def List getRMetaAttributesOfSymbol(RosettaSymbol symbol) { if (symbol instanceof Attribute) { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/DeepFeatureCallUtil.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/DeepFeatureCallUtil.java index c6f5c2c76..c3d763cff 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/DeepFeatureCallUtil.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/DeepFeatureCallUtil.java @@ -1,8 +1,11 @@ package com.regnosys.rosetta.utils; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import com.regnosys.rosetta.rosetta.expression.OneOfOperation; import com.regnosys.rosetta.rosetta.expression.RosettaExpression; @@ -14,7 +17,36 @@ import com.regnosys.rosetta.types.RDataType; import com.regnosys.rosetta.types.RType; -public class DeepFeatureCallUtil { +public class DeepFeatureCallUtil { + public List> findDeepFeaturePaths(RDataType type, RAttribute deepFeature) { + List> result = new ArrayList<>(); + findDeepFeaturePaths(type, new ArrayList<>(), deepFeature, result); + return result; + } + private void findDeepFeaturePaths(RDataType currentType, List currentPath, RAttribute deepFeature, List> paths) { + Collection allAttrs = currentType.getAllAttributes(); + Optional matchingAttribute = allAttrs.stream().filter(a -> match(a, deepFeature)).findAny(); + matchingAttribute + .ifPresentOrElse( + a -> { + currentPath.add(a); + paths.add(currentPath); + }, + () -> { + allAttrs.forEach(a -> { + RType attrType = a.getRMetaAnnotatedType().getRType(); + if (attrType instanceof RChoiceType) { + attrType = ((RChoiceType) attrType).asRDataType(); + } + if (attrType instanceof RDataType) { + List newPath = new ArrayList<>(currentPath); + newPath.add(a); + findDeepFeaturePaths((RDataType) attrType, newPath, deepFeature, paths); + } + }); + }); + } + public Collection findDeepFeatures(RDataType type) { return findDeepFeatureMap(type).values(); } 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 c4379c4c6..82b2603aa 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 @@ -837,21 +837,23 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { if (baseRType instanceof RChoiceType) { baseRType = baseRType.asRDataType } - if (!(baseRType instanceof RDataType || baseRType instanceof RRecordType)) { + if (!(baseRType instanceof RDataType || baseRType instanceof RRecordType || baseRType == NOTHING)) { error('''Cannot construct an instance of type `«rType.name»`.''', ele.typeCall, null) } val seenFeatures = newHashSet for (pair : ele.values) { val feature = pair.key - val expr = pair.value - if (!seenFeatures.add(feature)) { - error('''Duplicate attribute `«feature.name»`.''', pair, CONSTRUCTOR_KEY_VALUE_PAIR__KEY) - } - checkType(feature.getRTypeOfFeature(null), expr, pair, CONSTRUCTOR_KEY_VALUE_PAIR__VALUE, INSIGNIFICANT_INDEX) - if(!cardinality.isFeatureMulti(feature) && cardinality.isMulti(expr)) { - error('''Expecting single cardinality for attribute `«feature.name»`.''', pair, - CONSTRUCTOR_KEY_VALUE_PAIR__VALUE) + if (feature.isResolved) { + val expr = pair.value + if (!seenFeatures.add(feature)) { + error('''Duplicate attribute `«feature.name»`.''', pair, CONSTRUCTOR_KEY_VALUE_PAIR__KEY) + } + checkType(feature.getRTypeOfFeature(null), expr, pair, CONSTRUCTOR_KEY_VALUE_PAIR__VALUE, INSIGNIFICANT_INDEX) + if(!cardinality.isFeatureMulti(feature) && cardinality.isMulti(expr)) { + error('''Expecting single cardinality for attribute `«feature.name»`.''', pair, + CONSTRUCTOR_KEY_VALUE_PAIR__VALUE) + } } } diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RuneLabelProvider.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RuneLabelProvider.java new file mode 100644 index 000000000..2d6d63a05 --- /dev/null +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/annotations/RuneLabelProvider.java @@ -0,0 +1,13 @@ +package com.rosetta.model.lib.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.rosetta.model.lib.functions.LabelProvider; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface RuneLabelProvider { + Class labelProvider(); +} diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/flatten/ModelObjectFlattener.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/flatten/ModelObjectFlattener.java new file mode 100644 index 000000000..7312597e8 --- /dev/null +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/flatten/ModelObjectFlattener.java @@ -0,0 +1,138 @@ +package com.rosetta.model.lib.flatten; + +import com.rosetta.model.lib.RosettaModelObject; +import com.rosetta.model.lib.RosettaModelObjectBuilder; +import com.rosetta.model.lib.meta.FieldWithMeta; +import com.rosetta.model.lib.path.RosettaPath; +import com.rosetta.model.lib.path.RosettaPathValue; +import com.rosetta.model.lib.process.AttributeMeta; +import com.rosetta.model.lib.process.BuilderProcessor; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Flattens a {@link RosettaModelObject} into a list of {@link RosettaPathValue} objects. + * This effectively transforms a nested object structure into a flat list of path-value pairs, + * omitting metadata fields. + */ +public class ModelObjectFlattener { + + /** + * Flattens the provided RosettaModelObject. + * + * @param modelObject The RosettaModelObject to flatten. + * @return A list of RosettaPathValue objects representing the flattened object. + */ + public List flatten(RosettaModelObject modelObject) { + FlattenerBuilderProcessor processor = new FlattenerBuilderProcessor(); + modelObject.toBuilder().process(RosettaPath.valueOf(modelObject.getType().getSimpleName()), processor); + + List metaPaths = processor.getMetaPaths(); + List pathValues = processor.getRosettaPathValue(); + + return removeAllMetaPaths(metaPaths, pathValues); + } + + /** + * Removes all metadata paths from the provided list of RosettaPathValues. + * + * @param metaPaths The list of metadata paths to remove. + * @param pathValues The list of RosettaPathValues to filter. + * @return A new list of RosettaPathValues with metadata paths removed. + */ + private List removeAllMetaPaths(List metaPaths, List pathValues) { + return pathValues.stream() + .map(pathValue -> new RosettaPathValue(removeAllMetaPaths(metaPaths, pathValue.getPath()), pathValue.getValue())) + .collect(Collectors.toList()); + } + + /** + * Removes all metadata path segments from the provided RosettaPath. + * + * @param metaPaths The list of metadata paths to use for filtering. + * @param path The RosettaPath to filter. + * @return A new RosettaPath with metadata segments removed. + */ + private RosettaPath removeAllMetaPaths(List metaPaths, RosettaPath path) { + LinkedList elements = path.allElements(); + metaPaths.stream() + .filter(path::startsWith) + .map(RosettaPath::allElements) + .map(LinkedList::size) + .map(i -> i - 1) + .distinct() + .sorted(Comparator.reverseOrder()) + .forEach(i -> elements.remove((int) i)); + + RosettaPath newPath = RosettaPath.createPathFromElements(elements); + return newPath.trimFirst(); + } + + /** + * A {@link BuilderProcessor} implementation used to extract path-value pairs and metadata paths + * during the flattening process. + */ + private static class FlattenerBuilderProcessor implements BuilderProcessor { + + private final List rosettaPathValues = new ArrayList<>(); + private final List metaPaths = new ArrayList<>(); + + /** + * Returns the list of identified metadata paths. + * @return The list of metadata paths. + */ + public List getMetaPaths() { + return metaPaths; + } + + /** + * Returns the list of RosettaPathValue objects, excluding metadata paths. + * @return The list of RosettaPathValue objects. + */ + public List getRosettaPathValue() { + return rosettaPathValues; + } + + @Override + public boolean processRosetta(RosettaPath path, Class rosettaType, RosettaModelObjectBuilder builder, RosettaModelObjectBuilder parent, AttributeMeta... metas) { + if (builder != null && parent instanceof FieldWithMeta) { + metaPaths.add(path); + } + return true; + } + + @Override + public boolean processRosetta(RosettaPath path, Class rosettaType, List builders, RosettaModelObjectBuilder parent, AttributeMeta... metas) { + int i = 0; + for (RosettaModelObjectBuilder builder : builders) { + processRosetta(path.withIndex(i++), rosettaType, builder, parent, metas); + } + return true; + } + + @Override + public void processBasic(RosettaPath path, Class rosettaType, T instance, RosettaModelObjectBuilder parent, AttributeMeta... metas) { + if (instance != null) { + if (parent instanceof FieldWithMeta) { + rosettaPathValues.add(new RosettaPathValue(path.getParent(), instance)); + } else { + rosettaPathValues.add(new RosettaPathValue(path, instance)); + } + } + } + + @Override + public void processBasic(RosettaPath path, Class rosettaType, Collection instances, RosettaModelObjectBuilder parent, AttributeMeta... metas) { + int i = 0; + for (T instance : instances) { + processBasic(path.withIndex(i++), rosettaType, instance, parent, metas); + } + } + + @Override + public Report report() { + return null; + } + } +} \ No newline at end of file diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/functions/LabelProvider.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/functions/LabelProvider.java new file mode 100644 index 000000000..dd5270032 --- /dev/null +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/functions/LabelProvider.java @@ -0,0 +1,12 @@ +package com.rosetta.model.lib.functions; + +import com.rosetta.model.lib.path.RosettaPath; + +/** + * Provides a label - a human readable name - for a given `RosettaPath`. + * + * A label may be null if the provider does not know one for the given path. + */ +public interface LabelProvider { + String getLabel(RosettaPath path); +} diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPath.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPath.java index 2a6182662..af54b840f 100644 --- a/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPath.java +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPath.java @@ -172,6 +172,25 @@ public boolean containsPath(RosettaPath subPath) { if (parent==null) return false; return parent.containsPath(subPath); } + + public boolean startsWith(RosettaPath other) { + LinkedList list = this.allElements(); + LinkedList prefix = other.allElements(); + if (prefix == null || prefix.isEmpty()) { + return true; + } + if (list == null || list.size() < prefix.size()) { + return false; + } + + for (int i = 0; i < prefix.size(); i++) { + if (!list.get(i).equals(prefix.get(i))) { + return false; + } + } + + return true; + } @Override public boolean equals(Object o) { diff --git a/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPathValue.java b/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPathValue.java new file mode 100644 index 000000000..1b6019973 --- /dev/null +++ b/rosetta-runtime/src/main/java/com/rosetta/model/lib/path/RosettaPathValue.java @@ -0,0 +1,27 @@ +package com.rosetta.model.lib.path; + +public class RosettaPathValue { + private final RosettaPath path; + private final Object value; + + public RosettaPathValue(RosettaPath path, Object value) { + this.path = path; + this.value = value; + } + + @Override + public String toString() { + return "FieldValue{" + + "path=" + path + + ", value=" + value + + '}'; + } + + public RosettaPath getPath() { + return path; + } + + public Object getValue() { + return value; + } +} diff --git a/rosetta-runtime/src/main/resources/model/annotations.rosetta b/rosetta-runtime/src/main/resources/model/annotations.rosetta index e2e163661..692a7acba 100644 --- a/rosetta-runtime/src/main/resources/model/annotations.rosetta +++ b/rosetta-runtime/src/main/resources/model/annotations.rosetta @@ -30,4 +30,3 @@ annotation enrich: <"Marks a function that performs enrichment operations"> annotation projection: <"Marks a function that performs projection operations with the out bound serialisation format"> JSON boolean (0..1) XML boolean (0..1) - \ No newline at end of file diff --git a/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/JavaTestModel.java b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/JavaTestModel.java new file mode 100644 index 000000000..fd194b9e0 --- /dev/null +++ b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/JavaTestModel.java @@ -0,0 +1,205 @@ +package com.regnosys.rosetta.tests.testmodel; + +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +import com.google.inject.Injector; +import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator; +import com.regnosys.rosetta.generator.java.types.RJavaEnum; +import com.regnosys.rosetta.generator.java.types.RJavaEnumValue; +import com.regnosys.rosetta.rosetta.RosettaEnumeration; +import com.regnosys.rosetta.rosetta.RosettaReport; +import com.regnosys.rosetta.rosetta.RosettaRule; +import com.regnosys.rosetta.rosetta.simple.Data; +import com.regnosys.rosetta.rosetta.simple.Function; +import com.regnosys.rosetta.tests.compiler.InMemoryJavacCompiler; +import com.regnosys.rosetta.tests.util.ExpressionJavaEvaluatorService; +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.rosetta.model.lib.RosettaModelObject; +import com.rosetta.model.lib.functions.LabelProvider; +import com.rosetta.model.lib.functions.RosettaFunction; +import com.rosetta.util.types.JavaType; + +/** + * A test utility for integration tests related to Java code generation. + * + * It allows access to generated Java source files and compiled classes + * based on their original Rune name in the model. Additionally, it allows + * creating instances of such classes, either by injecting (functions) or + * by evaluating expressions (types and values). + */ +public class JavaTestModel { + private final RosettaTestModel rosettaModel; + private final Map javaSourceCode; + + private InMemoryJavacCompiler inMemoryCompiler = null; + private Map> javaClasses = null; + + private final RObjectFactory rObjectFactory; + private final JavaTypeTranslator typeTranslator; + private final ExpressionJavaEvaluatorService evaluatorService; + private final Injector injector; + + public JavaTestModel(RosettaTestModel rosettaModel, Map javaSourceCode, RObjectFactory rObjectFactory, JavaTypeTranslator typeTranslator, ExpressionJavaEvaluatorService evaluatorService, Injector injector) { + this.rosettaModel = rosettaModel; + this.javaSourceCode = javaSourceCode; + + this.rObjectFactory = rObjectFactory; + this.typeTranslator = typeTranslator; + this.evaluatorService = evaluatorService; + this.injector = injector; + } + + private String getSource(JavaType javaType) { + String source = javaSourceCode.get(javaType.toString()); + if (source == null) { + throw new NoSuchElementException("No Java source found for " + javaType + ".\n\n" + javaSourceCode.keySet().stream().collect(Collectors.joining("\n"))); + } + return source; + } + private void assertCompiled() { + if (javaClasses == null) { + throw new IllegalStateException("Model is not compiled yet. Please call `JavaTestModel::compile` before loading classes."); + } + } + private Class getClass(Class superClass, JavaType javaType) { + assertCompiled(); + + Class javaClass = javaClasses.get(javaType.toString()); + if (javaClass == null) { + throw new NoSuchElementException("No Java class found for " + javaType + ".\n\n" + javaClasses.keySet().stream().collect(Collectors.joining("\n"))); + } + return javaClass.asSubclass(superClass); + } + + public JavaTestModel compile() { + if (javaClasses != null) { + throw new IllegalStateException("Model was already compiled!"); + } + + inMemoryCompiler = InMemoryJavacCompiler + .newInstance() + .useParentClassLoader(this.getClass().getClassLoader()) + .useOptions("--release", "8", "-Xlint:all", "-Xdiags:verbose"); + + javaSourceCode.forEach((className, sourceCode) -> inMemoryCompiler.addSource(className, sourceCode)); + + javaClasses = inMemoryCompiler.compileAll(); + + return this; + } + + public T evaluateExpression(Class resultType, CharSequence expr) { + assertCompiled(); + + return resultType.cast(evaluatorService.evaluate(expr, rosettaModel.getModel(), JavaType.from(resultType), inMemoryCompiler)); + } + + + public RosettaTestModel getRosettaModel() { + return rosettaModel; + } + + private JavaType getTypeJavaType(String name) { + Data type = rosettaModel.getType(name); + RDataType t = rObjectFactory.buildRDataType(type); + return typeTranslator.toJavaReferenceType(t); + } + public String getTypeJavaSource(String name) { + return getSource(getTypeJavaType(name)); + } + public Class getTypeJavaClass(String name) { + return getClass(RosettaModelObject.class, getTypeJavaType(name)); + } + + private RJavaEnum getEnumJavaType(String name) { + RosettaEnumeration enumeration = rosettaModel.getEnum(name); + REnumType t = rObjectFactory.buildREnumType(enumeration); + return typeTranslator.toJavaReferenceType(t); + } + public String getEnumJavaSource(String name) { + return getSource(getEnumJavaType(name)); + } + @SuppressWarnings("unchecked") + public Class> getEnumJavaClass(String name) { + return (Class>) getClass(Enum.class, getEnumJavaType(name)); + } + public Object getEnumJavaValue(String enumName, String valueName) { + RJavaEnum enumJavaType = getEnumJavaType(enumName); + Class enumJavaClass = getClass(Enum.class, enumJavaType); + RJavaEnumValue enumValueRepr = enumJavaType.getEnumValues().stream() + .filter(v -> valueName.equals(v.getRosettaName())) + .findAny() + .orElseThrow(() -> new NoSuchElementException("The enum " + enumName + " does not have a value named " + valueName)); + for (Object enumConst : enumJavaClass.getEnumConstants()) { + if (enumConst.toString().equals(enumValueRepr.getName())) { + return enumConst; + } + } + return null; + } + + private JavaType getFunctionJavaType(String name) { + Function func = rosettaModel.getFunction(name); + RFunction f = rObjectFactory.buildRFunction(func); + return typeTranslator.toFunctionJavaClass(f); + } + public String getFunctionJavaSource(String name) { + return getSource(getFunctionJavaType(name)); + } + public Class getFunctionJavaClass(String name) { + return getClass(RosettaFunction.class, getFunctionJavaType(name)); + } + public RosettaFunction getFunctionJavaInstance(String name) { + return injector.getInstance(getFunctionJavaClass(name)); + } + + private JavaType getFunctionJavaLabelProviderType(String name) { + Function func = rosettaModel.getFunction(name); + RFunction f = rObjectFactory.buildRFunction(func); + return typeTranslator.toLabelProviderJavaClass(f); + } + public String getFunctionJavaLabelProviderSource(String name) { + return getSource(getFunctionJavaLabelProviderType(name)); + } + public Class getFunctionJavaLabelProviderClass(String name) { + return getClass(LabelProvider.class, getFunctionJavaLabelProviderType(name)); + } + public LabelProvider getFunctionJavaLabelProviderInstance(String name) { + return injector.getInstance(getFunctionJavaLabelProviderClass(name)); + } + + private JavaType getRuleJavaType(String name) { + RosettaRule rule = rosettaModel.getRule(name); + RFunction f = rObjectFactory.buildRFunction(rule); + return typeTranslator.toFunctionJavaClass(f); + } + public String getRuleJavaSource(String name) { + return getSource(getRuleJavaType(name)); + } + public Class getRuleJavaClass(String name) { + return getClass(RosettaFunction.class, getRuleJavaType(name)); + } + public RosettaFunction getRuleJavaInstance(String name) { + return injector.getInstance(getRuleJavaClass(name)); + } + + private JavaType getReportJavaType(String body, String... corpusList) { + RosettaReport report = rosettaModel.getReport(body, corpusList); + RFunction f = rObjectFactory.buildRFunction(report); + return typeTranslator.toFunctionJavaClass(f); + } + public String getReportJavaSource(String body, String... corpusList) { + return getSource(getReportJavaType(body, corpusList)); + } + public Class getReportJavaClass(String body, String... corpusList) { + return getClass(RosettaFunction.class, getReportJavaType(body, corpusList)); + } + public RosettaFunction getReportJavaInstance(String body, String... corpusList) { + return injector.getInstance(getReportJavaClass(body, corpusList)); + } +} diff --git a/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/RosettaTestModel.java b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/RosettaTestModel.java new file mode 100644 index 000000000..63022bbe0 --- /dev/null +++ b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/RosettaTestModel.java @@ -0,0 +1,85 @@ +package com.regnosys.rosetta.tests.testmodel; + +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.eclipse.emf.ecore.EObject; + +import com.regnosys.rosetta.rosetta.RegulatoryDocumentReference; +import com.regnosys.rosetta.rosetta.RosettaEnumeration; +import com.regnosys.rosetta.rosetta.RosettaModel; +import com.regnosys.rosetta.rosetta.RosettaNamed; +import com.regnosys.rosetta.rosetta.RosettaReport; +import com.regnosys.rosetta.rosetta.RosettaRootElement; +import com.regnosys.rosetta.rosetta.RosettaRule; +import com.regnosys.rosetta.rosetta.RosettaTypeAlias; +import com.regnosys.rosetta.rosetta.simple.Data; +import com.regnosys.rosetta.rosetta.simple.Function; + +/** + * A test utility for accessing elements in a Rune model by name. + */ +public class RosettaTestModel { + private final String source; + private final RosettaModel model; + + public RosettaTestModel(CharSequence source, RosettaModel model) { + this.source = source.toString(); + this.model = model; + } + + public RosettaModel getModel() { + return model; + } + + private T getElementMatching(Class clazz, Predicate match, Supplier exceptionSupplier) { + return model.getElements().stream() + .filter(elem -> clazz.isInstance(elem)) + .map(elem -> clazz.cast(elem)) + .filter(match) + .findAny() + .orElseThrow(exceptionSupplier); + } + private T getNamedElement(Class clazz, String name) { + RosettaNamed elem = getElementMatching(RosettaNamed.class, x -> name.equals(x.getName()), () -> new NoSuchElementException("No element named '" + name + "' found in model.\n\n" + source)); + if (!clazz.isInstance(elem)) { + throw new ClassCastException("The element named '" + name + "' is of type " + elem.getClass().getSimpleName() + ", not " + clazz.getSimpleName() + ".\n\n" + source); + } + return clazz.cast(elem); + } + + public Data getType(String name) { + return getNamedElement(Data.class, name); + } + public RosettaEnumeration getEnum(String name) { + return getNamedElement(RosettaEnumeration.class, name); + } + public RosettaTypeAlias getTypeAlias(String name) { + return getNamedElement(RosettaTypeAlias.class, name); + } + public Function getFunction(String name) { + return getNamedElement(Function.class, name); + } + public RosettaRule getRule(String name) { + return getNamedElement(RosettaRule.class, name); + } + public RosettaReport getReport(String body, String... corpusList) { + return getElementMatching(RosettaReport.class, x -> { + RegulatoryDocumentReference ref = x.getRegulatoryBody(); + if (!body.equals(ref.getBody().getName())) { + return false; + } + if (corpusList.length != ref.getCorpusList().size()) { + return false; + } + for (int i=0; i new NoSuchElementException("No report with body " + body + " and corpus list " + Arrays.toString(corpusList) + " found in model.\n\n" + source)); + } +} diff --git a/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/RosettaTestModelService.java b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/RosettaTestModelService.java new file mode 100644 index 000000000..dc51d3df4 --- /dev/null +++ b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/testmodel/RosettaTestModelService.java @@ -0,0 +1,128 @@ +package com.regnosys.rosetta.tests.testmodel; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.commons.lang3.stream.Streams; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.testing.validation.ValidationTestHelper; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import com.google.inject.Injector; +import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator; +import com.regnosys.rosetta.rosetta.RosettaModel; +import com.regnosys.rosetta.tests.util.CodeGeneratorTestHelper; +import com.regnosys.rosetta.tests.util.ExpressionJavaEvaluatorService; +import com.regnosys.rosetta.tests.util.ModelHelper; +import com.regnosys.rosetta.types.RObjectFactory; + +/** + * A utility to create or load Rosetta models. + * + * The result is wrapped in a `RosettaTestModel` or `JavaTestModel` + * to allow easy access to Ecore objects or generated Java based on + * the name of the object in the model. + */ +public class RosettaTestModelService { + @Inject + private ModelHelper modelHelper; + @Inject + private ValidationTestHelper validationHelper; + @Inject + private CodeGeneratorTestHelper codeGeneratorHelper; + @Inject + private RObjectFactory rObjectFactory; + @Inject + private JavaTypeTranslator typeTranslator; + @Inject + private ExpressionJavaEvaluatorService evaluatorService; + @Inject + private Injector injector; + + /** + * Load a test model from a character sequence. It will assert that there are no issues in the model. + */ + public RosettaTestModel toTestModel(CharSequence source) { + return toTestModel(source, true); + } + /** + * Load a test model from a character sequence, optionally asserting that there are no issues in the model. + */ + public RosettaTestModel toTestModel(CharSequence source, boolean assertNoIssues) { + RosettaModel model; + if (assertNoIssues) { + model = modelHelper.parseRosettaWithNoIssues(source); + } else { + model = modelHelper.parseRosetta(source); + } + return new RosettaTestModel(source, model); + } + /** + * Load a test model from a file or folder on the classpath. It will assert that there are no issues in the model. + */ + public RosettaTestModel loadTestModelFromResources(String resourceFolderOrFile) throws IOException { + ResourceSet resourceSet = modelHelper.testResourceSet(); + + List resources = new ArrayList<>(); + URL resourceURL = getClass().getResource(resourceFolderOrFile); + Path resourcePath; + try { + resourcePath = Path.of(resourceURL.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + (Files.isDirectory(resourcePath) ? Files.walk(resourcePath, 1) : Streams.of(new Path[] {resourcePath})) + .filter(p -> !Files.isDirectory(p)) + .map(p -> URI.createURI(p.toUri().toString())) + .filter(uri -> uri.fileExtension().equals("rosetta")) + .forEach(uri -> { + Resource res = resourceSet.getResource(uri, true); + resources.add(res); + }); + resources.forEach(res -> { + EcoreUtil2.resolveAll(res); + validationHelper.assertNoIssues(res); + }); + + if (resources.size() != 1) { + throw new IllegalArgumentException("Expecting 1 rosetta file at " + resourceFolderOrFile + ", but found " + resources.size()); + } + XtextResource resource = (XtextResource) resources.get(0); + + String source = CharStreams.toString(new InputStreamReader(resourceSet.getURIConverter().createInputStream(resource.getURI(), resourceSet.getLoadOptions()), Charsets.UTF_8)); + RosettaModel model = (RosettaModel) resource.getContents().get(0); + return new RosettaTestModel(source, model); + } + + /** + * Load a test model from a character sequence, and generate Java code. + */ + public JavaTestModel toJavaTestModel(CharSequence source) { + RosettaTestModel rosettaModel = toTestModel(source); + Map javaCode = codeGeneratorHelper.generateCode(rosettaModel.getModel()); + return new JavaTestModel(rosettaModel, javaCode, rObjectFactory, typeTranslator, evaluatorService, injector); + } + + /** + * Load a test model from a file or folder on the classpath, and generate Java code. + */ + public JavaTestModel loadJavaTestModelFromResources(String resourceFolderOrFile) throws IOException { + RosettaTestModel rosettaModel = loadTestModelFromResources(resourceFolderOrFile); + Map javaCode = codeGeneratorHelper.generateCode(rosettaModel.getModel()); + return new JavaTestModel(rosettaModel, javaCode, rObjectFactory, typeTranslator, evaluatorService, injector); + } +} diff --git a/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ExpressionJavaEvaluatorService.xtend b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ExpressionJavaEvaluatorService.xtend new file mode 100644 index 000000000..4a91ba5cf --- /dev/null +++ b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ExpressionJavaEvaluatorService.xtend @@ -0,0 +1,67 @@ +package com.regnosys.rosetta.tests.util + +import com.regnosys.rosetta.tests.util.ExpressionParser +import javax.inject.Inject +import com.regnosys.rosetta.generator.java.expression.ExpressionGenerator +import com.rosetta.util.types.JavaType +import com.regnosys.rosetta.generator.java.JavaScope +import com.rosetta.util.DottedPath +import com.regnosys.rosetta.tests.compiler.InMemoryJavacCompiler +import com.regnosys.rosetta.generator.java.util.ImportManagerExtension +import org.eclipse.xtend2.lib.StringConcatenationClient +import org.eclipse.xtext.testing.validation.ValidationTestHelper +import com.regnosys.rosetta.rosetta.RosettaModel +import com.regnosys.rosetta.generator.java.expression.JavaDependencyProvider +import com.google.inject.Injector +import com.regnosys.rosetta.generator.java.JavaIdentifierRepresentationService + +class ExpressionJavaEvaluatorService { + @Inject + ExpressionParser expressionParser + @Inject + ExpressionGenerator expressionGenerator + @Inject + JavaDependencyProvider dependencyProvider + @Inject + extension JavaIdentifierRepresentationService + @Inject + ValidationTestHelper validationHelper + @Inject + Injector injector + @Inject + extension ImportManagerExtension + + def Object evaluate(CharSequence rosettaExpression, RosettaModel context, JavaType expectedType, InMemoryJavacCompiler compiler) { + val expr = expressionParser.parseExpression(rosettaExpression, #[context]) + validationHelper.assertNoIssues(expr) + + val packageName = DottedPath.splitOnDots("com.regnosys.rosetta.tests.testexpression") + val className = "TestExpressionEvaluator" + val methodName = "evaluate" + + val packageScope = new JavaScope(packageName) + val classScope = packageScope.classScope(className) + val evaluateScope = classScope.methodScope(methodName) + + val dependencies = dependencyProvider.javaDependencies(expr) + dependencies.forEach[classScope.createIdentifier(toDependencyInstance, simpleName.toFirstLower)] + + val javaCode = expressionGenerator.javaCode(expr, expectedType, evaluateScope) + val StringConcatenationClient content = ''' + public class «className» { + «FOR dep : dependencies» + @«Inject» + private «dep» «classScope.getIdentifierOrThrow(dep.toDependencyInstance)»; + «ENDFOR» + + public «expectedType» «methodName»() «javaCode.completeAsReturn.toBlock» + } + ''' + + val sourceCode = buildClass(packageName, content, packageScope) + val evaluatorClass = compiler.compile(packageName.child(className).withDots, sourceCode) + val instance = injector.getInstance(evaluatorClass) + + evaluatorClass.getDeclaredMethod(methodName).invoke(instance) + } +} diff --git a/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ModelHelper.xtend b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ModelHelper.xtend index c993a2285..83c63d327 100644 --- a/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ModelHelper.xtend +++ b/rosetta-testing/src/main/java/com/regnosys/rosetta/tests/util/ModelHelper.xtend @@ -37,16 +37,6 @@ class ModelHelper { segment section segment field - stereotype entityReferenceData - stereotype productReferenceData - stereotype commondReferenceData - stereotype preExecutionActivity - stereotype executionActivity - stereotype postExecutionActivity - stereotype contractualProduct - stereotype regulatoryEligibility - stereotype regulatoryReporting - synonym source FIX synonym source FpML synonym source DTCC