diff --git a/.gitignore b/.gitignore index 39b1aee..0fdb887 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ __pycache__ **/test/cdm_tests/common-domain-model/ src/generated/ *.todo +/build/ diff --git a/docs/FUNCTION_SUPPORT_DEV_ISSUES.md b/docs/FUNCTION_SUPPORT_DEV_ISSUES.md new file mode 100644 index 0000000..2a34db7 --- /dev/null +++ b/docs/FUNCTION_SUPPORT_DEV_ISSUES.md @@ -0,0 +1,157 @@ +# Development Documentation: Function Support Dev Issues + +## Resolved Issues + +### 1. Inconsistent Numeric Types (Literals and Constraints) +Rune `number` and `int` were handled inconsistently, leading to precision loss and `TypeError` when interacting with Python `Decimal` fields in financial models. + +* **Resolution**: Strictly mapped Rune `number` to Python `Decimal`. Literals and constraints are now explicitly wrapped in `Decimal('...')` during generation. +* **Status**: Fixed ([`fe798c1`](https://github.com/finos/rune-python-generator/commit/fe798c1)) +* **Summary of Impact**: + * **Precision**: Ensures that calculations involving monetary amounts maintain exact precision, avoiding the pitfalls of floating-point arithmetic. + * **Type Safety**: Prevents runtime crashes when Pydantic models expect `Decimal` but receive `float` or `int`. + +### 2. Redundant Logic Generation +Expression logic was previously duplicated between different generator components, leading to maintenance overhead and potential diverging implementations of the same DSL logic. + +* **Resolution**: Centralized all expression logic within `PythonExpressionGenerator`. Removed `generateIfBlocks` from the higher-level function generator to prevent duplicate emission of conditional statements. +* **Status**: Fixed ([`2fb276d`](https://github.com/finos/rune-python-generator/commit/2fb276d)) +* **Summary of Impact**: + * **Maintainability**: Logic changes (like the Switch fix) only need to be implemented in one place. + * **Code Quality**: The generated Python is cleaner and follows a predictable pattern for side-effecting blocks. + +### 3. Missing Function Dependencies (Recursion & Enums) +The dependency provider failed to identify imports for types deeply nested in expressions or those referenced via `REnumType`. + +* **Resolution**: Implemented recursive tree traversal in `PythonFunctionDependencyProvider` and added explicit handling for Enum types to ensure they are captured in the import list. +* **Status**: Fixed ([`4878326`](https://github.com/finos/rune-python-generator/commit/4878326)) +* **Summary of Impact**: + * **Runtime Stability**: Resolves `NameError` exceptions in generated code where functions used types that were not imported at the top of the module. + * **Enum Integration**: Functions can now safely use Rosetta-defined enums in conditions and assignments. + +### 4. Robust Dependency Management (DAG Population) +The generator's dependency graph (DAG) previously failed to capture attribute-level dependencies and function-internal dependencies, leading to `NameError` exceptions when a class or function was defined after it was referenced. + +* **Resolution**: Implemented recursive dependency extraction in `PythonModelObjectGenerator` and `PythonFunctionGenerator`. The DAG now includes edges for all class attributes, function inputs/outputs, and symbol references, guaranteeing a topologically correct definition order in `_bundle.py` for acyclic dependencies. +* **Status**: Fixed ([`b813ff0`](https://github.com/finos/rune-python-generator/commit/b813ff0)) +* **Verification**: `PythonFunctionOrderTest` (Java) confirms strict ordering for linear dependencies. +* **Summary of Impact**: + * **Generation Stability**: Eliminates `NameError` causing build failures. + * **Correctness**: Ensures that the generated Python code adheres to the "define-before-use" principle required by the language. + +### 5. Mapping of [metadata id] for Enums +Enums with metadata constraints failed to support validation and key referencing because they were generated as plain Python `Enum` classes, which cannot carry metadata payloads or validators. + +* **Resolution**: Enums with explicit metadata (e.g., `[metadata id]`, `[metadata reference]`) are now wrapped in `Annotated[Enum, serializer(), validator()]`. The `serializer` and `validator` methods are provided by the `EnumWithMetaMixin` runtime class. +* **Status**: Fixed ([`b813ff0`](https://github.com/finos/rune-python-generator/commit/b813ff0)) +* **Summary of Impact**: + * **Feature Parity**: Brings Enums up to par with complex types regarding metadata support. + * **Validation**: Enables `@key` and `@ref` validation for Enum fields. + +### 6. Inconsistent Type Mapping (Centralization) +Type string generation was scattered across multiple classes, making it impossible to implement global features like string forward-referencing or custom cardinality formatting. + +* **Resolution**: Centralized type mapping and formatting in `RuneToPythonMapper`, adding a flexible `formatPythonType` method that handles both legacy and modern typing styles. +* **Status**: Fixed ([`fe798c1`](https://github.com/finos/rune-python-generator/commit/fe798c1)) +* **Summary of Impact**: + * **Extensibility**: Enabled the groundwork for "String Forward References". + * **Cardinality Control**: Standardized how `list[...]` and `Optional[...]` are generated. + +### 7. Switch Expression Support +`generateSwitchOperation` returned a block of statements instead of a single expression, causing `SyntaxError` during variable assignment. + +* **Resolution**: Encapsulated switch logic within a unique helper function closure (`_switch_fn_0`) and returned a call to that function. +* **Status**: Fixed ([`4d394c5`](https://github.com/finos/rune-python-generator/commit/4d394c5)) + +--- + +## Unresolved Issues and Proposals + +### 1. Constructor-Related Issues + +#### Issue: Fragile Object Building (Direct Constructors) +**Problem**: The generator relies on a magic `_get_rune_object` helper which bypasses IDE checks and is hard to debug. +* **Recommendation**: Refactor `PythonFunctionGenerator` to use direct Python constructor calls (e.g., `MyClass(attr=val)`). +* **Status**: **Unresolved**. The codebase currently uses `_get_rune_object`. + +#### Issue: Constructor Keyword Arguments SyntaxError +**Problem**: Python forbids duplicate or invalid keyword arguments. +* **Recommendation**: Use unique counters for missing/duplicate keys. +* **Proposed Fix**: The generator should use unique fallback keys (`unknown_0`, `unknown_1`, etc.) when property names are missing or invalid. +* **Recommended Code Changes**: Use an `AtomicInteger` for unique fallback keys in `PythonExpressionGenerator.java`. + +#### Issue: Partial Object Construction (Required Fields) +**Problem**: Pydantic's default constructor enforces validation immediately, breaking multi-step `set` operations. +* **Recommendation**: Use `model_construct()`. +* **Proposed Solution**: Use `model_construct(**kwargs)` for initial object creation to skip validation, allowing the object to be filled via subsequent `set` calls before final consumption. + +--- + +### 2. Bundle Generation and Dependency Issues + +#### Issue: Circular Dependencies / Out-of-Order Definitions (The "Topological Sort" Limitation) +**Manifestation**: `NameError: name 'cdm_base_datetime_BusinessCenterTime' is not defined` during CDM import. + +**Problem**: The generator uses a Directed Acyclic Graph (DAG) to order definitions in `_bundle.py`. However, the current implementation only adds edges for **inheritance (SuperTypes)**. It ignores **Attribute types**, leading to out-of-order definitions. Furthermore, Rosetta allows recursive/circular types (e.g., A has attribute B, B has attribute A), which a DAG cannot resolve by design. + +**Reproducing Tests**: +* **JUnit**: `PythonFunctionOrderTest.testFunctionDependencyOrder` (asserts ClassA defined before ClassB). +* **Python**: `test_functions_order.py` (triggers NameError during Pydantic decorator execution). + +**Proposed Alternatives & Recommendation**: +Use **String Forward References + `model_rebuild()`** (The official "Pydantic Way" for v2). +* **The Hybrid DAG Strategy**: We will continue to use the DAG to organize the definition order of classes, but we will limit its scope to **Inheritance only** (`SuperType`). By using String Forward References for attributes, we eliminate the need for the DAG to handle the complex "web" of references, avoiding cycle detection failures while ensuring that parent classes are always defined before their children. + +#### Issue: FQN Type Hints for Clean API (Dots vs. Underscores) +**Problem**: The current generator uses "bundle-mangled" names with underscores (e.g., `cdm_base_datetime_AdjustableDate`) in function and constructor signatures to avoid collisions. + +**Proposal**: Use fully qualified, period-delimited names (e.g., `"cdm.base.datetime.AdjustableDate"`) in all signatures. +* **Mechanism**: Utilize a `_type_registry` mapping at the end of the `_bundle.py` that links Rosetta FQNs to the bundled Python class definitions. +* **Dependency**: This approach **requires** implementing the **String Forward Reference** solution for circular dependencies. +* **Benefits**: API Purity (matches CDM/Rosetta names exactly), Consistency, and Encapsulation. + +#### Issue: Bundle Loading Performance (Implicit Overhead) +**Problem**: The current "Bundle" architecture results in significant "load-all-at-once" overhead for large models like CDM. + +**Proposal**: Evolve the bundle architecture to support **Partitioned Loading** or **Lazy Rebuilds**. +* **Status**: **Unresolved**. Prerequisite is the fix for Circular Dependencies via String Forward References. + +--- + +--- + +### 3. Support for External Data Sources + +#### Issue: Unmapped External Function Calls (`[codeImplementation]`) +**Problem**: The Rosetta runtime allows functions to be marked with `[codeImplementation]`, indicating logic provided by the host language (e.g., loading XML codelists in Java). The Python generator does not yet emit the syntax to delegate these calls to Python external implementations. +* **Manifestation**: Validation functions like `ValidateFpMLCodingSchemeDomain` are either omitted or generate empty/invalid bodies. +* **Recommendation**: Update the generator to emit calls to a standard Python registry/dispatcher for external functions. + +#### Issue: Conditions on Basic Types (Strings) +**Problem**: The DSL allows attaching validation logic directly to basic types (e.g., `typeAlias BusinessCenter: string condition IsValid...`). This feature, introduced to support data validation against external sources, is not supported in the Python generator. +* **Gap**: The generator may not correctly wrap simple types to attach validators or trigger validation upon assignment. +* **Status**: **Unresolved Gap**. + +#### Issue: Missing Standard Library for External Data +**Problem**: The CDM Python implementation lacks the infrastructure to replicate the Java version's ability to load external data (e.g., FpML coding schemes) and validate values against them. +* **Impact**: Even if the generator called the validation function, the runtime mechanism to perform the check does not exist. +* **Recommendation**: Implement a Python equivalent of the Java `CodelistLoader` and expose it via the runtime library. + +--- + +## Backlog + +### Enum Wrappers (Global Proposal) +* **Problem**: While explicit metadata is now supported via `Annotated`, plain Enums do not have a uniform wrapper object at runtime. This leads to inconsistent behavior if code expects to attach metadata dynamically to any enum instance. +* **Proposal**: Wrap *all* enums in a proxy class (`_EnumWrapper`) that holds the enum value and a metadata dictionary. +* **Relevant Tests**: + * Java: `PythonEnumMetadataTest.testEnumWithoutMetadata` (Disabled) + * Python: `test_enum_metadata.py::test_enum_metadata_behavior` (Skipped) +* **Usage Note**: This is a proposed architectural change, not a defect fix. + +--- + +### General Support Suggestions + +* **Type Unification Support**: Evaluate if `BaseDataClass` inheriting from a metadata mixin provides a scalable way to handle metadata across various model sizes. +* **Operator Support**: Consider standardizing helper functions like `rune_with_meta` and `rune_default` to simplify generated code. diff --git a/head_content.rosetta b/head_content.rosetta new file mode 100644 index 0000000..1d17208 --- /dev/null +++ b/head_content.rosetta @@ -0,0 +1,101 @@ +namespace rosetta_dsl.test.semantic.collections : <"generate Python unit tests from Rosetta."> + +type CountItem: + name string (0..1) + value int (0..1) + +type CountContainer: + field1 int (0..*) + field2 CountItem (0..*) + +type CountTest: + bValue CountContainer (1..*) + condition TestCond: + if bValue -> field2 count > 0 + then True + else False + +type SumTest: + aValue int (1..1) + bValue int (1..1) + target int (1..1) + condition TestCond: + if [aValue, bValue] sum = target + then True + else False + +type MinTest: + a int (1..1) + condition Test: + if [a, 1] min = 1 + then True + else False + +type MaxTest: + a int (1..1) + condition Test: + if [a, 1, 20] max = 20 + then True + else False + +type LastTest: + aValue int (1..1) + bValue int (1..1) + cValue int (1..1) + target int (1..1) + condition TestCond: + if [aValue, bValue, cValue] last = target + then True + else False + +type SortTest: + condition Test: + if [3, 2, 1] sort first = 1 + then True + else False + +type JoinTest: + field1 string (1..1) + field2 string (1..1) + condition TestCond: + if [field1, field2] join "" = "ab" + then True + else False + +type FilterItem: + fi int (1..1) + +type FilterTest: + fis FilterItem (1..*) + target int (1..1) + condition TestCondFilter: + if [fis filter i [i->fi = target]] count = 1 + then True + else False + +type FlattenItem: + field1 int (1..*) + +type FlattenContainer: + fieldList FlattenItem (1..*) + +type FlattenBar: + numbers int (0..*) + +type FlattenFoo: + bars FlattenBar (0..*) + condition TestCondFoo: + [1, 2, 3] = (bars + extract numbers + then flatten) + +func FlattenTest: <"Test flatten operation in a condition"> + inputs: + bValue FlattenContainer (1..*) <"Test value"> + output: + result int (1..*) + set result: + bValue + extract fieldList + then extract field1 + then flatten diff --git a/pom.xml b/pom.xml index 0777106..15c0a31 100644 --- a/pom.xml +++ b/pom.xml @@ -437,8 +437,11 @@ + + plugin.properties + - com.regnosys.rosetta.generator.python.PythonCodeGenCLI + com.regnosys.rosetta.generator.python.PythonCodeGeneratorCLI @@ -475,6 +478,7 @@ org.apache.maven.plugins maven-javadoc-plugin + none false diff --git a/run.txt b/run.txt deleted file mode 100644 index 50ca955..0000000 --- a/run.txt +++ /dev/null @@ -1,261 +0,0 @@ -[INFO] Scanning for projects... -[INFO] -[INFO] ------------< com.regnosys.rosetta.code-generators:python >------------- -[INFO] Building code-gen-python 0.0.0.main-SNAPSHOT -[INFO] from pom.xml -[INFO] --------------------------------[ jar ]--------------------------------- -[INFO] -[INFO] --- clean:3.5.0:clean (default-clean) @ python --- -[INFO] Deleting /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/target -[INFO] -[INFO] --- enforcer:3.6.2:enforce (enforce-java) @ python --- -[INFO] Rule 0: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed -[INFO] Rule 1: org.apache.maven.enforcer.rules.dependency.BannedDependencies passed -[INFO] Rule 2: org.apache.maven.enforcer.rules.version.RequireMavenVersion passed -[INFO] -[INFO] --- xtend:2.38.0:compile (Generate xtend sources) @ python --- -[INFO] -[INFO] --- resources:3.3.1:resources (default-resources) @ python --- -[INFO] skip non existing resourceDirectory /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/resources -[INFO] -[INFO] --- compiler:3.14.1:compile (default-compile) @ python --- -[INFO] Recompiling the module because of changed source code. -[INFO] Compiling 13 source files with javac [debug release 21] to target/classes -[INFO] -[INFO] --- xtend:2.38.0:testCompile (Generate xtend test sources) @ python --- -[WARNING] -WARNING: RosettaExistsExpressionTest.xtend - /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaExistsExpressionTest.xtend -21: The value of the local variable pythonString is not used -[INFO] -[INFO] --- checkstyle:3.6.0:check (Check style) @ python --- -[INFO] Starting audit... -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java:9:1: Utility classes should not have a public or default constructor. [HideUtilityClassConstructor] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java:92: Line is longer than 120 characters (found 135). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java:93: Line is longer than 120 characters (found 156). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java:126:55: '+' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java:127:55: '+' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java:128:53: '+' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeWriter.java:9:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeWriter.java:10:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeWriter.java:11:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeWriter.java:54:9: 'if' construct must use '{}'s. [NeedBraces] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeWriter.java:103:5: Class 'PythonCodeWriter' looks like designed for extension (can be subclassed), but the method 'toString' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonCodeWriter' final or making the method 'toString' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java:15:1: Class RuneToPythonMapper should be declared as final. [FinalClass] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java:16:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java:152:37: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java:162:9: 'if' construct must use '{}'s. [NeedBraces] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java:168:52: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java:204:29: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/DefaultExternalGeneratorsProvider.java:1: File does not end with a newline. [NewlineAtEndOfFile] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/DefaultExternalGeneratorsProvider.java:14:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/DefaultExternalGeneratorsProvider.java:24:9: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:1: File does not end with a newline. [NewlineAtEndOfFile] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:2:3: Comment matches to-do format 'TODO:'. [TodoComment] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:4: Line is longer than 120 characters (found 125). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:4:3: Comment matches to-do format 'TODO:'. [TodoComment] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:5:3: Comment matches to-do format 'TODO:'. [TodoComment] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:6:3: Comment matches to-do format 'TODO:'. [TodoComment] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:7:3: Comment matches to-do format 'TODO:'. [TodoComment] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:27:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:85:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:87:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:89:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:91:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:94:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:95:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:101:5: Class 'PythonCodeGenerator' looks like designed for extension (can be subclassed), but the method 'beforeAllGenerate' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonCodeGenerator' final or making the method 'beforeAllGenerate' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:110:5: Class 'PythonCodeGenerator' looks like designed for extension (can be subclassed), but the method 'generate' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonCodeGenerator' final or making the method 'generate' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:129:39: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:130:42: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:131:41: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:154:5: Class 'PythonCodeGenerator' looks like designed for extension (can be subclassed), but the method 'afterAllGenerate' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonCodeGenerator' final or making the method 'afterAllGenerate' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:190:53: 'subfolders' hides a field. [HiddenField] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java:208:60: 'subfolders' hides a field. [HiddenField] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/enums/PythonEnumGenerator.java:10:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/enums/PythonEnumGenerator.java:72:63: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:17:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:25:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:28:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:31:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:34:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:37:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:38:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:40:5: Class 'PythonModelObjectGenerator' looks like designed for extension (can be subclassed), but the method 'beforeAllGenerate' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonModelObjectGenerator' final or making the method 'beforeAllGenerate' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:81:5: Class 'PythonModelObjectGenerator' looks like designed for extension (can be subclassed), but the method 'afterAllGenerate' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonModelObjectGenerator' final or making the method 'afterAllGenerate' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:173:13: switch without "default" clause. [MissingSwitchDefault] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java:229:17: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:6:34: Using the '.*' form of import should be avoided - com.regnosys.rosetta.types.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:12:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:20:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:23:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:26:5: Class 'PythonAttributeProcessor' looks like designed for extension (can be subclassed), but the method 'generateAllAttributes' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonAttributeProcessor' final or making the method 'generateAllAttributes' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:83:17: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:87:17: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:99:17: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:159:57: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:166:39: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:207:17: 'if' construct must use '{}'s. [NeedBraces] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java:284:5: Class 'PythonAttributeProcessor' looks like designed for extension (can be subclassed), but the method 'getImportsFromAttributes' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonAttributeProcessor' final or making the method 'getImportsFromAttributes' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonChoiceAliasProcessor.java:11:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonChoiceAliasProcessor.java:19:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonChoiceAliasProcessor.java:85:29: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/object/PythonChoiceAliasProcessor.java:94:41: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:4:36: Using the '.*' form of import should be avoided - com.regnosys.rosetta.rosetta.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:5:47: Using the '.*' form of import should be avoided - com.regnosys.rosetta.rosetta.expression.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:22:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:23:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:24:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:26:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'getImportsFound' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'getImportsFound' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:30:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'setImportsFound' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'setImportsFound' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:34:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'getIfCondBlocks' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'getIfCondBlocks' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:38:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'setIfCondBlocks' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'setIfCondBlocks' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:42:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'isSwitchCond' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'isSwitchCond' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:46:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'generateExpression' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'generateExpression' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:47:9: 'if' construct must use '{}'s. [NeedBraces] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:51:35: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:143:17: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:168:35: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:177:56: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:198:17: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:244:55: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:245:58: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:258:55: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:263:42: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:302:29: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:361:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'generateTypeOrFunctionConditions' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'generateTypeOrFunctionConditions' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:376:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'generateFunctionConditions' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'generateFunctionConditions' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:376:81: Name 'condition_type' must match pattern '^[a-z][a-zA-Z0-9]*$'. [ParameterName] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:387:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'generateThenElseForFunction' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'generateThenElseForFunction' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:417:46: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:429:97: Name 'condition_type' must match pattern '^[a-z][a-zA-Z0-9]*$'. [ParameterName] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:433:46: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java:481:5: Class 'PythonExpressionGenerator' looks like designed for extension (can be subclassed), but the method 'addImportsFromConditions' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonExpressionGenerator' final or making the method 'addImportsFromConditions' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:11:43: Using the '.*' form of import should be avoided - com.regnosys.rosetta.rosetta.simple.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:17:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:21:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:23:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:25:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:28:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:147:21: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:309:106: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:328:88: Avoid inline conditionals. [AvoidInlineConditionals] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:335:83: '+' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:341:34: '+' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:382:20: Name '_get_rune_object' must match pattern '^[a-z][a-zA-Z0-9]*$'. [MethodName] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java:387:5: Class 'PythonFunctionGenerator' looks like designed for extension (can be subclassed), but the method 'addImportsFromConditions' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonFunctionGenerator' final or making the method 'addImportsFromConditions' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:3:36: Using the '.*' form of import should be avoided - com.regnosys.rosetta.rosetta.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:4:47: Using the '.*' form of import should be avoided - com.regnosys.rosetta.rosetta.expression.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:13:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:21:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:24:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:26:5: Class 'FunctionDependencyProvider' looks like designed for extension (can be subclassed), but the method 'findDependencies' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'FunctionDependencyProvider' final or making the method 'findDependencies' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:33:5: Class 'FunctionDependencyProvider' looks like designed for extension (can be subclassed), but the method 'generateDependencies' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'FunctionDependencyProvider' final or making the method 'generateDependencies' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:77:62: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:78:61: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:79:50: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:80:59: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:81:49: '||' should be on a new line. [OperatorWrap] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:95:5: Class 'FunctionDependencyProvider' looks like designed for extension (can be subclassed), but the method 'findDependenciesFromIterable' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'FunctionDependencyProvider' final or making the method 'findDependenciesFromIterable' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:104:5: Class 'FunctionDependencyProvider' looks like designed for extension (can be subclassed), but the method 'rFunctionDependencies' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'FunctionDependencyProvider' final or making the method 'rFunctionDependencies' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:118:5: Class 'FunctionDependencyProvider' looks like designed for extension (can be subclassed), but the method 'rFunctionDependencies' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'FunctionDependencyProvider' final or making the method 'rFunctionDependencies' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java:127:5: Class 'FunctionDependencyProvider' looks like designed for extension (can be subclassed), but the method 'reset' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'FunctionDependencyProvider' final or making the method 'reset' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:1: File does not end with a newline. [NewlineAtEndOfFile] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:12:30: Using the '.*' form of import should be avoided - org.apache.commons.cli.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:21:21: Using the '.*' form of import should be avoided - java.nio.file.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:22:17: Using the '.*' form of import should be avoided - java.util.*. [AvoidStarImport] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:36: Line is longer than 120 characters (found 148). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:37: Line is longer than 120 characters (found 149). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:40: Line is longer than 120 characters (found 144). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:42: Line is longer than 120 characters (found 146). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:63:1: Utility classes should not have a public or default constructor. [HideUtilityClassConstructor] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:64:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:69: Line is longer than 120 characters (found 130). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:70: Line is longer than 120 characters (found 128). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:71: Line is longer than 120 characters (found 149). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:107:48: '(' is preceded with whitespace. [MethodParamPad] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:128:49: '(' is preceded with whitespace. [MethodParamPad] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:158:23: '(' is preceded with whitespace. [MethodParamPad] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:159:23: '(' is preceded with whitespace. [MethodParamPad] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java:249:9: Class 'PythonCodeGeneratorInstance' looks like designed for extension (can be subclassed), but the method 'get' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonCodeGeneratorInstance' final or making the method 'get' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:1: File does not end with a newline. [NewlineAtEndOfFile] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:18:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:33: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:49: Line is longer than 120 characters (found 149). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:72: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:120: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:127: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:194: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:236: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:275: Line is longer than 120 characters (found 127). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:278: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:317: Line is longer than 120 characters (found 127). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:320: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:331: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:355: Line is longer than 120 characters (found 121). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:376: Line is longer than 120 characters (found 205). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:392: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:404: Line is longer than 120 characters (found 183). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:421: Line is longer than 120 characters (found 144). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:439: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:451: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:453: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:466: Line is longer than 120 characters (found 203). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:483: Line is longer than 120 characters (found 236). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:514: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:522: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:552: Line is longer than 120 characters (found 126). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:555: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:562: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:580: Line is longer than 120 characters (found 203). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:601: Line is longer than 120 characters (found 170). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:623: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:685: Line is longer than 120 characters (found 203). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:697: Line is longer than 120 characters (found 203). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:713: Line is longer than 120 characters (found 173). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:718: Line is longer than 120 characters (found 128). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:725: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:734: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:735: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:770: Line is longer than 120 characters (found 201). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:775: Line is longer than 120 characters (found 128). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:787: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:799: Line is longer than 120 characters (found 194). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:810: Line is longer than 120 characters (found 193). [LineLength] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java:820: Line has trailing spaces. [RegexpSingleline] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:17:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:19:5: Missing a Javadoc comment. [JavadocVariable] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:22:5: Class 'PythonGeneratorTestUtils' looks like designed for extension (can be subclassed), but the method 'generatePythonFromRosettaModel' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonGeneratorTestUtils' final or making the method 'generatePythonFromRosettaModel' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:31:5: Class 'PythonGeneratorTestUtils' looks like designed for extension (can be subclassed), but the method 'generatePythonFromString' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonGeneratorTestUtils' final or making the method 'generatePythonFromString' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:47:5: Class 'PythonGeneratorTestUtils' looks like designed for extension (can be subclassed), but the method 'generatePythonAndExtractBundle' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonGeneratorTestUtils' final or making the method 'generatePythonAndExtractBundle' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:52:5: Class 'PythonGeneratorTestUtils' looks like designed for extension (can be subclassed), but the method 'assertGeneratedContainsExpectedString' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonGeneratorTestUtils' final or making the method 'assertGeneratedContainsExpectedString' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -[WARN] /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/java/com/regnosys/rosetta/generator/python/PythonGeneratorTestUtils.java:63:5: Class 'PythonGeneratorTestUtils' looks like designed for extension (can be subclassed), but the method 'assertBundleContainsExpectedString' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PythonGeneratorTestUtils' final or making the method 'assertBundleContainsExpectedString' static/final/abstract/empty, or adding allowed annotation for the method. [DesignForExtension] -Audit done. -[INFO] You have 0 Checkstyle violations. -[INFO] -[INFO] --- resources:3.3.1:testResources (default-testResources) @ python --- -[INFO] skip non existing resourceDirectory /Users/dls/projects/rune/rune-python-generator/FINOS/rune-python-generator/src/test/resources -[INFO] -[INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ python --- -[INFO] Recompiling the module because of changed dependency. -[INFO] Compiling 14 source files with javac [debug release 21] to target/test-classes -[INFO] -[INFO] --- surefire:3.5.4:test (default-test) @ python --- -[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider -[INFO] -[INFO] ------------------------------------------------------- -[INFO] T E S T S -[INFO] ------------------------------------------------------- -[INFO] Running com.regnosys.rosetta.generator.python.expressions.PythonExpressionGeneratorTest -[WARN] No configuration file was found. Falling back to the default configuration. -[WARNING] Tests run: 17, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 1.606 s -- in com.regnosys.rosetta.generator.python.expressions.PythonExpressionGeneratorTest -[INFO] -[INFO] Results: -[INFO] -[WARNING] Tests run: 17, Failures: 0, Errors: 0, Skipped: 1 -[INFO] -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 9.423 s -[INFO] Finished at: 2026-01-20T17:02:09-05:00 -[INFO] ------------------------------------------------------------------------ diff --git a/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java b/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java index 3efa1d6..f54af1c 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGenerator.java @@ -1,7 +1,5 @@ package com.regnosys.rosetta.generator.python; -// TODO: collect imports as a set rather than an array -// TODO: re-engineer type generation to use an object that has the features carried throughout the generation (imports, etc.) // TODO: function support // TODO: review and consolidate unit tests // TODO: review migrating choice alias processor to PythonModelObjectGenerator @@ -12,9 +10,10 @@ import com.regnosys.rosetta.generator.python.functions.PythonFunctionGenerator; import com.regnosys.rosetta.generator.python.object.PythonModelObjectGenerator; import com.regnosys.rosetta.generator.python.util.PythonCodeGeneratorUtil; +import com.regnosys.rosetta.generator.python.util.PythonCodeWriter; +import static com.regnosys.rosetta.generator.python.util.PythonCodeGeneratorConstants.*; import com.regnosys.rosetta.rosetta.RosettaEnumeration; -import com.regnosys.rosetta.rosetta.RosettaMetaType; import com.regnosys.rosetta.rosetta.RosettaModel; import com.regnosys.rosetta.rosetta.simple.Data; import com.regnosys.rosetta.rosetta.simple.Function; @@ -24,6 +23,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; + +import org.jgrapht.traverse.TopologicalOrderIterator; + import java.util.*; import java.util.stream.Collectors; @@ -47,7 +51,7 @@ *
  • Generates Python classes from Rosetta Data types
  • *
  • Generates Python enums from Rosetta enumerations
  • *
  • Generates Python functions from Rosetta function definitions
  • - *
  • Handles Rosetta model namespaces and organizes output into appropriate + *
  • Handles Rosetta model name spaces and organizes output into appropriate * Python packages
  • *
  • Produces project files for Python packaging (e.g., * pyproject.toml)
  • @@ -82,8 +86,6 @@ public class PythonCodeGenerator extends AbstractExternalGenerator { - private static final Logger LOGGER = LoggerFactory.getLogger(PythonCodeGenerator.class); - @Inject private PythonModelObjectGenerator pojoGenerator; @Inject @@ -91,34 +93,42 @@ public class PythonCodeGenerator extends AbstractExternalGenerator { @Inject private PythonEnumGenerator enumGenerator; - private List subfolders; - private Map> objects = null; // Python code for types by namespace, by type name + private static final Logger LOGGER = LoggerFactory.getLogger(PythonCodeGenerator.class); + + private Map contexts = null; public PythonCodeGenerator() { - super("python"); + super(PYTHON); + contexts = new HashMap<>(); } @Override public Map beforeAllGenerate(ResourceSet set, Collection models, String version) { - subfolders = new ArrayList<>(); - pojoGenerator.beforeAllGenerate(); - objects = new HashMap<>(); return Collections.emptyMap(); } @Override public Map generate(Resource resource, RosettaModel model, String version) { - String cleanVersion = cleanVersion(version); + if (model == null) { + throw new IllegalArgumentException("Model is null"); + } + LOGGER.debug("Processing module: {}", model.getName()); + + String nameSpace = PythonCodeGeneratorUtil.getNamespace(model); + PythonCodeGeneratorContext context = contexts.get(nameSpace); + if (context == null) { + context = new PythonCodeGeneratorContext(); + contexts.put(nameSpace, context); + } + + String cleanVersion = PythonCodeGeneratorUtil.cleanVersion(version); Map result = new HashMap<>(); List rosettaClasses = model.getElements().stream().filter(Data.class::isInstance).map(Data.class::cast) .collect(Collectors.toList()); - List metaDataItems = model.getElements().stream().filter(RosettaMetaType.class::isInstance) - .map(RosettaMetaType.class::cast).collect(Collectors.toList()); - List rosettaEnums = model.getElements().stream() .filter(RosettaEnumeration.class::isInstance).map(RosettaEnumeration.class::cast) .collect(Collectors.toList()); @@ -126,27 +136,18 @@ public PythonCodeGenerator() { List rosettaFunctions = model.getElements().stream().filter(Function.class::isInstance) .map(Function.class::cast).collect(Collectors.toList()); - if (!rosettaClasses.isEmpty() || - !metaDataItems.isEmpty() || - !rosettaEnums.isEmpty() || - !rosettaFunctions.isEmpty()) { - addSubfolder(model.getName()); + if (!rosettaClasses.isEmpty() || !rosettaEnums.isEmpty() || !rosettaFunctions.isEmpty()) { + context.addSubfolder(model.getName()); if (!rosettaFunctions.isEmpty()) { - addSubfolder(model.getName() + ".functions"); + context.addSubfolder(model.getName() + ".functions"); } } - LOGGER.debug("Processing module: {}", model.getName()); - - String namespace = PythonCodeGeneratorUtil.getNamespace(model); - Map currentObject = objects.get(namespace); - if (currentObject == null) { - currentObject = new HashMap(); - objects.put(namespace, currentObject); - } - currentObject.putAll(pojoGenerator.generate(rosettaClasses, cleanVersion)); + Map currentObjects = context.getObjects(); + currentObjects.putAll(pojoGenerator.generate(rosettaClasses, cleanVersion, context)); result.putAll(enumGenerator.generate(rosettaEnums, cleanVersion)); - result.putAll(funcGenerator.generate(rosettaFunctions, cleanVersion)); + Map currentFunctions = funcGenerator.generate(rosettaFunctions, cleanVersion, context); + currentObjects.putAll(currentFunctions); return result; } @@ -156,35 +157,97 @@ public PythonCodeGenerator() { ResourceSet set, Collection models, String version) { - String cleanVersion = cleanVersion(version); Map result = new HashMap<>(); - - List workspaces = getWorkspaces(subfolders); - result.putAll(generateWorkspaces(workspaces, cleanVersion)); - result.putAll(generateInits(subfolders)); - - for (String namespace : objects.keySet()) { - Map currentObject = objects.get(namespace); - if (currentObject != null && !currentObject.isEmpty()) { - result.put("pyproject.toml", PythonCodeGeneratorUtil.createPYProjectTomlFile(namespace, cleanVersion)); - result.putAll(pojoGenerator.afterAllGenerate(namespace, currentObject)); - } + String cleanVersion = PythonCodeGeneratorUtil.cleanVersion(version); + for (String nameSpace : contexts.keySet()) { + PythonCodeGeneratorContext context = contexts.get(nameSpace); + List subfolders = context.getSubfolders(); + result.putAll(generateWorkspaces(getWorkspaces(subfolders), cleanVersion)); + result.putAll(generateInits(subfolders)); + result.putAll(processDAG(nameSpace, context, cleanVersion)); } return result; } - private String cleanVersion(String version) { - if (version == null || version.equals("${project.version}")) { - return "0.0.0"; + private Map processDAG(String nameSpace, PythonCodeGeneratorContext context, + String cleanVersion) { + if (nameSpace == null || context == null || cleanVersion == null) { + throw new IllegalArgumentException("Invalid arguments"); } + Map result = new HashMap<>(); + Map nameSpaceObjects = context.getObjects(); + Graph dependencyDAG = context.getDependencyDAG(); + Set enumImports = context.getEnumImports(); + + if (nameSpaceObjects != null && !nameSpaceObjects.isEmpty() && dependencyDAG != null && enumImports != null) { + result.put(PYPROJECT_TOML, PythonCodeGeneratorUtil.createPYProjectTomlFile(nameSpace, cleanVersion)); + PythonCodeWriter bundleWriter = new PythonCodeWriter(); + TopologicalOrderIterator topologicalOrderIterator = new TopologicalOrderIterator<>( + dependencyDAG); + + // for each element in the ordered collection add the generated class to the + // bundle and add a stub class to the results + boolean isFirst = true; + while (topologicalOrderIterator.hasNext()) { + if (isFirst) { + bundleWriter.appendBlock(PythonCodeGeneratorUtil.createImports()); + List sortedEnumImports = new ArrayList<>(enumImports); + Collections.sort(sortedEnumImports); + for (String imp : sortedEnumImports) { + bundleWriter.appendLine(imp); + } + isFirst = false; + } + String name = topologicalOrderIterator.next(); + CharSequence object = nameSpaceObjects.get(name); + if (object != null) { + // append the class to the bundle + bundleWriter.newLine(); + bundleWriter.newLine(); + bundleWriter.appendBlock(object.toString()); + + // create the stub + String[] parsedName = name.split("\\."); + String stubFileName = SRC + String.join("/", parsedName) + ".py"; + + boolean isFunction = context.hasFunctionName(name); + PythonCodeWriter stubWriter = new PythonCodeWriter(); + stubWriter.appendLine("# pylint: disable=unused-import"); + if (isFunction) { + stubWriter.appendLine("import sys"); + stubWriter.appendLine("from rune.runtime.func_proxy import create_module_attr_guardian"); + } + stubWriter.append("from "); + stubWriter.append(parsedName[0]); + stubWriter.append("._bundle import "); + stubWriter.append(name.replace('.', '_')); + stubWriter.append(" as "); + stubWriter.append(parsedName[parsedName.length - 1]); + if (isFunction) { + stubWriter.newLine(); + stubWriter.newLine(); + stubWriter.appendLine( + "sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__)"); + } + stubWriter.newLine(); + stubWriter.newLine(); + stubWriter.appendLine("# EOF"); + + result.put(stubFileName, stubWriter.toString()); + } + } + if (context.hasFunctions()) { + bundleWriter.newLine(); + bundleWriter.appendLine( + "sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__)"); + } - String[] versionParts = version.split("\\."); - if (versionParts.length > 2) { - String thirdPart = versionParts[2].replaceAll("[^\\d]", ""); - return versionParts[0] + "." + versionParts[1] + "." + thirdPart; + bundleWriter.newLine(); + bundleWriter.newLine(); + bundleWriter.appendLine("# EOF"); + result.put(SRC + nameSpace + "/_bundle.py", bundleWriter.toString()); } - - return "0.0.0"; + return result; } private List getWorkspaces(List subfolders) { @@ -195,7 +258,7 @@ private Map generateWorkspaces(List workspaces, String v Map result = new HashMap<>(); for (String workspace : workspaces) { - result.put(PythonCodeGeneratorUtil.toPyFileName(workspace, "__init__"), + result.put(PythonCodeGeneratorUtil.toPyFileName(workspace, INIT), PythonCodeGeneratorUtil.createTopLevelInitFile(version)); result.put(PythonCodeGeneratorUtil.toPyFileName(workspace, "version"), PythonCodeGeneratorUtil.createVersionFile(version)); @@ -212,16 +275,11 @@ private Map generateInits(List subfolders) { String[] parts = subfolder.split("\\."); for (int i = 1; i < parts.length; i++) { String key = String.join(".", Arrays.copyOfRange(parts, 0, i + 1)); - result.putIfAbsent(PythonCodeGeneratorUtil.toPyFileName(key, "__init__"), " "); + result.putIfAbsent(PythonCodeGeneratorUtil.toPyFileName(key, INIT), " "); } } return result; } - private void addSubfolder(String subfolder) { - if (!subfolders.contains(subfolder)) { - subfolders.add(subfolder); - } - } } \ No newline at end of file diff --git a/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java b/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java index 94ade75..d98f4b2 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLI.java @@ -12,6 +12,8 @@ import org.apache.commons.cli.*; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.EObject; +import com.regnosys.rosetta.rosetta.RosettaNamed; import org.eclipse.emf.common.util.URI; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -22,37 +24,50 @@ import java.util.*; import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; +import org.eclipse.xtext.validation.CheckMode; +import org.eclipse.xtext.validation.IResourceValidator; +import org.eclipse.xtext.validation.Issue; +import org.eclipse.xtext.util.CancelIndicator; /** * Command-line interface for generating Python code from Rosetta models. *

    - * This CLI tool loads Rosetta model files (either from a directory or a single file), - * invokes the {@link PythonCodeGenerator}, and writes the generated Python code to the specified target directory. - * It is intended for use by developers and build systems to automate the translation of Rosetta DSL models to Python. + * This CLI tool loads Rosetta model files (either from a directory or a single + * file), + * invokes the {@link PythonCodeGenerator}, and writes the generated Python code + * to the specified target directory. + * It is intended for use by developers and build systems to automate the + * translation of Rosetta DSL models to Python. *

    * *

    Usage

    + * *
      *   java -cp <your-jar-or-classpath> com.regnosys.rosetta.generator.python.PythonCodeGeneratorCLI -s <source-dir> -t <target-dir>
      *   java -cp <your-jar-or-classpath> com.regnosys.rosetta.generator.python.PythonCodeGeneratorCLI -f <source-file> -t <target-dir>
      * 
    *
      - *
    • -s, --dir <source-dir>: Source directory containing Rosetta files (all .rosetta files will be processed)
    • - *
    • -f, --file <source-file>: Single Rosetta file to process
    • - *
    • -t, --tgt <target-dir>: Target directory for generated Python code (defaults to ./python if not specified)
    • - *
    • -h: Print usage/help
    • + *
    • -s, --dir <source-dir>: Source directory containing Rosetta + * files (all .rosetta files will be processed)
    • + *
    • -f, --file <source-file>: Single Rosetta file to + * process
    • + *
    • -t, --tgt <target-dir>: Target directory for generated + * Python code (defaults to ./python if not specified)
    • + *
    • -h: Print usage/help
    • *
    * *

    Example

    + * *
      *   java -jar target/python-0.0.0.main-SNAPSHOT-shaded.jar -s src/main/rosetta -t build/python
      * 
    * *

    Notes

    *
      - *
    • Either -s or -f must be specified.
    • - *
    • The tool will clean the target directory before writing new files.
    • - *
    • Requires a Java 11+ runtime and all dependencies on the classpath (or use the shaded/uber jar).
    • + *
    • Either -s or -f must be specified.
    • + *
    • The tool will clean the target directory before writing new files.
    • + *
    • Requires a Java 11+ runtime and all dependencies on the classpath (or use + * the shaded/uber jar).
    • *
    * * @author Plamen Neykov @@ -64,38 +79,57 @@ public class PythonCodeGeneratorCLI { private static final Logger LOGGER = LoggerFactory.getLogger(PythonCodeGeneratorCLI.class); public static void main(String[] args) { + System.exit(new PythonCodeGeneratorCLI().execute(args)); + } + + public int execute(String[] args) { + System.out.println("***** Running PythonCodeGeneratorCLI v2 *****"); Options options = new Options(); Option help = new Option("h", "Print usage"); - Option srcDirOpt = Option.builder("s").longOpt("dir").argName("srcDir").desc("Source Rosetta directory").hasArg().build(); - Option srcFileOpt = Option.builder("f").longOpt("file").argName("srcFile").desc("Source Rosetta file").hasArg().build(); - Option tgtDirOpt = Option.builder("t").longOpt("tgt").argName("tgtDir").desc("Target Python directory (default: ./python)").hasArg().build(); + Option srcDirOpt = Option.builder("s").longOpt("dir").argName("srcDir").desc("Source Rosetta directory") + .hasArg().build(); + Option srcFileOpt = Option.builder("f").longOpt("file").argName("srcFile").desc("Source Rosetta file").hasArg() + .build(); + Option tgtDirOpt = Option.builder("t").longOpt("tgt").argName("tgtDir") + .desc("Target Python directory (default: ./python)").hasArg().build(); + Option allowErrorsOpt = Option.builder("e").longOpt("allow-errors") + .desc("Continue generation even if validation errors occur").build(); + Option failOnWarningsOpt = Option.builder("w").longOpt("fail-on-warnings") + .desc("Treat validation warnings as errors").build(); options.addOption(help); options.addOption(srcDirOpt); options.addOption(srcFileOpt); options.addOption(tgtDirOpt); + options.addOption(allowErrorsOpt); + options.addOption(failOnWarningsOpt); CommandLineParser parser = new DefaultParser(); try { CommandLine cmd = parser.parse(options, args); if (cmd.hasOption("h")) { printUsage(options); - return; + return 0; } String tgtDir = cmd.getOptionValue("t", "./python"); + boolean allowErrors = cmd.hasOption("e"); + boolean failOnWarnings = cmd.hasOption("w"); + if (cmd.hasOption("s")) { String srcDir = cmd.getOptionValue("s"); - translateFromSourceDir(srcDir, tgtDir); + return translateFromSourceDir(srcDir, tgtDir, allowErrors, failOnWarnings); } else if (cmd.hasOption("f")) { String srcFile = cmd.getOptionValue("f"); - translateFromSourceFile(srcFile, tgtDir); + return translateFromSourceFile(srcFile, tgtDir, allowErrors, failOnWarnings); } else { System.err.println("Either a source directory (-s) or source file (-f) must be specified."); printUsage(options); + return 1; } } catch (ParseException e) { System.err.println("Failed to parse command line arguments: " + e.getMessage()); printUsage(options); + return 1; } } @@ -104,59 +138,67 @@ private static void printUsage(Options options) { formatter.printHelp("PythonCodeGeneratorCLI", options, true); } - private static void translateFromSourceDir (String srcDir, String tgtDir) { + protected int translateFromSourceDir(String srcDir, String tgtDir, boolean allowErrors, boolean failOnWarnings) { // Find all .rosetta files in a directory Path srcDirPath = Paths.get(srcDir); if (!Files.exists(srcDirPath)) { LOGGER.error("Source directory does not exist: {}", srcDir); - System.exit(1); + return 1; } if (!Files.isDirectory(srcDirPath)) { LOGGER.error("Source directory is not a directory: {}", srcDir); - System.exit(1); + return 1; } try { List rosettaFiles = Files.walk(srcDirPath) .filter(Files::isRegularFile) .filter(f -> f.getFileName().toString().endsWith(".rosetta")) .collect(Collectors.toList()); - processRosettaFiles(rosettaFiles, tgtDir); + return processRosettaFiles(rosettaFiles, tgtDir, allowErrors, failOnWarnings); } catch (IOException e) { LOGGER.error("Failed to process source directory: {}", srcDir, e); + return 1; } } - private static void translateFromSourceFile (String srcFile, String tgtDir) { + + protected int translateFromSourceFile(String srcFile, String tgtDir, boolean allowErrors, boolean failOnWarnings) { Path srcFilePath = Paths.get(srcFile); if (!Files.exists(srcFilePath)) { LOGGER.error("Source file does not exist: {}", srcFile); - System.exit(1); + return 1; } if (Files.isDirectory(srcFilePath)) { LOGGER.error("Source file is a directory: {}", srcFile); - System.exit(1); + return 1; } if (!srcFilePath.toString().endsWith(".rosetta")) { LOGGER.error("Source file does not end with .rosetta: {}", srcFile); - System.exit(1); + return 1; } List rosettaFiles = List.of(srcFilePath); - processRosettaFiles(rosettaFiles, tgtDir); + return processRosettaFiles(rosettaFiles, tgtDir, allowErrors, failOnWarnings); + } + + protected IResourceValidator getValidator(Injector injector) { + return injector.getInstance(IResourceValidator.class); } + // Common processing function - private static void processRosettaFiles(List rosettaFiles, String tgtDir) { + protected int processRosettaFiles(List rosettaFiles, String tgtDir, boolean allowErrors, + boolean failOnWarnings) { LOGGER.info("Processing {} .rosetta files, writing to: {}", rosettaFiles.size(), tgtDir); if (rosettaFiles.isEmpty()) { System.err.println("No .rosetta files found to process."); - System.exit(1); + return 1; } Injector injector = new PythonRosettaStandaloneSetup().createInjectorAndDoEMFRegistration(); ResourceSet resourceSet = injector.getInstance(ResourceSet.class); List resources = new LinkedList<>(); RosettaBuiltinsService builtins = injector.getInstance(RosettaBuiltinsService.class); - resources.add (resourceSet.getResource(builtins.basicTypesURI, true)); - resources.add (resourceSet.getResource(builtins.annotationsURI, true)); + resources.add(resourceSet.getResource(builtins.basicTypesURI, true)); + resources.add(resourceSet.getResource(builtins.annotationsURI, true)); rosettaFiles.stream() .map(path -> resourceSet.getResource(URI.createFileURI(path.toString()), true)) .forEach(resources::add); @@ -167,15 +209,88 @@ private static void processRosettaFiles(List rosettaFiles, String tgtDir) List models = modelLoader.getRosettaModels(resources); if (models.isEmpty()) { LOGGER.error("No valid Rosetta models found."); - System.exit(1); + return 1; } String version = models.getFirst().getVersion(); LOGGER.info("Processing {} models, version: {}", models.size(), version); + IResourceValidator validator = getValidator(injector); Map generatedPython = new HashMap<>(); - pythonCodeGenerator.beforeAllGenerate(resourceSet, models, version); + + List validModels = new ArrayList<>(); + for (RosettaModel model : models) { + Resource resource = model.eResource(); + boolean hasErrors = false; + try { + List issues = validator.validate(resource, CheckMode.ALL, CancelIndicator.NullImpl); + for (Issue issue : issues) { + switch (issue.getSeverity()) { + case ERROR: + EObject offender = resource.getEObject(issue.getUriToProblem().fragment()); + String identification = (offender instanceof RosettaNamed) + ? ((RosettaNamed) offender).getName() + : (offender != null ? offender.eClass().getName() : "Unknown"); + + // Traverse up to find context (e.g. function or type name) if the offender + // itself isn't the root context + if (offender != null && !(offender instanceof com.regnosys.rosetta.rosetta.RosettaModel)) { + EObject current = offender.eContainer(); + while (current != null) { + if (current instanceof RosettaNamed) { + String contextName = ((RosettaNamed) current).getName(); + if (contextName != null && !contextName.equals(identification)) { + identification += " (in " + contextName + ")"; + } + break; + } + current = current.eContainer(); + } + } + + LOGGER.error("Validation ERROR in {} (Line {}): {} on element '{}'", + model.getName(), issue.getLineNumber(), issue.getMessage(), identification); + hasErrors = true; + break; + case WARNING: + LOGGER.warn("Validation WARNING in {} (Line {}): {}", model.getName(), + issue.getLineNumber(), issue.getMessage()); + if (failOnWarnings) { + hasErrors = true; + } + break; + default: + break; + } + } + } catch (Exception e) { + LOGGER.warn("Validation skipped for {} due to exception: {}", model.getName(), e.getMessage()); + validModels.add(model); + continue; + } + + if (hasErrors && !allowErrors) { + LOGGER.error("Skipping model {} due to validation errors (allowErrors=false).", model.getName()); + } else { + if (hasErrors) { + LOGGER.warn("Proceeding with model {} despite validation errors (allowErrors=true).", + model.getName()); + } + validModels.add(model); + } + } + + if (validModels.isEmpty()) { + LOGGER.error("No valid models found after validation. Exiting."); + return 1; + } + + // Use validModels for generation + LOGGER.info("Proceeding with generation for {} valid models.", validModels.size()); + + pythonCodeGenerator.beforeAllGenerate(resourceSet, validModels, version); + for (RosettaModel model : validModels) { LOGGER.info("Processing: " + model.getName()); generatedPython.putAll(pythonCodeGenerator.beforeGenerate(model.eResource(), model, version)); generatedPython.putAll(pythonCodeGenerator.generate(model.eResource(), model, version)); @@ -184,6 +299,7 @@ private static void processRosettaFiles(List rosettaFiles, String tgtDir) generatedPython.putAll(pythonCodeGenerator.afterAllGenerate(resourceSet, models, version)); writePythonFiles(generatedPython, tgtDir); + return 0; } private static void writePythonFiles(Map generatedPython, String tgtDir) { @@ -264,4 +380,5 @@ public Injector createInjector() { return Guice.createInjector(new PythonRosettaRuntimeModule()); } } + } \ No newline at end of file diff --git a/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorContext.java b/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorContext.java new file mode 100644 index 0000000..e88c95d --- /dev/null +++ b/src/main/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorContext.java @@ -0,0 +1,64 @@ +package com.regnosys.rosetta.generator.python; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.DirectedAcyclicGraph; + +public class PythonCodeGeneratorContext { + private List subfolders = null; + private Map objects = null; // Python code for types by nameSpace, by type name + private Graph dependencyDAG = null; + private Set enumImports = null; + private HashSet functionNames = null; + + public PythonCodeGeneratorContext() { + this.subfolders = new ArrayList<>(); + this.objects = new HashMap<>(); + this.dependencyDAG = new DirectedAcyclicGraph<>(DefaultEdge.class); + this.enumImports = new HashSet<>(); + this.functionNames = new HashSet<>(); + } + + public List getSubfolders() { + return subfolders; + } + + public Map getObjects() { + return objects; + } + + public Graph getDependencyDAG() { + return dependencyDAG; + } + + public Set getEnumImports() { + return enumImports; + } + + public void addSubfolder(String subfolder) { + if (!subfolders.contains(subfolder)) { + subfolders.add(subfolder); + } + } + + public void addFunctionName(String functionName) { + if (!functionNames.contains(functionName)) { + functionNames.add(functionName); + } + } + + public boolean hasFunctionName(String functionName) { + return functionNames.contains(functionName); + } + + public boolean hasFunctions() { + return !functionNames.isEmpty(); + } +} diff --git a/src/main/java/com/regnosys/rosetta/generator/python/enums/PythonEnumGenerator.java b/src/main/java/com/regnosys/rosetta/generator/python/enums/PythonEnumGenerator.java index 0acd0bc..f46edd7 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/enums/PythonEnumGenerator.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/enums/PythonEnumGenerator.java @@ -33,9 +33,6 @@ public Map generate(Iterable rosettaEnums, S writer.newLine(); generateEnumClass(writer, enumeration); - - // Note: Xtend code used replace('\t', ' '), maintaining that style although - // PythonCodeWriter uses 4 spaces. result.put(PythonCodeGeneratorUtil.toPyFileName(namespace, enumeration.getName()), writer.toString()); } return result; diff --git a/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java b/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java index ef6b775..62e1a6a 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGenerator.java @@ -3,13 +3,14 @@ import com.regnosys.rosetta.generator.java.enums.EnumHelper; import com.regnosys.rosetta.rosetta.*; import com.regnosys.rosetta.rosetta.expression.*; +import com.regnosys.rosetta.rosetta.expression.ExistsModifier; import com.regnosys.rosetta.rosetta.simple.Attribute; import com.regnosys.rosetta.rosetta.simple.Condition; import com.regnosys.rosetta.rosetta.simple.Data; import com.regnosys.rosetta.rosetta.simple.ShortcutDeclaration; import com.regnosys.rosetta.rosetta.simple.impl.FunctionImpl; import com.regnosys.rosetta.generator.python.util.PythonCodeWriter; - +import com.regnosys.rosetta.generator.python.util.RuneToPythonMapper; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -19,18 +20,9 @@ */ public class PythonExpressionGenerator { - private List importsFound; private List ifCondBlocks = new ArrayList<>(); private boolean isSwitchCond = false; - public List getImportsFound() { - return importsFound; - } - - public void setImportsFound(List importsFound) { - this.importsFound = importsFound; - } - public List getIfCondBlocks() { return ifCondBlocks; } @@ -44,6 +36,7 @@ public boolean isSwitchCond() { } public String generateExpression(RosettaExpression expr, int ifLevel, boolean isLambda) { + if (expr == null) return "None"; @@ -52,7 +45,7 @@ public String generateExpression(RosettaExpression expr, int ifLevel, boolean is } else if (expr instanceof RosettaIntLiteral i) { return String.valueOf(i.getValue()); } else if (expr instanceof RosettaNumberLiteral n) { - return n.getValue().toString(); + return "Decimal('" + n.getValue().toString() + "')"; } else if (expr instanceof RosettaStringLiteral s) { return "\"" + s.getValue() + "\""; } else if (expr instanceof AsKeyOperation asKey) { @@ -83,11 +76,13 @@ public String generateExpression(RosettaExpression expr, int ifLevel, boolean is return generateThenOperation(then, ifLevel, isLambda); } else if (expr instanceof SumOperation sum) { return "sum(" + generateExpression(sum.getArgument(), ifLevel, isLambda) + ")"; + } else if (expr instanceof ReverseOperation reverse) { + return "list(reversed(" + generateExpression(reverse.getArgument(), ifLevel, isLambda) + "))"; } else if (expr instanceof SwitchOperation switchOp) { return generateSwitchOperation(switchOp, ifLevel, isLambda); } else if (expr instanceof ToEnumOperation toEnum) { - return toEnum.getEnumeration().getName() + "(" + generateExpression(toEnum.getArgument(), ifLevel, isLambda) - + ")"; + return toEnum.getEnumeration().getName() + "(" + + generateExpression(toEnum.getArgument(), ifLevel, isLambda) + ")"; } else if (expr instanceof ToStringOperation toString) { return "rune_str(" + generateExpression(toString.getArgument(), ifLevel, isLambda) + ")"; } else if (expr instanceof ToDateOperation toDate) { @@ -118,14 +113,22 @@ public String generateExpression(RosettaExpression expr, int ifLevel, boolean is } else if (expr instanceof RosettaEnumValueReference enumRef) { return enumRef.getEnumeration().getName() + "." + EnumHelper.convertValue(enumRef.getValue()); } else if (expr instanceof RosettaExistsExpression exists) { - return "rune_attr_exists(" + generateExpression(exists.getArgument(), ifLevel, isLambda) + ")"; + String arg = generateExpression(exists.getArgument(), ifLevel, isLambda); + if (exists.getModifier() == ExistsModifier.SINGLE) { + return "rune_attr_exists(" + arg + ", \"single\")"; + } else if (exists.getModifier() == ExistsModifier.MULTIPLE) { + return "rune_attr_exists(" + arg + ", \"multiple\")"; + } + return "rune_attr_exists(" + arg + ")"; } else if (expr instanceof RosettaFeatureCall featureCall) { return generateFeatureCall(featureCall, ifLevel, isLambda); } else if (expr instanceof RosettaOnlyElement onlyElement) { return "rune_get_only_element(" + generateExpression(onlyElement.getArgument(), ifLevel, isLambda) + ")"; } else if (expr instanceof RosettaOnlyExistsExpression onlyExists) { - return "rune_check_one_of(self, " + generateExpression(onlyExists.getArgs().get(0), ifLevel, isLambda) - + ")"; + String args = onlyExists.getArgs().stream() + .map(arg -> generateExpression(arg, ifLevel, isLambda)) + .collect(Collectors.joining(", ")); + return "rune_check_one_of(self, " + args + ")"; } else if (expr instanceof RosettaSymbolReference symbolRef) { return generateSymbolReference(symbolRef, ifLevel, isLambda); } else if (expr instanceof RosettaImplicitVariable implicit) { @@ -155,9 +158,6 @@ private String generateConditionalExpression(RosettaConditionalExpression expr, private String generateFeatureCall(RosettaFeatureCall expr, int ifLevel, boolean isLambda) { if (expr.getFeature() instanceof RosettaEnumValue evalue) { - RosettaSymbol symbol = ((RosettaSymbolReference) expr.getReceiver()).getSymbol(); - RosettaModel model = (RosettaModel) symbol.eContainer(); - addImportsFromConditions(symbol.getName(), model.getName()); return generateEnumString(evalue); } String right = expr.getFeature().getName(); @@ -181,14 +181,17 @@ private String generateThenOperation(ThenOperation expr, int ifLevel, boolean is private String generateFilterOperation(FilterOperation expr, int ifLevel, boolean isLambda) { String argument = generateExpression(expr.getArgument(), ifLevel, isLambda); + String param = expr.getFunction().getParameters().isEmpty() ? "item" + : expr.getFunction().getParameters().get(0).getName(); String filterExpression = generateExpression(expr.getFunction().getBody(), ifLevel, true); - return "rune_filter(" + argument + ", lambda item: " + filterExpression + ")"; + return "rune_filter(" + argument + ", lambda " + param + ": " + filterExpression + ")"; } private String generateMapOperation(MapOperation expr, int ifLevel, boolean isLambda) { InlineFunction inlineFunc = expr.getFunction(); + String param = inlineFunc.getParameters().isEmpty() ? "item" : inlineFunc.getParameters().get(0).getName(); String funcBody = generateExpression(inlineFunc.getBody(), ifLevel, true); - String lambdaFunction = "lambda item: " + funcBody; + String lambdaFunction = "lambda " + param + ": " + funcBody; String argument = generateExpression(expr.getArgument(), ifLevel, isLambda); return "list(map(" + lambdaFunction + ", " + argument + "))"; } @@ -197,9 +200,12 @@ private String generateConstructorExpression(RosettaConstructorExpression expr, String type = (expr.getTypeCall() != null && expr.getTypeCall().getType() != null) ? expr.getTypeCall().getType().getName() : null; + String fullyQualifiedType = RuneToPythonMapper.getBundleObjectName(expr.getTypeCall().getType()); + type = fullyQualifiedType; if (type != null) { return type + "(" + expr.getValues().stream() - .map(pair -> pair.getKey().getName() + "=" + generateExpression(pair.getValue(), ifLevel, isLambda)) + .map(pair -> pair.getKey().getName() + "=" + + generateExpression(pair.getValue(), ifLevel, isLambda)) .collect(Collectors.joining(", ")) + ")"; } else { return "{" + expr.getValues().stream() @@ -219,8 +225,8 @@ private String getGuardExpression(SwitchCaseGuard caseGuard, boolean isLambda) { } RosettaEnumValue enumGuard = caseGuard.getEnumGuard(); if (enumGuard != null) { - return "switchAttribute == rune_resolve_attr(" + generateEnumString(enumGuard) + ",\"" + enumGuard.getName() - + "\")"; + return "switchAttribute == rune_resolve_attr(" + generateEnumString(enumGuard) + ",\"" + + enumGuard.getName() + "\")"; } RosettaFeature optionGuard = caseGuard.getChoiceOptionGuard(); if (optionGuard != null) { @@ -233,44 +239,8 @@ private String getGuardExpression(SwitchCaseGuard caseGuard, boolean isLambda) { throw new UnsupportedOperationException("Unsupported SwitchCaseGuard type"); } - private String generateSwitchOperation(SwitchOperation expr, int ifLevel, boolean isLambda) { - String attr = generateExpression(expr.getArgument(), 0, isLambda); - PythonCodeWriter writer = new PythonCodeWriter(); - isSwitchCond = true; - - var cases = expr.getCases(); - for (int i = 0; i < cases.size(); i++) { - var currentCase = cases.get(i); - String funcName = currentCase.isDefault() ? "_then_default" : "_then_" + (i + 1); - String thenExprDef = currentCase.isDefault() ? generateExpression(expr.getDefault(), 0, isLambda) - : generateExpression(currentCase.getExpression(), ifLevel + 1, isLambda); - - writer.appendLine("def " + funcName + "():"); - writer.indent(); - writer.appendLine("return " + thenExprDef); - writer.unindent(); - } - - writer.appendLine("switchAttribute = " + attr); - - for (int i = 0; i < cases.size(); i++) { - var currentCase = cases.get(i); - String funcName = currentCase.isDefault() ? "_then_default" : "_then_" + (i + 1); - if (currentCase.isDefault()) { - writer.appendLine("else:"); - } else { - SwitchCaseGuard guard = currentCase.getGuard(); - String prefix = (i == 0) ? "if " : "elif "; - writer.appendLine(prefix + getGuardExpression(guard, isLambda) + ":"); - } - writer.indent(); - writer.appendLine("return " + funcName + "()"); - writer.unindent(); - } - return writer.toString(); - } - private String generateSymbolReference(RosettaSymbolReference expr, int ifLevel, boolean isLambda) { + RosettaSymbol symbol = expr.getSymbol(); if (symbol instanceof Data || symbol instanceof RosettaEnumeration) { return symbol.getName(); @@ -280,7 +250,9 @@ private String generateSymbolReference(RosettaSymbolReference expr, int ifLevel, return generateEnumString(evalue); } else if (symbol instanceof RosettaCallableWithArgs callable) { return generateCallableWithArgsCall(callable, expr, ifLevel, isLambda); - } else if (symbol instanceof ShortcutDeclaration || symbol instanceof ClosureParameter) { + } else if (symbol instanceof ClosureParameter) { + return symbol.getName(); + } else if (symbol instanceof ShortcutDeclaration) { return "rune_resolve_attr(self, \"" + symbol.getName() + "\")"; } else { throw new UnsupportedOperationException( @@ -316,18 +288,22 @@ private String generateEnumString(RosettaEnumValue rev) { private String generateCallableWithArgsCall(RosettaCallableWithArgs s, RosettaSymbolReference expr, int ifLevel, boolean isLambda) { - if (s instanceof FunctionImpl) { - addImportsFromConditions(s.getName(), ((RosettaModel) s.eContainer()).getName() + ".functions"); + // Dependency handled by PythonFunctionDependencyProvider + String args = expr.getArgs().stream().map(arg -> generateExpression(arg, ifLevel, isLambda)) + .collect(Collectors.joining(", ")); + String funcName = s.getName(); + if ("Max".equals(funcName)) { + funcName = "max"; + } else if ("Min".equals(funcName)) { + funcName = "min"; } else { - addImportsFromConditions(s.getName(), ((RosettaModel) s.eContainer()).getName()); + funcName = RuneToPythonMapper.getBundleObjectName(s); } - String args = expr.getArgs().stream() - .map(arg -> generateExpression(arg, ifLevel, isLambda)) - .collect(Collectors.joining(", ")); - return s.getName() + "(" + args + ")"; + return funcName + "(" + args + ")"; } private String generateBinaryExpression(RosettaBinaryOperation expr, int ifLevel, boolean isLambda) { + if (expr instanceof ModifiableBinaryOperation mod) { if (mod.getCardMod() == null) { throw new UnsupportedOperationException( @@ -350,7 +326,10 @@ private String generateBinaryExpression(RosettaBinaryOperation expr, int ifLevel + generateExpression(expr.getRight(), ifLevel, isLambda) + ")"; case "disjoint" -> "rune_disjoint(" + generateExpression(expr.getLeft(), ifLevel, isLambda) + ", " + generateExpression(expr.getRight(), ifLevel, isLambda) + ")"; - case "join" -> generateExpression(expr.getLeft(), ifLevel, isLambda) + ".join(" + case "join" -> generateExpression(expr.getRight(), ifLevel, isLambda) + ".join(" + + generateExpression(expr.getLeft(), ifLevel, isLambda) + ")"; + case "default" -> "(" + generateExpression(expr.getLeft(), ifLevel, isLambda) + " if " + + generateExpression(expr.getLeft(), ifLevel, isLambda) + " is not None else " + generateExpression(expr.getRight(), ifLevel, isLambda) + ")"; default -> "(" + generateExpression(expr.getLeft(), ifLevel, isLambda) + " " + expr.getOperator() + " " + generateExpression(expr.getRight(), ifLevel, isLambda) + ")"; @@ -374,30 +353,18 @@ public String generateTypeOrFunctionConditions(Data cls) { } public String generateFunctionConditions(List conditions, String condition_type) { + int nConditions = 0; StringBuilder result = new StringBuilder(); for (Condition cond : conditions) { result.append(generateFunctionConditionBoilerPlate(cond, nConditions, condition_type)); result.append(generateIfThenElseOrSwitch(cond)); + nConditions++; } return result.toString(); } - public String generateThenElseForFunction(RosettaExpression expr, List ifLevel) { - ifCondBlocks.clear(); - generateExpression(expr, ifLevel.get(0), false); - PythonCodeWriter writer = new PythonCodeWriter(); - if (!ifCondBlocks.isEmpty()) { - ifLevel.set(0, ifLevel.get(0) + 1); - for (String arg : ifCondBlocks) { - writer.appendBlock(arg); - writer.newLine(); - } - } - return writer.toString(); - } - private boolean isConstraintCondition(Condition cond) { return isOneOf(cond) || isChoice(cond); } @@ -431,13 +398,14 @@ private String generateFunctionConditionBoilerPlate(Condition cond, int nConditi writer.newLine(); writer.appendLine("@rune_local_condition(" + condition_type + ")"); String name = cond.getName() != null ? cond.getName() : ""; - writer.appendLine("def condition_" + nConditions + "_" + name + "(self):"); + writer.appendLine("def condition_" + nConditions + "_" + name + "():"); writer.indent(); if (cond.getDefinition() != null) { writer.appendLine("\"\"\""); writer.appendLine(cond.getDefinition()); writer.appendLine("\"\"\""); } + writer.appendLine("item = self"); return writer.toString(); } @@ -458,16 +426,16 @@ private String generateConstraintCondition(Data cls, Condition cond) { return writer.toString(); } + private int switchCounter = 0; + private String generateIfThenElseOrSwitch(Condition c) { ifCondBlocks.clear(); - isSwitchCond = false; + switchCounter = 0; String expr = generateExpression(c.getExpression(), 0, false); + PythonCodeWriter writer = new PythonCodeWriter(); writer.indent(); - if (isSwitchCond) { - writer.appendBlock(expr); - return writer.toString(); - } + if (!ifCondBlocks.isEmpty()) { for (String arg : ifCondBlocks) { writer.appendBlock(arg); @@ -478,10 +446,50 @@ private String generateIfThenElseOrSwitch(Condition c) { return writer.toString(); } - public void addImportsFromConditions(String variable, String namespace) { - String imp = "from " + namespace + "." + variable + " import " + variable; - if (importsFound != null && !importsFound.contains(imp)) { - importsFound.add(imp); + // ... (helper methods like isConstraintCondition, etc. remain unchanged) ... + + private String generateSwitchOperation(SwitchOperation expr, int ifLevel, boolean isLambda) { + String attr = generateExpression(expr.getArgument(), 0, isLambda); + PythonCodeWriter writer = new PythonCodeWriter(); + + String switchFuncName = "_switch_fn_" + switchCounter++; + + writer.appendLine("def " + switchFuncName + "():"); + writer.indent(); + + var cases = expr.getCases(); + for (int i = 0; i < cases.size(); i++) { + var currentCase = cases.get(i); + String funcName = currentCase.isDefault() ? "_then_default" : "_then_" + (i + 1); + String thenExprDef = currentCase.isDefault() ? generateExpression(expr.getDefault(), 0, isLambda) + : generateExpression(currentCase.getExpression(), ifLevel + 1, isLambda); + + writer.appendLine("def " + funcName + "():"); + writer.indent(); + writer.appendLine("return " + thenExprDef); + writer.unindent(); + } + + writer.appendLine("switchAttribute = " + attr); + + for (int i = 0; i < cases.size(); i++) { + var currentCase = cases.get(i); + String funcName = currentCase.isDefault() ? "_then_default" : "_then_" + (i + 1); + if (currentCase.isDefault()) { + writer.appendLine("else:"); + } else { + SwitchCaseGuard guard = currentCase.getGuard(); + String prefix = (i == 0) ? "if " : "elif "; + writer.appendLine(prefix + getGuardExpression(guard, isLambda) + ":"); + } + writer.indent(); + writer.appendLine("return " + funcName + "()"); + writer.unindent(); } + + writer.unindent(); + + ifCondBlocks.add(writer.toString()); + return switchFuncName + "()"; } } diff --git a/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java b/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java deleted file mode 100644 index b540e8c..0000000 --- a/src/main/java/com/regnosys/rosetta/generator/python/functions/FunctionDependencyProvider.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.regnosys.rosetta.generator.python.functions; - -import com.regnosys.rosetta.rosetta.*; -import com.regnosys.rosetta.rosetta.expression.*; -import com.regnosys.rosetta.rosetta.simple.Data; -import com.regnosys.rosetta.rosetta.simple.Function; -import com.regnosys.rosetta.types.RFunction; -import com.regnosys.rosetta.types.RObjectFactory; -import jakarta.inject.Inject; -import org.eclipse.emf.ecore.EObject; -import org.eclipse.xtext.EcoreUtil2; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -/** - * Determine the Rosetta dependencies for a Rosetta object - */ -public class FunctionDependencyProvider { - @Inject - private RObjectFactory rTypeBuilderFactory; - - private final Set visited = new HashSet<>(); - - public Set findDependencies(EObject object) { - if (visited.contains(object)) { - return Collections.emptySet(); - } - return generateDependencies(object); - } - - public Set generateDependencies(EObject object) { - if (object == null) { - return Collections.emptySet(); - } - - Set dependencies; - if (object instanceof RosettaBinaryOperation binary) { - dependencies = new HashSet<>(); - dependencies.addAll(generateDependencies(binary.getLeft())); - dependencies.addAll(generateDependencies(binary.getRight())); - } else if (object instanceof RosettaConditionalExpression cond) { - dependencies = new HashSet<>(); - dependencies.addAll(generateDependencies(cond.getIf())); - dependencies.addAll(generateDependencies(cond.getIfthen())); - dependencies.addAll(generateDependencies(cond.getElsethen())); - } else if (object instanceof RosettaOnlyExistsExpression onlyExists) { - dependencies = findDependenciesFromIterable(onlyExists.getArgs()); - } else if (object instanceof RosettaFunctionalOperation functional) { - dependencies = new HashSet<>(); - dependencies.addAll(generateDependencies(functional.getArgument())); - dependencies.addAll(generateDependencies(functional.getFunction())); - } else if (object instanceof RosettaUnaryOperation unary) { - dependencies = generateDependencies(unary.getArgument()); - } else if (object instanceof RosettaFeatureCall featureCall) { - dependencies = generateDependencies(featureCall.getReceiver()); - } else if (object instanceof RosettaSymbolReference symbolRef) { - dependencies = new HashSet<>(); - dependencies.addAll(generateDependencies(symbolRef.getSymbol())); - dependencies.addAll(findDependenciesFromIterable(symbolRef.getArgs())); - } else if (object instanceof Function || object instanceof Data || object instanceof RosettaEnumeration) { - dependencies = new HashSet<>(Collections.singleton(object)); - } else if (object instanceof InlineFunction inline) { - dependencies = generateDependencies(inline.getBody()); - } else if (object instanceof ListLiteral listLiteral) { - dependencies = listLiteral.getElements().stream() - .flatMap(el -> generateDependencies(el).stream()) - .collect(Collectors.toSet()); - } else if (object instanceof RosettaConstructorExpression constructor) { - dependencies = new HashSet<>(); - if (constructor.getTypeCall() != null && constructor.getTypeCall().getType() != null) { - dependencies.add(constructor.getTypeCall().getType()); - } - constructor.getValues() - .forEach(valuePair -> dependencies.addAll(generateDependencies(valuePair.getValue()))); - } else if (object instanceof RosettaExternalFunction || - object instanceof RosettaEnumValueReference || - object instanceof RosettaLiteral || - object instanceof RosettaImplicitVariable || - object instanceof RosettaSymbol || - object instanceof RosettaDeepFeatureCall) { - dependencies = Collections.emptySet(); - } else { - throw new IllegalArgumentException(object.eClass().getName() - + ": generating dependency in a function for this type is not yet implemented."); - } - - if (!dependencies.isEmpty()) { - visited.add(object); - } - return dependencies; - } - - public Set findDependenciesFromIterable(Iterable objects) { - if (objects == null) { - return Collections.emptySet(); - } - return StreamSupport.stream(objects.spliterator(), false) - .flatMap(obj -> generateDependencies(obj).stream()) - .collect(Collectors.toSet()); - } - - public Set rFunctionDependencies(RosettaExpression expression) { - List symbolRefs = EcoreUtil2.eAllOfType(expression, RosettaSymbolReference.class); - Set result = new HashSet<>(); - for (RosettaSymbolReference ref : symbolRefs) { - RosettaSymbol symbol = ref.getSymbol(); - if (symbol instanceof Function f) { - result.add(rTypeBuilderFactory.buildRFunction(f)); - } else if (symbol instanceof RosettaRule r) { - result.add(rTypeBuilderFactory.buildRFunction(r)); - } - } - return result; - } - - public Set rFunctionDependencies(Iterable expressions) { - if (expressions == null) { - return Collections.emptySet(); - } - return StreamSupport.stream(expressions.spliterator(), false) - .flatMap(expr -> rFunctionDependencies(expr).stream()) - .collect(Collectors.toSet()); - } - - public void reset() { - visited.clear(); - } -} diff --git a/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionDependencyProvider.java b/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionDependencyProvider.java new file mode 100644 index 0000000..9313391 --- /dev/null +++ b/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionDependencyProvider.java @@ -0,0 +1,118 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.rosetta.*; +import com.regnosys.rosetta.rosetta.expression.*; +import com.regnosys.rosetta.rosetta.simple.Data; +import com.regnosys.rosetta.rosetta.simple.Function; + +import com.regnosys.rosetta.types.REnumType; +import com.regnosys.rosetta.types.RFunction; +import com.regnosys.rosetta.types.RObjectFactory; +import com.regnosys.rosetta.types.RType; +import jakarta.inject.Inject; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.EcoreUtil2; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Determine the Rosetta dependencies for a Rosetta object + */ +public class PythonFunctionDependencyProvider { + @Inject + private RObjectFactory rTypeBuilderFactory; + + public void addDependencies(EObject object, Set enumImports) { + if (object instanceof RosettaEnumeration enumeration) { + String name = enumeration.getName(); + RosettaModel model = (RosettaModel) enumeration.eContainer(); + String prefix = model.getName(); + enumImports.add("import " + prefix + "." + name); + } else if (object instanceof RosettaEnumValueReference ref) { + addDependencies(ref.getEnumeration(), enumImports); + } else if (object instanceof RosettaBinaryOperation binary) { + addDependencies(binary.getLeft(), enumImports); + addDependencies(binary.getRight(), enumImports); + } else if (object instanceof RosettaConditionalExpression cond) { + addDependencies(cond.getIf(), enumImports); + addDependencies(cond.getIfthen(), enumImports); + addDependencies(cond.getElsethen(), enumImports); + } else if (object instanceof RosettaOnlyExistsExpression onlyExists) { + onlyExists.getArgs().forEach(arg -> addDependencies(arg, enumImports)); + } else if (object instanceof RosettaFunctionalOperation functional) { + if (functional.getArgument() != null) { + addDependencies(functional.getArgument(), enumImports); + } + if (functional instanceof FilterOperation filter) { + addDependencies(filter.getFunction(), enumImports); + } else if (functional instanceof MapOperation map) { + addDependencies(map.getFunction(), enumImports); + } + } else if (object instanceof RosettaUnaryOperation unary) { + addDependencies(unary.getArgument(), enumImports); + } else if (object instanceof RosettaFeatureCall featureCall) { + addDependencies(featureCall.getReceiver(), enumImports); + } else if (object instanceof RosettaSymbolReference symbolRef) { + addDependencies(symbolRef.getSymbol(), enumImports); + symbolRef.getArgs().forEach(arg -> addDependencies(arg, enumImports)); + } else if (object instanceof InlineFunction inline) { + addDependencies(inline.getBody(), enumImports); + } else if (object instanceof ListLiteral listLiteral) { + listLiteral.getElements().forEach(el -> addDependencies(el, enumImports)); + } else if (object instanceof RosettaConstructorExpression constructor) { + if (constructor.getTypeCall() != null && constructor.getTypeCall().getType() != null) { + addDependencies(constructor.getTypeCall().getType(), enumImports); + } + constructor.getValues() + .forEach(valuePair -> addDependencies(valuePair.getValue(), enumImports)); + } else if (object instanceof Function || + object instanceof Data || + object instanceof RosettaExternalFunction || + object instanceof RosettaLiteral || + object instanceof RosettaImplicitVariable || + object instanceof RosettaSymbol || + object instanceof RosettaDeepFeatureCall || + object instanceof RosettaBasicType || + object instanceof RosettaRecordType || + object instanceof RosettaTypeAlias) { + return; + } else if (object != null) { + // Recurse into all children for unknown EObjects to ensure thorough dependency + // collection + object.eContents().forEach(child -> addDependencies(child, enumImports)); + } + } + + public void addDependencies(RType type, Set enumImports) { + if (type instanceof REnumType enumType) { + String name = enumType.getName(); + String prefix = enumType.getNamespace().toString(); + enumImports.add("import " + prefix + "." + name); + } + } + + public Set rFunctionDependencies(RosettaExpression expression) { + List symbolRefs = EcoreUtil2.eAllOfType(expression, RosettaSymbolReference.class); + Set result = new HashSet<>(); + for (RosettaSymbolReference ref : symbolRefs) { + RosettaSymbol symbol = ref.getSymbol(); + if (symbol instanceof Function f) { + result.add(rTypeBuilderFactory.buildRFunction(f)); + } else if (symbol instanceof RosettaRule r) { + result.add(rTypeBuilderFactory.buildRFunction(r)); + } + } + return result; + } + + public Set rFunctionDependencies(Iterable expressions) { + if (expressions == null) { + return Collections.emptySet(); + } + return StreamSupport.stream(expressions.spliterator(), false) + .flatMap(expr -> rFunctionDependencies(expr).stream()) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java b/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java index 98b47c6..1ca8b1a 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionGenerator.java @@ -1,122 +1,220 @@ package com.regnosys.rosetta.generator.python.functions; -import com.regnosys.rosetta.generator.python.expressions.PythonExpressionGenerator; -import com.regnosys.rosetta.generator.python.util.PythonCodeGeneratorUtil; -import com.regnosys.rosetta.generator.python.util.PythonCodeWriter; -import com.regnosys.rosetta.generator.python.util.RuneToPythonMapper; +import com.regnosys.rosetta.rosetta.RosettaModel; import com.regnosys.rosetta.rosetta.RosettaEnumeration; import com.regnosys.rosetta.rosetta.RosettaFeature; -import com.regnosys.rosetta.rosetta.RosettaModel; import com.regnosys.rosetta.rosetta.RosettaTyped; import com.regnosys.rosetta.rosetta.simple.*; import jakarta.inject.Inject; -import org.eclipse.emf.ecore.EObject; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultEdge; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.regnosys.rosetta.rosetta.RosettaNamed; +import com.regnosys.rosetta.rosetta.expression.*; +import com.regnosys.rosetta.types.RObjectFactory; +import org.jgrapht.graph.GraphCycleProhibitedException; import java.util.*; +import com.regnosys.rosetta.generator.python.PythonCodeGeneratorContext; +import com.regnosys.rosetta.generator.python.expressions.PythonExpressionGenerator; +import com.regnosys.rosetta.generator.python.util.RuneToPythonMapper; +import com.regnosys.rosetta.generator.python.util.PythonCodeWriter; + public class PythonFunctionGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(PythonFunctionGenerator.class); - private final List importsFound = new ArrayList<>(); - @Inject - private FunctionDependencyProvider functionDependencyProvider; + private PythonFunctionDependencyProvider functionDependencyProvider; @Inject private PythonExpressionGenerator expressionGenerator; + @Inject + private RObjectFactory rObjectFactory; + /** * Generate Python from the collection of Rosetta functions. * - * @param rosettaFunctions the collection of Rosetta functions to generate - * @param version the version for this collection of functions + * @param rFunctions the collection of Rosetta functions to generate + * @param version the version for this collection of functions * @return a Map of all the generated Python indexed by the file name */ - public Map generate(List rosettaFunctions, String version) { + + public Map generate(Iterable rFunctions, String version, + PythonCodeGeneratorContext context) { + Graph dependencyDAG = context.getDependencyDAG(); + if (dependencyDAG == null) { + throw new RuntimeException("Dependency DAG not initialized"); + } + + Set enumImports = context.getEnumImports(); + if (enumImports == null) { + throw new RuntimeException("Enum imports not initialized"); + } + Map result = new HashMap<>(); - if (!rosettaFunctions.isEmpty()) { - for (Function func : rosettaFunctions) { - RosettaModel tr = (RosettaModel) func.eContainer(); - String namespace = tr.getName(); - try { - String funcs = generateFunctions(func, version); - result.put(PythonCodeGeneratorUtil.toPyFunctionFileName(namespace, func.getName()), - PythonCodeGeneratorUtil.createImportsFunc(func.getName()) + funcs); - } catch (Exception ex) { - LOGGER.error("Exception occurred generating func {}", func.getName(), ex); - } + for (Function rf : rFunctions) { + RosettaModel model = (RosettaModel) rf.eContainer(); + if (model == null) { + LOGGER.warn("Function {} has no container, skipping", rf.getName()); + continue; + } + // String nameSpace = PythonCodeGeneratorUtil.getNamespace(model); + try { + String pythonFunction = generateFunction(rf, version, enumImports); + + String functionName = RuneToPythonMapper.getFullyQualifiedObjectName(rf); + result.put(functionName, pythonFunction); + dependencyDAG.addVertex(functionName); + context.addFunctionName(functionName); + + addFunctionDependencies(dependencyDAG, functionName, rf); + } catch (Exception ex) { + LOGGER.error("Exception occurred generating rf {}", rf.getName(), ex); + throw new RuntimeException("Error generating Python for function " + rf.getName(), ex); } } return result; } - private String generateFunctions(Function function, String version) { - Set dependencies = collectFunctionDependencies(function); + private void addFunctionDependencies(Graph dependencyDAG, String functionName, Function rf) { + Set dependencies = new HashSet<>(); + + rf.getInputs().forEach(input -> { + if (input.getTypeCall() != null && input.getTypeCall().getType() != null) { + dependencies.add(input.getTypeCall().getType()); + } + }); + if (rf.getOutput() != null && rf.getOutput().getTypeCall() != null + && rf.getOutput().getTypeCall().getType() != null) { + dependencies.add(rf.getOutput().getTypeCall().getType()); + } + + Iterator allContents = rf.eAllContents(); + while (allContents.hasNext()) { + Object content = allContents.next(); + if (content instanceof RosettaSymbolReference ref) { + if (ref.getSymbol() instanceof Function || ref.getSymbol() instanceof Data + || ref.getSymbol() instanceof RosettaEnumeration) { + dependencies.add((RosettaNamed) ref.getSymbol()); + } + } else if (content instanceof RosettaConstructorExpression cons) { + if (cons.getTypeCall() != null && cons.getTypeCall().getType() != null) { + dependencies.add(cons.getTypeCall().getType()); + } + } + } + + for (RosettaNamed dep : dependencies) { + String depName = ""; + if (dep instanceof Data) { + depName = rObjectFactory.buildRDataType((Data) dep).getQualifiedName().toString(); + } else if (dep instanceof Function) { + depName = RuneToPythonMapper.getFullyQualifiedObjectName((Function) dep); + } else if (dep instanceof RosettaEnumeration) { + depName = rObjectFactory.buildREnumType((RosettaEnumeration) dep).getQualifiedName().toString(); + } + + if (!depName.isEmpty() && !functionName.equals(depName)) { + addDependency(dependencyDAG, functionName, depName); + } + } + } + + private void addDependency(Graph dependencyDAG, String className, String dependencyName) { + dependencyDAG.addVertex(dependencyName); + if (!className.equals(dependencyName)) { + try { + dependencyDAG.addEdge(dependencyName, className); + } catch (GraphCycleProhibitedException e) { + // Ignore + } + } + } + + private String generateFunction(Function rf, String version, Set enumImports) { + if (rf == null) { + throw new RuntimeException("Function is null"); + } + if (enumImports == null) { + throw new RuntimeException("Enum imports is null"); + } + enumImports.addAll(collectFunctionDependencies(rf)); PythonCodeWriter writer = new PythonCodeWriter(); - writer.appendBlock(generateImports(dependencies, function)); writer.appendLine(""); writer.appendLine(""); writer.appendLine("@replaceable"); - writer.appendLine("def " + function.getName() + generatesInputs(function) + ":"); + writer.appendLine("@validate_call"); + writer.appendLine("def " + RuneToPythonMapper.getBundleObjectName(rf) + generateInputs(rf) + ":"); writer.indent(); - writer.appendBlock(generateDescription(function)); + writer.appendBlock(generateDescription(rf)); - if (!function.getConditions().isEmpty()) { + if (!rf.getConditions().isEmpty()) { writer.appendLine("_pre_registry = {}"); } - if (!function.getPostConditions().isEmpty()) { + if (!rf.getPostConditions().isEmpty()) { writer.appendLine("_post_registry = {}"); } writer.appendLine("self = inspect.currentframe()"); writer.appendLine(""); - if (function.getConditions().isEmpty()) { + if (rf.getConditions().isEmpty()) { writer.appendLine(""); } - writer.appendBlock(generateTypeOrFunctionConditions(function)); + writer.appendBlock(generateConditions(rf)); - generateIfBlocks(writer, function); - generateAlias(writer, function); - generateOperations(writer, function); - generatesOutput(writer, function); + int[] level = { 0 }; + generateAlias(writer, rf, level); + generateOperations(writer, rf, level); + generateOutput(writer, rf); writer.unindent(); writer.newLine(); - writer.appendLine( - "sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__)"); return writer.toString(); } - private String generateImports(Iterable dependencies, Function function) { - PythonCodeWriter writer = new PythonCodeWriter(); + private String generateInputs(Function function) { + StringBuilder result = new StringBuilder("("); + List inputs = function.getInputs(); + for (int i = 0; i < inputs.size(); i++) { + Attribute input = inputs.get(i); + String inputBundleName = RuneToPythonMapper.getBundleObjectName(input.getTypeCall().getType()); + String inputType = RuneToPythonMapper.formatPythonType( + inputBundleName, + input.getCard().getInf(), + input.getCard().getSup(), + true); + result.append(input.getName()).append(": ").append(inputType); - for (EObject dependency : dependencies) { - RosettaModel tr = (RosettaModel) dependency.eContainer(); - String importPath = tr.getName(); - if (dependency instanceof Function func) { - writer.appendLine("from " + importPath + ".functions." + func.getName() + " import " + func.getName()); - } else if (dependency instanceof RosettaEnumeration enumeration) { - writer.appendLine( - "from " + importPath + "." + enumeration.getName() + " import " + enumeration.getName()); - } else if (dependency instanceof Data data) { - writer.appendLine("from " + importPath + "." + data.getName() + " import " + data.getName()); + if (i < inputs.size() - 1) { + result.append(", "); } } - writer.newLine(); - writer.appendLine("__all__ = ['" + function.getName() + "']"); - - return writer.toString(); + result.append(") -> "); + Attribute output = function.getOutput(); + if (output != null) { + String outputBundleName = RuneToPythonMapper.getBundleObjectName(output.getTypeCall().getType()); + String outputType = RuneToPythonMapper.formatPythonType( + outputBundleName, + 1, // Force min=1 to suppress Optional/| None for return types + output.getCard().getSup(), + true); + result.append(outputType); + } else { + result.append("None"); + } + return result.toString(); } - private void generatesOutput(PythonCodeWriter writer, Function function) { + private void generateOutput(PythonCodeWriter writer, Function function) { Attribute output = function.getOutput(); if (output != null) { if (function.getOperations().isEmpty() && function.getShortcuts().isEmpty()) { @@ -131,36 +229,9 @@ private void generatesOutput(PythonCodeWriter writer, Function function) { writer.appendLine(""); writer.appendLine(""); } - writer.appendLine("return " + output.getName()); - } - } - private String generatesInputs(Function function) { - List inputs = function.getInputs(); - Attribute output = function.getOutput(); - - StringBuilder result = new StringBuilder("("); - for (int i = 0; i < inputs.size(); i++) { - Attribute input = inputs.get(i); - String typeName = input.getTypeCall().getType().getName(); - String type = input.getCard().getSup() == 0 - ? "list[" + RuneToPythonMapper.toPythonBasicType(typeName) + "]" - : RuneToPythonMapper.toPythonBasicType(typeName); - result.append(input.getName()).append(": ").append(type); - if (input.getCard().getInf() == 0) { - result.append(" | None"); - } - if (i < inputs.size() - 1) { - result.append(", "); - } - } - result.append(") -> "); - if (output != null) { - result.append(RuneToPythonMapper.toPythonBasicType(output.getTypeCall().getType().getName())); - } else { - result.append("None"); + writer.appendLine("return " + output.getName()); } - return result.toString(); } private String generateDescription(Function function) { @@ -174,10 +245,15 @@ private String generateDescription(Function function) { writer.appendLine(description); } writer.appendLine(""); - writer.appendLine("Parameters "); + writer.appendLine("Parameters"); writer.appendLine("----------"); for (Attribute input : inputs) { - writer.appendLine(input.getName() + " : " + input.getTypeCall().getType().getName()); + String paramName = RuneToPythonMapper.formatPythonType( + RuneToPythonMapper.getFullyQualifiedObjectName(input.getTypeCall().getType()), + 1, // Force min=1 to match legacy docstring format (no Optional) + input.getCard().getSup(), + true); + writer.appendLine(input.getName() + " : " + paramName); if (input.getDefinition() != null) { writer.appendLine(input.getDefinition()); } @@ -186,7 +262,13 @@ private String generateDescription(Function function) { writer.appendLine("Returns"); writer.appendLine("-------"); if (output != null) { - writer.appendLine(output.getName() + " : " + output.getTypeCall().getType().getName()); + String paramName = RuneToPythonMapper.formatPythonType( + RuneToPythonMapper.getFullyQualifiedObjectName(output.getTypeCall().getType()), + 1, // Force min=1 to match legacy docstring format (no Optional) + output.getCard().getSup(), + true); + + writer.appendLine(output.getName() + " : " + paramName); } else { writer.appendLine("No Return"); } @@ -195,53 +277,39 @@ private String generateDescription(Function function) { return writer.toString(); } - private Set collectFunctionDependencies(Function func) { - Set dependencies = new HashSet<>(); - - func.getShortcuts().forEach( - shortcut -> dependencies.addAll(functionDependencyProvider.findDependencies(shortcut.getExpression()))); - func.getOperations().forEach(operation -> dependencies - .addAll(functionDependencyProvider.findDependencies(operation.getExpression()))); + private Set collectFunctionDependencies(Function rf) { + Set enumImports = new HashSet<>(); + rf.getShortcuts().forEach( + shortcut -> functionDependencyProvider.addDependencies(shortcut.getExpression(), enumImports)); + rf.getOperations().forEach( + operation -> functionDependencyProvider.addDependencies(operation.getExpression(), enumImports)); - List allConditions = new ArrayList<>(func.getConditions()); - allConditions.addAll(func.getPostConditions()); - allConditions.forEach(condition -> dependencies - .addAll(functionDependencyProvider.findDependencies(condition.getExpression()))); + List allConditions = new ArrayList<>(rf.getConditions()); + allConditions.addAll(rf.getPostConditions()); + allConditions.forEach( + condition -> functionDependencyProvider.addDependencies(condition.getExpression(), enumImports)); - func.getInputs().forEach(input -> { + rf.getInputs().forEach(input -> { if (input.getTypeCall() != null && input.getTypeCall().getType() != null) { - dependencies.add(input.getTypeCall().getType()); + functionDependencyProvider.addDependencies(input.getTypeCall().getType(), enumImports); } }); - if (func.getOutput() != null && func.getOutput().getTypeCall() != null - && func.getOutput().getTypeCall().getType() != null) { - dependencies.add(func.getOutput().getTypeCall().getType()); - } - - dependencies.removeIf(it -> it instanceof Function f && f.getName().equals(func.getName())); - - return dependencies; - } - - private void generateIfBlocks(PythonCodeWriter writer, Function function) { - List levelList = new ArrayList<>(Collections.singletonList(0)); - for (ShortcutDeclaration shortcut : function.getShortcuts()) { - writer.appendBlock(expressionGenerator.generateThenElseForFunction(shortcut.getExpression(), levelList)); - } - for (Operation operation : function.getOperations()) { - writer.appendBlock(expressionGenerator.generateThenElseForFunction(operation.getExpression(), levelList)); + if (rf.getOutput() != null && rf.getOutput().getTypeCall() != null + && rf.getOutput().getTypeCall().getType() != null) { + functionDependencyProvider.addDependencies(rf.getOutput().getTypeCall().getType(), enumImports); } + return enumImports; } - private String generateTypeOrFunctionConditions(Function function) { + private String generateConditions(Function function) { if (!function.getConditions().isEmpty()) { PythonCodeWriter writer = new PythonCodeWriter(); writer.appendLine("# conditions"); writer.appendBlock( expressionGenerator.generateFunctionConditions(function.getConditions(), "_pre_registry")); writer.appendLine("# Execute all registered conditions"); - writer.appendLine("execute_local_conditions(_pre_registry, 'Pre-condition')"); + writer.appendLine("rune_execute_local_conditions(_pre_registry, 'Pre-condition')"); writer.appendLine(""); return writer.toString(); } @@ -255,34 +323,42 @@ private String generatePostConditions(Function function) { writer.appendBlock( expressionGenerator.generateFunctionConditions(function.getPostConditions(), "_post_registry")); writer.appendLine("# Execute all registered post-conditions"); - writer.appendLine("execute_local_conditions(_post_registry, 'Post-condition')"); + writer.appendLine("rune_execute_local_conditions(_post_registry, 'Post-condition')"); return writer.toString(); } return ""; } - private void generateAlias(PythonCodeWriter writer, Function function) { - int level = 0; - + private void generateAlias(PythonCodeWriter writer, Function function, int[] level) { for (ShortcutDeclaration shortcut : function.getShortcuts()) { expressionGenerator.setIfCondBlocks(new ArrayList<>()); - String expression = expressionGenerator.generateExpression(shortcut.getExpression(), level, false); + String expression = expressionGenerator.generateExpression(shortcut.getExpression(), level[0], false); + + for (String block : expressionGenerator.getIfCondBlocks()) { + writer.appendBlock(block); + writer.newLine(); + } if (!expressionGenerator.getIfCondBlocks().isEmpty()) { - level += 1; + level[0] += expressionGenerator.getIfCondBlocks().size(); } writer.appendLine(shortcut.getName() + " = " + expression); } } - private void generateOperations(PythonCodeWriter writer, Function function) { - int level = 0; + private void generateOperations(PythonCodeWriter writer, Function function, int[] level) { if (function.getOutput() != null) { List setNames = new ArrayList<>(); for (Operation operation : function.getOperations()) { AssignPathRoot root = operation.getAssignRoot(); - String expression = expressionGenerator.generateExpression(operation.getExpression(), level, false); + expressionGenerator.setIfCondBlocks(new ArrayList<>()); + String expression = expressionGenerator.generateExpression(operation.getExpression(), level[0], false); + + for (String block : expressionGenerator.getIfCondBlocks()) { + writer.appendBlock(block); + writer.newLine(); + } if (!expressionGenerator.getIfCondBlocks().isEmpty()) { - level += 1; + level[0] += expressionGenerator.getIfCondBlocks().size(); } if (operation.isAdd()) { generateAddOperation(writer, root, operation, function, expression, setNames); @@ -296,26 +372,30 @@ private void generateOperations(PythonCodeWriter writer, Function function) { private void generateAddOperation(PythonCodeWriter writer, AssignPathRoot root, Operation operation, Function function, String expression, List setNames) { Attribute attribute = (Attribute) root; - + String rootName = root.getName(); if (attribute.getTypeCall().getType() instanceof RosettaEnumeration) { - if (!setNames.contains(root.getName())) { - setNames.add(root.getName()); - writer.appendLine(root.getName() + " = []"); + if (!setNames.contains(rootName)) { + setNames.add(rootName); + writer.appendLine(rootName + " = []"); } - writer.appendLine(root.getName() + ".extend(" + expression + ")"); + writer.appendLine(rootName + ".extend(" + expression + ")"); } else { - if (!setNames.contains(root.getName())) { - setNames.add(root.getName()); - String spacer = (expression.startsWith("if_cond_fn") || root.getName().equals("result")) ? " = " - : " = "; - writer.appendLine(root.getName() + spacer + expression); + if (!setNames.contains(rootName)) { + setNames.add(rootName); + writer.appendLine(rootName + " = " + expression); } else { if (operation.getPath() == null) { - writer.appendLine(root.getName() + ".add_rune_attr(self, " + expression + ")"); + writer.appendLine(rootName + ".add_rune_attr(self, " + expression + ")"); } else { String path = generateAttributesPath(operation.getPath()); - writer.appendLine(root.getName() + ".add_rune_attr(rune_resolve_attr(rune_resolve_attr(self, " - + root.getName() + "), " + path + "), " + expression + ")"); + writer.appendLine(rootName + + ".add_rune_attr(rune_resolve_attr(rune_resolve_attr(self, " + + rootName + + "), " + + path + + "), " + + expression + + ")"); } } } @@ -324,19 +404,19 @@ private void generateAddOperation(PythonCodeWriter writer, AssignPathRoot root, private void generateSetOperation(PythonCodeWriter writer, AssignPathRoot root, Operation operation, Function function, String expression, List setNames) { Attribute attributeRoot = (Attribute) root; - String name = attributeRoot.getName(); - String spacer = (expression.startsWith("if_cond_fn") || name.equals("result")) ? " = " : " = "; + String equalsSign = " = "; if (attributeRoot.getTypeCall().getType() instanceof RosettaEnumeration || operation.getPath() == null) { - writer.appendLine(attributeRoot.getName() + spacer + expression); + writer.appendLine(attributeRoot.getName() + equalsSign + expression); } else { + String bundleName = RuneToPythonMapper.getBundleObjectName(attributeRoot.getTypeCall().getType()); if (!setNames.contains(attributeRoot.getName())) { setNames.add(attributeRoot.getName()); - writer.appendLine(attributeRoot.getName() + spacer + "_get_rune_object('" - + attributeRoot.getTypeCall().getType().getName() + "', " + + writer.appendLine(attributeRoot.getName() + equalsSign + "_get_rune_object('" + + bundleName + "', " + getNextPathElementName(operation.getPath()) + ", " + buildObject(expression, operation.getPath()) + ")"); } else { - writer.appendLine(attributeRoot.getName() + spacer + "set_rune_attr(rune_resolve_attr(self, '" + writer.appendLine(attributeRoot.getName() + equalsSign + "set_rune_attr(rune_resolve_attr(self, '" + attributeRoot.getName() + "'), " + generateAttributesPath(operation.getPath()) + ", " + expression + ")"); @@ -359,11 +439,7 @@ private String generateAttributesPath(Segment path) { } private String getNextPathElementName(Segment path) { - if (path != null) { - RosettaFeature feature = path.getFeature(); - return "'" + feature.getName() + "'"; - } - return "null"; + return (path == null) ? null : "'" + path.getFeature().getName() + "'"; } private String buildObject(String expression, Segment path) { @@ -373,21 +449,17 @@ private String buildObject(String expression, Segment path) { RosettaFeature feature = path.getFeature(); if (feature instanceof RosettaTyped typed) { - return _get_rune_object(typed.getTypeCall().getType().getName(), path.getNext(), expression); + String bundleName = RuneToPythonMapper.getBundleObjectName(typed.getTypeCall().getType()); + Segment nextPath = path.getNext(); + return "_get_rune_object('" + + bundleName + + "', " + + getNextPathElementName(nextPath) + + ", " + + buildObject(expression, nextPath) + + ")"; } throw new IllegalArgumentException("Cannot build object for feature " + feature.getName() + " of type " + feature.getClass().getSimpleName()); } - - private String _get_rune_object(String typeName, Segment nextPath, String expression) { - return "_get_rune_object('" + typeName + "', " + getNextPathElementName(nextPath) + ", " - + buildObject(expression, nextPath) + ")"; - } - - public void addImportsFromConditions(String variable, String namespace) { - String imp = "from " + namespace + "." + variable + " import " + variable; - if (!importsFound.contains(imp)) { - importsFound.add(imp); - } - } } diff --git a/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java b/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java index 5c294ec..06afa81 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/object/PythonAttributeProcessor.java @@ -23,22 +23,22 @@ public class PythonAttributeProcessor { @Inject private TypeSystem typeSystem; - public String generateAllAttributes(Data rosettaClass, Map> keyRefConstraints) { - RDataType buildRDataType = rObjectFactory.buildRDataType(rosettaClass); + public String generateAllAttributes(Data rc, Map> keyRefConstraints) { + RDataType buildRDataType = rObjectFactory.buildRDataType(rc); Collection allAttributes = buildRDataType.getOwnAttributes(); - if (allAttributes.isEmpty() && rosettaClass.getConditions().isEmpty()) { + if (allAttributes.isEmpty() && rc.getConditions().isEmpty()) { return "pass"; } PythonCodeWriter writer = new PythonCodeWriter(); for (RAttribute ra : allAttributes) { - generateAttribute(writer, rosettaClass, ra, keyRefConstraints); + generateAttribute(writer, rc, ra, keyRefConstraints); } return writer.toString(); } - private void generateAttribute(PythonCodeWriter writer, Data rosettaClass, RAttribute ra, + private void generateAttribute(PythonCodeWriter writer, Data rc, RAttribute ra, Map> keyRefConstraints) { RType rt = ra.getRMetaAnnotatedType().getRType(); @@ -53,7 +53,7 @@ private void generateAttribute(PythonCodeWriter writer, Data rosettaClass, RAttr if (attrTypeName == null) { throw new RuntimeException( - "Attribute type is null for " + ra.getName() + " in class " + rosettaClass.getName()); + "Attribute type is null for " + ra.getName() + " in class " + rc.getName()); } Map attrProp = processProperties(rt); @@ -73,72 +73,65 @@ private void createAttributeString( Map cardinalityMap) { String propString = createPropString(attrProp); - boolean isRosettaBasicType = RuneToPythonMapper.isRosettaBasicType(rt); String attrName = RuneToPythonMapper.mangleName(ra.getName()); - String metaPrefix = ""; - String metaSuffix = ""; - String attrTypeName = (!validators.isEmpty()) ? RuneToPythonMapper.getAttributeTypeWithMeta(attrTypeNameIn) : attrTypeNameIn; - String attrTypeNameOut = (isRosettaBasicType || rt instanceof REnumType) - ? attrTypeName - : attrTypeName.replace('.', '_'); + String attrTypeNameOut = RuneToPythonMapper.getFlattenedTypeName(rt, attrTypeName); + + String metaPrefix = ""; + String metaSuffix = ""; if (!validators.isEmpty()) { metaPrefix = getMetaDataPrefix(validators); metaSuffix = getMetaDataSuffix(validators, attrTypeNameOut); - } else if (!isRosettaBasicType && !(rt instanceof REnumType)) { + } else if (!RuneToPythonMapper.isRosettaBasicType(rt) && !(rt instanceof REnumType)) { metaPrefix = "Annotated["; metaSuffix = ", " + attrTypeNameOut + ".serializer(), " + attrTypeNameOut + ".validator()]"; } + String baseType = metaPrefix + attrTypeNameOut + metaSuffix; + RCardinality cardinality = ra.getCardinality(); + int min = cardinality.getMin(); + int max = cardinality.getMax().orElse(-1); + boolean isList = (cardinality.isMulti() || max > 1); + + // If it is a list and we have properties (e.g. max_digits), these properties + // belong to the element, not the list. + // So we wrap the element type in Annotated[Type, Field(...properties...)]. + boolean propertiesAppliedToInnerType = false; + if (isList && !attrProp.isEmpty()) { + baseType = "Annotated[" + baseType + ", Field(" + propString + ")]"; + propertiesAppliedToInnerType = true; + } + + String pythonType = RuneToPythonMapper.formatPythonType(baseType, min, max, false); + String attrDesc = (ra.getDefinition() == null) ? "" : ra.getDefinition().replaceAll("\\s+", " ").replace("'", "\\'"); StringBuilder lineBuilder = new StringBuilder(); lineBuilder.append(attrName).append(": "); - - if (!attrProp.isEmpty() && !cardinalityMap.isEmpty() && cardinalityMap.get("cardinalityString").length() > 0) { - lineBuilder.append(cardinalityMap.get("cardinalityPrefix")); - lineBuilder.append("Annotated["); - lineBuilder.append(attrTypeNameOut); - lineBuilder.append(", Field("); + lineBuilder.append(pythonType); + + lineBuilder.append(" = Field("); + lineBuilder.append(cardinalityMap.get("fieldDefault")); + lineBuilder.append(", description='"); + lineBuilder.append(attrDesc); + lineBuilder.append("'"); + lineBuilder.append(cardinalityMap.get("cardinalityString")); + + // Only append propString to the outer Field if it hasn't been applied to the + // inner type + if (!propString.isEmpty() && !propertiesAppliedToInnerType) { + lineBuilder.append(", "); lineBuilder.append(propString); - if (!metaSuffix.isEmpty()) { - lineBuilder.append(metaSuffix); - } else { - lineBuilder.append(")]"); - } - lineBuilder.append(cardinalityMap.get("cardinalitySuffix")); - lineBuilder.append(" = Field("); - lineBuilder.append(cardinalityMap.get("fieldDefault")); - lineBuilder.append(", description='"); - lineBuilder.append(attrDesc); - lineBuilder.append("'"); - lineBuilder.append(cardinalityMap.get("cardinalityString")); - lineBuilder.append(")"); - } else { - lineBuilder.append(cardinalityMap.get("cardinalityPrefix")); - lineBuilder.append(metaPrefix); - lineBuilder.append(attrTypeNameOut); - lineBuilder.append(metaSuffix); - lineBuilder.append(cardinalityMap.get("cardinalitySuffix")); - lineBuilder.append(" = Field("); - lineBuilder.append(cardinalityMap.get("fieldDefault")); - lineBuilder.append(", description='"); - lineBuilder.append(attrDesc); - lineBuilder.append("'"); - lineBuilder.append(cardinalityMap.get("cardinalityString")); - if (!propString.isEmpty()) { - lineBuilder.append(", "); - lineBuilder.append(propString); - } - lineBuilder.append(")"); } + + lineBuilder.append(")"); writer.appendLine(lineBuilder.toString()); if (ra.getDefinition() != null) { @@ -212,8 +205,10 @@ private Map processProperties(RType rt) { if (!numberType.isInteger()) { numberType.getDigits().ifPresent(value -> attrProp.put("max_digits", value.toString())); numberType.getFractionalDigits().ifPresent(value -> attrProp.put("decimal_places", value.toString())); - numberType.getInterval().getMin().ifPresent(value -> attrProp.put("ge", value.toPlainString())); - numberType.getInterval().getMax().ifPresent(value -> attrProp.put("le", value.toPlainString())); + numberType.getInterval().getMin() + .ifPresent(value -> attrProp.put("ge", "Decimal('" + value.toPlainString() + "')")); + numberType.getInterval().getMax() + .ifPresent(value -> attrProp.put("le", "Decimal('" + value.toPlainString() + "')")); } } return attrProp; @@ -233,75 +228,69 @@ private Map processCardinality(RAttribute ra) { boolean upperBoundIsGTOne = (upperCardinality.isPresent() && upperCardinality.get() > 1); String fieldDefault = ""; - String cardinalityPrefix = ""; - String cardinalitySuffix = ""; String cardinalityString = ""; - switch (lowerBound) { - case 0 -> { - cardinalityPrefix = "Optional["; - cardinalitySuffix = "]"; - fieldDefault = "None"; - if (cardinality.isMulti() || upperBoundIsGTOne) { - cardinalityPrefix += "list["; - cardinalitySuffix += "]"; - if (upperBoundIsGTOne) { - cardinalityString = ", max_length=" + upperCardinality.get().toString(); - } + // Default constraints + if (lowerBound == 0) { + fieldDefault = "None"; + if (cardinality.isMulti() || upperBoundIsGTOne) { + if (upperBoundIsGTOne) { + cardinalityString = ", max_length=" + upperCardinality.get().toString(); } } - case 1 -> { - fieldDefault = "..."; - if (cardinality.isMulti() || upperBoundIsGTOne) { - cardinalityPrefix = "list["; - cardinalitySuffix = "]"; - cardinalityString = ", min_length=1"; - if (upperBoundIsGTOne) { - cardinalityString += ", max_length=" + upperCardinality.get().toString(); - } + } else if (lowerBound == 1) { + fieldDefault = "..."; + if (cardinality.isMulti() || upperBoundIsGTOne) { + cardinalityString = ", min_length=1"; + if (upperBoundIsGTOne) { + cardinalityString += ", max_length=" + upperCardinality.get().toString(); } } - default -> { - cardinalityPrefix = "list["; - cardinalitySuffix = "]"; - cardinalityString = ", min_length=" + lowerBound; - fieldDefault = "..."; - if (upperCardinality.isPresent()) { - int upperBound = upperCardinality.get(); - if (upperBound > 1) { - cardinalityString += ", max_length=" + upperBound; - } + } else { + fieldDefault = "..."; + cardinalityString = ", min_length=" + lowerBound; + if (upperCardinality.isPresent()) { + int upperBound = upperCardinality.get(); + if (upperBound > 1) { + cardinalityString += ", max_length=" + upperBound; } } } - cardinalityMap.put("cardinalityPrefix", cardinalityPrefix); - cardinalityMap.put("cardinalitySuffix", cardinalitySuffix); + cardinalityMap.put("cardinalityString", cardinalityString); cardinalityMap.put("fieldDefault", fieldDefault); return cardinalityMap; } - public Set getImportsFromAttributes(Data rosettaClass) { - RDataType buildRDataType = rObjectFactory.buildRDataType(rosettaClass); + public void getImportsFromAttributes(Data rc, Set enumImports) { + /** + * Get ENUM imports from attributes (all other dependencies are handled by the + * bundle) + * + * @param rc + * @param enumImports + */ + RDataType buildRDataType = rObjectFactory.buildRDataType(rc); Collection allAttributes = buildRDataType.getOwnAttributes(); - return allAttributes.stream() - .filter(it -> !it.getName().equals("reference") && !it.getName().equals("meta") - && !it.getName().equals("scheme")) - .filter(it -> !RuneToPythonMapper.isRosettaBasicType(it)) - .map(it -> { - RType rt = it.getRMetaAnnotatedType().getRType(); - if (rt instanceof RAliasType) { - rt = typeSystem.stripFromTypeAliases(rt); - } - if (rt == null) { - throw new RuntimeException( - "Attribute type is null for " + it.getName() + " for class " + rosettaClass.getName()); - } - return rt; - }) - .filter(rt -> rt instanceof REnumType) - .map(rt -> "import " + ((REnumType) rt).getQualifiedName()) - .collect(Collectors.toSet()); + for (RAttribute attr : allAttributes) { + if (!attr.getName().equals("reference") && + !attr.getName().equals("meta") && + !attr.getName().equals("scheme") && + !RuneToPythonMapper.isRosettaBasicType(attr)) { + RType rt = attr.getRMetaAnnotatedType().getRType(); + if (rt instanceof RAliasType) { + rt = typeSystem.stripFromTypeAliases(rt); + } + if (rt == null) { + throw new RuntimeException( + "Attribute type is null for " + attr.getName() + " for class " + rc.getName()); + } + + if (rt instanceof REnumType) { + enumImports.add("import " + ((REnumType) rt).getQualifiedName()); + } + } + } } } diff --git a/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java b/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java index 51356c4..0f72dcf 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/object/PythonModelObjectGenerator.java @@ -1,18 +1,22 @@ package com.regnosys.rosetta.generator.python.object; +import com.regnosys.rosetta.generator.python.PythonCodeGeneratorContext; + import com.regnosys.rosetta.generator.python.expressions.PythonExpressionGenerator; import com.regnosys.rosetta.generator.python.util.PythonCodeGeneratorUtil; +import com.regnosys.rosetta.generator.python.util.RuneToPythonMapper; import com.regnosys.rosetta.generator.python.util.PythonCodeWriter; import com.regnosys.rosetta.rosetta.RosettaModel; import com.regnosys.rosetta.rosetta.simple.Data; import com.regnosys.rosetta.types.RDataType; import com.regnosys.rosetta.types.RObjectFactory; import jakarta.inject.Inject; +import com.regnosys.rosetta.types.RAttribute; +import com.regnosys.rosetta.types.REnumType; +import com.regnosys.rosetta.types.RType; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultEdge; -import org.jgrapht.graph.DirectedAcyclicGraph; import org.jgrapht.graph.GraphCycleProhibitedException; -import org.jgrapht.traverse.TopologicalOrderIterator; import java.util.*; import java.util.stream.Collectors; @@ -34,106 +38,74 @@ public class PythonModelObjectGenerator { @Inject private PythonChoiceAliasProcessor pythonChoiceAliasProcessor; - private Graph dependencyDAG = null; - private Set imports = null; - - public void beforeAllGenerate() { - dependencyDAG = new DirectedAcyclicGraph<>(DefaultEdge.class); - imports = new HashSet<>(); - } - /** * Generate Python from the collection of Rosetta classes (of type Data). - * Note: this function updates the dependency graph used by afterAllGenerate to - * create the bundle * - * @param rosettaClasses the collection of Rosetta Classes for this model - * @param version the version for this collection of classes + * @param rClasses the collection of Rosetta Classes for this model + * @param version the version for this collection of classes * @return a Map of all the generated Python indexed by the class name */ - public Map generate(Iterable rosettaClasses, String version) { + public Map generate(Iterable rClasses, String version, + PythonCodeGeneratorContext context) { + Graph dependencyDAG = context.getDependencyDAG(); + if (dependencyDAG == null) { + throw new RuntimeException("Dependency DAG not initialized"); + } + Set enumImports = context.getEnumImports(); + if (enumImports == null) { + throw new RuntimeException("Enum imports not initialized"); + } + Map result = new HashMap<>(); - for (Data rosettaClass : rosettaClasses) { - RosettaModel model = (RosettaModel) rosettaClass.eContainer(); + for (Data rc : rClasses) { + RosettaModel model = (RosettaModel) rc.eContainer(); String nameSpace = PythonCodeGeneratorUtil.getNamespace(model); // Generate Python for the class - String pythonClass = generateClass(rosettaClass, nameSpace, version); + try { + String pythonClass = generateClass(rc, nameSpace, version, enumImports); - // use "." as a delimiter to preserve the use of "_" in the name - String className = model.getName() + "." + rosettaClass.getName(); - result.put(className, pythonClass); + // construct the class name using "." as a delimiter + String className = model.getName() + "." + rc.getName(); + result.put(className, pythonClass); - if (dependencyDAG != null) { dependencyDAG.addVertex(className); - if (rosettaClass.getSuperType() != null) { - Data superClass = rosettaClass.getSuperType(); + if (rc.getSuperType() != null) { + Data superClass = rc.getSuperType(); RosettaModel superModel = (RosettaModel) superClass.eContainer(); String superClassName = superModel.getName() + "." + superClass.getName(); - addDependency(className, superClassName); + + addDependency(dependencyDAG, className, superClassName); } + + addAttributeDependencies(dependencyDAG, className, rc); + } catch (Exception e) { + throw new RuntimeException("Error generating Python for class " + rc.getName(), e); } } return result; } - public Map afterAllGenerate(String namespace, Map objects) { - // create bundle and stub classes - Map result = new HashMap<>(); - if (dependencyDAG != null) { - PythonCodeWriter bundleWriter = new PythonCodeWriter(); - TopologicalOrderIterator topologicalOrderIterator = new TopologicalOrderIterator<>( - dependencyDAG); - - // for each element in the ordered collection add the generated class to the - // bundle and add a stub class to the results - boolean isFirst = true; - while (topologicalOrderIterator.hasNext()) { - if (isFirst) { - bundleWriter.appendBlock(PythonCodeGeneratorUtil.createImports()); - List sortedImports = new ArrayList<>(imports); - Collections.sort(sortedImports); - for (String imp : sortedImports) { - bundleWriter.appendLine(imp); - } - isFirst = false; + private void addAttributeDependencies(Graph dependencyDAG, String className, Data rc) { + RDataType buildRDataType = rObjectFactory.buildRDataType(rc); + for (RAttribute attr : buildRDataType.getOwnAttributes()) { + RType rt = attr.getRMetaAnnotatedType().getRType(); + if (rt instanceof RDataType || rt instanceof REnumType) { + String dependencyName = ""; + if (rt instanceof RDataType) { + dependencyName = ((RDataType) rt).getQualifiedName().toString(); + } else { + dependencyName = ((REnumType) rt).getQualifiedName().toString(); } - String name = topologicalOrderIterator.next(); - CharSequence object = objects.get(name); - if (object != null) { - // append the class to the bundle - bundleWriter.newLine(); - bundleWriter.newLine(); - bundleWriter.appendBlock(object.toString()); - - // create the stub - String[] parsedName = name.split("\\."); - String stubFileName = "src/" + String.join("/", parsedName) + ".py"; - - PythonCodeWriter stubWriter = new PythonCodeWriter(); - stubWriter.appendLine("# pylint: disable=unused-import"); - stubWriter.append("from "); - stubWriter.append(parsedName[0]); - stubWriter.append("._bundle import "); - stubWriter.append(name.replace('.', '_')); - stubWriter.append(" as "); - stubWriter.append(parsedName[parsedName.length - 1]); - stubWriter.newLine(); - stubWriter.newLine(); - stubWriter.appendLine("# EOF"); - - result.put(stubFileName, stubWriter.toString()); + if (!dependencyName.isEmpty() && !className.equals(dependencyName)) { + addDependency(dependencyDAG, className, dependencyName); } } - bundleWriter.newLine(); - bundleWriter.appendLine("# EOF"); - result.put("src/" + namespace + "/_bundle.py", bundleWriter.toString()); } - return result; } - private void addDependency(String className, String dependencyName) { + private void addDependency(Graph dependencyDAG, String className, String dependencyName) { dependencyDAG.addVertex(dependencyName); if (!className.equals(dependencyName)) { try { @@ -144,22 +116,26 @@ private void addDependency(String className, String dependencyName) { } } - private String generateClass(Data rosettaClass, String nameSpace, String version) { - if (rosettaClass.getSuperType() != null && rosettaClass.getSuperType().getName() == null) { + private String generateClass(Data rc, String nameSpace, String version, Set enumImports) { + if (rc == null) { + throw new RuntimeException("Rosetta class not initialized"); + } + if (rc.getSuperType() != null && rc.getSuperType().getName() == null) { throw new RuntimeException( - "The class superType for " + rosettaClass.getName() + " exists but its name is null"); + "The class superType for " + rc.getName() + " exists but its name is null"); + } + if (enumImports == null) { + throw new RuntimeException("Enum imports not initialized"); } - Set importsFound = pythonAttributeProcessor.getImportsFromAttributes(rosettaClass); - imports.addAll(importsFound); - expressionGenerator.setImportsFound(new ArrayList<>(importsFound)); + pythonAttributeProcessor.getImportsFromAttributes(rc, enumImports); - return generateBody(rosettaClass); + return generateBody(rc); } - private String getClassMetaDataString(Data rosettaClass) { + private String getClassMetaDataString(Data rc) { // generate _ALLOWED_METADATA string for the type - RDataType rcRData = rObjectFactory.buildRDataType(rosettaClass); + RDataType rcRData = rObjectFactory.buildRDataType(rc); PythonCodeWriter writer = new PythonCodeWriter(); boolean first = true; @@ -171,7 +147,7 @@ private String getClassMetaDataString(Data rosettaClass) { writer.append(", "); } switch (metaData.getName()) { - case "key" -> writer.append("'@key', '@key:external'"); + case "key", "id" -> writer.append("'@key', '@key:external'"); case "scheme" -> writer.append("'@scheme'"); } } @@ -210,51 +186,42 @@ private String keyRefConstraintsToString(Map> keyRefConstra return writer.toString(); } - private String getFullyQualifiedName(Data rosettaClass) { - RosettaModel model = (RosettaModel) rosettaClass.eContainer(); - return model.getName() + "." + rosettaClass.getName(); - } - - private String getBundleClassName(Data rosettaClass) { - return getFullyQualifiedName(rosettaClass).replace(".", "_"); - } - - private String generateBody(Data rosettaClass) { - RDataType rosettaDataType = rObjectFactory.buildRDataType(rosettaClass); + private String generateBody(Data rc) { + RDataType rosettaDataType = rObjectFactory.buildRDataType(rc); Map> keyRefConstraints = new HashMap<>(); PythonCodeWriter writer = new PythonCodeWriter(); - String superClassName = (rosettaClass.getSuperType() != null) - ? getBundleClassName(rosettaClass.getSuperType()) + String superClassName = (rc.getSuperType() != null) + ? RuneToPythonMapper.getBundleObjectName(rc.getSuperType()) : "BaseDataClass"; - writer.appendLine("class " + getBundleClassName(rosettaClass) + "(" + superClassName + "):"); + writer.appendLine("class " + RuneToPythonMapper.getBundleObjectName(rc) + "(" + superClassName + "):"); writer.indent(); - String metaData = getClassMetaDataString(rosettaClass); + String metaData = getClassMetaDataString(rc); if (!metaData.isEmpty()) { writer.appendBlock(metaData); } pythonChoiceAliasProcessor.generateChoiceAliases(writer, rosettaDataType); - if (rosettaClass.getDefinition() != null) { + if (rc.getDefinition() != null) { writer.appendLine("\"\"\""); - writer.appendLine(rosettaClass.getDefinition()); + writer.appendLine(rc.getDefinition()); writer.appendLine("\"\"\""); } - writer.appendLine("_FQRTN = '" + getFullyQualifiedName(rosettaClass) + "'"); + writer.appendLine("_FQRTN = '" + RuneToPythonMapper.getFullyQualifiedObjectName(rc) + "'"); - writer.appendBlock(pythonAttributeProcessor.generateAllAttributes(rosettaClass, keyRefConstraints)); + writer.appendBlock(pythonAttributeProcessor.generateAllAttributes(rc, keyRefConstraints)); String constraints = keyRefConstraintsToString(keyRefConstraints); if (!constraints.isEmpty()) { writer.appendBlock(constraints); } - writer.appendBlock(expressionGenerator.generateTypeOrFunctionConditions(rosettaClass)); + writer.appendBlock(expressionGenerator.generateTypeOrFunctionConditions(rc)); return writer.toString(); } diff --git a/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorConstants.java b/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorConstants.java new file mode 100644 index 0000000..4b67181 --- /dev/null +++ b/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorConstants.java @@ -0,0 +1,12 @@ +package com.regnosys.rosetta.generator.python.util; + +public final class PythonCodeGeneratorConstants { + private PythonCodeGeneratorConstants() { + // Restricted constructor + } + + public static final String PYTHON = "python"; + public static final String SRC = "src/"; + public static final String PYPROJECT_TOML = "pyproject.toml"; + public static final String INIT = "__init__"; +} diff --git a/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java b/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java index b5190b7..dc7258d 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/util/PythonCodeGeneratorUtil.java @@ -42,30 +42,6 @@ public static String classComment(String definition, Iterable attrib return writer.toString(); } - public static String createImports(String name) { - return """ - # pylint: disable=line-too-long, invalid-name, missing-function-docstring - # pylint: disable=bad-indentation, trailing-whitespace, superfluous-parens - # pylint: disable=wrong-import-position, unused-import, unused-wildcard-import - # pylint: disable=wildcard-import, wrong-import-order, missing-class-docstring - # pylint: disable=missing-module-docstring, unused-variable, unnecessary-pass - - from __future__ import annotations - from typing import Optional, Annotated - import datetime - import inspect - from decimal import Decimal - from pydantic import Field - from rune.runtime.base_data_class import BaseDataClass - from rune.runtime.metadata import * - from rune.runtime.utils import * - from rune.runtime.conditions import * - from rune.runtime.func_proxy import * - __all__ = ['%s'] - - """.formatted(name).stripIndent(); - } - public static String createImports() { return """ # pylint: disable=line-too-long, invalid-name, missing-function-docstring @@ -74,35 +50,20 @@ public static String createImports() { # pylint: disable=wildcard-import, wrong-import-order, missing-class-docstring # pylint: disable=missing-module-docstring from __future__ import annotations - from typing import Optional, Annotated import datetime import inspect + import sys from decimal import Decimal - from pydantic import Field + from typing import Annotated, Optional + + from pydantic import Field, validate_call + from rune.runtime.base_data_class import BaseDataClass - from rune.runtime.metadata import * - from rune.runtime.utils import * from rune.runtime.conditions import * from rune.runtime.func_proxy import * - """.stripIndent(); - } - - public static String createImportsFunc(String name) { - return """ - # pylint: disable=line-too-long, invalid-name, missing-function-docstring, missing-module-docstring, superfluous-parens - # pylint: disable=wrong-import-position, unused-import, unused-wildcard-import, wildcard-import, wrong-import-order, missing-class-docstring - from __future__ import annotations - import sys - import datetime - import inspect - from decimal import Decimal - from rune.runtime.base_data_class import BaseDataClass from rune.runtime.metadata import * from rune.runtime.utils import * - from rune.runtime.conditions import * - from rune.runtime.func_proxy import * - """ - .stripIndent(); + """.stripIndent(); } public static String toFileName(String namespace, String fileName) { @@ -150,4 +111,18 @@ public static String createPYProjectTomlFile(String namespace, String version) { [tool.setuptools.packages.find] where = ["src"]""".formatted(namespace, version).stripIndent(); } + + public static String cleanVersion(String version) { + if (version == null || version.equals("${project.version}")) { + return "0.0.0"; + } + + String[] versionParts = version.split("\\."); + if (versionParts.length > 2) { + String thirdPart = versionParts[2].replaceAll("[^\\d]", ""); + return versionParts[0] + "." + versionParts[1] + "." + thirdPart; + } + + return "0.0.0"; + } } diff --git a/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java b/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java index 1321265..8d1fc8c 100644 --- a/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java +++ b/src/main/java/com/regnosys/rosetta/generator/python/util/RuneToPythonMapper.java @@ -5,7 +5,12 @@ import com.regnosys.rosetta.types.RAttribute; import com.regnosys.rosetta.types.REnumType; +import com.regnosys.rosetta.rosetta.RosettaEnumeration; +import com.regnosys.rosetta.rosetta.RosettaModel; + import com.regnosys.rosetta.types.RType; +import com.regnosys.rosetta.rosetta.RosettaNamed; +import com.regnosys.rosetta.rosetta.simple.Function; /** * A utility class for mapping Rune (Rosetta) types and attributes to their @@ -141,6 +146,42 @@ public static String getAttributeTypeWithMeta(String attributeType) { } } + public static String getFullyQualifiedObjectName(RosettaNamed rn) { + RosettaModel model = (RosettaModel) rn.eContainer(); + if (model == null) { + throw new RuntimeException("Rosetta model not found for data " + rn.getName()); + } + + if (rn instanceof REnumType) { + return ((REnumType) rn).getQualifiedName().toString() + "." + rn.getName(); + } + String typeName = toPythonBasicTypeInnerFunction(rn.getName()); + if (typeName == null) { + String function = (rn instanceof Function) ? ".functions" : ""; + typeName = model.getName() + function + "." + rn.getName(); + if (rn instanceof RosettaEnumeration) { + typeName += "." + rn.getName(); + } + } + return typeName; + } + + public static String getBundleObjectName(RosettaNamed rn, boolean useQuotes) { + String fullyQualifiedObjectName = getFullyQualifiedObjectName(rn); + if (rn instanceof RosettaEnumeration || isRosettaBasicType(rn.getName())) { + return fullyQualifiedObjectName; + } + String bundleName = fullyQualifiedObjectName.replace(".", "_"); + if (useQuotes) { + return "\"" + bundleName + "\""; + } + return bundleName; + } + + public static String getBundleObjectName(RosettaNamed rn) { + return getBundleObjectName(rn, false); + } + /** * Convert from Rune type as string to Python type. * @@ -153,12 +194,15 @@ public static String toPythonBasicType(String rosettaType) { } /** - * Convert from Rune RType to Python type. + * Convert from Rune RType to Python type with optional quoting for forward + * references. * - * @param rt the Rune RType object + * @param rt the Rune RType object + * @param useQuotes whether to wrap the type name in quotes for forward + * references * @return the Python type name string, or null if rt is null */ - public static String toPythonType(RType rt) { + public static String toPythonType(RType rt, boolean useQuotes) { if (rt == null) return null; var pythonType = toPythonBasicTypeInnerFunction(rt.getName()); @@ -166,10 +210,27 @@ public static String toPythonType(RType rt) { String rtName = rt.getName(); pythonType = rt.getNamespace().toString() + "." + rtName; pythonType = (rt instanceof REnumType) ? pythonType + "." + rtName : pythonType; + + if (!isRosettaBasicType(rt) && !(rt instanceof REnumType)) { + pythonType = getFlattenedTypeName(rt, pythonType); + if (useQuotes) { + pythonType = "\"" + pythonType + "\""; + } + } } return pythonType; } + /** + * Convert from Rune RType to Python type. + * + * @param rt the Rune RType object + * @return the Python type name string, or null if rt is null + */ + public static String toPythonType(RType rt) { + return toPythonType(rt, false); + } + /** * Check if the given type is a basic type. * @@ -204,6 +265,42 @@ public static boolean isRosettaBasicType(RAttribute ra) { return (rt != null) ? isRosettaBasicType(rt.getName()) : false; } + /** + * Formats a Python type string based on cardinality and context. + * + * @param baseType the base Python type (e.g., "str", "Decimal") + * @param min the minimum cardinality + * @param max the maximum cardinality + * @param isInputArgument true if this is for a function input argument (uses " + * | None"), false for a class field (uses + * "Optional[...]"). + * @return the formatted Python type string + */ + public static String formatPythonType(String baseType, int min, int max, boolean isInputArgument) { + String type = baseType; + boolean isList = (max > 1 || max == -1 || max == 0); + + if (isList) { + type = "list[" + type + "]"; + } + + if (min == 0) { + if (isInputArgument) { + type = type + " | None"; + } else { + type = "Optional[" + type + "]"; + } + } + return type; + } + + public static String getFlattenedTypeName(RType type, String typeName) { + if (isRosettaBasicType(type) || type instanceof REnumType) { + return typeName; + } + return typeName.replace('.', '_'); + } + /** * Check if the given type is in the set of Python types. * diff --git a/src/test/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLITest.java b/src/test/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLITest.java new file mode 100644 index 0000000..5ff024a --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/PythonCodeGeneratorCLITest.java @@ -0,0 +1,159 @@ +package com.regnosys.rosetta.generator.python; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.eclipse.emf.ecore.util.EcoreUtil; + +import static org.junit.jupiter.api.Assertions.*; + +class PythonCodeGeneratorCLITest { + + @TempDir + Path tempDir; + + @BeforeAll + static void setup() { + System.out.println( + ">>> Starting PythonCodeGeneratorCLITest. Expected error and warning logs may follow as part of validation testing."); + } + + @AfterAll + static void tearDown() { + System.out.println(">>> Finished PythonCodeGeneratorCLITest."); + } + + @Test + void testMissingArgsReturnsError() { + PythonCodeGeneratorCLI cli = new PythonCodeGeneratorCLI(); + int exitCode = cli.execute(new String[] {}); + assertEquals(1, exitCode, "Should return 1 when no args provided"); + } + + @Test + void testHelpReturnsSuccess() { + PythonCodeGeneratorCLI cli = new PythonCodeGeneratorCLI(); + int exitCode = cli.execute(new String[] { "-h" }); + assertEquals(0, exitCode, "Should return 0 when help is requested"); + } + + @Test + void testInvalidSourceFileReturnsError() { + PythonCodeGeneratorCLI cli = new PythonCodeGeneratorCLI(); + int exitCode = cli.execute(new String[] { "-f", "non_existent_file.rosetta" }); + assertEquals(1, exitCode, "Should return 1 when source file does not exist"); + } + + @Test + void testValidationErrorsFailByDefault() throws IOException { + Path validFile = createValidRosettaFile(tempDir); // Use valid file, let MockValidator inject error + TestCLI cli = new TestCLI(); + cli.mockValidator.setReturnError(true); + + int exitCode = cli.execute(new String[] { + "-f", validFile.toString(), + "-t", tempDir.resolve("python").toString() + }); + + assertEquals(1, exitCode, "Should return 1 (fail) when validation errors occur by default"); + } + + @Test + void testAllowErrorsPasses() throws IOException { + Path validFile = createValidRosettaFile(tempDir); + TestCLI cli = new TestCLI(); + cli.mockValidator.setReturnError(true); + + int exitCode = cli.execute(new String[] { + "-f", validFile.toString(), + "-t", tempDir.resolve("python").toString(), + "-e" + }); + + assertEquals(0, exitCode, "Should return 0 (success) when validation errors occur but --allow-errors is set"); + } + + @Test + void testWarningsFailWithFlag() throws IOException { + Path validFile = createValidRosettaFile(tempDir); + TestCLI cli = new TestCLI(); + cli.mockValidator.setReturnWarning(true); + + int exitCode = cli.execute(new String[] { + "-f", validFile.toString(), + "-t", tempDir.resolve("python").toString(), + "-w" // --fail-on-warnings + }); + + assertEquals(1, exitCode, "Should return 1 (fail) when warnings occur and --fail-on-warnings is set"); + } + + private Path createValidRosettaFile(Path dir) throws IOException { + Path file = dir.resolve("valid.rosetta"); + String content = "namespace test.model\nversion \"1.0.0\"\ntype Foo:\n attr string (1..1)\n"; + Files.writeString(file, content); + return file; + } + + // --- Mocks --- + + static class TestCLI extends PythonCodeGeneratorCLI { + MockValidator mockValidator = new MockValidator(); + + @Override + protected org.eclipse.xtext.validation.IResourceValidator getValidator(com.google.inject.Injector injector) { + return mockValidator; + } + } + + static class MockValidator implements org.eclipse.xtext.validation.IResourceValidator { + private boolean returnError = false; + private boolean returnWarning = false; + + public void setReturnError(boolean returnError) { + this.returnError = returnError; + } + + public void setReturnWarning(boolean returnWarning) { + this.returnWarning = returnWarning; + } + + @Override + public java.util.List validate( + org.eclipse.emf.ecore.resource.Resource resource, + org.eclipse.xtext.validation.CheckMode mode, org.eclipse.xtext.util.CancelIndicator indicator) { + java.util.List issues = new java.util.ArrayList<>(); + + org.eclipse.emf.common.util.URI uri = null; + if (!resource.getContents().isEmpty()) { + org.eclipse.emf.ecore.EObject root = resource.getContents().get(0); + uri = EcoreUtil.getURI(root); + } else { + uri = org.eclipse.emf.common.util.URI.createURI("dummy#//"); + } + + if (returnError) { + issues.add(createIssue(org.eclipse.xtext.diagnostics.Severity.ERROR, "Mock Error", uri)); + } + if (returnWarning) { + issues.add(createIssue(org.eclipse.xtext.diagnostics.Severity.WARNING, "Mock Warning", uri)); + } + return issues; + } + + private org.eclipse.xtext.validation.Issue createIssue(org.eclipse.xtext.diagnostics.Severity severity, + String message, org.eclipse.emf.common.util.URI uri) { + org.eclipse.xtext.validation.Issue.IssueImpl issue = new org.eclipse.xtext.validation.Issue.IssueImpl(); + issue.setSeverity(severity); + issue.setMessage(message); + issue.setLineNumber(1); + issue.setUriToProblem(uri); + return issue; + } + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java deleted file mode 100644 index 07f20f2..0000000 --- a/src/test/java/com/regnosys/rosetta/generator/python/expressions/PythonExpressionGeneratorTest.java +++ /dev/null @@ -1,821 +0,0 @@ -package com.regnosys.rosetta.generator.python.expressions; - -import org.eclipse.xtext.testing.InjectWith; -import org.eclipse.xtext.testing.extensions.InjectionExtension; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; -import com.regnosys.rosetta.tests.RosettaInjectorProvider; - -import jakarta.inject.Inject; - -@ExtendWith(InjectionExtension.class) -@InjectWith(RosettaInjectorProvider.class) -public class PythonExpressionGeneratorTest { - - @Inject - private PythonGeneratorTestUtils testUtils; - - @Test - public void testArithmeticOperator() { - String generatedPython = testUtils.generatePythonFromString(""" - type ArithmeticTest: - a int (1..1) - b int (1..1) - condition Test: - if a + b = 3 then True - else False - """).toString(); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_ArithmeticTest(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.ArithmeticTest' - a: int = Field(..., description='') - b: int = Field(..., description='') - - @rune_condition - def condition_0_Test(self): - item = self - def _then_fn0(): - return True - - def _else_fn0(): - return False - - return if_cond_fn(rune_all_elements((rune_resolve_attr(self, "a") + rune_resolve_attr(self, "b")), "=", 3), _then_fn0, _else_fn0) - """ - ); - } - - @Test - public void testGenerateSwitch() { - testUtils.assertBundleContainsExpectedString(""" - type FooTest: - a int (1..1) <"Test field a"> - condition Test: - a switch - 1 then True, - 2 then True, - default False - """, - """ - class com_rosetta_test_model_FooTest(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.FooTest' - a: int = Field(..., description='Test field a') - \""" - Test field a - \""" - - @rune_condition - def condition_0_Test(self): - item = self - def _then_1(): - return True - def _then_2(): - return True - def _then_default(): - return False - switchAttribute = rune_resolve_attr(self, "a") - if switchAttribute == 1: - return _then_1() - elif switchAttribute == 2: - return _then_2() - else: - return _then_default() - """ - ); - } - - @Test - public void testGenerateChoiceCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test1:<"Test choice condition."> - field1 string (0..1) <"Test string field 1"> - field2 string (0..1) <"Test string field 2"> - field3 string (0..1) <"Test string field 3"> - condition TestChoice: optional choice field1, field2, field3 - """, - """ - class com_rosetta_test_model_Test1(BaseDataClass): - \""" - Test choice condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[str] = Field(None, description='Test string field 2') - \""" - Test string field 2 - \""" - field3: Optional[str] = Field(None, description='Test string field 3') - \""" - Test string field 3 - \""" - - @rune_condition - def condition_0_TestChoice(self): - item = self - return rune_check_one_of(self, 'field1', 'field2', 'field3', necessity=False)""" - ); - } - - @Test - public void testGenerateOneOfCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test1:<"Test one-of condition."> - field1 string (0..1) <"Test string field 1"> - condition OneOf: one-of - """, - """ - class com_rosetta_test_model_Test1(BaseDataClass): - _CHOICE_ALIAS_MAP ={"field1":[]} - \""" - Test one-of condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - - @rune_condition - def condition_0_OneOf(self): - item = self - return rune_check_one_of(self, 'field1', necessity=True)""" - ); - } - - @Test - public void testGenerateIfThenCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test1: <"Test if-then condition."> - field1 string (0..1) <"Test string field 1"> - field2 number (0..1) <"Test number field 2"> - condition TestCond: <"Test condition"> - if field1 exists - then field2=0 - """, - """ - class com_rosetta_test_model_Test1(BaseDataClass): - \""" - Test if-then condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[Decimal] = Field(None, description='Test number field 2') - \""" - Test number field 2 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) - - def _else_fn0(): - return True - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0)""" - ); - } - - @Test - public void testGenerateIfThenElseCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test1: <"Test if-then-else condition."> - field1 string (0..1) <"Test string field 1"> - field2 number (0..1) <"Test number field 2"> - condition TestCond: <"Test condition"> - if field1 exists - then field2=0 - else field2=1 - """, - """ - class com_rosetta_test_model_Test1(BaseDataClass): - \""" - Test if-then-else condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[Decimal] = Field(None, description='Test number field 2') - \""" - Test number field 2 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) - - def _else_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 1) - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0)""" - ); - } - - @Test - public void testGenerateBooleanCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test1: <"Test boolean condition."> - field1 boolean (1..1) <"Test booelan field 1"> - field2 number (0..1) <"Test number field 2"> - condition TestCond: <"Test condition"> - if field1= True - then field2=0 - else field2=5 - """, - """ - class com_rosetta_test_model_Test1(BaseDataClass): - \""" - Test boolean condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: bool = Field(..., description='Test booelan field 1') - \""" - Test booelan field 1 - \""" - field2: Optional[Decimal] = Field(None, description='Test number field 2') - \""" - Test number field 2 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) - - def _else_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 5) - - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field1"), "=", True), _then_fn0, _else_fn0)""" - ); - } - - @Test - public void testGenerateAbsentCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test1: <"Test absent condition."> - field1 boolean (1..1) <"Test booelan field 1"> - field2 number (0..1) <"Test number field 2"> - condition TestCond: <"Test condition"> - if field1= True - then field2=0 - else field2 is absent - """, - """ - class com_rosetta_test_model_Test1(BaseDataClass): - \""" - Test absent condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: bool = Field(..., description='Test booelan field 1') - \""" - Test booelan field 1 - \""" - field2: Optional[Decimal] = Field(None, description='Test number field 2') - \""" - Test number field 2 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) - - def _else_fn0(): - return (not rune_attr_exists(rune_resolve_attr(self, "field2"))) - - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field1"), "=", True), _then_fn0, _else_fn0)""" - ); - } - - @Test - public void testGenerateOnlyElementCondition() { - String generatedPython = testUtils.generatePythonFromString(""" - enum TestEnum: <"Enum to test"> - TestEnumValue1 <"Test enum value 1"> - TestEnumValue2 <"Test enum value 2"> - type Test1: <"Test only-element condition."> - field1 TestEnum (0..1) <"Test enum field 1"> - field2 number (0..1) <"Test number field 2"> - condition TestCond: <"Test condition"> - if field1 only-element= TestEnum->TestEnumValue1 - then field2=0 - """).toString(); - - String expectedTestEnum = """ - class TestEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Enum to test - \""" - TEST_ENUM_VALUE_1 = "TestEnumValue1" - \""" - Test enum value 1 - \""" - TEST_ENUM_VALUE_2 = "TestEnumValue2" - \""" - Test enum value 2 - \""""; - - String expectedTest1 = """ - class com_rosetta_test_model_Test1(BaseDataClass): - \""" - Test only-element condition. - \""" - _FQRTN = 'com.rosetta.test.model.Test1' - field1: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Test enum field 1') - \""" - Test enum field 1 - \""" - field2: Optional[Decimal] = Field(None, description='Test number field 2') - \""" - Test number field 2 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) - - def _else_fn0(): - return True - - return if_cond_fn(rune_all_elements(rune_get_only_element(rune_resolve_attr(self, "field1")), "=", com.rosetta.test.model.TestEnum.TestEnum.TEST_ENUM_VALUE_1), _then_fn0, _else_fn0)"""; - - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedTestEnum); - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedTest1); - } - - @Test - public void testGenerateOnlyExistsCondition() { - String generatedPython = testUtils.generatePythonFromString(""" - type A: <"Test type"> - field1 number (0..1) <"Test number field 1"> - - type Test: <"Test only exists condition"> - aValue A (1..1) <"Test A type aValue"> - - condition TestCond: <"Test condition"> - if aValue -> field1 exists - then aValue -> field1 only exists - """).toString(); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_Test(BaseDataClass): - \""" - Test only exists condition - \""" - _FQRTN = 'com.rosetta.test.model.Test' - aValue: Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()] = Field(..., description='Test A type aValue') - \""" - Test A type aValue - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_check_one_of(self, rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field1")) - - def _else_fn0(): - return True - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field1")), _then_fn0, _else_fn0)""" - ); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_A(BaseDataClass): - \""" - Test type - \""" - _FQRTN = 'com.rosetta.test.model.A' - field1: Optional[Decimal] = Field(None, description='Test number field 1') - \""" - Test number field 1 - \""" - """ - ); - } - - @Test - public void testGenerateCountCondition() { - String generatedPython = testUtils.generatePythonFromString(""" - type A: <"Test type"> - field1 int (0..*) <"Test int field 1"> - field2 int (1..*) <"Test int field 2"> - field3 int (1..3) <"Test int field 3"> - field4 int (0..3) <"Test int field 4"> - - type Test: <"Test count operation condition"> - aValue A (1..*) <"Test A type aValue"> - - condition TestCond: <"Test condition"> - if aValue -> field1 count <> aValue -> field2 count - then True - else False - """).toString(); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_Test(BaseDataClass): - \""" - Test count operation condition - \""" - _FQRTN = 'com.rosetta.test.model.Test' - aValue: list[Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()]] = Field(..., description='Test A type aValue', min_length=1) - \""" - Test A type aValue - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return True - - def _else_fn0(): - return False - - return if_cond_fn(rune_any_elements(rune_count(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field1")), "<>", rune_count(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field2"))), _then_fn0, _else_fn0) - """ - ); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_A(BaseDataClass): - \""" - Test type - \""" - _FQRTN = 'com.rosetta.test.model.A' - field1: Optional[list[int]] = Field(None, description='Test int field 1') - \""" - Test int field 1 - \""" - field2: list[int] = Field(..., description='Test int field 2', min_length=1) - \""" - Test int field 2 - \""" - field3: list[int] = Field(..., description='Test int field 3', min_length=1, max_length=3) - \""" - Test int field 3 - \""" - field4: Optional[list[int]] = Field(None, description='Test int field 4', max_length=3) - \""" - Test int field 4 - \""" - """ - ); - } - - @Test - public void testGenerateAnyCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test: <"Test any operation condition"> - field1 string (1..1) <"Test string field1"> - field2 string (1..1) <"Test boolean field2"> - condition TestCond: <"Test condition"> - if field1="A" - then ["B", "C", "D"] any = field2 - """, - """ - class com_rosetta_test_model_Test(BaseDataClass): - \""" - Test any operation condition - \""" - _FQRTN = 'com.rosetta.test.model.Test' - field1: str = Field(..., description='Test string field1') - \""" - Test string field1 - \""" - field2: str = Field(..., description='Test boolean field2') - \""" - Test boolean field2 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(["B", "C", "D"], "=", rune_resolve_attr(self, "field2")) - - def _else_fn0(): - return True - - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field1"), "=", "A"), _then_fn0, _else_fn0)""" - ); - } - - @Test - public void testGenerateDistinctCondition() { - String generatedPython = testUtils.generatePythonFromString(""" - type A: <"Test type"> - field1 int (1..*) <"Test int field 1"> - field2 int (1..*) <"Test int field 2"> - - type Test: <"Test distinct operation condition"> - aValue A (1..*) <"Test A type aValue"> - field3 number (1..1)<"Test number field 3"> - condition TestCond: <"Test condition"> - if aValue -> field1 distinct count = 1 - then field3=0 - else field3=1 - """).toString(); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_Test(BaseDataClass): - \""" - Test distinct operation condition - \""" - _FQRTN = 'com.rosetta.test.model.Test' - aValue: list[Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()]] = Field(..., description='Test A type aValue', min_length=1) - \""" - Test A type aValue - \""" - field3: Decimal = Field(..., description='Test number field 3') - \""" - Test number field 3 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field3"), "=", 0) - - def _else_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field3"), "=", 1) - - return if_cond_fn(rune_all_elements(rune_count(set(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field1"))), "=", 1), _then_fn0, _else_fn0)""" - ); - - testUtils.assertGeneratedContainsExpectedString( - generatedPython, - """ - class com_rosetta_test_model_A(BaseDataClass): - \""" - Test type - \""" - _FQRTN = 'com.rosetta.test.model.A' - field1: list[int] = Field(..., description='Test int field 1', min_length=1) - \""" - Test int field 1 - \""" - field2: list[int] = Field(..., description='Test int field 2', min_length=1) - \""" - Test int field 2 - \""" - """ - ); - } - - @Test - public void testGenerateBinContainsCondition() { - String generatedPython = testUtils.generatePythonFromString(""" - enum C: <"Test type C"> - field4 <"Test enum field 4"> - field5 <"Test enum field 5"> - type A: <"Test type"> - field1 int (1..*) <"Test int field 1"> - cValue C (1..*) <"Test C type cValue"> - type B: <"Test type B"> - field2 int (1..*) <"Test int field 2"> - aValue A (1..*) <"Test A type aValue"> - type Test: <"Test filter operation condition"> - bValue B (1..*) <"Test B type bValue"> - field3 boolean (0..1) <"Test bool type field3"> - condition TestCond: <"Test condition"> - if field3=True - then bValue->aValue->cValue contains C->field4 - """).toString(); - - String expectedC = """ - class C(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Test type C - \""" - FIELD_4 = "field4" - \""" - Test enum field 4 - \""" - FIELD_5 = "field5" - \""" - Test enum field 5 - \""" - """; - - String expectedA = """ - class com_rosetta_test_model_A(BaseDataClass): - \""" - Test type - \""" - _FQRTN = 'com.rosetta.test.model.A' - field1: list[int] = Field(..., description='Test int field 1', min_length=1) - \""" - Test int field 1 - \""" - cValue: list[com.rosetta.test.model.C.C] = Field(..., description='Test C type cValue', min_length=1) - \""" - Test C type cValue - \""" - """; - - String expectedB = """ - class com_rosetta_test_model_B(BaseDataClass): - \""" - Test type B - \""" - _FQRTN = 'com.rosetta.test.model.B' - field2: list[int] = Field(..., description='Test int field 2', min_length=1) - \""" - Test int field 2 - \""" - aValue: list[Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()]] = Field(..., description='Test A type aValue', min_length=1) - \""" - Test A type aValue - \""" - """; - - String expectedTest = """ - class com_rosetta_test_model_Test(BaseDataClass): - \""" - Test filter operation condition - \""" - _FQRTN = 'com.rosetta.test.model.Test' - bValue: list[Annotated[com_rosetta_test_model_B, com_rosetta_test_model_B.serializer(), com_rosetta_test_model_B.validator()]] = Field(..., description='Test B type bValue', min_length=1) - \""" - Test B type bValue - \""" - field3: Optional[bool] = Field(None, description='Test bool type field3') - \""" - Test bool type field3 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn0(): - return rune_contains(rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(self, "bValue"), "aValue"), "cValue"), com.rosetta.test.model.C.C.FIELD_4) - - def _else_fn0(): - return True - - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field3"), "=", True), _then_fn0, _else_fn0)"""; - - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedC); - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedA); - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedB); - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedTest); - } - - @Test - public void testGenerateBinDisjointCondition() { - testUtils.assertBundleContainsExpectedString(""" - type Test: <"Test disjoint binary expression condition"> - field1 string (1..1) <"Test string field1"> - field2 string (1..1) <"Test string field2"> - field3 boolean (1..1) <"Test boolean field3"> - condition TestCond: <"Test condition"> - if field3=False - then if ["B", "C", "D"] any = field2 and ["A"] disjoint field1 - then field3=True - """, - """ - class com_rosetta_test_model_Test(BaseDataClass): - \""" - Test disjoint binary expression condition - \""" - _FQRTN = 'com.rosetta.test.model.Test' - field1: str = Field(..., description='Test string field1') - \""" - Test string field1 - \""" - field2: str = Field(..., description='Test string field2') - \""" - Test string field2 - \""" - field3: bool = Field(..., description='Test boolean field3') - \""" - Test boolean field3 - \""" - - @rune_condition - def condition_0_TestCond(self): - \""" - Test condition - \""" - item = self - def _then_fn1(): - return rune_all_elements(rune_resolve_attr(self, "field3"), "=", True) - - def _else_fn1(): - return True - - def _then_fn0(): - return if_cond_fn((rune_all_elements(["B", "C", "D"], "=", rune_resolve_attr(self, "field2")) and rune_disjoint(["A"], rune_resolve_attr(self, "field1"))), _then_fn1, _else_fn1) - - def _else_fn0(): - return True - - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field3"), "=", False), _then_fn0, _else_fn0)""" - ); - } - - @Test - public void testGenerateFlattenCondition() { - String generatedPython = testUtils.generatePythonFromString(""" - type Bar: - numbers int (0..*) - type Foo: <"Test flatten operation condition"> - bars Bar (0..*) <"test bar"> - condition TestCondition: <"Test Condition"> - [1, 2, 3] = - (bars - extract numbers - then flatten) - """).toString(); - - String expectedFoo = """ - class com_rosetta_test_model_Foo(BaseDataClass): - \""" - Test flatten operation condition - \""" - _FQRTN = 'com.rosetta.test.model.Foo' - bars: Optional[list[Annotated[com_rosetta_test_model_Bar, com_rosetta_test_model_Bar.serializer(), com_rosetta_test_model_Bar.validator()]]] = Field(None, description='test bar') - \""" - test bar - \""" - - @rune_condition - def condition_0_TestCondition(self): - \""" - Test Condition - \""" - item = self - return rune_all_elements([1, 2, 3], "=", (lambda item: rune_flatten_list(item))(list(map(lambda item: rune_resolve_attr(item, "numbers"), rune_resolve_attr(self, "bars"))))) - """; - - testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedFoo); - } - - @Disabled - @Test - public void setUp() { - // Disabled logic from Xtend - } -} \ No newline at end of file diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaAnyOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaAnyOperationTest.java new file mode 100644 index 0000000..6cbf667 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaAnyOperationTest.java @@ -0,0 +1,59 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaAnyOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateAnyCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test: <"Test any operation condition"> + field1 string (1..1) <"Test string field1"> + field2 string (1..1) <"Test boolean field2"> + condition TestCond: <"Test condition"> + if field1="A" + then ["B", "C", "D"] any = field2 + """, + """ + class com_rosetta_test_model_Test(BaseDataClass): + \""" + Test any operation condition + \""" + _FQRTN = 'com.rosetta.test.model.Test' + field1: str = Field(..., description='Test string field1') + \""" + Test string field1 + \""" + field2: str = Field(..., description='Test boolean field2') + \""" + Test boolean field2 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(["B", "C", "D"], "=", rune_resolve_attr(self, "field2")) + + def _else_fn0(): + return True + + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field1"), "=", "A"), _then_fn0, _else_fn0)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaAsKeyOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaAsKeyOperationTest.java new file mode 100644 index 0000000..186ad5f --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaAsKeyOperationTest.java @@ -0,0 +1,56 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaAsKeyOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testAsKeyOperation() { + testUtils.assertBundleContainsExpectedString(""" + type Bar: + field string (0..1) + [metadata reference] + + func TestAsKey: + inputs: val string (1..1) + output: bar Bar (1..1) + set bar -> field: + val as-key + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAsKey(val: str) -> com_rosetta_test_model_Bar: + \"\"\" + + Parameters + ---------- + val : str + + Returns + ------- + bar : com.rosetta.test.model.Bar + + \"\"\" + self = inspect.currentframe() + + + bar = _get_rune_object('com_rosetta_test_model_Bar', 'field', {rune_resolve_attr(self, "val"): True}) + + + return bar"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaChoiceExpressionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaChoiceExpressionTest.java new file mode 100644 index 0000000..4161a27 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaChoiceExpressionTest.java @@ -0,0 +1,78 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaChoiceExpressionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateChoiceCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test1:<"Test choice condition."> + field1 string (0..1) <"Test string field 1"> + field2 string (0..1) <"Test string field 2"> + field3 string (0..1) <"Test string field 3"> + condition TestChoice: optional choice field1, field2, field3 + """, + """ + class com_rosetta_test_model_Test1(BaseDataClass): + \""" + Test choice condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: Optional[str] = Field(None, description='Test string field 1') + \""" + Test string field 1 + \""" + field2: Optional[str] = Field(None, description='Test string field 2') + \""" + Test string field 2 + \""" + field3: Optional[str] = Field(None, description='Test string field 3') + \""" + Test string field 3 + \""" + + @rune_condition + def condition_0_TestChoice(self): + item = self + return rune_check_one_of(self, 'field1', 'field2', 'field3', necessity=False)"""); + } + + @Test + public void testGenerateOneOfCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test1:<"Test one-of condition."> + field1 string (0..1) <"Test string field 1"> + condition OneOf: one-of + """, + """ + class com_rosetta_test_model_Test1(BaseDataClass): + _CHOICE_ALIAS_MAP ={"field1":[]} + \""" + Test one-of condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: Optional[str] = Field(None, description='Test string field 1') + \""" + Test string field 1 + \""" + + @rune_condition + def condition_0_OneOf(self): + item = self + return rune_check_one_of(self, 'field1', necessity=True)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConditionalExpressionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConditionalExpressionTest.java new file mode 100644 index 0000000..34a11fa --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConditionalExpressionTest.java @@ -0,0 +1,182 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaConditionalExpressionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateIfThenCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test1: <"Test if-then condition."> + field1 string (0..1) <"Test string field 1"> + field2 number (0..1) <"Test number field 2"> + condition TestCond: <"Test condition"> + if field1 exists + then field2=0 + """, + """ + class com_rosetta_test_model_Test1(BaseDataClass): + \""" + Test if-then condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: Optional[str] = Field(None, description='Test string field 1') + \""" + Test string field 1 + \""" + field2: Optional[Decimal] = Field(None, description='Test number field 2') + \""" + Test number field 2 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) + + def _else_fn0(): + return True + + return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0)"""); + } + + @Test + public void testGenerateIfThenElseCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test1: <"Test if-then-else condition."> + field1 string (0..1) <"Test string field 1"> + field2 number (0..1) <"Test number field 2"> + condition TestCond: <"Test condition"> + if field1 exists + then field2=0 + else field2=1 + """, + """ + class com_rosetta_test_model_Test1(BaseDataClass): + \""" + Test if-then-else condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: Optional[str] = Field(None, description='Test string field 1') + \""" + Test string field 1 + \""" + field2: Optional[Decimal] = Field(None, description='Test number field 2') + \""" + Test number field 2 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) + + def _else_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 1) + + return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0)"""); + } + + @Test + public void testGenerateBooleanCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test1: <"Test boolean condition."> + field1 boolean (1..1) <"Test booelan field 1"> + field2 number (0..1) <"Test number field 2"> + condition TestCond: <"Test condition"> + if field1= True + then field2=0 + else field2=5 + """, + """ + class com_rosetta_test_model_Test1(BaseDataClass): + \""" + Test boolean condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: bool = Field(..., description='Test booelan field 1') + \""" + Test booelan field 1 + \""" + field2: Optional[Decimal] = Field(None, description='Test number field 2') + \""" + Test number field 2 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) + + def _else_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 5) + + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field1"), "=", True), _then_fn0, _else_fn0)"""); + } + + @Test + public void testGenerateAbsentCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test1: <"Test absent condition."> + field1 boolean (1..1) <"Test booelan field 1"> + field2 number (0..1) <"Test number field 2"> + condition TestCond: <"Test condition"> + if field1= True + then field2=0 + else field2 is absent + """, + """ + class com_rosetta_test_model_Test1(BaseDataClass): + \""" + Test absent condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: bool = Field(..., description='Test booelan field 1') + \""" + Test booelan field 1 + \""" + field2: Optional[Decimal] = Field(None, description='Test number field 2') + \""" + Test number field 2 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) + + def _else_fn0(): + return (not rune_attr_exists(rune_resolve_attr(self, "field2"))) + + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field1"), "=", True), _then_fn0, _else_fn0)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConstructorExpressionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConstructorExpressionTest.java new file mode 100644 index 0000000..ff1edfc --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConstructorExpressionTest.java @@ -0,0 +1,44 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaConstructorExpressionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testConstructorExpression() { + String generatedPython = testUtils.generatePythonFromString(""" + type Foo: + a int (1..1) + b int (1..1) + + type TestConst: + f Foo (1..1) + condition ConstCheck: + f = Foo { a: 1, b: 2 } + """).toString(); + + testUtils.assertGeneratedContainsExpectedString(generatedPython, + """ + class com_rosetta_test_model_TestConst(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestConst' + f: Annotated[com_rosetta_test_model_Foo, com_rosetta_test_model_Foo.serializer(), com_rosetta_test_model_Foo.validator()] = Field(..., description='') + + @rune_condition + def condition_0_ConstCheck(self): + item = self + return rune_all_elements(rune_resolve_attr(self, "f"), "=", com_rosetta_test_model_Foo(a=1, b=2))"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaContainsOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaContainsOperationTest.java new file mode 100644 index 0000000..2814b99 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaContainsOperationTest.java @@ -0,0 +1,121 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaContainsOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateBinContainsCondition() { + String generatedPython = testUtils.generatePythonFromString(""" + enum C: <"Test type C"> + field4 <"Test enum field 4"> + field5 <"Test enum field 5"> + type A: <"Test type"> + field1 int (1..*) <"Test int field 1"> + cValue C (1..*) <"Test C type cValue"> + type B: <"Test type B"> + field2 int (1..*) <"Test int field 2"> + aValue A (1..*) <"Test A type aValue"> + type Test: <"Test filter operation condition"> + bValue B (1..*) <"Test B type bValue"> + field3 boolean (0..1) <"Test bool type field3"> + condition TestCond: <"Test condition"> + if field3=True + then bValue->aValue->cValue contains C->field4 + """).toString(); + + String expectedC = """ + class C(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Test type C + \""" + FIELD_4 = "field4" + \""" + Test enum field 4 + \""" + FIELD_5 = "field5" + \""" + Test enum field 5 + \""" + """; + + String expectedA = """ + class com_rosetta_test_model_A(BaseDataClass): + \""" + Test type + \""" + _FQRTN = 'com.rosetta.test.model.A' + field1: list[int] = Field(..., description='Test int field 1', min_length=1) + \""" + Test int field 1 + \""" + cValue: list[com.rosetta.test.model.C.C] = Field(..., description='Test C type cValue', min_length=1) + \""" + Test C type cValue + \""" + """; + + String expectedB = """ + class com_rosetta_test_model_B(BaseDataClass): + \""" + Test type B + \""" + _FQRTN = 'com.rosetta.test.model.B' + field2: list[int] = Field(..., description='Test int field 2', min_length=1) + \""" + Test int field 2 + \""" + aValue: list[Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()]] = Field(..., description='Test A type aValue', min_length=1) + \""" + Test A type aValue + \""" + """; + + String expectedTest = """ + class com_rosetta_test_model_Test(BaseDataClass): + \""" + Test filter operation condition + \""" + _FQRTN = 'com.rosetta.test.model.Test' + bValue: list[Annotated[com_rosetta_test_model_B, com_rosetta_test_model_B.serializer(), com_rosetta_test_model_B.validator()]] = Field(..., description='Test B type bValue', min_length=1) + \""" + Test B type bValue + \""" + field3: Optional[bool] = Field(None, description='Test bool type field3') + \""" + Test bool type field3 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_contains(rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(self, "bValue"), "aValue"), "cValue"), com.rosetta.test.model.C.C.FIELD_4) + + def _else_fn0(): + return True + + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field3"), "=", True), _then_fn0, _else_fn0)"""; + + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedC); + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedA); + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedB); + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedTest); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConversionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConversionTest.java new file mode 100644 index 0000000..0149cc0 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaConversionTest.java @@ -0,0 +1,83 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaConversionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testBasicConversions() { + testUtils.assertBundleContainsExpectedString(""" + type TestConv: + val int (1..1) + s string (1..1) + condition ConvCheck: + val to-string = "1" and + s to-int = 1 + """, + """ + class com_rosetta_test_model_TestConv(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestConv' + val: int = Field(..., description='') + s: str = Field(..., description='') + + @rune_condition + def condition_0_ConvCheck(self): + item = self + return (rune_all_elements(rune_str(rune_resolve_attr(self, "val")), "=", "1") and rune_all_elements(int(rune_resolve_attr(self, "s")), "=", 1))"""); + } + + @Test + public void testDateConversions() { + testUtils.assertBundleContainsExpectedString(""" + type TestDateConv: + s string (1..1) + condition DateConvCheck: + s to-date = "2023-11-20" to-date and + s to-date-time = "2023-11-20 12:00:00" to-date-time and + s to-time = "12:00:00" to-time + """, + """ + class com_rosetta_test_model_TestDateConv(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestDateConv' + s: str = Field(..., description='') + + @rune_condition + def condition_0_DateConvCheck(self): + item = self + return ((rune_all_elements(datetime.datetime.strptime(rune_resolve_attr(self, "s"), "%Y-%m-%d").date(), "=", datetime.datetime.strptime("2023-11-20", "%Y-%m-%d").date()) and rune_all_elements(datetime.datetime.strptime(rune_resolve_attr(self, "s"), "%Y-%m-%d %H:%M:%S"), "=", datetime.datetime.strptime("2023-11-20 12:00:00", "%Y-%m-%d %H:%M:%S"))) and rune_all_elements(datetime.datetime.strptime(rune_resolve_attr(self, "s"), "%H:%M:%S").time(), "=", datetime.datetime.strptime("12:00:00", "%H:%M:%S").time()))"""); + } + + @Test + public void testEnumConversion() { + testUtils.assertBundleContainsExpectedString(""" + enum MyEnum: + Value1 + type TestEnumConv: + s string (1..1) + condition EnumConvCheck: + s to-enum MyEnum = MyEnum -> Value1 + """, + """ + class com_rosetta_test_model_TestEnumConv(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestEnumConv' + s: str = Field(..., description='') + + @rune_condition + def condition_0_EnumConvCheck(self): + item = self + return rune_all_elements(MyEnum(rune_resolve_attr(self, "s")), "=", com.rosetta.test.model.MyEnum.MyEnum.VALUE_1)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaCountOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaCountOperationTest.java new file mode 100644 index 0000000..19e6690 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaCountOperationTest.java @@ -0,0 +1,92 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaCountOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateCountCondition() { + String generatedPython = testUtils.generatePythonFromString(""" + type A: <"Test type"> + field1 int (0..*) <"Test int field 1"> + field2 int (1..*) <"Test int field 2"> + field3 int (1..3) <"Test int field 3"> + field4 int (0..3) <"Test int field 4"> + + type Test: <"Test count operation condition"> + aValue A (1..*) <"Test A type aValue"> + + condition TestCond: <"Test condition"> + if aValue -> field1 count <> aValue -> field2 count + then True + else False + """).toString(); + + testUtils.assertGeneratedContainsExpectedString( + generatedPython, + """ + class com_rosetta_test_model_Test(BaseDataClass): + \""" + Test count operation condition + \""" + _FQRTN = 'com.rosetta.test.model.Test' + aValue: list[Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()]] = Field(..., description='Test A type aValue', min_length=1) + \""" + Test A type aValue + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return True + + def _else_fn0(): + return False + + return if_cond_fn(rune_any_elements(rune_count(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field1")), "<>", rune_count(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field2"))), _then_fn0, _else_fn0) + """); + + testUtils.assertGeneratedContainsExpectedString( + generatedPython, + """ + class com_rosetta_test_model_A(BaseDataClass): + \""" + Test type + \""" + _FQRTN = 'com.rosetta.test.model.A' + field1: Optional[list[int]] = Field(None, description='Test int field 1') + \""" + Test int field 1 + \""" + field2: list[int] = Field(..., description='Test int field 2', min_length=1) + \""" + Test int field 2 + \""" + field3: list[int] = Field(..., description='Test int field 3', min_length=1, max_length=3) + \""" + Test int field 3 + \""" + field4: Optional[list[int]] = Field(None, description='Test int field 4', max_length=3) + \""" + Test int field 4 + \""" + """); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaDisjointOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaDisjointOperationTest.java new file mode 100644 index 0000000..d53cd83 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaDisjointOperationTest.java @@ -0,0 +1,71 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaDisjointOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateBinDisjointCondition() { + testUtils.assertBundleContainsExpectedString(""" + type Test: <"Test disjoint binary expression condition"> + field1 string (1..1) <"Test string field1"> + field2 string (1..1) <"Test string field2"> + field3 boolean (1..1) <"Test boolean field3"> + condition TestCond: <"Test condition"> + if field3=False + then if ["B", "C", "D"] any = field2 and ["A"] disjoint field1 + then field3=True + """, + """ + class com_rosetta_test_model_Test(BaseDataClass): + \""" + Test disjoint binary expression condition + \""" + _FQRTN = 'com.rosetta.test.model.Test' + field1: str = Field(..., description='Test string field1') + \""" + Test string field1 + \""" + field2: str = Field(..., description='Test string field2') + \""" + Test string field2 + \""" + field3: bool = Field(..., description='Test boolean field3') + \""" + Test boolean field3 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn1(): + return rune_all_elements(rune_resolve_attr(self, "field3"), "=", True) + + def _else_fn1(): + return True + + def _then_fn0(): + return if_cond_fn((rune_all_elements(["B", "C", "D"], "=", rune_resolve_attr(self, "field2")) and rune_disjoint(["A"], rune_resolve_attr(self, "field1"))), _then_fn1, _else_fn1) + + def _else_fn0(): + return True + + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "field3"), "=", False), _then_fn0, _else_fn0)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaDistinctOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaDistinctOperationTest.java new file mode 100644 index 0000000..1b331fe --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaDistinctOperationTest.java @@ -0,0 +1,85 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaDistinctOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateDistinctCondition() { + String generatedPython = testUtils.generatePythonFromString(""" + type A: <"Test type"> + field1 int (1..*) <"Test int field 1"> + field2 int (1..*) <"Test int field 2"> + + type Test: <"Test distinct operation condition"> + aValue A (1..*) <"Test A type aValue"> + field3 number (1..1)<"Test number field 3"> + condition TestCond: <"Test condition"> + if aValue -> field1 distinct count = 1 + then field3=0 + else field3=1 + """).toString(); + + testUtils.assertGeneratedContainsExpectedString( + generatedPython, + """ + class com_rosetta_test_model_Test(BaseDataClass): + \""" + Test distinct operation condition + \""" + _FQRTN = 'com.rosetta.test.model.Test' + aValue: list[Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()]] = Field(..., description='Test A type aValue', min_length=1) + \""" + Test A type aValue + \""" + field3: Decimal = Field(..., description='Test number field 3') + \""" + Test number field 3 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field3"), "=", 0) + + def _else_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field3"), "=", 1) + + return if_cond_fn(rune_all_elements(rune_count(set(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "field1"))), "=", 1), _then_fn0, _else_fn0)"""); + + testUtils.assertGeneratedContainsExpectedString( + generatedPython, + """ + class com_rosetta_test_model_A(BaseDataClass): + \""" + Test type + \""" + _FQRTN = 'com.rosetta.test.model.A' + field1: list[int] = Field(..., description='Test int field 1', min_length=1) + \""" + Test int field 1 + \""" + field2: list[int] = Field(..., description='Test int field 2', min_length=1) + \""" + Test int field 2 + \""" + """); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaExistsExpressionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaExistsExpressionTest.java index 3c0a784..87147a2 100644 --- a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaExistsExpressionTest.java +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaExistsExpressionTest.java @@ -1,7 +1,5 @@ package com.regnosys.rosetta.generator.python.expressions; -import static org.junit.jupiter.api.Assertions.assertFalse; - import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; import com.regnosys.rosetta.tests.RosettaInjectorProvider; import org.eclipse.xtext.testing.InjectWith; @@ -19,150 +17,304 @@ public class RosettaExistsExpressionTest { private PythonGeneratorTestUtils testUtils; @Test - public void setUp() { - String pythonString = testUtils.generatePythonFromString( + public void testExistsBasic() { + testUtils.assertBundleContainsExpectedString( """ - type Foo: - bar Bar (0..*) - baz Baz (0..1) - type Bar: - before number (0..1) - after number (0..1) - other number (0..1) - beforeWithScheme number (0..1) - [metadata scheme] - afterWithScheme number (0..1) - [metadata scheme] - beforeList number (0..*) - afterList number (0..*) - beforeListWithScheme number (0..*) - [metadata scheme] - afterListWithScheme number (0..*) - [metadata scheme] + field number (0..1) + + func ExistsBasic: + inputs: bar Bar (1..1) + output: result boolean (1..1) + set result: + bar -> field exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_ExistsBasic(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" + + Parameters + ---------- + bar : com.rosetta.test.model.Bar + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "field")) - type Baz: - bazValue number (0..1) - other number (0..1) - func Exists: - inputs: foo Foo (1..1) + return result"""); + } + + @Test + public void testAbsentBasic() { + testUtils.assertBundleContainsExpectedString( + """ + type Bar: + field number (0..1) + + func AbsentBasic: + inputs: bar Bar (1..1) output: result boolean (1..1) set result: - foo -> bar -> before exists + bar -> field is absent + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_AbsentBasic(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" + + Parameters + ---------- + bar : com.rosetta.test.model.Bar + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = (not rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "field"))) + + + return result"""); + } + + @Test + public void testSingleExists() { + testUtils.assertBundleContainsExpectedString( + """ + type Bar: + field number (0..1) func SingleExists: - inputs: foo Foo (1..1) + inputs: bar Bar (1..1) output: result boolean (1..1) set result: - foo -> bar -> before single exists + bar -> field single exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_SingleExists(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" + + Parameters + ---------- + bar : com.rosetta.test.model.Bar + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "field"), "single") + + + return result"""); + } + + @Test + public void testMultipleExists() { + testUtils.assertBundleContainsExpectedString( + """ + type Bar: + fieldList number (0..*) func MultipleExists: - inputs: foo Foo (1..1) + inputs: bar Bar (1..1) output: result boolean (1..1) set result: - foo -> bar -> before multiple exists + bar -> fieldList multiple exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_MultipleExists(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" - func OnlyExists: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - foo -> bar -> before only exists + Parameters + ---------- + bar : com.rosetta.test.model.Bar - func OnlyExistsMultiplePaths: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - ( foo -> bar -> before, foo -> bar -> after ) only exists + Returns + ------- + result : bool - func OnlyExistsPathWithScheme: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - ( foo -> bar -> before, foo -> bar -> afterWithScheme ) only exists + \"\"\" + self = inspect.currentframe() - func OnlyExistsBothPathsWithScheme: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - ( foo -> bar -> beforeWithScheme, foo -> bar -> afterWithScheme ) only exists - func OnlyExistsListMultiplePaths: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - ( foo -> bar -> before, foo -> bar -> afterList ) only exists + result = rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "fieldList"), "multiple") - func OnlyExistsListPathWithScheme: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - ( foo -> bar -> before, foo -> bar -> afterListWithScheme ) only exists - func OnlyExistsListBothPathsWithScheme: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - ( foo -> bar -> beforeListWithScheme, foo -> bar -> afterListWithScheme ) only exists + return result"""); + } - // TODO tests compilation only, add unit test - func MultipleSeparateOr_NoAliases_Exists: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - foo -> bar -> before exists or foo -> bar -> after exists + @Test + public void testExistsWithMetadata() { + testUtils.assertBundleContainsExpectedString( + """ + type Bar: + field number (0..1) + [metadata scheme] - // TODO tests compilation only, add unit test - func MultipleOr_NoAliases_Exists: - inputs: foo Foo (1..1) + func ExistsWithMetadata: + inputs: bar Bar (1..1) output: result boolean (1..1) set result: - foo -> bar -> before exists or foo -> bar -> after exists or foo -> baz -> other exists + bar -> field exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_ExistsWithMetadata(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" - // TODO tests compilation only, add unit test - func MultipleOrBranchNode_NoAliases_Exists: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - foo -> bar exists or foo -> baz exists + Parameters + ---------- + bar : com.rosetta.test.model.Bar - // TODO tests compilation only, add unit test - func MultipleAnd_NoAliases_Exists: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - foo -> bar -> before exists and foo -> bar -> after exists and foo -> baz -> other exists + Returns + ------- + result : bool - // TODO tests compilation only, add unit test - func MultipleOrAnd_NoAliases_Exists: - inputs: foo Foo (1..1) - output: result boolean (1..1) - set result: - foo -> bar -> before exists or ( foo -> bar -> after exists and foo -> baz -> other exists ) + \"\"\" + self = inspect.currentframe() + + + result = rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "field")) - // TODO tests compilation only, add unit test - func MultipleOrAnd_NoAliases_Exists2: - inputs: foo Foo (1..1) + + return result"""); + } + + @Test + public void testExistsWithLogicalOperators() { + testUtils.assertBundleContainsExpectedString( + """ + type Bar: + field1 number (0..1) + field2 number (0..1) + + func ExistsWithLogical: + inputs: bar Bar (1..1) output: result boolean (1..1) set result: - (foo -> bar -> before exists and foo -> bar -> after exists) or foo -> baz -> other exists or foo -> baz -> bazValue exists + bar -> field1 exists and bar -> field2 exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_ExistsWithLogical(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" + + Parameters + ---------- + bar : com.rosetta.test.model.Bar + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() - // TODO tests compilation only, add unit test - func MultipleOrAnd_NoAliases_Exists3: - inputs: foo Foo (1..1) + + result = (rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "field1")) and rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "bar"), "field2"))) + + + return result"""); + } + + @Test + public void testDeepPathSingleExists() { + testUtils.assertBundleContainsExpectedString( + """ + type Sub: + field number (0..1) + type Bar: + sub Sub (0..1) + + func DeepExists: + inputs: bar Bar (1..1) output: result boolean (1..1) set result: - (foo -> bar -> before exists or foo -> bar -> after exists) or (foo -> baz -> other exists and foo -> baz -> bazValue exists) + bar -> sub -> field single exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_DeepExists(bar: com_rosetta_test_model_Bar) -> bool: + \"\"\" - // TODO tests compilation only, add unit test - func MultipleExistsWithOrAnd: - inputs: foo Foo (1..1) - output: result boolean (1..1) + Parameters + ---------- + bar : com.rosetta.test.model.Bar + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = rune_attr_exists(rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(self, "bar"), "sub"), "field"), "single") + + + return result"""); + } + + @Test + public void testExistsInFunctionArguments() { + testUtils.assertBundleContainsExpectedString( + """ + func ExistsArg: + inputs: + arg1 number (0..1) + arg2 number (0..1) + output: + result boolean (1..1) set result: - foo -> bar -> before exists or ( foo -> baz -> other exists and foo -> bar -> after exists ) or foo -> baz -> bazValue exists - """) - .toString(); + arg1 exists or arg2 exists + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_ExistsArg(arg1: Decimal | None, arg2: Decimal | None) -> bool: + \"\"\" + + Parameters + ---------- + arg1 : Decimal + + arg2 : Decimal + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = (rune_attr_exists(rune_resolve_attr(self, "arg1")) or rune_attr_exists(rune_resolve_attr(self, "arg2"))) + - assertFalse(pythonString.isEmpty(), "Generated Python string should not be empty"); + return result"""); } } diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaFilterOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaFilterOperationTest.java new file mode 100644 index 0000000..564238f --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaFilterOperationTest.java @@ -0,0 +1,75 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaFilterOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testFilterOperation() { + testUtils.assertBundleContainsExpectedString(""" + type Item: + val int (1..1) + type TestFilter: + items Item (0..*) + condition FilterCheck: + (items filter [ val > 5 ] then count) = 0 + """, + """ + class com_rosetta_test_model_TestFilter(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestFilter' + items: Optional[list[Annotated[com_rosetta_test_model_Item, com_rosetta_test_model_Item.serializer(), com_rosetta_test_model_Item.validator()]]] = Field(None, description='') + + @rune_condition + def condition_0_FilterCheck(self): + item = self + return rune_all_elements((lambda item: rune_count(item))(rune_filter(rune_resolve_attr(self, "items"), lambda item: rune_all_elements(rune_resolve_attr(item, "val"), ">", 5))), "=", 0)"""); + } + + @Test + public void testNestedFilterMapCount() { + testUtils.assertBundleContainsExpectedString(""" + type Item: + val int (1..1) + func TestNestedNested: + inputs: items Item (0..*) + output: result int (1..1) + set result: + items filter [ val > 5 ] then count + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestNestedNested(items: list[com_rosetta_test_model_Item] | None) -> int: + \"\"\" + + Parameters + ---------- + items : list[com.rosetta.test.model.Item] + + Returns + ------- + result : int + + \"\"\" + self = inspect.currentframe() + + + result = (lambda item: rune_count(item))(rune_filter(rune_resolve_attr(self, "items"), lambda item: rune_all_elements(rune_resolve_attr(item, "val"), ">", 5))) + + + return result"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaFlattenOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaFlattenOperationTest.java new file mode 100644 index 0000000..0ae9fc0 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaFlattenOperationTest.java @@ -0,0 +1,55 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaFlattenOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateFlattenCondition() { + String generatedPython = testUtils.generatePythonFromString(""" + type Bar: + numbers int (0..*) + type Foo: <"Test flatten operation condition"> + bars Bar (0..*) <"test bar"> + condition TestCondition: <"Test Condition"> + [1, 2, 3] = + (bars + extract numbers + then flatten) + """).toString(); + + String expectedFoo = """ + class com_rosetta_test_model_Foo(BaseDataClass): + \""" + Test flatten operation condition + \""" + _FQRTN = 'com.rosetta.test.model.Foo' + bars: Optional[list[Annotated[com_rosetta_test_model_Bar, com_rosetta_test_model_Bar.serializer(), com_rosetta_test_model_Bar.validator()]]] = Field(None, description='test bar') + \""" + test bar + \""" + + @rune_condition + def condition_0_TestCondition(self): + \""" + Test Condition + \""" + item = self + return rune_all_elements([1, 2, 3], "=", (lambda item: rune_flatten_list(item))(list(map(lambda item: rune_resolve_attr(item, "numbers"), rune_resolve_attr(self, "bars")))))"""; + + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedFoo); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaJoinOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaJoinOperationTest.java new file mode 100644 index 0000000..8592115 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaJoinOperationTest.java @@ -0,0 +1,42 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaJoinOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testJoinOperation() { + String generatedPython = testUtils.generatePythonFromString(""" + type TestJoin: + field1 string (1..*) + delimiter string (1..1) + condition JoinCheck: + field1 join delimiter = "A,B" + """).toString(); + + testUtils.assertGeneratedContainsExpectedString(generatedPython, + """ + class com_rosetta_test_model_TestJoin(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestJoin' + field1: list[str] = Field(..., description='', min_length=1) + delimiter: str = Field(..., description='') + + @rune_condition + def condition_0_JoinCheck(self): + item = self + return rune_all_elements(rune_resolve_attr(self, "delimiter").join(rune_resolve_attr(self, "field1")), "=", "A,B")"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaListOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaListOperationTest.java new file mode 100644 index 0000000..66ac074 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaListOperationTest.java @@ -0,0 +1,227 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaListOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testAggregations() { + testUtils.assertBundleContainsExpectedString(""" + func TestAggregations: + inputs: items int (0..*) + output: result boolean (1..1) + set result: + items sum = 10 and + items max = 5 and + items min = 1 + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAggregations(items: list[int] | None) -> bool: + \"\"\" + + Parameters + ---------- + items : list[int] + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = ((rune_all_elements(sum(rune_resolve_attr(self, \"items\")), \"=\", 10) and rune_all_elements(max(rune_resolve_attr(self, \"items\")), \"=\", 5)) and rune_all_elements(min(rune_resolve_attr(self, \"items\")), \"=\", 1)) + + + return result + """); + } + + @Test + public void testAccessors() { + testUtils.assertBundleContainsExpectedString(""" + func TestAccessors: + inputs: items int (0..*) + output: result boolean (1..1) + set result: + items first = 1 and + items last = 5 + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAccessors(items: list[int] | None) -> bool: + \"\"\" + + Parameters + ---------- + items : list[int] + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = (rune_all_elements(rune_resolve_attr(self, \"items\")[0], \"=\", 1) and rune_all_elements(rune_resolve_attr(self, \"items\")[-1], \"=\", 5)) + + + return result + """); + } + + @Test + public void testSortOperation() { + testUtils.assertBundleContainsExpectedString(""" + func TestSort: + inputs: items int (0..*) + output: result int (0..*) + set result: + items sort + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestSort(items: list[int] | None) -> list[int]: + \"\"\" + + Parameters + ---------- + items : list[int] + + Returns + ------- + result : list[int] + + \"\"\" + self = inspect.currentframe() + + + result = sorted(rune_resolve_attr(self, \"items\")) + + + return result + """); + } + + @Test + public void testListComparison() { + testUtils.assertBundleContainsExpectedString(""" + func TestListComparison: + inputs: + list1 int (0..*) + list2 int (0..*) + output: result boolean (1..1) + set result: + list1 = list2 + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestListComparison(list1: list[int] | None, list2: list[int] | None) -> bool: + \"\"\" + + Parameters + ---------- + list1 : list[int] + + list2 : list[int] + + Returns + ------- + result : bool + + \"\"\" + self = inspect.currentframe() + + + result = rune_all_elements(rune_resolve_attr(self, \"list1\"), \"=\", rune_resolve_attr(self, \"list2\")) + + + return result + """); + } + + @Test + public void testCollectionLiteral() { + testUtils.assertBundleContainsExpectedString(""" + func TestLiteral: + output: result int (0..*) + set result: + [1, 2, 3] + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestLiteral() -> list[int]: + \"\"\" + + Parameters + ---------- + Returns + ------- + result : list[int] + + \"\"\" + self = inspect.currentframe() + + + result = [1, 2, 3] + + + return result + """); + } + + @Test + public void testReverseOperation() { + testUtils.assertBundleContainsExpectedString(""" + func TestReverse: + inputs: items int (0..*) + output: result int (0..*) + set result: + items reverse + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestReverse(items: list[int] | None) -> list[int]: + \"\"\" + + Parameters + ---------- + items : list[int] + + Returns + ------- + result : list[int] + + \"\"\" + self = inspect.currentframe() + + + result = list(reversed(rune_resolve_attr(self, \"items\"))) + + + return result + """); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaMapOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaMapOperationTest.java new file mode 100644 index 0000000..9527528 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaMapOperationTest.java @@ -0,0 +1,42 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaMapOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testMapOperation() { + String generatedPython = testUtils.generatePythonFromString(""" + type Item: + val int (1..1) + type TestMap: + items Item (0..*) + condition MapCheck: + (items extract val then count) = 0 + """).toString(); + + testUtils.assertGeneratedContainsExpectedString(generatedPython, + """ + class com_rosetta_test_model_TestMap(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestMap' + items: Optional[list[Annotated[com_rosetta_test_model_Item, com_rosetta_test_model_Item.serializer(), com_rosetta_test_model_Item.validator()]]] = Field(None, description='') + + @rune_condition + def condition_0_MapCheck(self): + item = self + return rune_all_elements((lambda item: rune_count(item))(list(map(lambda item: rune_resolve_attr(item, "val"), rune_resolve_attr(self, "items")))), "=", 0)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaMathOperationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaMathOperationTest.java new file mode 100644 index 0000000..6c5479b --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaMathOperationTest.java @@ -0,0 +1,81 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaMathOperationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testMathOperations() { + String generatedPython = testUtils.generatePythonFromString(""" + type TestMath: + a int (1..1) + b int (1..1) + condition MathCheck: + if a * b = 10 and a - b = 3 and a / b = 2 + then True + """).toString(); + + testUtils.assertGeneratedContainsExpectedString(generatedPython, + """ + class com_rosetta_test_model_TestMath(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestMath' + a: int = Field(..., description='') + b: int = Field(..., description='') + + @rune_condition + def condition_0_MathCheck(self): + item = self + def _then_fn0(): + return True + + def _else_fn0(): + return True + + return if_cond_fn(((rune_all_elements((rune_resolve_attr(self, "a") * rune_resolve_attr(self, "b")), "=", 10) and rune_all_elements((rune_resolve_attr(self, "a") - rune_resolve_attr(self, "b")), "=", 3)) and rune_all_elements((rune_resolve_attr(self, "a") / rune_resolve_attr(self, "b")), "=", 2)), _then_fn0, _else_fn0)"""); + } + + @Test + public void testArithmeticOperator() { + // This was already full output style + String generatedPython = testUtils.generatePythonFromString(""" + type ArithmeticTest: + a int (1..1) + b int (1..1) + condition Test: + if a + b = 3 then True + else False + """).toString(); + + testUtils.assertGeneratedContainsExpectedString( + generatedPython, + """ + class com_rosetta_test_model_ArithmeticTest(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.ArithmeticTest' + a: int = Field(..., description='') + b: int = Field(..., description='') + + @rune_condition + def condition_0_Test(self): + item = self + def _then_fn0(): + return True + + def _else_fn0(): + return False + + return if_cond_fn(rune_all_elements((rune_resolve_attr(self, "a") + rune_resolve_attr(self, "b")), "=", 3), _then_fn0, _else_fn0)"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaOnlyElementTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaOnlyElementTest.java new file mode 100644 index 0000000..112f061 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaOnlyElementTest.java @@ -0,0 +1,80 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaOnlyElementTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateOnlyElementCondition() { + String generatedPython = testUtils.generatePythonFromString(""" + enum TestEnum: <"Enum to test"> + TestEnumValue1 <"Test enum value 1"> + TestEnumValue2 <"Test enum value 2"> + type Test1: <"Test only-element condition."> + field1 TestEnum (0..1) <"Test enum field 1"> + field2 number (0..1) <"Test number field 2"> + condition TestCond: <"Test condition"> + if field1 only-element= TestEnum->TestEnumValue1 + then field2=0 + """).toString(); + + String expectedTestEnum = """ + class TestEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Enum to test + \""" + TEST_ENUM_VALUE_1 = "TestEnumValue1" + \""" + Test enum value 1 + \""" + TEST_ENUM_VALUE_2 = "TestEnumValue2" + \""" + Test enum value 2 + \""""; + + String expectedTest1 = """ + class com_rosetta_test_model_Test1(BaseDataClass): + \""" + Test only-element condition. + \""" + _FQRTN = 'com.rosetta.test.model.Test1' + field1: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Test enum field 1') + \""" + Test enum field 1 + \""" + field2: Optional[Decimal] = Field(None, description='Test number field 2') + \""" + Test number field 2 + \""" + + @rune_condition + def condition_0_TestCond(self): + \""" + Test condition + \""" + item = self + def _then_fn0(): + return rune_all_elements(rune_resolve_attr(self, "field2"), "=", 0) + + def _else_fn0(): + return True + + return if_cond_fn(rune_all_elements(rune_get_only_element(rune_resolve_attr(self, "field1")), "=", com.rosetta.test.model.TestEnum.TestEnum.TEST_ENUM_VALUE_1), _then_fn0, _else_fn0)"""; + + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedTestEnum); + testUtils.assertGeneratedContainsExpectedString(generatedPython, expectedTest1); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaOnlyExistsExpressionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaOnlyExistsExpressionTest.java new file mode 100644 index 0000000..4f0278f --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaOnlyExistsExpressionTest.java @@ -0,0 +1,87 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaOnlyExistsExpressionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + @Disabled("Functions are being phased out in tests.") + public void testOnlyExistsSinglePath() { + testUtils.assertBundleContainsExpectedString(""" + type A: + field1 number (0..1) + + type Test: + aValue A (1..1) + + condition TestCond: + if aValue -> field1 exists + then aValue -> field1 only exists + """, + "return rune_check_one_of(self, rune_resolve_attr(rune_resolve_attr(self, \"aValue\"), \"field1\"))"); + } + + @Test + public void testOnlyExistsMultiplePaths() { + testUtils.assertBundleContainsExpectedString(""" + type Bar: + before number (0..1) + after number (0..1) + + func OnlyExistsMultiplePaths: + inputs: bar Bar (1..1) + output: result boolean (1..1) + set result: + ( bar -> before, bar -> after ) only exists + """, + "result = rune_check_one_of(self, rune_resolve_attr(rune_resolve_attr(self, \"bar\"), \"before\"), rune_resolve_attr(rune_resolve_attr(self, \"bar\"), \"after\"))"); + } + + @Test + public void testOnlyExistsWithMetadata() { + testUtils.assertBundleContainsExpectedString(""" + type Bar: + before number (0..1) + [metadata scheme] + + func OnlyExistsWithMetadata: + inputs: bar Bar (1..1) + output: result boolean (1..1) + set result: + bar -> before only exists + """, + "result = rune_check_one_of(self, rune_resolve_attr(rune_resolve_attr(self, \"bar\"), \"before\"))"); + } + + @Test + public void testOnlyExistsThreePaths() { + testUtils.assertBundleContainsExpectedString(""" + type Bar: + a number (0..1) + b number (0..1) + c number (0..1) + + func OnlyExistsThree: + inputs: bar Bar (1..1) + output: result boolean (1..1) + set result: + ( bar -> a, bar -> b, bar -> c ) only exists + """, + "result = rune_check_one_of(self, rune_resolve_attr(rune_resolve_attr(self, \"bar\"), \"a\"), rune_resolve_attr(rune_resolve_attr(self, \"bar\"), \"b\"), rune_resolve_attr(rune_resolve_attr(self, \"bar\"), \"c\"))"); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaShortcutTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaShortcutTest.java new file mode 100644 index 0000000..b6d9eba --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaShortcutTest.java @@ -0,0 +1,55 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaShortcutTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testFunctionAlias() { + testUtils.assertBundleContainsExpectedString(""" + func UseShortcut: + inputs: val int (1..1) + output: res int (1..1) + alias MyShortcut: val + 5 + set res: MyShortcut * 2 + """, + """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_UseShortcut(val: int) -> int: + \"\"\" + + Parameters + ---------- + val : int + + Returns + ------- + res : int + + \"\"\" + self = inspect.currentframe() + + + MyShortcut = (rune_resolve_attr(self, "val") + 5) + res = (rune_resolve_attr(self, "MyShortcut") * 2) + + + return res"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaSwitchExpressionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaSwitchExpressionTest.java new file mode 100644 index 0000000..0ed6cbb --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/expressions/RosettaSwitchExpressionTest.java @@ -0,0 +1,60 @@ +package com.regnosys.rosetta.generator.python.expressions; + +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +import jakarta.inject.Inject; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class RosettaSwitchExpressionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateSwitch() { + testUtils.assertBundleContainsExpectedString(""" + type FooTest: + a int (1..1) <"Test field a"> + condition Test: + a switch + 1 then True, + 2 then True, + default False + """, + """ + class com_rosetta_test_model_FooTest(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.FooTest' + a: int = Field(..., description='Test field a') + \""" + Test field a + \""" + + @rune_condition + def condition_0_Test(self): + item = self + def _switch_fn_0(): + def _then_1(): + return True + def _then_2(): + return True + def _then_default(): + return False + switchAttribute = rune_resolve_attr(self, "a") + if switchAttribute == 1: + return _then_1() + elif switchAttribute == 2: + return _then_2() + else: + return _then_default() + + return _switch_fn_0() + """); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/func/PythonFunctionsTest.java b/src/test/java/com/regnosys/rosetta/generator/python/func/PythonFunctionsTest.java deleted file mode 100644 index 584b41e..0000000 --- a/src/test/java/com/regnosys/rosetta/generator/python/func/PythonFunctionsTest.java +++ /dev/null @@ -1,977 +0,0 @@ -package com.regnosys.rosetta.generator.python.func; - -import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; -import com.regnosys.rosetta.tests.RosettaInjectorProvider; -import org.eclipse.xtext.testing.InjectWith; -import org.eclipse.xtext.testing.extensions.InjectionExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import jakarta.inject.Inject; -import java.util.Map; - -@ExtendWith(InjectionExtension.class) -@InjectWith(RosettaInjectorProvider.class) -public class PythonFunctionsTest { - - @Inject - private PythonGeneratorTestUtils testUtils; - - @Test - public void testSimpleSet() { - String generatedFunction = testUtils.generatePythonFromString( - """ - func Abs: <"Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned."> - inputs: - arg number (1..1) - output: - result number (1..1) - set result: - if arg < 0 then -1 * arg else arg - """) - .get("src/com/rosetta/test/model/functions/Abs.py").toString(); - - String expected = """ - @replaceable - def Abs(arg: Decimal) -> Decimal: - \""" - Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned. - - Parameters\s - ---------- - arg : number - - Returns - ------- - result : number - - \""" - self = inspect.currentframe() - - - def _then_fn0(): - return (-1 * rune_resolve_attr(self, "arg")) - - def _else_fn0(): - return rune_resolve_attr(self, "arg") - - result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "arg"), "<", 0), _then_fn0, _else_fn0) - - - return result - """; - testUtils.assertGeneratedContainsExpectedString(generatedFunction, expected); - } - - @Test - public void testSimpleAdd() { - String pythonString = testUtils.generatePythonFromString( - """ - func AppendToVector: <"Append a single value to a vector (list of numbers)."> - inputs: - vector number (0..*) <"Input vector."> - value number (1..1) <"Value to add to the vector."> - output: - resultVector number (0..*) <"Resulting vector."> - - add resultVector: vector - add resultVector: value - """).toString(); - - String expected = """ - @replaceable - def AppendToVector(vector: list[Decimal] | None, value: Decimal) -> Decimal: - \""" - Append a single value to a vector (list of numbers). - - Parameters\s - ---------- - vector : number - Input vector. - - value : number - Value to add to the vector. - - Returns - ------- - resultVector : number - - \""" - self = inspect.currentframe() - - - resultVector = rune_resolve_attr(self, "vector") - resultVector.add_rune_attr(self, rune_resolve_attr(self, "value")) - - - return resultVector - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - } - - // Test set with a basemodel output - @Test - public void testSetWithBasemodel() { - String python = testUtils.generatePythonFromString( - """ - func Create_UnitType: <"Create UnitType with given currency or financial unit."> - inputs: - currency string (0..1) - [metadata scheme] - financialUnit FinancialUnitEnum (0..1) - output: - unitType UnitType (1..1) - - condition CurrencyOrFinancialUnitExists: - currency exists or financialUnit exists - - set unitType -> currency: currency - set unitType -> financialUnit: financialUnit - enum FinancialUnitEnum: <"Provides enumerated values for financial units, generally used in the context of defining quantities for securities."> - Contract <"Denotes financial contracts, such as listed futures and options."> - ContractualProduct <"Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount."> - IndexUnit <"Denotes a price expressed in index points, e.g. for a stock index."> - LogNormalVolatility <"Denotes a log normal volatility, expressed in %/month, where the percentage is represented as a decimal. For example, 0.15 means a log-normal volatility of 15% per month."> - Share <"Denotes the number of units of financial stock shares."> - ValuePerDay <"Denotes a value (expressed in currency units) for a one day change in a valuation date, which is typically used for expressing sensitivity to the passage of time, also known as theta risk, or carry, or other names."> - ValuePerPercent <"Denotes a value (expressed in currency units) per percent change in the underlying rate which is typically used for expressing sensitivity to volatility changes, also known as vega risk."> - Weight <"Denotes a quantity (expressed as a decimal value) represented the weight of a component in a basket."> - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - capacityUnit CapacityUnitEnum (0..1) <"Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities."> - weatherUnit WeatherUnitEnum (0..1) <"Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities."> - financialUnit FinancialUnitEnum (0..1) <"Provides an enumerated value for financial units, generally used in the context of defining quantities for securities."> - currency string (0..1) <"Defines the currency to be used as a unit for a price, quantity, or other purpose."> - [metadata scheme] - - condition UnitType: <"Requires that a unit type must be set."> - one-of - enum CapacityUnitEnum: <"Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities."> - ALW <"Denotes Allowances as standard unit."> - BBL <"Denotes a Barrel as a standard unit."> - BCF <"Denotes Billion Cubic Feet as a standard unit."> - BDFT <"Denotes Board Feet as a standard unit."> - CBM <"Denotes Cubic Meters as a standard unit."> - CER <"Denotes Certified Emissions Reduction as a standard unit."> - CRT <"Denotes Climate Reserve Tonnes as a standard unit."> - DAG <"Denotes 10 grams as a standard unit used in precious metals contracts (e.g MCX)."> - DAY <"Denotes a single day as a standard unit used in time charter trades."> - DMTU <"Denotes Dry Metric Ton (Tonne) Units - Consists of a metric ton of mass excluding moisture."> - ENVCRD <"Denotes Environmental Credit as a standard unit."> - ENVOFST <"Denotes Environmental Offset as a standard unit."> - FEU <"Denotes a 40 ft. Equivalent Unit container as a standard unit."> - G <"Denotes a Gram as a standard unit."> - GBBSH <"Denotes a GB Bushel as a standard unit."> - GBBTU <"Denotes a GB British Thermal Unit as a standard unit."> - GBCWT <"Denotes a GB Hundredweight unit as standard unit."> - GBGAL <"Denotes a GB Gallon unit as standard unit."> - GBMBTU <"Denotes a Thousand GB British Thermal Units as a standard unit."> - GBMMBTU <"Denotes a Million GB British Thermal Units as a standard unit."> - GBT <"Denotes a GB Ton as a standard unit."> - GBTHM <"Denotes a GB Thermal Unit as a standard unit."> - GJ <"Denotes a Gigajoule as a standard unit."> - GW <"Denotes a Gigawatt as a standard unit."> - GWH <"Denotes a Gigawatt-hour as a standard unit."> - HL <"Denotes a Hectolitre as a standard unit."> - HOGB <"Denotes a 100-troy ounces Gold Bar as a standard unit."> - ISOBTU <"Denotes an ISO British Thermal Unit as a standard unit."> - ISOMBTU <"Denotes a Thousand ISO British Thermal Unit as a standard unit."> - ISOMMBTU <"Denotes a Million ISO British Thermal Unit as a standard unit."> - ISOTHM <"Denotes an ISO Thermal Unit as a standard unit."> - KG <"Denotes a Kilogram as a standard unit."> - KL <"Denotes a Kilolitre as a standard unit."> - KW <"Denotes a Kilowatt as a standard unit."> - KWD <"Denotes a Kilowatt-day as a standard unit."> - KWH <"Denotes a Kilowatt-hour as a standard unit."> - KWM <"Denotes a Kilowatt-month as a standard unit."> - KWMIN <"Denotes a Kilowatt-minute as a standard unit."> - KWY <"Denotes a Kilowatt-year as a standard unit."> - L <"Denotes a Litre as a standard unit."> - LB <"Denotes a Pound as a standard unit."> - MB <"Denotes a Thousand Barrels as a standard unit."> - MBF <"Denotes a Thousand board feet, which are used in contracts on forestry underlyers as a standard unit."> - MJ <"Denotes a Megajoule as a standard unit."> - MMBF <"Denotes a Million board feet, which are used in contracts on forestry underlyers as a standard unit."> - MMBBL <"Denotes a Million Barrels as a standard unit."> - MSF <"Denotes a Thousand square feet as a standard unit."> - MT <"Denotes a Metric Ton as a standard unit."> - MW <"Denotes a Megawatt as a standard unit."> - MWD <"Denotes a Megawatt-day as a standard unit."> - MWH <"Denotes a Megawatt-hour as a standard unit."> - MWM <"Denotes a Megawatt-month as a standard unit."> - MWMIN <"Denotes a Megawatt-minute as a standard unit."> - MWY <"Denotes a Megawatt-year as a standard unit."> - OZT <"Denotes a Troy Ounce as a standard unit."> - SGB <"Denotes a Standard Gold Bar as a standard unit."> - TEU <"Denotes a 20 ft. Equivalent Unit container as a standard unit."> - USBSH <"Denotes a US Bushel as a standard unit."> - USBTU <"Denotes a US British Thermal Unit as a standard unit."> - USCWT <"Denotes US Hundredweight unit as a standard unit."> - USGAL <"Denotes a US Gallon unit as a standard unit."> - USMBTU <"Denotes a Thousand US British Thermal Units as a standard unit."> - USMMBTU <"Denotes a Million US British Thermal Units as a standard unit."> - UST <"Denotes a US Ton as a standard unit."> - USTHM <"Denotes a US Thermal Unit as a standard unit."> - enum WeatherUnitEnum: <"Provides enumerated values for weather units, generally used in the context of defining quantities for commodities."> - CDD <"Denotes Cooling Degree Days as a standard unit."> - CPD <"Denotes Critical Precipitation Day as a standard unit."> - HDD <"Heating Degree Day as a standard unit."> - """) - .toString(); - - String expected = """ - @replaceable - def Create_UnitType(currency: str | None, financialUnit: FinancialUnitEnum | None) -> UnitType: - \""" - Create UnitType with given currency or financial unit. - - Parameters\s - ---------- - currency : string - - financialUnit : FinancialUnitEnum - - Returns - ------- - unitType : UnitType - - \""" - _pre_registry = {} - self = inspect.currentframe() - - # conditions - - @rune_local_condition(_pre_registry) - def condition_0_CurrencyOrFinancialUnitExists(self): - return (rune_attr_exists(rune_resolve_attr(self, "currency")) or rune_attr_exists(rune_resolve_attr(self, "financialUnit"))) - # Execute all registered conditions - execute_local_conditions(_pre_registry, 'Pre-condition') - - unitType = _get_rune_object('UnitType', 'currency', rune_resolve_attr(self, "currency")) - unitType = set_rune_attr(rune_resolve_attr(self, 'unitType'), 'financialUnit', rune_resolve_attr(self, "financialUnit")) - - - return unitType - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - } - - // Test add and set in the same function with basemodel output - @Test - public void testAddAndSet() { - String pythonString = testUtils.generatePythonFromString( - """ - func ResolvePerformanceReset: <"Defines how to resolve the reset value for a performance payout."> - inputs: - observation Observation (1..1) <"Represents the observation that will be used to compute the reset value."> - date date (1..1) <"Specifies the date of the reset."> - output: - reset Reset (1..1) - set reset -> resetValue: <"Assigns the observed value to the reset value."> - observation -> observedValue - set reset -> resetDate: - date - add reset -> observations: <"Assigns the observation required to compute the rest value as audit."> - observation - type Reset: <"Defines the reset value or fixing value produced in cashflow calculations, during the life-cycle of a financial instrument. The reset process defined in Create_Reset function joins product definition details with observations to compute the reset value."> - [metadata key] - resetValue Price (1..1) <"Specifies the reset or fixing value. The fixing value could be a cash price, interest rate, or other value."> - resetDate date (1..1) <"Specifies the date on which the reset occurred."> - observations Observation (1..*) - type Price : - price int(1..1) - - type Observation: <"Defines a single, numerical value that was observed in the marketplace. Observations of market data are made independently to business events or trade life-cycle events, so data instances of Observation can be created independently of any other model type, hence it is annotated as a root type. Observations will be broadly reused in many situations, so references to Observation are supported via the 'key' annotation."> - [rootType] - [metadata key] - observedValue Price (1..1) <"Specifies the observed value as a number."> - """) - .toString(); - - String expected = """ - @replaceable - def ResolvePerformanceReset(observation: Observation, date: datetime.date) -> Reset: - \""" - Defines how to resolve the reset value for a performance payout. - - Parameters\s - ---------- - observation : Observation - Represents the observation that will be used to compute the reset value. - - date : date - Specifies the date of the reset. - - Returns - ------- - reset : Reset - - \""" - self = inspect.currentframe() - - - reset = _get_rune_object('Reset', 'resetValue', rune_resolve_attr(rune_resolve_attr(self, "observation"), "observedValue")) - reset = set_rune_attr(rune_resolve_attr(self, 'reset'), 'resetDate', rune_resolve_attr(self, "date")) - reset.add_rune_attr(rune_resolve_attr(rune_resolve_attr(self, reset), 'observations'), rune_resolve_attr(self, "observation")) - - - return reset - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - } - - @Test - public void testFilterOperation() { - String python = testUtils.generatePythonFromString( - """ - func FilterQuantity: <"Filter list of quantities based on unit type."> - inputs: - quantities Quantity (0..*) <"List of quantities to filter."> - unit UnitType (1..1) <"Currency unit type."> - output: - filteredQuantities Quantity (0..*) - - add filteredQuantities: - quantities - filter quantities -> unit all = unit - type Quantity: <"Specifies a quantity as a single value to be associated to a financial product, for example a transfer amount resulting from a trade. This data type extends QuantitySchedule and requires that only the single amount value exists."> - value number (0..1) <"Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted."> - unit UnitType (0..1) <"Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit)."> - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - value int (1..1) - """) - .toString(); - - String expected = """ - @replaceable - def FilterQuantity(quantities: list[Quantity] | None, unit: UnitType) -> Quantity: - \""" - Filter list of quantities based on unit type. - - Parameters\s - ---------- - quantities : Quantity - List of quantities to filter. - - unit : UnitType - Currency unit type. - - Returns - ------- - filteredQuantities : Quantity - - \""" - self = inspect.currentframe() - - - filteredQuantities = rune_filter(rune_resolve_attr(self, "quantities"), lambda item: rune_all_elements(rune_resolve_attr(rune_resolve_attr(self, "quantities"), "unit"), "=", rune_resolve_attr(self, "unit"))) - - - return filteredQuantities - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - - } - - // Test generation with an enum - @Test - public void testWithEnumAttr() { - - Map python = testUtils.generatePythonFromString( - """ - enum ArithmeticOperationEnum: <"An arithmetic operator that can be passed to a function"> - Add <"Addition"> - Subtract <"Subtraction"> - Multiply <"Multiplication"> - Divide <"Division"> - Max <"Max of 2 values"> - Min <"Min of 2 values"> - - func ArithmeticOperation: - inputs: - n1 number (1..1) - op ArithmeticOperationEnum (1..1) - n2 number (1..1) - output: - result number (1..1) - - set result: - if op = ArithmeticOperationEnum -> Add then - n1 + n2 - else if op = ArithmeticOperationEnum -> Subtract then - n1 - n2 - else if op = ArithmeticOperationEnum -> Multiply then - n1 * n2 - else if op = ArithmeticOperationEnum -> Divide then - n1 / n2 - else if op = ArithmeticOperationEnum -> Max then - Max( n1, n2 ) - else if op = ArithmeticOperationEnum -> Min then - Min( n1, n2 ) - """); - String generatedFunction = python.get("src/com/rosetta/test/model/functions/ArithmeticOperation.py").toString(); - - String expected = """ - @replaceable - def ArithmeticOperation(n1: Decimal, op: ArithmeticOperationEnum, n2: Decimal) -> Decimal: - \""" - - Parameters\s - ---------- - n1 : number - - op : ArithmeticOperationEnum - - n2 : number - - Returns - ------- - result : number - - \""" - self = inspect.currentframe() - - - def _then_fn5(): - return Min(rune_resolve_attr(self, "n1"), rune_resolve_attr(self, "n2")) - - def _else_fn5(): - return True - - def _then_fn4(): - return Max(rune_resolve_attr(self, "n1"), rune_resolve_attr(self, "n2")) - - def _else_fn4(): - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.MIN), _then_fn5, _else_fn5) - - def _then_fn3(): - return (rune_resolve_attr(self, "n1") / rune_resolve_attr(self, "n2")) - - def _else_fn3(): - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.MAX), _then_fn4, _else_fn4) - - def _then_fn2(): - return (rune_resolve_attr(self, "n1") * rune_resolve_attr(self, "n2")) - - def _else_fn2(): - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.DIVIDE), _then_fn3, _else_fn3) - - def _then_fn1(): - return (rune_resolve_attr(self, "n1") - rune_resolve_attr(self, "n2")) - - def _else_fn1(): - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.MULTIPLY), _then_fn2, _else_fn2) - - def _then_fn0(): - return (rune_resolve_attr(self, "n1") + rune_resolve_attr(self, "n2")) - - def _else_fn0(): - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.SUBTRACT), _then_fn1, _else_fn1) - - result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.ADD), _then_fn0, _else_fn0) - - - return result - """; - testUtils.assertGeneratedContainsExpectedString(generatedFunction, expected); - } - - @Test - public void testFilterOperation2() { - String python = testUtils.generatePythonFromString( - """ - func FilterQuantityByCurrencyExists: <"Filter list of quantities based on unit type."> - inputs: - quantities QuantitySchedule (0..*) <"List of quantities to filter."> - output: - filteredQuantities QuantitySchedule (0..*) - - add filteredQuantities: - quantities - filter item -> unit -> currency exists - type QuantitySchedule: <"Specifies a quantity as a single value to be associated to a financial product, for example a transfer amount resulting from a trade. This data type extends QuantitySchedule and requires that only the single amount value exists."> - value number (0..1) <"Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted."> - unit UnitType (0..1) <"Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit)."> - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - currency string(0..1) - """) - .toString(); - - String expected = """ - @replaceable - def FilterQuantityByCurrencyExists(quantities: list[QuantitySchedule] | None) -> QuantitySchedule: - \""" - Filter list of quantities based on unit type. - - Parameters\s - ---------- - quantities : QuantitySchedule - List of quantities to filter. - - Returns - ------- - filteredQuantities : QuantitySchedule - - \""" - self = inspect.currentframe() - - - filteredQuantities = rune_filter(rune_resolve_attr(self, "quantities"), lambda item: rune_attr_exists(rune_resolve_attr(rune_resolve_attr(item, "unit"), "currency"))) - - - return filteredQuantities - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - - } - - @Test - public void testAlias1() { - - String python = testUtils.generatePythonFromString( - """ - func testAlias: - inputs: - inp1 number(1..1) - inp2 number(1..1) - output: - result number(1..1) - alias Alias: - if inp1 < 0 then inp1 - - set result: - Alias - """).toString(); - - String expected = """ - @replaceable - def testAlias(inp1: Decimal, inp2: Decimal) -> Decimal: - \""" - - Parameters\s - ---------- - inp1 : number - - inp2 : number - - Returns - ------- - result : number - - \""" - self = inspect.currentframe() - - - def _then_fn0(): - return rune_resolve_attr(self, "inp1") - - def _else_fn0(): - return True - - Alias = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "inp1"), "<", 0), _then_fn0, _else_fn0) - result = rune_resolve_attr(self, "Alias") - - - return result - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - - } - - // Test alias with basemodels inputs - @Test - public void testAlias2() { - - String python = testUtils.generatePythonFromString( - """ - type A: - valueA number(1..1) - - type B: - valueB number(1..1) - - type C: - valueC number(1..1) - - func testAlias: - inputs: - a A (1..1) - b B (1..1) - output: - c C (1..1) - alias Alias1: - a->valueA - alias Alias2: - b->valueB - set c->valueC: - Alias1*Alias2 - """).toString(); - - String expected = """ - @replaceable - def testAlias(a: A, b: B) -> C: - \""" - - Parameters\s - ---------- - a : A - - b : B - - Returns - ------- - c : C - - \""" - self = inspect.currentframe() - - - Alias1 = rune_resolve_attr(rune_resolve_attr(self, "a"), "valueA") - Alias2 = rune_resolve_attr(rune_resolve_attr(self, "b"), "valueB") - c = _get_rune_object('C', 'valueC', (rune_resolve_attr(self, "Alias1") * rune_resolve_attr(self, "Alias2"))) - - - return c - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - - testUtils.assertGeneratedContainsExpectedString(python, expected); - - } - - @Test - public void testComplexSetConstructors() { - - String pythonString = testUtils.generatePythonFromString( - """ - type InterestRatePayout: <" A class to specify all of the terms necessary to define and calculate a cash flow based on a fixed, a floating or an inflation index rate. The interest rate payout can be applied to interest rate swaps and FRA (which both have two associated interest rate payouts), credit default swaps (to represent the fee leg when subject to periodic payments) and equity swaps (to represent the funding leg). The associated globalKey denotes the ability to associate a hash value to the InterestRatePayout instantiations for the purpose of model cross-referencing, in support of functionality such as the event effect and the lineage."> - [metadata key] - rateSpecification RateSpecification (0..1) <"The specification of the rate value(s) applicable to the contract using either a floating rate calculation, a single fixed rate, a fixed rate schedule, or an inflation rate calculation."> - - type RateSpecification: <" A class to specify the fixed interest rate, floating interest rate or inflation rate."> - floatingRate FloatingRateSpecification (0..1) <"The floating interest rate specification, which includes the definition of the floating rate index. the tenor, the initial value, and, when applicable, the spread, the rounding convention, the averaging method and the negative interest rate treatment."> - - type FloatingRateSpecification: <"A class defining a floating interest rate through the specification of the floating rate index, the tenor, the multiplier schedule, the spread, the qualification of whether a specific rate treatment and/or a cap or floor apply."> - [metadata key] - - rateOption FloatingRateOption (0..1) - - type FloatingRateOption: <"Specification of a floating rate option as a floating rate index and tenor."> - value int(1..1) - - type ObservationIdentifier: <"Defines the parameters needed to uniquely identify a piece of data among the population of all available market data."> - observable Observable (1..1) <"Represents the asset or rate to which the observation relates."> - observationDate date (1..1) <"Specifies the date value to use when resolving the market data."> - - type Observable: <"Specifies the object to be observed for a price, it could be an asset or a reference."> - [metadata key] - - rateOption FloatingRateOption (0..1) <"Specifies a floating rate index and tenor."> - - func ResolveInterestRateObservationIdentifiers: <"Defines which attributes on the InterestRatePayout should be used to locate and resolve the underlier's price, for example for the reset process."> - inputs: - payout InterestRatePayout (1..1) - date date (1..1) - output: - identifiers ObservationIdentifier (1..1) - - set identifiers -> observable -> rateOption: - payout -> rateSpecification -> floatingRate -> rateOption - set identifiers -> observationDate: - date - """) - .toString(); - - String expected = """ - @replaceable - def ResolveInterestRateObservationIdentifiers(payout: InterestRatePayout, date: datetime.date) -> ObservationIdentifier: - \""" - Defines which attributes on the InterestRatePayout should be used to locate and resolve the underlier's price, for example for the reset process. - - Parameters\s - ---------- - payout : InterestRatePayout - - date : date - - Returns - ------- - identifiers : ObservationIdentifier - - \""" - self = inspect.currentframe() - - - identifiers = _get_rune_object('ObservationIdentifier', 'observable', _get_rune_object('Observable', 'rateOption', rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(self, "payout"), "rateSpecification"), "floatingRate"), "rateOption"))) - identifiers = set_rune_attr(rune_resolve_attr(self, 'identifiers'), 'observationDate', rune_resolve_attr(self, "date")) - - - return identifiers - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - - } - - @Test - public void testCondition() { - String python = testUtils.generatePythonFromString( - """ - func RoundToNearest: - inputs: - value number (1..1) - nearest number (1..1) - roundingMode RoundingModeEnum (1..1) - output: - roundedValue number (1..1) - condition PositiveNearest: - nearest > 0 - enum RoundingModeEnum: - Down - Up - """).toString(); - - String expected = """ - @replaceable - def RoundToNearest(value: Decimal, nearest: Decimal, roundingMode: RoundingModeEnum) -> Decimal: - \""" - - Parameters\s - ---------- - value : number - - nearest : number - - roundingMode : RoundingModeEnum - - Returns - ------- - roundedValue : number - - \""" - _pre_registry = {} - self = inspect.currentframe() - - # conditions - - @rune_local_condition(_pre_registry) - def condition_0_PositiveNearest(self): - return rune_all_elements(rune_resolve_attr(self, "nearest"), ">", 0) - # Execute all registered conditions - execute_local_conditions(_pre_registry, 'Pre-condition') - - roundedValue = rune_resolve_attr(self, "roundedValue") - - - return roundedValue - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - } - - @Test - public void testMultipleConditions() { - String python = testUtils.generatePythonFromString( - """ - func RoundToNearest: - inputs: - value number (1..1) - nearest number (1..1) - roundingMode RoundingModeEnum (1..1) - output: - roundedValue number (1..1) - condition PositiveNearest: - nearest > 0 - condition valueNegative: - value < 0 - enum RoundingModeEnum: - Down - Up - """).toString(); - - String expected = """ - @replaceable - def RoundToNearest(value: Decimal, nearest: Decimal, roundingMode: RoundingModeEnum) -> Decimal: - \""" - - Parameters\s - ---------- - value : number - - nearest : number - - roundingMode : RoundingModeEnum - - Returns - ------- - roundedValue : number - - \""" - _pre_registry = {} - self = inspect.currentframe() - - # conditions - - @rune_local_condition(_pre_registry) - def condition_0_PositiveNearest(self): - return rune_all_elements(rune_resolve_attr(self, "nearest"), ">", 0) - - @rune_local_condition(_pre_registry) - def condition_1_valueNegative(self): - return rune_all_elements(rune_resolve_attr(self, "value"), "<", 0) - # Execute all registered conditions - execute_local_conditions(_pre_registry, 'Pre-condition') - - roundedValue = rune_resolve_attr(self, "roundedValue") - - - return roundedValue - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - } - - @Test - public void testPostCondition() { - String python = testUtils.generatePythonFromString( - """ - func NewFloatingPayout: <"Function specification to create the interest rate (floating) payout part of an Equity Swap according to the 2018 ISDA CDM Equity Confirmation template."> - inputs: masterConfirmation EquitySwapMasterConfirmation2018 (0..1) - output: interestRatePayout InterestRatePayout (1..1) - - post-condition InterestRatePayoutTerms: <"Interest rate payout must inherit terms from the Master Confirmation Agreement when it exists."> - if masterConfirmation exists then - //interestRatePayout -> calculationPeriodDates = masterConfirmation -> equityCalculationPeriod and - interestRatePayout -> paymentDates = masterConfirmation -> equityCashSettlementDates - type EquitySwapMasterConfirmation2018: - equityCashSettlementDates PaymentDates (1..1) - type PaymentDates: - date date(0..1) - type InterestRatePayout: - paymentDates PaymentDates(0..1) - """) - .get("src/com/rosetta/test/model/functions/NewFloatingPayout.py").toString(); - - String expected = """ - @replaceable - def NewFloatingPayout(masterConfirmation: EquitySwapMasterConfirmation2018 | None) -> InterestRatePayout: - \""" - Function specification to create the interest rate (floating) payout part of an Equity Swap according to the 2018 ISDA CDM Equity Confirmation template. - - Parameters\s - ---------- - masterConfirmation : EquitySwapMasterConfirmation2018 - - Returns - ------- - interestRatePayout : InterestRatePayout - - \""" - _post_registry = {} - self = inspect.currentframe() - - - interestRatePayout = rune_resolve_attr(self, "interestRatePayout") - - # post-conditions - - @rune_local_condition(_post_registry) - def condition_0_InterestRatePayoutTerms(self): - \""" - Interest rate payout must inherit terms from the Master Confirmation Agreement when it exists. - \""" - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(rune_resolve_attr(self, "interestRatePayout"), "paymentDates"), "=", rune_resolve_attr(rune_resolve_attr(self, "masterConfirmation"), "equityCashSettlementDates")) - - def _else_fn0(): - return True - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "masterConfirmation")), _then_fn0, _else_fn0) - # Execute all registered post-conditions - execute_local_conditions(_post_registry, 'Post-condition') - - return interestRatePayout - - sys.modules[__name__].__class__ = create_module_attr_guardian(sys.modules[__name__].__class__) - """; - testUtils.assertGeneratedContainsExpectedString(python, expected); - - } - - @Test - public void functionCallTest() { - String python = testUtils.generatePythonFromString( - """ - type InterestRatePayout: <" A class to specify all of the terms necessary to define and calculate a cash flow based on a fixed, a floating or an inflation index rate. The interest rate payout can be applied to interest rate swaps and FRA (which both have two associated interest rate payouts), credit default swaps (to represent the fee leg when subject to periodic payments) and equity swaps (to represent the funding leg). The associated globalKey denotes the ability to associate a hash value to the InterestRatePayout instantiations for the purpose of model cross-referencing, in support of functionality such as the event effect and the lineage."> - [metadata key] - rateSpecification RateSpecification (0..1) <"The specification of the rate value(s) applicable to the contract using either a floating rate calculation, a single fixed rate, a fixed rate schedule, or an inflation rate calculation."> - - type RateSpecification: <" A class to specify the fixed interest rate, floating interest rate or inflation rate."> - floatingRate FloatingRateSpecification (0..1) <"The floating interest rate specification, which includes the definition of the floating rate index. the tenor, the initial value, and, when applicable, the spread, the rounding convention, the averaging method and the negative interest rate treatment."> - - type FloatingRateSpecification: <"A class defining a floating interest rate through the specification of the floating rate index, the tenor, the multiplier schedule, the spread, the qualification of whether a specific rate treatment and/or a cap or floor apply."> - [metadata key] - - rateOption FloatingRateOption (0..1) - - type FloatingRateOption: <"Specification of a floating rate option as a floating rate index and tenor."> - value int(1..1) - func FixedAmount: - [calculation] - inputs: - interestRatePayout InterestRatePayout (1..1) - date date (1..1) - output: - fixedAmount number (1..1) - - alias dayCountFraction: DayCountFraction(interestRatePayout, date) - func DayCountFraction: - inputs: - interestRatePayout InterestRatePayout (1..1) - date date(1..1) - output: - a number(1..1) - """) - .toString(); - - String expected = """ - @replaceable - def DayCountFraction(interestRatePayout: InterestRatePayout, date: datetime.date) -> Decimal: - \""" - - Parameters\s - ---------- - interestRatePayout : InterestRatePayout - - date : date - - Returns - ------- - a : number - - \""" - self = inspect.currentframe() - - - a = rune_resolve_attr(self, "a") - - - return a"""; - - testUtils.assertGeneratedContainsExpectedString(python, expected); - } -} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionAccumulationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionAccumulationTest.java new file mode 100644 index 0000000..9f3cc56 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionAccumulationTest.java @@ -0,0 +1,115 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionAccumulationTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateFunctionWithAppendToList() { + Map gf = testUtils.generatePythonFromString( + """ + func AppendToList: <\"Append a single value to a list of numbers.\"> + inputs: + list number (0..*) <\"Input list.\"> + value number (1..1) <\"Value to add to a list.\"> + output: + result number (0..*) <\"Resulting list.\"> + + add result: list + add result: value + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_AppendToList(list: list[Decimal] | None, value: Decimal) -> list[Decimal]: + \"\"\" + Append a single value to a list of numbers. + + Parameters + ---------- + list : list[Decimal] + Input list. + + value : Decimal + Value to add to a list. + + Returns + ------- + result : list[Decimal] + + \"\"\" + self = inspect.currentframe() + + + result = rune_resolve_attr(self, "list") + result.add_rune_attr(self, rune_resolve_attr(self, "value")) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testGenerateFunctionWithAddOperation() { + Map gf = testUtils.generatePythonFromString( + """ + type Quantity: + value number (0..1) + unit UnitType (0..1) + type UnitType: + value int (1..1) + func FilterQuantity: + inputs: + quantities Quantity (0..*) + unit UnitType (1..1) + output: + filteredQuantities Quantity (0..*) + add filteredQuantities: + quantities + filter quantities -> unit all = unit + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_FilterQuantity(quantities: list[com_rosetta_test_model_Quantity] | None, unit: com_rosetta_test_model_UnitType) -> list[com_rosetta_test_model_Quantity]: + \"\"\" + + Parameters + ---------- + quantities : list[com.rosetta.test.model.Quantity] + + unit : com.rosetta.test.model.UnitType + + Returns + ------- + filteredQuantities : list[com.rosetta.test.model.Quantity] + + \"\"\" + self = inspect.currentframe() + + + filteredQuantities = rune_filter(rune_resolve_attr(self, "quantities"), lambda item: rune_all_elements(rune_resolve_attr(rune_resolve_attr(self, "quantities"), "unit"), "=", rune_resolve_attr(self, "unit"))) + + + return filteredQuantities + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionAliasTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionAliasTest.java new file mode 100644 index 0000000..636312b --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionAliasTest.java @@ -0,0 +1,134 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionAliasTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testAliasSimple() { + + Map gf = testUtils.generatePythonFromString( + """ + func TestAlias: + inputs: + inp1 number(1..1) + inp2 number(1..1) + output: + result number(1..1) + alias Alias: + if inp1 < 0 then inp1 else inp2 + + set result: + Alias + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAlias(inp1: Decimal, inp2: Decimal) -> Decimal: + \"\"\" + + Parameters + ---------- + inp1 : Decimal + + inp2 : Decimal + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + def _then_fn0(): + return rune_resolve_attr(self, "inp1") + + def _else_fn0(): + return rune_resolve_attr(self, "inp2") + + Alias = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "inp1"), "<", 0), _then_fn0, _else_fn0) + result = rune_resolve_attr(self, "Alias") + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + + } + + @Test + public void testAliasWithTypeOutput() { + + Map gf = testUtils.generatePythonFromString( + """ + type A: + valueA number(1..1) + + type B: + valueB number(1..1) + + type C: + valueC number(1..1) + + func TestAliasWithTypeOutput: + inputs: + a A (1..1) + b B (1..1) + output: + c C (1..1) + alias Alias1: + a->valueA + alias Alias2: + b->valueB + set c->valueC: + Alias1*Alias2 + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAliasWithTypeOutput(a: com_rosetta_test_model_A, b: com_rosetta_test_model_B) -> com_rosetta_test_model_C: + \"\"\" + + Parameters + ---------- + a : com.rosetta.test.model.A + + b : com.rosetta.test.model.B + + Returns + ------- + c : com.rosetta.test.model.C + + \"\"\" + self = inspect.currentframe() + + + Alias1 = rune_resolve_attr(rune_resolve_attr(self, "a"), "valueA") + Alias2 = rune_resolve_attr(rune_resolve_attr(self, "b"), "valueB") + c = _get_rune_object('com_rosetta_test_model_C', 'valueC', (rune_resolve_attr(self, "Alias1") * rune_resolve_attr(self, "Alias2"))) + + + return c + """; + + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionBasicTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionBasicTest.java new file mode 100644 index 0000000..7a67d1c --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionBasicTest.java @@ -0,0 +1,136 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionBasicTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGeneratedFunctionWithAddingNumbers() { + Map gf = testUtils.generatePythonFromString( + """ + func AddTwoNumbers: <\"Add two numbers together.\"> + inputs: + number1 number (1..1) <\"The first number to add.\"> + number2 number (1..1) <\"The second number to add.\"> + output: + result number (1..1) + set result: + number1 + number2 + """); + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_AddTwoNumbers(number1: Decimal, number2: Decimal) -> Decimal: + \"\"\" + Add two numbers together. + + Parameters + ---------- + number1 : Decimal + The first number to add. + + number2 : Decimal + The second number to add. + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + result = (rune_resolve_attr(self, \"number1\") + rune_resolve_attr(self, \"number2\")) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testFunctionWithFunctionCallingFunction() { + Map gf = testUtils.generatePythonFromString( + """ + func BaseFunction: + inputs: + value number (1..1) + output: + result number (1..1) + set result: + value * 2 + func MainFunction: + inputs: + value number (1..1) + output: + result number (1..1) + set result: + BaseFunction(value) + """); + + String expectedBundleBaseFunction = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_BaseFunction(value: Decimal) -> Decimal: + \"\"\" + + Parameters + ---------- + value : Decimal + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + result = (rune_resolve_attr(self, "value") * 2) + + + return result + """; + String expectedBundleMainFunction = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_MainFunction(value: Decimal) -> Decimal: + \"\"\" + + Parameters + ---------- + value : Decimal + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + result = com_rosetta_test_model_functions_BaseFunction(rune_resolve_attr(self, "value")) + + + return result + """; + + String expectedBundleString = gf.get("src/com/_bundle.py").toString(); + testUtils.assertGeneratedContainsExpectedString(expectedBundleString, expectedBundleBaseFunction); + testUtils.assertGeneratedContainsExpectedString(expectedBundleString, expectedBundleMainFunction); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionConditionTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionConditionTest.java new file mode 100644 index 0000000..8b78aea --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionConditionTest.java @@ -0,0 +1,238 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionConditionTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testSimpleCondition() { + Map gf = testUtils.generatePythonFromString( + """ + func MinMaxWithSimpleCondition: + inputs: + in1 number (1..1) + in2 number (1..1) + direction string (1..1) + output: + result number (1..1) + condition Directiom: + direction = "min" or direction = "max" + set result: + if direction = "min" then + [in1, in2] min + else if direction = "max" then + [in1, in2] max + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_MinMaxWithSimpleCondition(in1: Decimal, in2: Decimal, direction: str) -> Decimal: + \"\"\" + + Parameters + ---------- + in1 : Decimal + + in2 : Decimal + + direction : str + + Returns + ------- + result : Decimal + + \"\"\" + _pre_registry = {} + self = inspect.currentframe() + + # conditions + + @rune_local_condition(_pre_registry) + def condition_0_Directiom(): + item = self + return (rune_all_elements(rune_resolve_attr(self, \"direction\"), "=", \"min\") or rune_all_elements(rune_resolve_attr(self, \"direction\"), "=", \"max\")) + # Execute all registered conditions + rune_execute_local_conditions(_pre_registry, 'Pre-condition') + + def _then_fn1(): + return max([rune_resolve_attr(self, \"in1\"), rune_resolve_attr(self, \"in2\")]) + + def _else_fn1(): + return True + + def _then_fn0(): + return min([rune_resolve_attr(self, "in1"), rune_resolve_attr(self, "in2")]) + + def _else_fn0(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "direction"), "=", "max"), _then_fn1, _else_fn1) + + result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "direction"), "=", "min"), _then_fn0, _else_fn0) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testPostCondition() { + Map gf = testUtils.generatePythonFromString( + """ + func MinMaxWithPostCondition: + inputs: + in1 number (1..1) + in2 number (1..1) + direction string (1..1) + output: + result number (1..1) + set result: + if direction = "min" then + [in1, in2] min + else if direction = "max" then + [in1, in2] max + post-condition Directiom: + direction = "min" or direction = "max" """); + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_MinMaxWithPostCondition(in1: Decimal, in2: Decimal, direction: str) -> Decimal: + \"\"\" + + Parameters + ---------- + in1 : Decimal + + in2 : Decimal + + direction : str + + Returns + ------- + result : Decimal + + \"\"\" + _post_registry = {} + self = inspect.currentframe() + + + def _then_fn1(): + return max([rune_resolve_attr(self, \"in1\"), rune_resolve_attr(self, \"in2\")]) + + def _else_fn1(): + return True + + def _then_fn0(): + return min([rune_resolve_attr(self, \"in1\"), rune_resolve_attr(self, \"in2\")]) + + def _else_fn0(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, \"direction\"), "=", \"max\"), _then_fn1, _else_fn1) + + result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, \"direction\"), "=", \"min\"), _then_fn0, _else_fn0) + + # post-conditions + + @rune_local_condition(_post_registry) + def condition_0_Directiom(): + item = self + return (rune_all_elements(rune_resolve_attr(self, \"direction\"), "=", \"min\") or rune_all_elements(rune_resolve_attr(self, \"direction\"), "=", \"max\")) + # Execute all registered post-conditions + rune_execute_local_conditions(_post_registry, 'Post-condition') + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testMultipleConditions() { + Map gf = testUtils.generatePythonFromString( + """ + func MinMaxWithMPositiveNumbersAndMultipleCondition: + inputs: + in1 number (1..1) + in2 number (1..1) + direction string (1..1) + + output: + result number (1..1) + condition Directiom: + direction = "min" or direction = "max" + condition PositiveNumbers: + in1 > 0 and in2 > 0 + set result: + if direction = "min" then + [in1, in2] min + else if direction = "max" then + [in1, in2] max + """); + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_MinMaxWithMPositiveNumbersAndMultipleCondition(in1: Decimal, in2: Decimal, direction: str) -> Decimal: + \"\"\" + + Parameters + ---------- + in1 : Decimal + + in2 : Decimal + + direction : str + + Returns + ------- + result : Decimal + + \"\"\" + _pre_registry = {} + self = inspect.currentframe() + + # conditions + + @rune_local_condition(_pre_registry) + def condition_0_Directiom(): + item = self + return (rune_all_elements(rune_resolve_attr(self, "direction"), "=", "min") or rune_all_elements(rune_resolve_attr(self, "direction"), "=", "max")) + + @rune_local_condition(_pre_registry) + def condition_1_PositiveNumbers(): + item = self + return (rune_all_elements(rune_resolve_attr(self, "in1"), ">", 0) and rune_all_elements(rune_resolve_attr(self, "in2"), ">", 0)) + # Execute all registered conditions + rune_execute_local_conditions(_pre_registry, 'Pre-condition') + + def _then_fn1(): + return max([rune_resolve_attr(self, "in1"), rune_resolve_attr(self, "in2")]) + + def _else_fn1(): + return True + + def _then_fn0(): + return min([rune_resolve_attr(self, "in1"), rune_resolve_attr(self, "in2")]) + + def _else_fn0(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "direction"), "=", "max"), _then_fn1, _else_fn1) + + result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "direction"), "=", "min"), _then_fn0, _else_fn0) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionControlFlowTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionControlFlowTest.java new file mode 100644 index 0000000..1ca81c5 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionControlFlowTest.java @@ -0,0 +1,67 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionControlFlowTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGeneratedFunctionAbs() { + Map gf = testUtils.generatePythonFromString( + """ + func Abs: <"Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned."> + inputs: + arg number (1..1) + output: + result number (1..1) + set result: + if arg < 0 then -1 * arg else arg + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_Abs(arg: Decimal) -> Decimal: + \"\"\" + Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned. + + Parameters + ---------- + arg : Decimal + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + def _then_fn0(): + return (-1 * rune_resolve_attr(self, "arg")) + + def _else_fn0(): + return rune_resolve_attr(self, "arg") + + result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "arg"), "<", 0), _then_fn0, _else_fn0) + + + return result + """; + + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionOrderTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionOrderTest.java new file mode 100644 index 0000000..1142e21 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionOrderTest.java @@ -0,0 +1,85 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Disabled; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionOrderTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testFunctionDependencyOrder() { + // Define ClassB which depends on ClassA, and a function which depends on both. + // Rosetta allows defining them in any order, but Python requires definition + // before use (or forward refs). + Map gf = testUtils.generatePythonFromString( + """ + type ClassB: + attr ClassA (1..1) + + func MyFunc: + inputs: + arg ClassB (1..1) + output: + out ClassA (1..1) + set out: + arg->attr + + type ClassA: + val string (1..1) + """); + + String bundle = gf.get("src/com/_bundle.py").toString(); + + // Check ordering in the bundle. + // We expect ClassA to be defined before ClassB (because B depends on A) + // and both to be defined before MyFunc (because MyFunc depends on B and A). + + int classAIndex = bundle.indexOf("class com_rosetta_test_model_ClassA"); + int classBIndex = bundle.indexOf("class com_rosetta_test_model_ClassB"); + int funcIndex = bundle.indexOf("def com_rosetta_test_model_functions_MyFunc"); + + assertTrue(classAIndex < classBIndex, "ClassA should be defined before ClassB"); + assertTrue(classBIndex < funcIndex, "ClassB should be defined before MyFunc"); + } + + @Test + @Disabled("Circular dependencies are currently not supported and will cause a topological sort error or runtime NameError") + public void testCircularDependencyOrder() { + // Define ClassA that depends on ClassB, and ClassB that depends on ClassA. + // This is a classic circular dependency which the topological sort cannot + // handle. + Map gf = testUtils.generatePythonFromString( + """ + type ClassA: + b ClassB (0..1) + + type ClassB: + a ClassA (0..1) + """); + + String bundle = gf.get("src/com/_bundle.py").toString(); + + int classAIndex = bundle.indexOf("class com_rosetta_test_model_ClassA"); + int classBIndex = bundle.indexOf("class com_rosetta_test_model_ClassB"); + + // Ideally, one should be defined, and the other use a forward reference or + // string type hint. + // For now, we just assert that generation succeeds (which it might not if DAG + // cyclic error occurs). + assertTrue(classAIndex != -1 && classBIndex != -1, "Both classes should be generated"); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionTypeTest.java b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionTypeTest.java new file mode 100644 index 0000000..c8f0111 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/functions/PythonFunctionTypeTest.java @@ -0,0 +1,350 @@ +package com.regnosys.rosetta.generator.python.functions; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; + +@Disabled("Functions are being phased out in tests.") +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonFunctionTypeTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGeneratedFunctionTypeAsInput() { + Map gf = testUtils.generatePythonFromString( + """ + type AInput : <\"A type\"> + a number (1..1) + + func TestAbsType: <\"Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned.\"> + inputs: + arg AInput (1..1) + output: + result number (1..1) + set result: + if arg->a < 0 then -1 * arg->a else arg->a + """); + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAbsType(arg: com_rosetta_test_model_AInput) -> Decimal: + \"\"\" + Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned. + + Parameters + ---------- + arg : com.rosetta.test.model.AInput + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + def _then_fn0(): + return (-1 * rune_resolve_attr(rune_resolve_attr(self, \"arg\"), \"a\")) + + def _else_fn0(): + return rune_resolve_attr(rune_resolve_attr(self, \"arg\"), \"a\") + + result = if_cond_fn(rune_all_elements(rune_resolve_attr(rune_resolve_attr(self, \"arg\"), \"a\"), \"<\", 0), _then_fn0, _else_fn0) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testGeneratedFunctionTypeAsOutput() { + Map gf = testUtils.generatePythonFromString( + """ + type AOutput : <\"AOutput type\"> + a number (1..1) + + func TestAbsOutputType: <\"Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned.\"> + inputs: + arg number (1..1) + output: + result AOutput (1..1) + set result: AOutput { + a: if arg < 0 then arg * -1 else arg + } + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestAbsOutputType(arg: Decimal) -> com_rosetta_test_model_AOutput: + \"\"\" + Returns the absolute value of a number. If the argument is not negative, the argument is returned. If the argument is negative, the negation of the argument is returned. + + Parameters + ---------- + arg : Decimal + + Returns + ------- + result : com.rosetta.test.model.AOutput + + \"\"\" + self = inspect.currentframe() + + + def _then_fn0(): + return (rune_resolve_attr(self, "arg") * -1) + + def _else_fn0(): + return rune_resolve_attr(self, "arg") + + result = com_rosetta_test_model_AOutput(a=if_cond_fn(rune_all_elements(rune_resolve_attr(self, "arg"), "<", 0), _then_fn0, _else_fn0)) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testNumericPrecisionWithDecimals() { + Map gf = testUtils.generatePythonFromString( + """ + func TestPrecision: + output: + result number (1..1) + set result: + 0.1 + 0.2 + """); + String generated = gf.get("src/com/_bundle.py").toString(); + testUtils.assertGeneratedContainsExpectedString(generated, + "def com_rosetta_test_model_functions_TestPrecision() -> Decimal:"); + testUtils.assertGeneratedContainsExpectedString(generated, "result = (Decimal('0.1') + Decimal('0.2'))"); + testUtils.assertGeneratedContainsExpectedString(generated, "return result"); + } + + @Test + public void testGenerateFunctionWithEnum() { + Map gf = testUtils.generatePythonFromString( + """ + enum ArithmeticOperationEnum: <"An arithmetic operator that can be passed to a function"> + Add <"Addition"> + Subtract <"Subtraction"> + Multiply <"Multiplication"> + Divide <"Division"> + Max <"Max of 2 values"> + Min <"Min of 2 values"> + + func ArithmeticOperation: + inputs: + n1 number (1..1) + op ArithmeticOperationEnum (1..1) + n2 number (1..1) + output: + result number (1..1) + + set result: + if op = ArithmeticOperationEnum -> Add then + n1 + n2 + else if op = ArithmeticOperationEnum -> Subtract then + n1 - n2 + else if op = ArithmeticOperationEnum -> Multiply then + n1 * n2 + else if op = ArithmeticOperationEnum -> Divide then + n1 / n2 + else if op = ArithmeticOperationEnum -> Max then + [n1, n2] max + else if op = ArithmeticOperationEnum -> Min then + [n1, n2] min + """); + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_ArithmeticOperation(n1: Decimal, op: com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum, n2: Decimal) -> Decimal: + \"\"\" + + Parameters + ---------- + n1 : Decimal + + op : com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum + + n2 : Decimal + + Returns + ------- + result : Decimal + + \"\"\" + self = inspect.currentframe() + + + def _then_fn5(): + return min([rune_resolve_attr(self, "n1"), rune_resolve_attr(self, "n2")]) + + def _else_fn5(): + return True + + def _then_fn4(): + return max([rune_resolve_attr(self, "n1"), rune_resolve_attr(self, "n2")]) + + def _else_fn4(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.MIN), _then_fn5, _else_fn5) + + def _then_fn3(): + return (rune_resolve_attr(self, "n1") / rune_resolve_attr(self, "n2")) + + def _else_fn3(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.MAX), _then_fn4, _else_fn4) + + def _then_fn2(): + return (rune_resolve_attr(self, "n1") * rune_resolve_attr(self, "n2")) + + def _else_fn2(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.DIVIDE), _then_fn3, _else_fn3) + + def _then_fn1(): + return (rune_resolve_attr(self, "n1") - rune_resolve_attr(self, "n2")) + + def _else_fn1(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.MULTIPLY), _then_fn2, _else_fn2) + + def _then_fn0(): + return (rune_resolve_attr(self, "n1") + rune_resolve_attr(self, "n2")) + + def _else_fn0(): + return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.SUBTRACT), _then_fn1, _else_fn1) + + result = if_cond_fn(rune_all_elements(rune_resolve_attr(self, "op"), "=", com.rosetta.test.model.ArithmeticOperationEnum.ArithmeticOperationEnum.ADD), _then_fn0, _else_fn0) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Test + public void testObjectCreationFromFields() { + Map gf = testUtils.generatePythonFromString( + """ + type BaseObject: + value1 int (1..1) + value2 int (1..1) + func TestObjectCreationFromFields: + inputs: + baseObject BaseObject (1..1) + output: + result BaseObject (1..1) + set result: + BaseObject { + value1: baseObject->value1, + value2: baseObject->value2 + } + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_TestObjectCreationFromFields(baseObject: com_rosetta_test_model_BaseObject) -> com_rosetta_test_model_BaseObject: + \"\"\" + + Parameters + ---------- + baseObject : com.rosetta.test.model.BaseObject + + Returns + ------- + result : com.rosetta.test.model.BaseObject + + \"\"\" + self = inspect.currentframe() + + + result = com_rosetta_test_model_BaseObject(value1=rune_resolve_attr(rune_resolve_attr(self, \"baseObject\"), \"value1\"), value2=rune_resolve_attr(rune_resolve_attr(self, \"baseObject\"), \"value2\")) + + + return result + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } + + @Disabled + @Test + public void testComplexSetConstructors() { + Map gf = testUtils.generatePythonFromString( + """ + type InterestRatePayout: + [metadata key] + rateSpecification RateSpecification (0..1) + + type RateSpecification: + floatingRate FloatingRateSpecification (0..1) + + type FloatingRateSpecification: + [metadata key] + rateOption FloatingRateOption (0..1) + + type FloatingRateOption: + value int(1..1) + + type ObservationIdentifier: + observable Observable (1..1) + observationDate date (1..1) + + type Observable: + [metadata key] + rateOption FloatingRateOption (0..1) + + func ResolveInterestRateObservationIdentifiers: + inputs: + payout InterestRatePayout (1..1) + date date (1..1) + output: + identifiers ObservationIdentifier (1..1) + + set identifiers -> observable -> rateOption: + payout -> rateSpecification -> floatingRate -> rateOption + set identifiers -> observationDate: + date + """); + + String expectedBundle = """ + @replaceable + @validate_call + def com_rosetta_test_model_functions_ResolveInterestRateObservationIdentifiers(payout: com_rosetta_test_model_InterestRatePayout, date: datetime.date) -> com_rosetta_test_model_ObservationIdentifier: + \"\"\" + + Parameters + ---------- + payout : com.rosetta.test.model.InterestRatePayout + + date : datetime.date + + Returns + ------- + identifiers : com.rosetta.test.model.ObservationIdentifier + + \"\"\" + self = inspect.currentframe() + + + identifiers = _get_rune_object('com_rosetta_test_model_ObservationIdentifier', 'observable', _get_rune_object('com_rosetta_test_model_Observable', 'rateOption', rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(rune_resolve_attr(self, "payout"), "rateSpecification"), "floatingRate"), "rateOption"))) + identifiers = set_rune_attr(rune_resolve_attr(self, 'identifiers'), 'observationDate', rune_resolve_attr(self, "date")) + + + return identifiers + """; + testUtils.assertGeneratedContainsExpectedString(gf.get("src/com/_bundle.py").toString(), expectedBundle); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonBasicTypeGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonBasicTypeGeneratorTest.java new file mode 100644 index 0000000..a999566 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonBasicTypeGeneratorTest.java @@ -0,0 +1,298 @@ +package com.regnosys.rosetta.generator.python.object; + +import jakarta.inject.Inject; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonBasicTypeGeneratorTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testGenerateBasicTypeString() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one string (0..1) + list string (1..*) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[str] = Field(None, description='') + list: list[str] = Field(..., description='', min_length=1)"""); + } + + @Test + public void testGenerateBasicTypeInt() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one int (0..1) + list int (1..*) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[int] = Field(None, description='') + list: list[int] = Field(..., description='', min_length=1)"""); + } + + @Test + public void testGenerateBasicTypeNumber() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one number (0..1) + list number (1..*) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[Decimal] = Field(None, description='') + list: list[Decimal] = Field(..., description='', min_length=1)"""); + } + + @Test + public void testGenerateBasicTypeBoolean() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one boolean (0..1) + list boolean (1..*) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[bool] = Field(None, description='') + list: list[bool] = Field(..., description='', min_length=1)"""); + } + + @Test + public void testGenerateBasicTypeDate() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one date (0..1) + list date (1..*) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[datetime.date] = Field(None, description='') + list: list[datetime.date] = Field(..., description='', min_length=1)"""); + } + + @Test + public void testGenerateBasicTypeDateTime() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one date (0..1) + list date (1..*) + zoned zonedDateTime (0..1) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[datetime.date] = Field(None, description='') + list: list[datetime.date] = Field(..., description='', min_length=1) + zoned: Optional[datetime.datetime] = Field(None, description='')"""); + } + + @Test + public void testGenerateBasicTypeTime() { + testUtils.assertBundleContainsExpectedString( + """ + type Tester: + one time (0..1) + list time (1..*) + """, + """ + class com_rosetta_test_model_Tester(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Tester' + one: Optional[datetime.time] = Field(None, description='') + list: list[datetime.time] = Field(..., description='', min_length=1)"""); + } + + @Test + public void testOmitGlobalKeyAnnotationWhenNotDefined() { + testUtils.assertBundleContainsExpectedString( + """ + type AttributeGlobalKeyTest: + withoutGlobalKey string (1..1) + """, + """ + class com_rosetta_test_model_AttributeGlobalKeyTest(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.AttributeGlobalKeyTest' + withoutGlobalKey: str = Field(..., description='')"""); + } + + @Test + public void testGenerateRosettaCalculationTypeAsString() { + testUtils.assertBundleContainsExpectedString( + """ + type Foo: + bar calculation (0..1) + """, + """ + class com_rosetta_test_model_Foo(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Foo' + bar: Optional[str] = Field(None, description='')"""); + } + + @Test + public void testGenerateTypes() { + String pythonString = testUtils.generatePythonFromString( + """ + type TestType: <"Test type description."> + testTypeValue1 string (1..1) <"Test string"> + testTypeValue2 string (0..1) <"Test optional string"> + testTypeValue3 string (1..*) <"Test string list"> + testTypeValue4 TestType2 (1..1) <"Test TestType2"> + testEnum TestEnum (0..1) <"Optional test enum"> + + type TestType2: + testType2Value1 number(1..*) <"Test number list"> + testType2Value2 date(0..1) <"Test date"> + testEnum TestEnum (0..1) <"Optional test enum"> + + enum TestEnum: <"Test enum description."> + TestEnumValue1 <"Test enum value 1"> + TestEnumValue2 <"Test enum value 2"> + """).toString(); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_TestType(BaseDataClass): + \""" + Test type description. + \""" + _FQRTN = 'com.rosetta.test.model.TestType' + testTypeValue1: str = Field(..., description='Test string') + \""" + Test string + \""" + testTypeValue2: Optional[str] = Field(None, description='Test optional string') + \""" + Test optional string + \""" + testTypeValue3: list[str] = Field(..., description='Test string list', min_length=1) + \""" + Test string list + \""" + testTypeValue4: Annotated[com_rosetta_test_model_TestType2, com_rosetta_test_model_TestType2.serializer(), com_rosetta_test_model_TestType2.validator()] = Field(..., description='Test TestType2') + \""" + Test TestType2 + \""" + testEnum: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Optional test enum') + \""" + Optional test enum + \""" + """); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_TestType2(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestType2' + testType2Value1: list[Decimal] = Field(..., description='Test number list', min_length=1) + \""" + Test number list + \""" + testType2Value2: Optional[datetime.date] = Field(None, description='Test date') + \""" + Test date + \""" + testEnum: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Optional test enum') + \""" + Optional test enum + \""" + """); + } + + @Test + public void testGenerateTypesMethod2() { + String pythonString = testUtils.generatePythonFromString( + """ + type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> + currency string (0..1) <"Defines the currency to be used as a unit for a price, quantity, or other purpose."> + + type MeasureBase: <"Provides an abstract base class shared by Price and Quantity."> + amount number (1..1) <"Specifies an amount to be qualified and used in a Price or Quantity definition."> + unitOfAmount UnitType (1..1) <"Qualifies the unit by which the amount is measured."> + + type Quantity extends MeasureBase: <"Specifies a quantity to be associated to a financial product, for example a trade amount or a cashflow amount resulting from a trade."> + multiplier number (0..1) <"Defines the number to be multiplied by the amount to derive a total quantity."> + multiplierUnit UnitType (0..1) <"Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons)."> + """) + .toString(); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_MeasureBase(BaseDataClass): + \""" + Provides an abstract base class shared by Price and Quantity. + \""" + _FQRTN = 'com.rosetta.test.model.MeasureBase' + amount: Decimal = Field(..., description='Specifies an amount to be qualified and used in a Price or Quantity definition.') + \""" + Specifies an amount to be qualified and used in a Price or Quantity definition. + \""" + unitOfAmount: Annotated[com_rosetta_test_model_UnitType, com_rosetta_test_model_UnitType.serializer(), com_rosetta_test_model_UnitType.validator()] = Field(..., description='Qualifies the unit by which the amount is measured.') + \""" + Qualifies the unit by which the amount is measured. + \""" + """); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_UnitType(BaseDataClass): + \""" + Defines the unit to be used for price, quantity, or other purposes + \""" + _FQRTN = 'com.rosetta.test.model.UnitType' + currency: Optional[str] = Field(None, description='Defines the currency to be used as a unit for a price, quantity, or other purpose.') + \""" + Defines the currency to be used as a unit for a price, quantity, or other purpose. + \""" + """); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_Quantity(com_rosetta_test_model_MeasureBase): + \""" + Specifies a quantity to be associated to a financial product, for example a trade amount or a cashflow amount resulting from a trade. + \""" + _FQRTN = 'com.rosetta.test.model.Quantity' + multiplier: Optional[Decimal] = Field(None, description='Defines the number to be multiplied by the amount to derive a total quantity.') + \""" + Defines the number to be multiplied by the amount to derive a total quantity. + \""" + multiplierUnit: Optional[Annotated[com_rosetta_test_model_UnitType, com_rosetta_test_model_UnitType.serializer(), com_rosetta_test_model_UnitType.validator()]] = Field(None, description='Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons).') + \""" + Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons). + \""" + """); + } + + @Test + public void testMultilineAttributeDefinition() { + testUtils.assertBundleContainsExpectedString( + """ + type MultilineDefinition: + field string (1..1) <"This is a multiline + definition for a field."> + """, + """ + class com_rosetta_test_model_MultilineDefinition(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.MultilineDefinition' + field: str = Field(..., description='This is a multiline definition for a field.')"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumGeneratorTest.java index 9d91027..b1fef7b 100644 --- a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumGeneratorTest.java +++ b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumGeneratorTest.java @@ -7,10 +7,7 @@ import org.eclipse.xtext.testing.extensions.InjectionExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import com.regnosys.rosetta.generator.java.enums.EnumHelper; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import java.util.Map; +import org.junit.jupiter.api.Disabled; @ExtendWith(InjectionExtension.class) @InjectWith(RosettaInjectorProvider.class) @@ -19,122 +16,216 @@ public class PythonEnumGeneratorTest { @Inject private PythonGeneratorTestUtils testUtils; + @Disabled("testGenerateTypes3") @Test - public void testEnumWithConditions() { - Map python = testUtils.generatePythonFromString( + public void testGenerateTypes3() { + String pythonString = testUtils.generatePythonFromString( """ - enum PeriodExtendedEnum /*extends PeriodEnum*/ : <"The enumerated values to specify a time period containing the additional value of Term."> - H <"Hour"> - D <"Day"> - W <"Week"> - M <"Month"> - Y <"Year"> - T <"Term. The period commencing on the effective date and ending on the termination date. The T period always appears in association with periodMultiplier = 1, and the notation is intended for use in contexts where the interval thus qualified (e.g. accrual period, payment period, reset period, ...) spans the entire term of the trade."> - C <"CalculationPeriod - the period corresponds to the calculation period For example, used in the Commodity Markets to indicate that a reference contract is the one that corresponds to the period of the calculation period."> - - type Frequency: <"A class for defining a date frequency, e.g. one day, three months, through the combination of an integer value and a standardized period value that is specified as part of an enumeration."> + enum AncillaryRoleEnum: <"Defines the enumerated values to specify the ancillary roles to the transaction. The product is agnostic to the actual parties involved in the transaction, with the party references abstracted away from the product definition and replaced by the AncillaryRoleEnum. The AncillaryRoleEnum can then be positioned in the product and the AncillaryParty type, which is positioned outside of the product definition, allows the AncillaryRoleEnum to be associated with an actual party reference."> + DisruptionEventsDeterminingParty <"Specifies the party which determines additional disruption events."> + ExtraordinaryDividendsParty <"Specifies the party which determines if dividends are extraordinary in relation to normal levels."> + + enum TelephoneTypeEnum: <"The enumerated values to specify the type of telephone number, e.g. work vs. mobile."> + Work <"A number used primarily for work-related calls. Includes home office numbers used primarily for work purposes."> + Mobile <"A number on a mobile telephone that is often or usually used for work-related calls. This type of number can be used for urgent work related business when a work number is not sufficient to contact the person or firm."> + + type LegalEntity: <"A class to specify a legal entity, with a required name and an optional entity identifier (such as the LEI)."> [metadata key] + entityId string (0..*) <"A legal entity identifier (e.g. RED entity code)."> + [metadata scheme] + name string (1..1) <"The legal entity name."> + [metadata scheme] + + type TelephoneNumber: <"A class to specify a telephone number as a type of phone number (e.g. work, personal, ...) alongside with the actual number."> + _FQRTN = 'com.rosetta.test.model.TelephoneNumber' + telephoneNumberType TelephoneTypeEnum (0..1) <"The type of telephone number, e.g. work, mobile."> + number string (1..1) <"The actual telephone number."> - periodMultiplier int (1..1) <"A time period multiplier, e.g. 1, 2, or 3. If the period value is T (Term) then period multiplier must contain the value 1."> - period PeriodExtendedEnum (1..1) <"A time period, e.g. a day, week, month, year or term of the stream."> + type AncillaryEntity: <"Holds an identifier for an ancillary entity, either identified directly via its ancillary role or directly as a legal entity."> + _FQRTN = 'com.rosetta.test.model.AncillaryEntity' + ancillaryParty AncillaryRoleEnum (0..1) <"Identifies a party via its ancillary role on a transaction (e.g. CCP or DCO through which the trade test be cleared.)"> + legalEntity LegalEntity (0..1) - condition TermPeriod: <"FpML specifies that if period value is T (Term) then periodMultiplier must contain the value 1."> - if period = PeriodExtendedEnum -> T then periodMultiplier = 1 + condition: one-of + """) + .toString(); - condition PositivePeriodMultiplier: <"FpML specifies periodMultiplier as a positive integer."> - periodMultiplier > 0 - """); - CharSequence generatedBundleSeq = python.get("src/com/_bundle.py"); - if (generatedBundleSeq == null) { - throw new AssertionError("src/com/_bundle.py not found in generated output. Keys: " + python.keySet()); - } - String generatedBundle = generatedBundleSeq.toString(); - String expected = """ - class com_rosetta_test_model_Frequency(BaseDataClass): - _ALLOWED_METADATA = {'@key', '@key:external'} + String expectedTestType1 = """ + class com_rosetta_test_model_LegalEntity(BaseDataClass): + \""" + A class to specify a legal entity, with a required name and an optional entity identifier (such as the LEI). + \""" + _FQRTN = 'com.rosetta.test.model.LegalEntity' + entityId: list[AttributeWithMeta[str] | str] = Field([], description='A legal entity identifier (e.g. RED entity code).') + \""" + A legal entity identifier (e.g. RED entity code). + \""" + name: AttributeWithMeta[str] | str = Field(..., description='The legal entity name."> + \""" + The legal entity name. + \""" + """; + String expectedTestType2 = """ + class com_rosetta_test_model_TelephoneNumber(BaseDataClass): + \""" + A class to specify a telephone number as a type of phone number (e.g. work, personal, ...) alongside with the actual number. + \""" + _FQRTN = 'com.rosetta.test.model.TelephoneNumber' + telephoneNumberType: Optional[com.metadata.test.model.TelephoneTypeEnum.TelephoneTypeEnum] = Field(None, description='The type of telephone number, e.g. work, mobile.') + \""" + The type of telephone number, e.g. work, mobile. + \""" + number: str = Field(..., description='The actual telephone number.') \""" - A class for defining a date frequency, e.g. one day, three months, through the combination of an integer value and a standardized period value that is specified as part of an enumeration. + The actual telephone number. \""" - _FQRTN = 'com.rosetta.test.model.Frequency' - periodMultiplier: int = Field(..., description='A time period multiplier, e.g. 1, 2, or 3. If the period value is T (Term) then period multiplier must contain the value 1.') + """; + String expectedTestType3 = """ + class com_rosetta_test_model_AncillaryEntity(BaseDataClass): \""" - A time period multiplier, e.g. 1, 2, or 3. If the period value is T (Term) then period multiplier must contain the value 1. + Holds an identifier for an ancillary entity, either identified directly via its ancillary role or directly as a legal entity. \""" - period: com.rosetta.test.model.PeriodExtendedEnum.PeriodExtendedEnum = Field(..., description='A time period, e.g. a day, week, month, year or term of the stream.') + _FQRTN = 'com.rosetta.test.model.AncillaryEntity' + ancillaryParty: Optional[com.metadata.test.model.AncillaryRoleEnum.AncillaryRoleEnum] = Field(None, description='Identifies a party via its ancillary role on a transaction (e.g. CCP or DCO through which the trade test be cleared.)') \""" - A time period, e.g. a day, week, month, year or term of the stream. + Identifies a party via its ancillary role on a transaction (e.g. CCP or DCO through which the trade test be cleared.) \""" + legalEntity: Optional[com_rosetta_test_model_LegalEntity] = Field(None, description='') @rune_condition - def condition_0_TermPeriod(self): - \""" - FpML specifies that if period value is T (Term) then periodMultiplier must contain the value 1. - \""" + def condition_0_(self): item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "periodMultiplier"), "=", 1) - - def _else_fn0(): - return True - - return if_cond_fn(rune_all_elements(rune_resolve_attr(self, "period"), "=", com.rosetta.test.model.PeriodExtendedEnum.PeriodExtendedEnum.T), _then_fn0, _else_fn0) + return rune_check_one_of(self, 'ancillaryParty', 'legalEntity', necessity=True) + """; - @rune_condition - def condition_1_PositivePeriodMultiplier(self): - \""" - FpML specifies periodMultiplier as a positive integer. - \""" - item = self - return rune_all_elements(rune_resolve_attr(self, "periodMultiplier"), ">", 0) + String expectedTestType4 = """ + class AncillaryRoleEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Defines the enumerated values to specify the ancillary roles to the transaction. The product is agnostic to the actual parties involved in the transaction, with the party references abstracted away from the product definition and replaced by the AncillaryRoleEnum. The AncillaryRoleEnum can then be positioned in the product and the AncillaryParty type, which is positioned outside of the product definition, allows the AncillaryRoleEnum to be associated with an actual party reference. + \""" + DISRUPTION_EVENTS_DETERMINING_PARTY = "DisruptionEventsDeterminingParty" + \""" + Specifies the party which determines additional disruption events. + \""" + EXTRAORDINARY_DIVIDENDS_PARTY = "ExtraordinaryDividendsParty" + \""" + Specifies the party which determines if dividends are extraordinary in relation to normal levels. + \""" + """; + String expectedTestType5 = """ + class TelephoneTypeEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + The enumerated values to specify the type of telephone number, e.g. work vs. mobile. + \""" + MOBILE = "Mobile" + \""" + A number on a mobile telephone that is often or usually used for work-related calls. This type of number can be used for urgent work related business when a work number is not sufficient to contact the person or firm. + \""" + WORK = "Work" + \""" + A number used primarily for work-related calls. Includes home office numbers used primarily for work purposes. + \""" """; - testUtils.assertGeneratedContainsExpectedString(generatedBundle, expected); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType1); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType4); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType5); } + @Disabled("testGenerateTypes2") @Test - public void testEnumGeneration() { + public void testGenerateTypes2() { String pythonString = testUtils.generatePythonFromString( """ - enum TestEnum: <"Test enum description."> - TestEnumValue1 <"Test enum value 1"> - TestEnumValue2 <"Test enum value 2"> - TestEnumValue3 <"Test enum value 3"> - _1 displayName "1" <"Rolls on the 1st day of the month."> - """).toString(); + enum CapacityUnitEnum: <"Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities."> + ALW <"Denotes Allowances as standard unit."> + BBL <"Denotes a Barrel as a standard unit."> - String expected = """ - class TestEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + enum WeatherUnitEnum: <"Provides enumerated values for weather units, generally used in the context of defining quantities for commodities."> + CDD <"Denotes Cooling Degree Days as a standard unit."> + CPD <"Denotes Critical Precipitation Day as a standard unit."> + + enum FinancialUnitEnum: <"Provides enumerated values for financial units, generally used in the context of defining quantities for securities."> + Contract <"Denotes financial contracts, such as listed futures and options."> + ContractualProduct <"Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount."> + + type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> + capacityUnit CapacityUnitEnum (0..1) <"Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities."> + weatherUnit WeatherUnitEnum (0..1) <"Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities."> + [metadata scheme] + + condition UnitType: <"Requires that a unit type must be set."> + one-of + """) + .toString(); + + String expectedTestType = """ + class class com_rosetta_test_model_UnitType(BaseDataClass): \""" - Test enum description. + Defines the unit to be used for price, quantity, or other purposes \""" - TEST_ENUM_VALUE_1 = "TestEnumValue1" + _FQRTN = 'com.rosetta.test.model.UnitType' + + capacityUnit: Optional[com.metadata.test.model.CapacityUnitEnum.CapacityUnitEnum] = Field(None, description='Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities.') + \""" + Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities. \""" - Test enum value 1 + weatherUnit: Optional[com.metadata.test.model.WeatherUnitEnum.WeatherUnitEnum] = Field(None, description='Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities.') \""" - TEST_ENUM_VALUE_2 = "TestEnumValue2" + Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities. \""" - Test enum value 2 + """; + String expectedTestType2 = """ + class FinancialUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): \""" - TEST_ENUM_VALUE_3 = "TestEnumValue3" + Provides enumerated values for financial units, generally used in the context of defining quantities for securities. \""" - Test enum value 3 + CONTRACT = "Contract" \""" - _1 = "1" + Denotes financial contracts, such as listed futures and options. \""" - Rolls on the 1st day of the month. + CONTRACTUAL_PRODUCT = "ContractualProduct" \""" + + @rune_condition + def condition_0_UnitType(self): + \""" + Requires that a unit type must be set. + \""" + item = self + return rune_check_one_of(self, 'capacityUnit', 'weatherUnit', 'financialUnit', 'currency', necessity=True) + """; + String expectedTestType3 = """ + class WeatherUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Provides enumerated values for weather units, generally used in the context of defining quantities for commodities. + \""" + CDD = "CDD" + \""" + Denotes Cooling Degree Days as a standard unit. + \""" + CPD = "CPD" + \""" + """; + String expectedTestType4 = """ + class CapacityUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities. + \""" + ALW = "ALW" + \""" + Denotes Allowances as standard unit. + \""" + BBL = "BBL" + \""" + Denotes a Barrel as a standard unit. + \""" """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - } - @Test - public void testEnumGenerationWithUppercaseUnderscoreFormattedNames() { - assertThat(EnumHelper.formatEnumName("ISDA1993Commodity"), is("ISDA_1993_COMMODITY")); - assertThat(EnumHelper.formatEnumName("ISDA1998FX"), is("ISDA1998FX")); - assertThat(EnumHelper.formatEnumName("iTraxxEuropeDealer"), is("I_TRAXX_EUROPE_DEALER")); - assertThat(EnumHelper.formatEnumName("StandardLCDS"), is("STANDARD_LCDS")); - assertThat(EnumHelper.formatEnumName("_1_1"), is("_1_1")); - assertThat(EnumHelper.formatEnumName("_30E_360_ISDA"), is("_30E_360_ISDA")); - assertThat(EnumHelper.formatEnumName("ACT_365L"), is("ACT_365L")); - assertThat(EnumHelper.formatEnumName("OSPPrice"), is("OSP_PRICE")); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType4); } } diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumMetadataTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumMetadataTest.java new file mode 100644 index 0000000..4a9abe0 --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonEnumMetadataTest.java @@ -0,0 +1,75 @@ +package com.regnosys.rosetta.generator.python.object; + +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import jakarta.inject.Inject; +import java.util.Map; +import org.junit.jupiter.api.Disabled; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonEnumMetadataTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testEnumWithMetadataId() { + // This test captures the "Unresolved Issue" where enums with [metadata id] + // fail to generate the necessary metadata handling infrastructure. + Map gf = testUtils.generatePythonFromString( + """ + namespace test.metadata : <"test"> + + enum CurrencyEnum: + USD + EUR + GBP + + type CashTransfer: + amount number (1..1) + currency CurrencyEnum (1..1) + [metadata id] + """); + + String bundle = gf.get("src/test/_bundle.py").toString(); + + // Assert that the 'currency' field in CashTransfer is using a metadata + // wrapper with the correct @key tags. + testUtils.assertGeneratedContainsExpectedString(bundle, + "currency: Annotated[test.metadata.CurrencyEnum.CurrencyEnum, test.metadata.CurrencyEnum.CurrencyEnum.serializer(), test.metadata.CurrencyEnum.CurrencyEnum.validator(('@key', '@key:external'))]"); + + // Also verify the key-ref constraints + testUtils.assertGeneratedContainsExpectedString(bundle, + "'currency': {'@key', '@key:external'}"); + } + + @Test + @Disabled("Blocked by Enum Wrapper implementation - see Backlog") + public void testEnumWithoutMetadata() { + Map gf = testUtils.generatePythonFromString( + """ + namespace test.metadata : <"test"> + + enum CurrencyEnum: + USD + EUR + GBP + + type CashTransfer: + amount number (1..1) + currency CurrencyEnum (1..1) + """); + + String bundle = gf.get("src/test/_bundle.py").toString(); + + // Enums should be consistently wrapped in Annotated to support metadata + // during deserialization even if not explicitly required in Rosetta. + testUtils.assertGeneratedContainsExpectedString(bundle, + "currency: Annotated[test.metadata.CurrencyEnum.CurrencyEnum, test.metadata.CurrencyEnum.CurrencyEnum.serializer(), test.metadata.CurrencyEnum.CurrencyEnum.validator()]"); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonInheritanceGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonInheritanceGeneratorTest.java new file mode 100644 index 0000000..6fe280c --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonInheritanceGeneratorTest.java @@ -0,0 +1,296 @@ +package com.regnosys.rosetta.generator.python.object; + +import jakarta.inject.Inject; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.Disabled; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonInheritanceGeneratorTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testExtendATypeWithSameAttribute() { + testUtils.assertBundleContainsExpectedString( + """ + type Foo: + a string (0..1) + b string (0..1) + + type Bar extends Foo: + a string (0..1) + """, + """ + class com_rosetta_test_model_Foo(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Foo' + a: Optional[str] = Field(None, description='') + b: Optional[str] = Field(None, description='')"""); + testUtils.assertBundleContainsExpectedString( + """ + type Foo: + a string (0..1) + b string (0..1) + + type Bar extends Foo: + a string (0..1) + """, + """ + class com_rosetta_test_model_Bar(com_rosetta_test_model_Foo): + _FQRTN = 'com.rosetta.test.model.Bar' + a: Optional[str] = Field(None, description='')"""); + } + + @Test + public void testSetAttributesOnEmptyClassWithInheritance() { + testUtils.assertBundleContainsExpectedString( + """ + type B: + b string (1..1) + + type A extends B: + """, + """ + class com_rosetta_test_model_A(com_rosetta_test_model_B): + _FQRTN = 'com.rosetta.test.model.A' + pass"""); + } + + @Test + public void testGenerateTypesExtends() { + String pythonString = testUtils.generatePythonFromString( + """ + type TestType extends TestType2: + TestTypeValue1 string (1..1) <"Test string"> + TestTypeValue2 int (0..1) <"Test int"> + + type TestType2 extends TestType3: + TestType2Value1 number (0..1) <"Test number"> + TestType2Value2 date (1..*) <"Test date"> + + type TestType3: + TestType3Value1 string (0..1) <"Test string"> + TestType4Value2 int (1..*) <"Test int"> + """).toString(); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_TestType(com_rosetta_test_model_TestType2): + _FQRTN = 'com.rosetta.test.model.TestType' + TestTypeValue1: str = Field(..., description='Test string') + \""" + Test string + \""" + TestTypeValue2: Optional[int] = Field(None, description='Test int') + \""" + Test int + \""" + """); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_TestType2(com_rosetta_test_model_TestType3): + _FQRTN = 'com.rosetta.test.model.TestType2' + TestType2Value1: Optional[Decimal] = Field(None, description='Test number') + \""" + Test number + \""" + TestType2Value2: list[datetime.date] = Field(..., description='Test date', min_length=1) + \""" + Test date + \""" + """); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_TestType3(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.TestType3' + TestType3Value1: Optional[str] = Field(None, description='Test string') + \""" + Test string + \""" + TestType4Value2: list[int] = Field(..., description='Test int', min_length=1) + \""" + Test int + \""" + """); + } + + @Disabled("testGenerateTypesExtends2") + @Test + public void testGenerateTypesExtends2() { + String pythonString = testUtils.generatePythonFromString( + """ + enum CapacityUnitEnum: <"Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities."> + ALW <"Denotes Allowances as standard unit."> + BBL <"Denotes a Barrel as a standard unit."> + BCF <"Denotes Billion Cubic Feet as a standard unit."> + + enum WeatherUnitEnum: <"Provides enumerated values for weather units, generally used in the context of defining quantities for commodities."> + CDD <"Denotes Cooling Degree Days as a standard unit."> + CPD <"Denotes Critical Precipitation Day as a standard unit."> + HDD <"Heating Degree Day as a standard unit."> + + enum FinancialUnitEnum: <"Provides enumerated values for financial units, generally used in the context of defining quantities for securities."> + Contract <"Denotes financial contracts, such as listed futures and options."> + ContractualProduct <"Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount."> + IndexUnit <"Denotes a price expressed in index points, e.g. for a stock index."> + + type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> + capacityUnit CapacityUnitEnum (0..1) <"Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities."> + weatherUnit WeatherUnitEnum (0..1) <"Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities."> + financialUnit FinancialUnitEnum (0..1) <"Provides an enumerated value for financial units, generally used in the context of defining quantities for securities."> + currency string (0..1) <"Defines the currency to be used as a unit for a price, quantity, or other purpose."> + [metadata scheme] + + condition UnitType: <"Requires that a unit type must be set."> + one-of + + type Measure extends MeasureBase: <"Defines a concrete measure as a number associated to a unit. It extends MeasureBase by requiring the value attribute to be present. A measure may be unit-less so the unit attribute is still optional."> + + condition ValueExists: <"The value attribute must be present in a concrete measure."> + value exists + + type MeasureBase: <"Provides an abstract type to define a measure as a number associated to a unit. This type is abstract because all its attributes are optional. The types that extend it can specify further existence constraints."> + + value number (0..1) <"Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted."> + unit UnitType (0..1) <"Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit)."> + """) + .toString(); + + String expectedTestType1 = """ + class com_rosetta_test_model_MeasureBase(BaseDataClass): + \""" + Provides an abstract type to define a measure as a number associated to a unit. This type is abstract because all its attributes are optional. The types that extend it can specify further existence constraints. + \""" + _FQRTN = 'com.rosetta.test.model.MeasureBase' + + value: Optional[Decimal] = Field(None, description='Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted.') + \""" + Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted. + \""" + unit: Optional[com_rosetta_test_model_UnitType] = Field(None, description='Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit).') + \""" + Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit). + \""" + """; + + String expectedTestType2 = """ + class com_rosetta_test_model_Measure(com_rosetta_test_model_MeasureBase): + \""" + Defines a concrete measure as a number associated to a unit. It extends MeasureBase by requiring the value attribute to be present. A measure may be unit-less so the unit attribute is still optional. + \""" + _FQRTN = 'com.rosetta.test.model.Measure' + + @rune_condition + def condition_0_ValueExists(self): + \""" + The value attribute must be present in a concrete measure. + \""" + item = self + return rune_attr_exists(rune_resolve_attr(self, "value")) + """; + + String expectedTestType3 = """ + class WeatherUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Provides enumerated values for weather units, generally used in the context of defining quantities for commodities. + \""" + CDD = "CDD" + \""" + Denotes Cooling Degree Days as a standard unit. + \""" + CPD = "CPD" + \""" + Denotes Critical Precipitation Day as a standard unit. + \""" + HDD = "HDD" + \""" + Heating Degree Day as a standard unit. + \""" + """; + + String expectedTestType4 = """ + class FinancialUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Provides enumerated values for financial units, generally used in the context of defining quantities for securities. + \""" + CONTRACT = "Contract" + \""" + Denotes financial contracts, such as listed futures and options. + \""" + CONTRACTUAL_PRODUCT = "ContractualProduct" + \""" + Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount. + \""" + INDEX_UNIT = "IndexUnit" + \""" + Denotes a price expressed in index points, e.g. for a stock index. + \""" + """; + String expectedTestType5 = """ + class UnitType(BaseDataClass): + \""" + Defines the unit to be used for price, quantity, or other purposes + \""" + _FQRTN = 'com.rosetta.test.model.UnitType' + + capacityUnit: Optional[com.metadata.test.model.CapacityUnitEnum.CapacityUnitEnum] = Field(None, description='Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities.') + \""" + Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities. + \""" + weatherUnit: Optional[com.metadata.test.model.WeatherUnitEnum.WeatherUnitEnum] = Field(None, description='Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities.') + \""" + Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities. + \""" + financialUnit: Optional[com.metadata.test.model.FinancialUnitEnum.FinancialUnitEnum] = Field(None, description='Provides an enumerated value for financial units, generally used in the context of defining quantities for securities.') + \""" + Provides an enumerated value for financial units, generally used in the context of defining quantities for securities. + \""" + currency: Optional[AttributeWithMeta[str] | str] = Field(None, description='Defines the currency to be used as a unit for a price, quantity, or other purpose.') + \""" + Defines the currency to be used as a unit for a price, quantity, or other purpose. + \""" + + @rune_condition + def condition_0_UnitType(self): + \""" + Requires that a unit type must be set. + \""" + item = self + return rune_check_one_of(self, 'capacityUnit', 'weatherUnit', 'financialUnit', 'currency', necessity=True) + """; + + String expectedTestType6 = """ + class CapacityUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): + \""" + Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities. + \""" + ALW = "ALW" + \""" + Denotes Allowances as standard unit. + \""" + BBL = "BBL" + \""" + Denotes a Barrel as a standard unit. + \""" + BCF = "BCF" + \""" + Denotes Billion Cubic Feet as a standard unit. + \""" + """; + + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType1); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType4); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType5); + testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType6); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectConditionGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectConditionGeneratorTest.java new file mode 100644 index 0000000..63078dc --- /dev/null +++ b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectConditionGeneratorTest.java @@ -0,0 +1,241 @@ +package com.regnosys.rosetta.generator.python.object; + +import jakarta.inject.Inject; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; +import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class PythonObjectConditionGeneratorTest { + + @Inject + private PythonGeneratorTestUtils testUtils; + + @Test + public void testConditions1() { + testUtils.assertBundleContainsExpectedString( + """ + type A: + a0 string (0..1) + a1 string (0..1) + + condition C1: + a0 exists or a1 exists + """, + """ + class com_rosetta_test_model_A(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.A' + a0: Optional[str] = Field(None, description='') + a1: Optional[str] = Field(None, description='') + + @rune_condition + def condition_0_C1(self): + item = self + return (rune_attr_exists(rune_resolve_attr(self, "a0")) or rune_attr_exists(rune_resolve_attr(self, "a1")))"""); + } + + @Test + public void testGenerateTypesChoiceCondition() { + String pythonString = testUtils.generatePythonFromString( + """ + type TestType: <"Test type description."> + testTypeValue1 string (0..1) <"Test string"> + testTypeValue2 string (0..1) <"Test optional string"> + + condition TestChoice: <"Test choice description."> + optional choice testTypeValue1, testTypeValue2 + """) + .toString(); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_TestType(BaseDataClass): + \""" + Test type description. + \""" + _FQRTN = 'com.rosetta.test.model.TestType' + testTypeValue1: Optional[str] = Field(None, description='Test string') + \""" + Test string + \""" + testTypeValue2: Optional[str] = Field(None, description='Test optional string') + \""" + Test optional string + \""" + + @rune_condition + def condition_0_TestChoice(self): + \""" + Test choice description. + \""" + item = self + return rune_check_one_of(self, 'testTypeValue1', 'testTypeValue2', necessity=False) + """); + } + + @Test + public void testGenerateIfThenCondition() { + testUtils.assertBundleContainsExpectedString( + """ + type AttributeIfThenTest: + attr1 string (0..1) + attr2 string (0..1) + + condition TestIfThen: + if attr1 exists + then attr2 exists + """, + """ + class com_rosetta_test_model_AttributeIfThenTest(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.AttributeIfThenTest' + attr1: Optional[str] = Field(None, description='') + attr2: Optional[str] = Field(None, description='') + + @rune_condition + def condition_0_TestIfThen(self): + item = self + def _then_fn0(): + return rune_attr_exists(rune_resolve_attr(self, "attr2")) + + def _else_fn0(): + return True + + return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "attr1")), _then_fn0, _else_fn0)"""); + } + + @Test + public void testConditionsGeneration() { + String pythonString = testUtils.generatePythonFromString( + """ + type A: + a0 int (0..1) + a1 int (0..1) + condition: one-of + type B: + intValue1 int (0..1) + intValue2 int (0..1) + aValue A (1..1) + condition Rule: + intValue1 < 100 + condition OneOrTwo: <"Choice rule to represent an FpML choice construct."> + optional choice intValue1, intValue2 + condition ReqOneOrTwo: <"Choice rule to represent an FpML choice construct."> + required choice intValue1, intValue2 + condition SecondOneOrTwo: <"FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]."> + aValue->a0 exists + or (intValue2 exists and intValue1 exists and intValue1 exists) + or (intValue2 exists and intValue1 exists and intValue1 is absent) + """) + .toString(); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_A(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.A' + a0: Optional[int] = Field(None, description='') + a1: Optional[int] = Field(None, description='') + + @rune_condition + def condition_0_(self): + item = self + return rune_check_one_of(self, 'a0', 'a1', necessity=True) + """); + testUtils.assertGeneratedContainsExpectedString( + pythonString, + """ + class com_rosetta_test_model_B(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.B' + intValue1: Optional[int] = Field(None, description='') + intValue2: Optional[int] = Field(None, description='') + aValue: Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()] = Field(..., description='') + + @rune_condition + def condition_0_Rule(self): + item = self + return rune_all_elements(rune_resolve_attr(self, "intValue1"), "<", 100) + + @rune_condition + def condition_1_OneOrTwo(self): + \""" + Choice rule to represent an FpML choice construct. + \""" + item = self + return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=False) + + @rune_condition + def condition_2_ReqOneOrTwo(self): + \""" + Choice rule to represent an FpML choice construct. + \""" + item = self + return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=True) + + @rune_condition + def condition_3_SecondOneOrTwo(self): + \""" + FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]. + \""" + item = self + return ((rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "a0")) or ((rune_attr_exists(rune_resolve_attr(self, "intValue2")) and rune_attr_exists(rune_resolve_attr(self, "intValue1"))) and rune_attr_exists(rune_resolve_attr(self, "intValue1")))) or ((rune_attr_exists(rune_resolve_attr(self, "intValue2")) and rune_attr_exists(rune_resolve_attr(self, "intValue1"))) and (not rune_attr_exists(rune_resolve_attr(self, "intValue1")))))"""); + } + + @Test + public void testGenerateIfThenElseCondition() { + testUtils.assertBundleContainsExpectedString( + """ + type AttributeIfThenElseTest: + attr1 string (0..1) + attr2 string (0..1) + attr3 string (0..1) + + condition TestIfThenElse: + if attr1 exists + then attr2 exists + else attr3 exists + """, + """ + class com_rosetta_test_model_AttributeIfThenElseTest(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.AttributeIfThenElseTest' + attr1: Optional[str] = Field(None, description='') + attr2: Optional[str] = Field(None, description='') + attr3: Optional[str] = Field(None, description='') + + @rune_condition + def condition_0_TestIfThenElse(self): + item = self + def _then_fn0(): + return rune_attr_exists(rune_resolve_attr(self, "attr2")) + + def _else_fn0(): + return rune_attr_exists(rune_resolve_attr(self, "attr3")) + + return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "attr1")), _then_fn0, _else_fn0)"""); + } + + @Test + public void testConditionLessOrEqual() { + testUtils.assertBundleContainsExpectedString( + """ + type Foo: + a number (0..1) + b number (0..1) + + condition: + a <= b + """, + """ + class com_rosetta_test_model_Foo(BaseDataClass): + _FQRTN = 'com.rosetta.test.model.Foo' + a: Optional[Decimal] = Field(None, description='') + b: Optional[Decimal] = Field(None, description='') + + @rune_condition + def condition_0_(self): + item = self + return rune_all_elements(rune_resolve_attr(self, "a"), "<=", rune_resolve_attr(self, "b"))"""); + } +} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectGenerationTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectGenerationTest.java deleted file mode 100644 index a8701ba..0000000 --- a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectGenerationTest.java +++ /dev/null @@ -1,977 +0,0 @@ -package com.regnosys.rosetta.generator.python.object; - -import jakarta.inject.Inject; -import com.regnosys.rosetta.tests.RosettaInjectorProvider; -import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; -import org.eclipse.xtext.testing.InjectWith; -import org.eclipse.xtext.testing.extensions.InjectionExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.Disabled; - -@ExtendWith(InjectionExtension.class) -@InjectWith(RosettaInjectorProvider.class) -public class PythonObjectGenerationTest { - - @Inject - private PythonGeneratorTestUtils testUtils; - - @Test - public void testMultilineAttributeDefinition() { - String pythonString = testUtils.generatePythonFromString( - """ - type Foo: - attr int (1..1) - <"This is a - multiline - definition"> - """).toString(); - - String expectedFoo = """ - class com_rosetta_test_model_Foo(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Foo' - attr: int = Field(..., description='This is a multiline definition') - \""" - This is a - multiline - definition - \""" - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedFoo); - } - - @Test - public void testConditions1() { - String pythonString = testUtils.generatePythonFromString( - """ - type A: - a0 int (0..1) - a1 int (0..1) - condition: one-of - - type B: - intValue1 int (0..1) - intValue2 int (0..1) - aValue A (1..1) - - condition Rule: - intValue1 < 100 - - condition OneOrTwo: <"Explicit choice rule"> - optional choice intValue1, intValue2 - - condition SecondOneOrTwo: <"Implicit choice rule"> - aValue->a0 exists - or (intValue2 exists and intValue1 is absent) - or (intValue1 exists and intValue2 is absent) - """).toString(); - - String expectedA = """ - class com_rosetta_test_model_A(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.A' - a0: Optional[int] = Field(None, description='') - a1: Optional[int] = Field(None, description='') - - @rune_condition - def condition_0_(self): - item = self - return rune_check_one_of(self, 'a0', 'a1', necessity=True) - """; - - String expectedB = """ - class com_rosetta_test_model_B(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.B' - intValue1: Optional[int] = Field(None, description='') - intValue2: Optional[int] = Field(None, description='') - aValue: Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()] = Field(..., description='') - - @rune_condition - def condition_0_Rule(self): - item = self - return rune_all_elements(rune_resolve_attr(self, "intValue1"), "<", 100) - - @rune_condition - def condition_1_OneOrTwo(self): - \""" - Explicit choice rule - \""" - item = self - return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=False) - - @rune_condition - def condition_2_SecondOneOrTwo(self): - \""" - Implicit choice rule - \""" - item = self - return ((rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "a0")) or (rune_attr_exists(rune_resolve_attr(self, "intValue2")) and (not rune_attr_exists(rune_resolve_attr(self, "intValue1"))))) or (rune_attr_exists(rune_resolve_attr(self, "intValue1")) and (not rune_attr_exists(rune_resolve_attr(self, "intValue2"))))) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedA); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedB); - } - - @Test - public void testGenerateTypes1() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType: <"Test type description."> - testTypeValue1 string (1..1) <"Test string"> - testTypeValue2 string (0..1) <"Test optional string"> - testTypeValue3 string (1..*) <"Test string list"> - testTypeValue4 TestType2 (1..1) <"Test TestType2"> - testEnum TestEnum (0..1) <"Optional test enum"> - - type TestType2: - testType2Value1 number(1..*) <"Test number list"> - testType2Value2 date(0..1) <"Test date"> - testEnum TestEnum (0..1) <"Optional test enum"> - - enum TestEnum: <"Test enum description."> - TestEnumValue1 <"Test enum value 1"> - TestEnumValue2 <"Test enum value 2"> - """).toString(); - - String expectedTestType = """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type description. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - testTypeValue1: str = Field(..., description='Test string') - \""" - Test string - \""" - testTypeValue2: Optional[str] = Field(None, description='Test optional string') - \""" - Test optional string - \""" - testTypeValue3: list[str] = Field(..., description='Test string list', min_length=1) - \""" - Test string list - \""" - testTypeValue4: Annotated[com_rosetta_test_model_TestType2, com_rosetta_test_model_TestType2.serializer(), com_rosetta_test_model_TestType2.validator()] = Field(..., description='Test TestType2') - \""" - Test TestType2 - \""" - testEnum: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Optional test enum') - \""" - Optional test enum - \""" - """; - String expectedTestType2 = """ - class com_rosetta_test_model_TestType2(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.TestType2' - testType2Value1: list[Decimal] = Field(..., description='Test number list', min_length=1) - \""" - Test number list - \""" - testType2Value2: Optional[datetime.date] = Field(None, description='Test date') - \""" - Test date - \""" - testEnum: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Optional test enum') - \""" - Optional test enum - \""" - """; - - String expectedTestEnum = """ - class TestEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Test enum description. - \""" - TEST_ENUM_VALUE_1 = "TestEnumValue1" - \""" - Test enum value 1 - \""" - TEST_ENUM_VALUE_2 = "TestEnumValue2" - \""" - Test enum value 2 - \""" - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestEnum); - } - - @Test - public void testGenerateTypesExtends1() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType extends TestType2: - TestTypeValue1 string (1..1) <"Test string"> - TestTypeValue2 int (0..1) <"Test int"> - - type TestType2 extends TestType3: - TestType2Value1 number (0..1) <"Test number"> - TestType2Value2 date (1..*) <"Test date"> - - type TestType3: - TestType3Value1 string (0..1) <"Test string"> - TestType4Value2 int (1..*) <"Test int"> - """).toString(); - - String expectedTestType = """ - class com_rosetta_test_model_TestType(com_rosetta_test_model_TestType2): - _FQRTN = 'com.rosetta.test.model.TestType' - TestTypeValue1: str = Field(..., description='Test string') - \""" - Test string - \""" - TestTypeValue2: Optional[int] = Field(None, description='Test int') - \""" - Test int - \""" - """; - String expectedTestType2 = """ - class com_rosetta_test_model_TestType2(com_rosetta_test_model_TestType3): - _FQRTN = 'com.rosetta.test.model.TestType2' - TestType2Value1: Optional[Decimal] = Field(None, description='Test number') - \""" - Test number - \""" - TestType2Value2: list[datetime.date] = Field(..., description='Test date', min_length=1) - \""" - Test date - \""" - """; - String expectedTestType3 = """ - class com_rosetta_test_model_TestType3(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.TestType3' - TestType3Value1: Optional[str] = Field(None, description='Test string') - \""" - Test string - \""" - TestType4Value2: list[int] = Field(..., description='Test int', min_length=1) - \""" - Test int - \""" - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); - } - - @Test - public void testGenerateTypesChoiceCondition() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType: <"Test type with one-of condition."> - field1 string (0..1) <"Test string field 1"> - field2 string (0..1) <"Test string field 2"> - field3 number (0..1) <"Test number field 3"> - field4 number (1..*) <"Test number field 4"> - condition BusinessCentersChoice: <"Choice rule to represent an FpML choice construct."> - required choice field1, field2 - """).toString(); - - String expected = """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type with one-of condition. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[str] = Field(None, description='Test string field 2') - \""" - Test string field 2 - \""" - field3: Optional[Decimal] = Field(None, description='Test number field 3') - \""" - Test number field 3 - \""" - field4: list[Decimal] = Field(..., description='Test number field 4', min_length=1) - \""" - Test number field 4 - \""" - - @rune_condition - def condition_0_BusinessCentersChoice(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'field1', 'field2', necessity=True) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - } - - @Test - public void testGenerateIfThenCondition() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType: <"Test type with one-of condition."> - field1 string (0..1) <"Test string field 1"> - field2 string (0..1) <"Test string field 2"> - field3 number (0..1) <"Test number field 3"> - field4 number (1..*) <"Test number field 4"> - condition BusinessCentersChoice: <"Choice rule to represent an FpML choice construct."> - if field1 exists - then field3 > 0 - """).toString(); - - String expected = """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type with one-of condition. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[str] = Field(None, description='Test string field 2') - \""" - Test string field 2 - \""" - field3: Optional[Decimal] = Field(None, description='Test number field 3') - \""" - Test number field 3 - \""" - field4: list[Decimal] = Field(..., description='Test number field 4', min_length=1) - \""" - Test number field 4 - \""" - - @rune_condition - def condition_0_BusinessCentersChoice(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field3"), ">", 0) - - def _else_fn0(): - return True - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - } - - @Test - public void testGenerateIfThenElseCondition() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType: <"Test type with one-of condition."> - field1 string (0..1) <"Test string field 1"> - field2 string (0..1) <"Test string field 2"> - field3 number (0..1) <"Test number field 3"> - field4 number (1..*) <"Test number field 4"> - condition BusinessCentersChoice: <"Choice rule to represent an FpML choice construct."> - if field1 exists - then field3 > 0 - else field4 > 0 - """).toString(); - - String expected = """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type with one-of condition. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[str] = Field(None, description='Test string field 2') - \""" - Test string field 2 - \""" - field3: Optional[Decimal] = Field(None, description='Test number field 3') - \""" - Test number field 3 - \""" - field4: list[Decimal] = Field(..., description='Test number field 4', min_length=1) - \""" - Test number field 4 - \""" - - @rune_condition - def condition_0_BusinessCentersChoice(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field3"), ">", 0) - - def _else_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field4"), ">", 0) - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expected); - } - - @Test - public void testConditionLessOrEqual() { - String pythonString = testUtils.generatePythonFromString( - """ - type DateRange: <"A class defining a contiguous series of calendar dates. The date range is defined as all the dates between and including the start and the end date. The start date must fall on or before the end date."> - - startDate date (1..1) <"The first date of a date range."> - endDate date (1..1) <"The last date of a date range."> - - condition DatesOrdered: <"The start date must fall on or before the end date (a date range of only one date is allowed)."> - startDate <= endDate - """) - .toString(); - - String expectedCondition = """ - class com_rosetta_test_model_DateRange(BaseDataClass): - \""" - A class defining a contiguous series of calendar dates. The date range is defined as all the dates between and including the start and the end date. The start date must fall on or before the end date. - \""" - _FQRTN = 'com.rosetta.test.model.DateRange' - startDate: datetime.date = Field(..., description='The first date of a date range.') - \""" - The first date of a date range. - \""" - endDate: datetime.date = Field(..., description='The last date of a date range.') - \""" - The last date of a date range. - \""" - - @rune_condition - def condition_0_DatesOrdered(self): - \""" - The start date must fall on or before the end date (a date range of only one date is allowed). - \""" - item = self - return rune_all_elements(rune_resolve_attr(self, "startDate"), "<=", rune_resolve_attr(self, "endDate")) - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedCondition); - } - - @Test - public void testConditionsGeneration1() { - String pythonString = testUtils.generatePythonFromString( - """ - type A: - a0 int (0..1) - a1 int (0..1) - condition: one-of - type B: - intValue1 int (0..1) - intValue2 int (0..1) - aValue A (1..1) - condition Rule: - intValue1 < 100 - condition OneOrTwo: <"Choice rule to represent an FpML choice construct."> - optional choice intValue1, intValue2 - condition ReqOneOrTwo: <"Choice rule to represent an FpML choice construct."> - required choice intValue1, intValue2 - condition SecondOneOrTwo: <"FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]."> - aValue->a0 exists - or (intValue2 exists and intValue1 exists and intValue1 exists) - or (intValue2 exists and intValue1 exists and intValue1 is absent) - """) - .toString(); - - String expectedA = """ - class com_rosetta_test_model_A(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.A' - a0: Optional[int] = Field(None, description='') - a1: Optional[int] = Field(None, description='') - - @rune_condition - def condition_0_(self): - item = self - return rune_check_one_of(self, 'a0', 'a1', necessity=True) - """; - - String expectedB = """ - class com_rosetta_test_model_B(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.B' - intValue1: Optional[int] = Field(None, description='') - intValue2: Optional[int] = Field(None, description='') - aValue: Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()] = Field(..., description='') - - @rune_condition - def condition_0_Rule(self): - item = self - return rune_all_elements(rune_resolve_attr(self, "intValue1"), "<", 100) - - @rune_condition - def condition_1_OneOrTwo(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=False) - - @rune_condition - def condition_2_ReqOneOrTwo(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=True) - - @rune_condition - def condition_3_SecondOneOrTwo(self): - \""" - FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]. - \""" - item = self - return ((rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "a0")) or ((rune_attr_exists(rune_resolve_attr(self, "intValue2")) and rune_attr_exists(rune_resolve_attr(self, "intValue1"))) and rune_attr_exists(rune_resolve_attr(self, "intValue1")))) or ((rune_attr_exists(rune_resolve_attr(self, "intValue2")) and rune_attr_exists(rune_resolve_attr(self, "intValue1"))) and (not rune_attr_exists(rune_resolve_attr(self, "intValue1"))))) - """; - - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedA); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedB); - } - - @Test - public void testGenerateTypesMethod2() { - String pythonString = testUtils.generatePythonFromString( - """ - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - currency string (0..1) <"Defines the currency to be used as a unit for a price, quantity, or other purpose."> - - type MeasureBase: <"Provides an abstract base class shared by Price and Quantity."> - amount number (1..1) <"Specifies an amount to be qualified and used in a Price or Quantity definition."> - unitOfAmount UnitType (1..1) <"Qualifies the unit by which the amount is measured."> - - type Quantity extends MeasureBase: <"Specifies a quantity to be associated to a financial product, for example a trade amount or a cashflow amount resulting from a trade."> - multiplier number (0..1) <"Defines the number to be multiplied by the amount to derive a total quantity."> - multiplierUnit UnitType (0..1) <"Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons)."> - """) - .toString(); - - String expectedMeasureBase = """ - class com_rosetta_test_model_MeasureBase(BaseDataClass): - \""" - Provides an abstract base class shared by Price and Quantity. - \""" - _FQRTN = 'com.rosetta.test.model.MeasureBase' - amount: Decimal = Field(..., description='Specifies an amount to be qualified and used in a Price or Quantity definition.') - \""" - Specifies an amount to be qualified and used in a Price or Quantity definition. - \""" - unitOfAmount: Annotated[com_rosetta_test_model_UnitType, com_rosetta_test_model_UnitType.serializer(), com_rosetta_test_model_UnitType.validator()] = Field(..., description='Qualifies the unit by which the amount is measured.') - \""" - Qualifies the unit by which the amount is measured. - \""" - """; - String expectedUnitType = """ - class com_rosetta_test_model_UnitType(BaseDataClass): - \""" - Defines the unit to be used for price, quantity, or other purposes - \""" - _FQRTN = 'com.rosetta.test.model.UnitType' - currency: Optional[str] = Field(None, description='Defines the currency to be used as a unit for a price, quantity, or other purpose.') - \""" - Defines the currency to be used as a unit for a price, quantity, or other purpose. - \""" - """; - String expectedQuantity = """ - class com_rosetta_test_model_Quantity(com_rosetta_test_model_MeasureBase): - \""" - Specifies a quantity to be associated to a financial product, for example a trade amount or a cashflow amount resulting from a trade. - \""" - _FQRTN = 'com.rosetta.test.model.Quantity' - multiplier: Optional[Decimal] = Field(None, description='Defines the number to be multiplied by the amount to derive a total quantity.') - \""" - Defines the number to be multiplied by the amount to derive a total quantity. - \""" - multiplierUnit: Optional[Annotated[com_rosetta_test_model_UnitType, com_rosetta_test_model_UnitType.serializer(), com_rosetta_test_model_UnitType.validator()]] = Field(None, description='Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons).') - \""" - Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons). - \""" - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedMeasureBase); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedUnitType); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedQuantity); - } - - @Disabled("testGenerateTypes3") - @Test - public void testGenerateTypes3() { - String pythonString = testUtils.generatePythonFromString( - """ - enum AncillaryRoleEnum: <"Defines the enumerated values to specify the ancillary roles to the transaction. The product is agnostic to the actual parties involved in the transaction, with the party references abstracted away from the product definition and replaced by the AncillaryRoleEnum. The AncillaryRoleEnum can then be positioned in the product and the AncillaryParty type, which is positioned outside of the product definition, allows the AncillaryRoleEnum to be associated with an actual party reference."> - DisruptionEventsDeterminingParty <"Specifies the party which determines additional disruption events."> - ExtraordinaryDividendsParty <"Specifies the party which determines if dividends are extraordinary in relation to normal levels."> - - enum TelephoneTypeEnum: <"The enumerated values to specify the type of telephone number, e.g. work vs. mobile."> - Work <"A number used primarily for work-related calls. Includes home office numbers used primarily for work purposes."> - Mobile <"A number on a mobile telephone that is often or usually used for work-related calls. This type of number can be used for urgent work related business when a work number is not sufficient to contact the person or firm."> - - type LegalEntity: <"A class to specify a legal entity, with a required name and an optional entity identifier (such as the LEI)."> - [metadata key] - entityId string (0..*) <"A legal entity identifier (e.g. RED entity code)."> - [metadata scheme] - name string (1..1) <"The legal entity name."> - [metadata scheme] - - type TelephoneNumber: <"A class to specify a telephone number as a type of phone number (e.g. work, personal, ...) alongside with the actual number."> - _FQRTN = 'com.rosetta.test.model.TelephoneNumber' - telephoneNumberType TelephoneTypeEnum (0..1) <"The type of telephone number, e.g. work, mobile."> - number string (1..1) <"The actual telephone number."> - - type AncillaryEntity: <"Holds an identifier for an ancillary entity, either identified directly via its ancillary role or directly as a legal entity."> - _FQRTN = 'com.rosetta.test.model.AncillaryEntity' - ancillaryParty AncillaryRoleEnum (0..1) <"Identifies a party via its ancillary role on a transaction (e.g. CCP or DCO through which the trade test be cleared.)"> - legalEntity LegalEntity (0..1) - - condition: one-of - """) - .toString(); - - String expectedTestType1 = """ - class com_rosetta_test_model_LegalEntity(BaseDataClass): - \""" - A class to specify a legal entity, with a required name and an optional entity identifier (such as the LEI). - \""" - _FQRTN = 'com.rosetta.test.model.LegalEntity' - entityId: list[AttributeWithMeta[str] | str] = Field([], description='A legal entity identifier (e.g. RED entity code).') - \""" - A legal entity identifier (e.g. RED entity code). - \""" - name: AttributeWithMeta[str] | str = Field(..., description='The legal entity name.') - \""" - The legal entity name. - \""" - """; - String expectedTestType2 = """ - class com_rosetta_test_model_TelephoneNumber(BaseDataClass): - \""" - A class to specify a telephone number as a type of phone number (e.g. work, personal, ...) alongside with the actual number. - \""" - _FQRTN = 'com.rosetta.test.model.TelephoneNumber' - telephoneNumberType: Optional[com.rosetta.test.model.TelephoneTypeEnum.TelephoneTypeEnum] = Field(None, description='The type of telephone number, e.g. work, mobile.') - \""" - The type of telephone number, e.g. work, mobile. - \""" - number: str = Field(..., description='The actual telephone number.') - \""" - The actual telephone number. - \""" - """; - String expectedTestType3 = """ - class com_rosetta_test_model_AncillaryEntity(BaseDataClass): - \""" - Holds an identifier for an ancillary entity, either identified directly via its ancillary role or directly as a legal entity. - \""" - _FQRTN = 'com.rosetta.test.model.AncillaryEntity' - ancillaryParty: Optional[com.rosetta.test.model.AncillaryRoleEnum.AncillaryRoleEnum] = Field(None, description='Identifies a party via its ancillary role on a transaction (e.g. CCP or DCO through which the trade test be cleared.)') - \""" - Identifies a party via its ancillary role on a transaction (e.g. CCP or DCO through which the trade test be cleared.) - \""" - legalEntity: Optional[com_rosetta_test_model_LegalEntity] = Field(None, description='') - - @rune_condition - def condition_0_(self): - item = self - return rune_check_one_of(self, 'ancillaryParty', 'legalEntity', necessity=True) - """; - - String expectedTestType4 = """ - class AncillaryRoleEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Defines the enumerated values to specify the ancillary roles to the transaction. The product is agnostic to the actual parties involved in the transaction, with the party references abstracted away from the product definition and replaced by the AncillaryRoleEnum. The AncillaryRoleEnum can then be positioned in the product and the AncillaryParty type, which is positioned outside of the product definition, allows the AncillaryRoleEnum to be associated with an actual party reference. - \""" - DISRUPTION_EVENTS_DETERMINING_PARTY = "DisruptionEventsDeterminingParty" - \""" - Specifies the party which determines additional disruption events. - \""" - EXTRAORDINARY_DIVIDENDS_PARTY = "ExtraordinaryDividendsParty" - \""" - Specifies the party which determines if dividends are extraordinary in relation to normal levels. - \""" - """; - String expectedTestType5 = """ - class TelephoneTypeEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - The enumerated values to specify the type of telephone number, e.g. work vs. mobile. - \""" - MOBILE = "Mobile" - \""" - A number on a mobile telephone that is often or usually used for work-related calls. This type of number can be used for urgent work related business when a work number is not sufficient to contact the person or firm. - \""" - WORK = "Work" - \""" - A number used primarily for work-related calls. Includes home office numbers used primarily for work purposes. - \""" - """; - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType1); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType4); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType5); - } - - @Disabled("testGenerateTypesExtends2") - @Test - public void testGenerateTypesExtends2() { - String pythonString = testUtils.generatePythonFromString( - """ - enum CapacityUnitEnum: <"Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities."> - ALW <"Denotes Allowances as standard unit."> - BBL <"Denotes a Barrel as a standard unit."> - BCF <"Denotes Billion Cubic Feet as a standard unit."> - - enum WeatherUnitEnum: <"Provides enumerated values for weather units, generally used in the context of defining quantities for commodities."> - CDD <"Denotes Cooling Degree Days as a standard unit."> - CPD <"Denotes Critical Precipitation Day as a standard unit."> - HDD <"Heating Degree Day as a standard unit."> - - enum FinancialUnitEnum: <"Provides enumerated values for financial units, generally used in the context of defining quantities for securities."> - Contract <"Denotes financial contracts, such as listed futures and options."> - ContractualProduct <"Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount."> - IndexUnit <"Denotes a price expressed in index points, e.g. for a stock index."> - - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - capacityUnit CapacityUnitEnum (0..1) <"Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities."> - weatherUnit WeatherUnitEnum (0..1) <"Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities."> - financialUnit FinancialUnitEnum (0..1) <"Provides an enumerated value for financial units, generally used in the context of defining quantities for securities."> - currency string (0..1) <"Defines the currency to be used as a unit for a price, quantity, or other purpose."> - [metadata scheme] - - condition UnitType: <"Requires that a unit type must be set."> - one-of - - type Measure extends MeasureBase: <"Defines a concrete measure as a number associated to a unit. It extends MeasureBase by requiring the value attribute to be present. A measure may be unit-less so the unit attribute is still optional."> - - condition ValueExists: <"The value attribute must be present in a concrete measure."> - value exists - - type MeasureBase: <"Provides an abstract type to define a measure as a number associated to a unit. This type is abstract because all its attributes are optional. The types that extend it can specify further existence constraints."> - - value number (0..1) <"Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted."> - unit UnitType (0..1) <"Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit)."> - """) - .toString(); - - String expectedTestType1 = """ - class com_rosetta_test_model_MeasureBase(BaseDataClass): - \""" - Provides an abstract type to define a measure as a number associated to a unit. This type is abstract because all its attributes are optional. The types that extend it can specify further existence constraints. - \""" - _FQRTN = 'com.rosetta.test.model.MeasureBase' - - value: Optional[Decimal] = Field(None, description='Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted.') - \""" - Specifies the value of the measure as a number. Optional because in a measure vector or schedule, this single value may be omitted. - \""" - unit: Optional[com_rosetta_test_model_UnitType] = Field(None, description='Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit).') - \""" - Qualifies the unit by which the amount is measured. Optional because a measure may be unit-less (e.g. when representing a ratio between amounts in the same unit). - \""" - """; - - String expectedTestType2 = """ - class com_rosetta_test_model_Measure(com_rosetta_test_model_MeasureBase): - \""" - Defines a concrete measure as a number associated to a unit. It extends MeasureBase by requiring the value attribute to be present. A measure may be unit-less so the unit attribute is still optional. - \""" - _FQRTN = 'com.rosetta.test.model.Measure' - - @rune_condition - def condition_0_ValueExists(self): - \""" - The value attribute must be present in a concrete measure. - \""" - item = self - return rune_attr_exists(rune_resolve_attr(self, "value")) - """; - - String expectedTestType3 = """ - class WeatherUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Provides enumerated values for weather units, generally used in the context of defining quantities for commodities. - \""" - CDD = "CDD" - \""" - Denotes Cooling Degree Days as a standard unit. - \""" - CPD = "CPD" - \""" - Denotes Critical Precipitation Day as a standard unit. - \""" - HDD = "HDD" - \""" - Heating Degree Day as a standard unit. - \""" - """; - - String expectedTestType4 = """ - class FinancialUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Provides enumerated values for financial units, generally used in the context of defining quantities for securities. - \""" - CONTRACT = "Contract" - \""" - Denotes financial contracts, such as listed futures and options. - \""" - CONTRACTUAL_PRODUCT = "ContractualProduct" - \""" - Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount. - \""" - INDEX_UNIT = "IndexUnit" - \""" - Denotes a price expressed in index points, e.g. for a stock index. - \""" - """; - String expectedTestType5 = """ - class UnitType(BaseDataClass): - \""" - Defines the unit to be used for price, quantity, or other purposes - \""" - _FQRTN = 'com.rosetta.test.model.UnitType' - - capacityUnit: Optional[com.rosetta.test.model.CapacityUnitEnum.CapacityUnitEnum] = Field(None, description='Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities.') - \""" - Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities. - \""" - weatherUnit: Optional[com.rosetta.test.model.WeatherUnitEnum.WeatherUnitEnum] = Field(None, description='Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities.') - \""" - Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities. - \""" - financialUnit: Optional[com.rosetta.test.model.FinancialUnitEnum.FinancialUnitEnum] = Field(None, description='Provides an enumerated value for financial units, generally used in the context of defining quantities for securities.') - \""" - Provides an enumerated value for financial units, generally used in the context of defining quantities for securities. - \""" - currency: Optional[AttributeWithMeta[str] | str] = Field(None, description='Defines the currency to be used as a unit for a price, quantity, or other purpose.') - \""" - Defines the currency to be used as a unit for a price, quantity, or other purpose. - \""" - - @rune_condition - def condition_0_UnitType(self): - \""" - Requires that a unit type must be set. - \""" - item = self - return rune_check_one_of(self, 'capacityUnit', 'weatherUnit', 'financialUnit', 'currency', necessity=True) - """; - - String expectedTestType6 = """ - class CapacityUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities. - \""" - ALW = "ALW" - \""" - Denotes Allowances as standard unit. - \""" - BBL = "BBL" - \""" - Denotes a Barrel as a standard unit. - \""" - BCF = "BCF" - \""" - Denotes Billion Cubic Feet as a standard unit. - \""" - """; - - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType1); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType4); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType5); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType6); - } - - // TODO: tests disabled to align to new meta data support - add them back - @Disabled("testGenerateTypes2") - @Test - public void testGenerateTypes2() { - String pythonString = testUtils.generatePythonFromString( - """ - enum CapacityUnitEnum: <"Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities."> - ALW <"Denotes Allowances as standard unit."> - BBL <"Denotes a Barrel as a standard unit."> - - enum WeatherUnitEnum: <"Provides enumerated values for weather units, generally used in the context of defining quantities for commodities."> - CDD <"Denotes Cooling Degree Days as a standard unit."> - CPD <"Denotes Critical Precipitation Day as a standard unit."> - - enum FinancialUnitEnum: <"Provides enumerated values for financial units, generally used in the context of defining quantities for securities."> - Contract <"Denotes financial contracts, such as listed futures and options."> - ContractualProduct <"Denotes a Contractual Product as defined in the CDM. This unit type would be used when the price applies to the whole product, for example, in the case of a premium expressed as a cash amount."> - - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - capacityUnit CapacityUnitEnum (0..1) <"Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities."> - weatherUnit WeatherUnitEnum (0..1) <"Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities."> - [metadata scheme] - - condition UnitType: <"Requires that a unit type must be set."> - one-of - """) - .toString(); - - String expectedTestType = """ - class class com_rosetta_test_model_UnitType(BaseDataClass): - \""" - Defines the unit to be used for price, quantity, or other purposes - \""" - _FQRTN = 'com.rosetta.test.model.UnitType' - - capacityUnit: Optional[com.rosetta.test.model.CapacityUnitEnum.CapacityUnitEnum] = Field(None, description='Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities.') - \""" - Provides an enumerated value for a capacity unit, generally used in the context of defining quantities for commodities. - \""" - weatherUnit: Optional[com.rosetta.test.model.WeatherUnitEnum.WeatherUnitEnum] = Field(None, description='Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities.') - \""" - Provides an enumerated values for a weather unit, generally used in the context of defining quantities for commodities. - \""" - """; - String expectedTestType2 = """ - class FinancialUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Provides enumerated values for financial units, generally used in the context of defining quantities for securities. - \""" - CONTRACT = "Contract" - \""" - Denotes financial contracts, such as listed futures and options. - \""" - CONTRACTUAL_PRODUCT = "ContractualProduct" - \""" - - @rune_condition - def condition_0_UnitType(self): - \""" - Requires that a unit type must be set. - \""" - item = self - return rune_check_one_of(self, 'capacityUnit', 'weatherUnit', 'financialUnit', 'currency', necessity=True) - """; - String expectedTestType3 = """ - class WeatherUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Provides enumerated values for weather units, generally used in the context of defining quantities for commodities. - \""" - CDD = "CDD" - \""" - Denotes Cooling Degree Days as a standard unit. - \""" - CPD = "CPD" - \""" - """; - String expectedTestType4 = """ - class CapacityUnitEnum(rune.runtime.metadata.EnumWithMetaMixin, Enum): - \""" - Provides enumerated values for capacity units, generally used in the context of defining quantities for commodities. - \""" - ALW = "ALW" - \""" - Denotes Allowances as standard unit. - \""" - BBL = "BBL" - \""" - Denotes a Barrel as a standard unit. - \""" - """; - - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType2); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType3); - testUtils.assertGeneratedContainsExpectedString(pythonString, expectedTestType4); - } -} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectGeneratorTest.java deleted file mode 100644 index 35caefc..0000000 --- a/src/test/java/com/regnosys/rosetta/generator/python/object/PythonObjectGeneratorTest.java +++ /dev/null @@ -1,689 +0,0 @@ -package com.regnosys.rosetta.generator.python.object; - -import jakarta.inject.Inject; -import com.regnosys.rosetta.tests.RosettaInjectorProvider; -import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; -import org.eclipse.xtext.testing.InjectWith; -import org.eclipse.xtext.testing.extensions.InjectionExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith(InjectionExtension.class) -@InjectWith(RosettaInjectorProvider.class) -public class PythonObjectGeneratorTest { - - @Inject - private PythonGeneratorTestUtils testUtils; - - @Test - public void testGenerateBasicTypeString() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one string (0..1) - list string (1..*) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[str] = Field(None, description='') - list: list[str] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testGenerateBasicTypeInt() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one int (0..1) - list int (1..*) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[int] = Field(None, description='') - list: list[int] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testGenerateBasicTypeNumber() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one number (0..1) - list number (1..*) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[Decimal] = Field(None, description='') - list: list[Decimal] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testGenerateBasicTypeBoolean() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one boolean (0..1) - list boolean (1..*) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[bool] = Field(None, description='') - list: list[bool] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testGenerateBasicTypeDate() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one date (0..1) - list date (1..*) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[datetime.date] = Field(None, description='') - list: list[datetime.date] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testGenerateBasicTypeDateTime() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one date (0..1) - list date (1..*) - zoned zonedDateTime (0..1) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[datetime.date] = Field(None, description='') - list: list[datetime.date] = Field(..., description='', min_length=1) - zoned: Optional[datetime.datetime] = Field(None, description='')"""); - } - - @Test - public void testGenerateBasicTypeTime() { - testUtils.assertBundleContainsExpectedString( - """ - type Tester: - one time (0..1) - list time (1..*) - """, - """ - class com_rosetta_test_model_Tester(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Tester' - one: Optional[datetime.time] = Field(None, description='') - list: list[datetime.time] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testOmitGlobalKeyAnnotationWhenNotDefined() { - testUtils.assertBundleContainsExpectedString( - """ - type AttributeGlobalKeyTest: - withoutGlobalKey string (1..1) - """, - """ - class com_rosetta_test_model_AttributeGlobalKeyTest(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.AttributeGlobalKeyTest' - withoutGlobalKey: str = Field(..., description='')"""); - } - - @Test - public void testGenerateClasslist() { - String pythonString = testUtils.generatePythonFromString( - """ - type A extends B: - c C (1..*) - - type B: - - type C : - one int (0..1) - list int (1..*) - - type D: - s string (1..*) - """).toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_A(com_rosetta_test_model_B): - _FQRTN = 'com.rosetta.test.model.A' - c: list[Annotated[com_rosetta_test_model_C, com_rosetta_test_model_C.serializer(), com_rosetta_test_model_C.validator()]] = Field(..., description='', min_length=1) - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_B(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.B' - pass - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_C(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.C' - one: Optional[int] = Field(None, description='') - list: list[int] = Field(..., description='', min_length=1)"""); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_D(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.D' - s: list[str] = Field(..., description='', min_length=1)"""); - } - - @Test - public void testExtendATypeWithSameAttribute() { - testUtils.assertBundleContainsExpectedString( - """ - type Foo: - a string (0..1) - b string (0..1) - - type Bar extends Foo: - a string (0..1) - """, - """ - class com_rosetta_test_model_Foo(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Foo' - a: Optional[str] = Field(None, description='') - b: Optional[str] = Field(None, description='')"""); - testUtils.assertBundleContainsExpectedString( - """ - type Foo: - a string (0..1) - b string (0..1) - - type Bar extends Foo: - a string (0..1) - """, - """ - class com_rosetta_test_model_Bar(com_rosetta_test_model_Foo): - _FQRTN = 'com.rosetta.test.model.Bar' - a: Optional[str] = Field(None, description='')"""); - } - - @Test - public void testGenerateRosettaCalculationTypeAsString() { - testUtils.assertBundleContainsExpectedString( - """ - type Foo: - bar calculation (0..1) - """, - """ - class com_rosetta_test_model_Foo(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Foo' - bar: Optional[str] = Field(None, description='')"""); - } - - @Test - public void testSetAttributesOnEmptyClassWithInheritance() { - testUtils.assertBundleContainsExpectedString( - """ - type Foo: - attr string (0..1) - - type Bar extends Foo: - """, - """ - class com_rosetta_test_model_Foo(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.Foo' - attr: Optional[str] = Field(None, description='')"""); - testUtils.assertBundleContainsExpectedString( - """ - type Foo: - attr string (0..1) - - type Bar extends Foo: - """, - """ - class com_rosetta_test_model_Bar(com_rosetta_test_model_Foo): - _FQRTN = 'com.rosetta.test.model.Bar' - pass"""); - } - - @Test - public void testConditions1() { - String pythonString = testUtils.generatePythonFromString( - """ - type A: - a0 int (0..1) - a1 int (0..1) - condition: one-of - - type B: - intValue1 int (0..1) - intValue2 int (0..1) - aValue A (1..1) - - condition Rule: - intValue1 < 100 - - condition OneOrTwo: <"Choice rule to represent an FpML choice construct."> - optional choice intValue1, intValue2 - - condition SecondOneOrTwo: <"FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]."> - aValue->a0 exists - or (intValue2 exists and intValue1 exists and intValue1 exists) - or (intValue2 exists and intValue1 exists and intValue1 is absent) - """) - .toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_A(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.A' - a0: Optional[int] = Field(None, description='') - a1: Optional[int] = Field(None, description='') - - @rune_condition - def condition_0_(self): - item = self - return rune_check_one_of(self, 'a0', 'a1', necessity=True)"""); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_B(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.B' - intValue1: Optional[int] = Field(None, description='') - intValue2: Optional[int] = Field(None, description='') - aValue: Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()] = Field(..., description='') - - @rune_condition - def condition_0_Rule(self): - item = self - return rune_all_elements(rune_resolve_attr(self, "intValue1"), "<", 100) - - @rune_condition - def condition_1_OneOrTwo(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=False) - - @rune_condition - def condition_2_SecondOneOrTwo(self): - \""" - FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]. - \""" - item = self - return """ - + " ((rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, \"aValue\"), \"a0\")) or ((rune_attr_exists(rune_resolve_attr(self, \"intValue2\")) and rune_attr_exists(rune_resolve_attr(self, \"intValue1\"))) and rune_attr_exists(rune_resolve_attr(self, \"intValue1\")))) or ((rune_attr_exists(rune_resolve_attr(self, \"intValue2\")) and rune_attr_exists(rune_resolve_attr(self, \"intValue1\"))) and (not rune_attr_exists(rune_resolve_attr(self, \"intValue1\")))))"); - } - - @Test - public void testGenerateTypes() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType: <"Test type description."> - testTypeValue1 string (1..1) <"Test string"> - testTypeValue2 string (0..1) <"Test optional string"> - testTypeValue3 string (1..*) <"Test string list"> - testTypeValue4 TestType2 (1..1) <"Test TestType2"> - testEnum TestEnum (0..1) <"Optional test enum"> - - type TestType2: - testType2Value1 number(1..*) <"Test number list"> - testType2Value2 date(0..1) <"Test date"> - testEnum TestEnum (0..1) <"Optional test enum"> - - enum TestEnum: <"Test enum description."> - TestEnumValue1 <"Test enum value 1"> - TestEnumValue2 <"Test enum value 2"> - """).toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type description. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - testTypeValue1: str = Field(..., description='Test string') - \""" - Test string - \""" - testTypeValue2: Optional[str] = Field(None, description='Test optional string') - \""" - Test optional string - \""" - testTypeValue3: list[str] = Field(..., description='Test string list', min_length=1) - \""" - Test string list - \""" - testTypeValue4: Annotated[com_rosetta_test_model_TestType2, com_rosetta_test_model_TestType2.serializer(), com_rosetta_test_model_TestType2.validator()] = Field(..., description='Test TestType2') - \""" - Test TestType2 - \""" - testEnum: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Optional test enum') - \""" - Optional test enum - \""" - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_TestType2(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.TestType2' - testType2Value1: list[Decimal] = Field(..., description='Test number list', min_length=1) - \""" - Test number list - \""" - testType2Value2: Optional[datetime.date] = Field(None, description='Test date') - \""" - Test date - \""" - testEnum: Optional[com.rosetta.test.model.TestEnum.TestEnum] = Field(None, description='Optional test enum') - \""" - Optional test enum - \""" - """); - } - - @Test - public void testGenerateTypesMethod2() { - String pythonString = testUtils.generatePythonFromString( - """ - type UnitType: <"Defines the unit to be used for price, quantity, or other purposes"> - currency string (0..1) <"Defines the currency to be used as a unit for a price, quantity, or other purpose."> - - type MeasureBase: <"Provides an abstract base class shared by Price and Quantity."> - amount number (1..1) <"Specifies an amount to be qualified and used in a Price or Quantity definition."> - unitOfAmount UnitType (1..1) <"Qualifies the unit by which the amount is measured."> - - type Quantity extends MeasureBase: <"Specifies a quantity to be associated to a financial product, for example a trade amount or a cashflow amount resulting from a trade."> - multiplier number (0..1) <"Defines the number to be multiplied by the amount to derive a total quantity."> - multiplierUnit UnitType (0..1) <"Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons)."> - """) - .toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_MeasureBase(BaseDataClass): - \""" - Provides an abstract base class shared by Price and Quantity. - \""" - _FQRTN = 'com.rosetta.test.model.MeasureBase' - amount: Decimal = Field(..., description='Specifies an amount to be qualified and used in a Price or Quantity definition.') - \""" - Specifies an amount to be qualified and used in a Price or Quantity definition. - \""" - unitOfAmount: Annotated[com_rosetta_test_model_UnitType, com_rosetta_test_model_UnitType.serializer(), com_rosetta_test_model_UnitType.validator()] = Field(..., description='Qualifies the unit by which the amount is measured.') - \""" - Qualifies the unit by which the amount is measured. - \""" - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_UnitType(BaseDataClass): - \""" - Defines the unit to be used for price, quantity, or other purposes - \""" - _FQRTN = 'com.rosetta.test.model.UnitType' - currency: Optional[str] = Field(None, description='Defines the currency to be used as a unit for a price, quantity, or other purpose.') - \""" - Defines the currency to be used as a unit for a price, quantity, or other purpose. - \""" - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_Quantity(com_rosetta_test_model_MeasureBase): - \""" - Specifies a quantity to be associated to a financial product, for example a trade amount or a cashflow amount resulting from a trade. - \""" - _FQRTN = 'com.rosetta.test.model.Quantity' - multiplier: Optional[Decimal] = Field(None, description='Defines the number to be multiplied by the amount to derive a total quantity.') - \""" - Defines the number to be multiplied by the amount to derive a total quantity. - \""" - multiplierUnit: Optional[Annotated[com_rosetta_test_model_UnitType, com_rosetta_test_model_UnitType.serializer(), com_rosetta_test_model_UnitType.validator()]] = Field(None, description='Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons).') - \""" - Qualifies the multiplier with the applicable unit. For example in the case of the Coal (API2) CIF ARA (ARGUS-McCloskey) Futures Contract on the CME, where the unitOfAmount would be contracts, the multiplier would 1,000 and the mulitiplier Unit would be 1,000 MT (Metric Tons). - \""" - """); - } - - @Test - public void testGenerateTypesExtends() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType extends TestType2: - TestTypeValue1 string (1..1) <"Test string"> - TestTypeValue2 int (0..1) <"Test int"> - - type TestType2 extends TestType3: - TestType2Value1 number (0..1) <"Test number"> - TestType2Value2 date (1..*) <"Test date"> - - type TestType3: - TestType3Value1 string (0..1) <"Test string"> - TestType4Value2 int (1..*) <"Test int"> - """).toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_TestType(com_rosetta_test_model_TestType2): - _FQRTN = 'com.rosetta.test.model.TestType' - TestTypeValue1: str = Field(..., description='Test string') - \""" - Test string - \""" - TestTypeValue2: Optional[int] = Field(None, description='Test int') - \""" - Test int - \""" - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_TestType2(com_rosetta_test_model_TestType3): - _FQRTN = 'com.rosetta.test.model.TestType2' - TestType2Value1: Optional[Decimal] = Field(None, description='Test number') - \""" - Test number - \""" - TestType2Value2: list[datetime.date] = Field(..., description='Test date', min_length=1) - \""" - Test date - \""" - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_TestType3(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.TestType3' - TestType3Value1: Optional[str] = Field(None, description='Test string') - \""" - Test string - \""" - TestType4Value2: list[int] = Field(..., description='Test int', min_length=1) - \""" - Test int - \""" - """); - } - - @Test - public void testGenerateTypesChoiceCondition() { - String pythonString = testUtils.generatePythonFromString( - """ - type TestType: <"Test type with one-of condition."> - field1 string (0..1) <"Test string field 1"> - field2 string (0..1) <"Test string field 2"> - field3 number (0..1) <"Test number field 3"> - field4 number (1..*) <"Test number field 4"> - condition BusinessCentersChoice: <"Choice rule to represent an FpML choice construct."> - required choice field1, field2 - """).toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type with one-of condition. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[str] = Field(None, description='Test string field 2') - \""" - Test string field 2 - \""" - field3: Optional[Decimal] = Field(None, description='Test number field 3') - \""" - Test number field 3 - \""" - field4: list[Decimal] = Field(..., description='Test number field 4', min_length=1) - \""" - Test number field 4 - \""" - - @rune_condition - def condition_0_BusinessCentersChoice(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'field1', 'field2', necessity=True)"""); - } - - @Test - public void testGenerateIfThenCondition() { - testUtils.assertBundleContainsExpectedString( - """ - type TestType: <"Test type with one-of condition."> - field1 string (0..1) <"Test string field 1"> - field2 string (0..1) <"Test string field 2"> - field3 number (0..1) <"Test number field 3"> - field4 number (1..*) <"Test number field 4"> - condition BusinessCentersChoice: <"Choice rule to represent an FpML choice construct."> - if field1 exists - then field3 > 0 - """, - """ - class com_rosetta_test_model_TestType(BaseDataClass): - \""" - Test type with one-of condition. - \""" - _FQRTN = 'com.rosetta.test.model.TestType' - field1: Optional[str] = Field(None, description='Test string field 1') - \""" - Test string field 1 - \""" - field2: Optional[str] = Field(None, description='Test string field 2') - \""" - Test string field 2 - \""" - field3: Optional[Decimal] = Field(None, description='Test number field 3') - \""" - Test number field 3 - \""" - field4: list[Decimal] = Field(..., description='Test number field 4', min_length=1) - \""" - Test number field 4 - \""" - - @rune_condition - def condition_0_BusinessCentersChoice(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - def _then_fn0(): - return rune_all_elements(rune_resolve_attr(self, "field3"), ">", 0) - - def _else_fn0(): - return True - - return if_cond_fn(rune_attr_exists(rune_resolve_attr(self, "field1")), _then_fn0, _else_fn0)"""); - } - - @Test - public void testConditionsGeneration() { - String pythonString = testUtils.generatePythonFromString( - """ - type A: - a0 int (0..1) - a1 int (0..1) - condition: one-of - type B: - intValue1 int (0..1) - intValue2 int (0..1) - aValue A (1..1) - condition Rule: - intValue1 < 100 - condition OneOrTwo: <"Choice rule to represent an FpML choice construct."> - optional choice intValue1, intValue2 - condition ReqOneOrTwo: <"Choice rule to represent an FpML choice construct."> - required choice intValue1, intValue2 - condition SecondOneOrTwo: <"FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]."> - aValue->a0 exists - or (intValue2 exists and intValue1 exists and intValue1 exists) - or (intValue2 exists and intValue1 exists and intValue1 is absent) - """) - .toString(); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_A(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.A' - a0: Optional[int] = Field(None, description='') - a1: Optional[int] = Field(None, description='') - - @rune_condition - def condition_0_(self): - item = self - return rune_check_one_of(self, 'a0', 'a1', necessity=True) - """); - testUtils.assertGeneratedContainsExpectedString( - pythonString, - """ - class com_rosetta_test_model_B(BaseDataClass): - _FQRTN = 'com.rosetta.test.model.B' - intValue1: Optional[int] = Field(None, description='') - intValue2: Optional[int] = Field(None, description='') - aValue: Annotated[com_rosetta_test_model_A, com_rosetta_test_model_A.serializer(), com_rosetta_test_model_A.validator()] = Field(..., description='') - - @rune_condition - def condition_0_Rule(self): - item = self - return rune_all_elements(rune_resolve_attr(self, "intValue1"), "<", 100) - - @rune_condition - def condition_1_OneOrTwo(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=False) - - @rune_condition - def condition_2_ReqOneOrTwo(self): - \""" - Choice rule to represent an FpML choice construct. - \""" - item = self - return rune_check_one_of(self, 'intValue1', 'intValue2', necessity=True) - - @rune_condition - def condition_3_SecondOneOrTwo(self): - \""" - FpML specifies a choice between adjustedDate and [unadjustedDate (required), dateAdjutsments (required), adjustedDate (optional)]. - \""" - item = self - return ((rune_attr_exists(rune_resolve_attr(rune_resolve_attr(self, "aValue"), "a0")) or ((rune_attr_exists(rune_resolve_attr(self, "intValue2")) and rune_attr_exists(rune_resolve_attr(self, "intValue1"))) and rune_attr_exists(rune_resolve_attr(self, "intValue1")))) or ((rune_attr_exists(rune_resolve_attr(self, "intValue2")) and rune_attr_exists(rune_resolve_attr(self, "intValue1"))) and (not rune_attr_exists(rune_resolve_attr(self, "intValue1")))))"""); - } -} diff --git a/src/test/java/com/regnosys/rosetta/generator/python/rule/PythonDataRuleGeneratorTest.java b/src/test/java/com/regnosys/rosetta/generator/python/rule/PythonDataRuleGeneratorTest.java index 5d0eff1..1d9358f 100644 --- a/src/test/java/com/regnosys/rosetta/generator/python/rule/PythonDataRuleGeneratorTest.java +++ b/src/test/java/com/regnosys/rosetta/generator/python/rule/PythonDataRuleGeneratorTest.java @@ -5,6 +5,7 @@ import com.regnosys.rosetta.generator.python.PythonGeneratorTestUtils; import org.eclipse.xtext.testing.InjectWith; import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -209,7 +210,7 @@ class com_rosetta_test_model_Quote(BaseDataClass): def condition_0_Quote_Price(self): item = self def _then_fn0(): - return rune_all_elements(rune_resolve_attr(rune_resolve_attr(self, "quotePrice"), "bidPrice"), "=", 0.0) + return rune_all_elements(rune_resolve_attr(rune_resolve_attr(self, "quotePrice"), "bidPrice"), "=", Decimal('0.0')) def _else_fn0(): return True @@ -226,6 +227,7 @@ class com_rosetta_test_model_QuotePrice(BaseDataClass): } @Test + @Disabled("Functions are being phased out in tests.") public void dataRuleWithDoIfAndFunction() { String pythonString = testUtils.generatePythonFromString( """ @@ -252,7 +254,7 @@ class com_rosetta_test_model_Quote(BaseDataClass): def condition_0_(self): item = self def _then_fn0(): - return rune_all_elements(Foo(rune_resolve_attr(self, "price")), "=", 5.0) + return rune_all_elements(com_rosetta_test_model_functions_Foo(rune_resolve_attr(self, "price")), "=", Decimal('5.0')) def _else_fn0(): return True @@ -263,6 +265,7 @@ def _else_fn0(): } @Test + @Disabled("Functions are being phased out in tests.") public void dataRuleWithDoIfAndFunctionAndElse() { String pythonString = testUtils.generatePythonFromString( """ @@ -290,7 +293,7 @@ class com_rosetta_test_model_Quote(BaseDataClass): def condition_0_(self): item = self def _then_fn0(): - return rune_all_elements(Foo(rune_resolve_attr(self, "price")), "=", 5.0) + return rune_all_elements(com_rosetta_test_model_functions_Foo(rune_resolve_attr(self, "price")), "=", Decimal('5.0')) def _else_fn0(): return True diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..e69de29 diff --git a/test/cdm_tests/cdm_setup/build_cdm.sh b/test/cdm_tests/cdm_setup/build_cdm.sh index a39e5a2..535cc00 100755 --- a/test/cdm_tests/cdm_setup/build_cdm.sh +++ b/test/cdm_tests/cdm_setup/build_cdm.sh @@ -20,11 +20,24 @@ function error export PYTHONDONTWRITEBYTECODE=1 -type -P python > /dev/null && PYEXE=python || PYEXE=python3 -if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' > /dev/null 2>&1; then - echo "Found $($PYEXE -V)" - echo "Expecting at least python 3.11 - exiting!" - exit 1 +# If a virtual environment is active, or if .pyenv/bin is in PATH, scrub it +# This ensures we use a system python to create the new venv +VENV_NAME=".pyenv" +CLEAN_PATH=$(echo "$PATH" | sed -E "s|[^:]*/$VENV_NAME/[^:]*:?||g") + +if command -v python3 &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python3) +elif command -v python &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python) +else + echo "Python is not installed." + error +fi + +if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' >/dev/null 2>&1; then + echo "Found $($PYEXE -V)" + echo "Expecting at least python 3.11 - exiting!" + exit 1 fi MY_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" @@ -32,6 +45,7 @@ PROJECT_ROOT_PATH="$MY_PATH/../../.." CDM_SOURCE_PATH="$MY_PATH/../common-domain-model/rosetta-source/src/main/rosetta" PYTHON_TARGET_PATH=$PROJECT_ROOT_PATH/target/python-cdm PYTHON_SETUP_PATH="$MY_PATH/../../python_setup" +JAR_PATH="$PROJECT_ROOT_PATH/target/python-0.0.0.main-SNAPSHOT.jar" cd ${MY_PATH} || error # Parse command-line arguments for --skip-cdm @@ -48,8 +62,21 @@ else echo "Skipping get_cdm.sh as requested." fi +if [[ ! -f "$JAR_PATH" ]]; then + echo "Could not find generator jar at: $JAR_PATH" + echo "Building with maven..." + if ! (cd "$PROJECT_ROOT_PATH" && mvn clean package); then + echo "Maven build failed - exiting." + exit 1 + fi + if [[ ! -f "$JAR_PATH" ]]; then + echo "Maven build completed but $JAR_PATH still missing - exiting." + exit 1 + fi +fi + echo "***** build CDM" -java -cp $PROJECT_ROOT_PATH/target/python-0.0.0.main-SNAPSHOT.jar com.regnosys.rosetta.generator.python.PythonCodeGeneratorCLI -s $CDM_SOURCE_PATH -t $PYTHON_TARGET_PATH +java -cp "$JAR_PATH" com.regnosys.rosetta.generator.python.PythonCodeGeneratorCLI -s $CDM_SOURCE_PATH -t $PYTHON_TARGET_PATH JAVA_EXIT_CODE=$? if [[ $JAVA_EXIT_CODE -eq 1 ]]; then echo "Java program returned exit code 1. Stopping script." @@ -61,12 +88,13 @@ source $PYTHON_SETUP_PATH/setup_python_env.sh echo "***** activating virtual environment" VENV_NAME=".pyenv" -source $PROJECT_ROOT_PATH/$VENV_NAME/${PY_SCRIPTS}/activate || error +if [ -z "${WINDIR}" ]; then PY_SCRIPTS='bin'; else PY_SCRIPTS='Scripts'; fi +source "$PROJECT_ROOT_PATH/$VENV_NAME/${PY_SCRIPTS}/activate" || error echo "***** build CDM Python package" cd $PYTHON_TARGET_PATH rm python_cdm-*.*.*-py3-none-any.whl -$PYEXE -m pip wheel --no-deps --only-binary :all: . || processError +python -m pip wheel --no-deps --only-binary :all: . || processError echo "***** cleanup" diff --git a/test/cdm_tests/setup_cdm_test_env.sh b/test/cdm_tests/setup_cdm_test_env.sh index 6912f12..0950c28 100755 --- a/test/cdm_tests/setup_cdm_test_env.sh +++ b/test/cdm_tests/setup_cdm_test_env.sh @@ -12,11 +12,24 @@ function error } export PYTHONDONTWRITEBYTECODE=1 -type -P python > /dev/null && PYEXE=python || PYEXE=python3 -if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' > /dev/null 2>&1; then - echo "Found $($PYEXE -V)" - echo "Expecting at least python 3.11 - exiting!" - exit 1 +# If a virtual environment is active, or if .pyenv/bin is in PATH, scrub it +# This ensures we use a system python to create the new venv +VENV_NAME=".pyenv" +CLEAN_PATH=$(echo "$PATH" | sed -E "s|[^:]*/$VENV_NAME/[^:]*:?||g") + +if command -v python3 &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python3) +elif command -v python &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python) +else + echo "Python is not installed." + error +fi + +if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' >/dev/null 2>&1; then + echo "Found $($PYEXE -V)" + echo "Expecting at least python 3.11 - exiting!" + exit 1 fi MY_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" @@ -28,11 +41,12 @@ source $MY_PATH/$PYTHONSETUPPATH/setup_python_env.sh echo "***** activating virtual environment" VENV_NAME=".pyenv" -VENV_PATH="../.." -source $MY_PATH/$PYTHONSETUPPATH/$VENV_PATH/$VENV_NAME/${PY_SCRIPTS}/activate || error +VENV_PATH="../../$VENV_NAME" +if [ -z "${WINDIR}" ]; then PY_SCRIPTS='bin'; else PY_SCRIPTS='Scripts'; fi +source "$MY_PATH/$PYTHONSETUPPATH/$VENV_PATH/${PY_SCRIPTS}/activate" || error # install cdm package PYTHONCDMDIR="../../target/python-cdm" echo "**** Install CDM package ****" -$PYEXE -m pip install $MY_PATH/$PYTHONCDMDIR/python_cdm-*-py3-none-any.whl \ No newline at end of file +python -m pip install $MY_PATH/$PYTHONCDMDIR/python_cdm-*-py3-none-any.whl \ No newline at end of file diff --git a/test/python_setup/setup_python_env.sh b/test/python_setup/setup_python_env.sh index c8dd7c1..81ab247 100755 --- a/test/python_setup/setup_python_env.sh +++ b/test/python_setup/setup_python_env.sh @@ -1,14 +1,16 @@ - #!/bin/bash +#!/bin/bash + function error { echo echo "***************************************************************************" - echo "* *" - echo "* DEV ENV Initialization FAILED! *" - echo "* *" + echo "* *" + echo "* DEV ENV Initialization FAILED! *" + echo "* *" echo "***************************************************************************" echo exit 1 } + # Determine the Python executable if command -v python &>/dev/null; then PYEXE=python @@ -18,45 +20,66 @@ else echo "Python is not installed." error fi + # Check Python version if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' > /dev/null 2>&1; then echo "Found $($PYEXE -V)" echo "Expecting at least python 3.11 - exiting!" exit 1 fi + export PYTHONDONTWRITEBYTECODE=1 ENV_BUILD_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" cd "${ENV_BUILD_PATH}" || error + echo "***** setup virtual environment in [project_root]/.pyenv" VENV_NAME=".pyenv" -VENV_PATH="../.." +VENV_PATH="../../$VENV_NAME" + # Determine the scripts directory if [ -z "${WINDIR}" ]; then PY_SCRIPTS='bin' else PY_SCRIPTS='Scripts' fi -rm -rf "${VENV_PATH}/${VENV_NAME}" -${PYEXE} -m venv --clear "${VENV_PATH}/${VENV_NAME}" || error -source "${VENV_PATH}/${VENV_NAME}/${PY_SCRIPTS}/activate" || error + +rm -rf "${VENV_PATH}" +${PYEXE} -m venv --clear "${VENV_PATH}" || error +source "${VENV_PATH}/${PY_SCRIPTS}/activate" || error + ${PYEXE} -m pip install --upgrade pip || error ${PYEXE} -m pip install -r requirements.txt || error + echo "***** Get and Install Runtime" -RUNTIMEURL="https://api.github.com/repos/finos/rune-python-runtime/releases/latest" -# Fetch the latest release data from the GitHub API -release_data=$(curl -s $RUNTIMEURL) -# Extract the download URL of the first asset -download_url=$(echo "$release_data" | grep '"browser_download_url":' | head -n 1 | sed -E 's/.*"([^"]+)".*/\1/') -# Download the artifact using wget or curl -if command -v wget &>/dev/null; then - wget "$download_url" -elif command -v curl &>/dev/null; then - curl -LO "$download_url" + +if [ -n "$RUNE_RUNTIME_FILE" ]; then + # --- Local Installation Logic --- + echo "Using local runtime source: $RUNE_RUNTIME_FILE" + if [ -f "$RUNE_RUNTIME_FILE" ]; then + ${PYEXE} -m pip install "$RUNE_RUNTIME_FILE" --force-reinstall || error + else + echo "Error: Local file $RUNE_RUNTIME_FILE not found." + error + fi else - echo "Neither wget nor curl is installed." - error + # --- Remote Repository Logic --- + echo "No local source provided. Pulling from repo..." + RUNTIMEURL="https://api.github.com/repos/finos/rune-python-runtime/releases/latest" + + release_data=$(curl -s $RUNTIMEURL) + download_url=$(echo "$release_data" | grep '"browser_download_url":' | head -n 1 | sed -E 's/.*"([^"]+)".*/\1/') + + if command -v wget &>/dev/null; then + wget "$download_url" + elif command -v curl &>/dev/null; then + curl -LO "$download_url" + else + echo "Neither wget nor curl is installed." + error + fi + + ${PYEXE} -m pip install rune_runtime*-py3-*.whl --force-reinstall || error + rm rune_runtime*-py3-*.whl fi -${PYEXE} -m pip install rune_runtime*-py3-*.whl --force-reinstall || error -rm rune_runtime*-py3-*.whl + deactivate - \ No newline at end of file diff --git a/test/python_unit_tests/features/TestEnumUsage.rosetta b/test/python_unit_tests/features/TestEnumUsage.rosetta new file mode 100644 index 0000000..daead85 --- /dev/null +++ b/test/python_unit_tests/features/TestEnumUsage.rosetta @@ -0,0 +1,17 @@ +namespace rosetta_dsl.test.semantic.test_enum_usage : <"generate Python unit tests from Rosetta."> + +enum TrafficLight: + Red + Yellow + Green + +type TrafficLightType: + val TrafficLight(1..1) + +type CheckLightTest: + color TrafficLight(1..1) + target string(1..1) + condition TestCond: + (if color = TrafficLight -> Red + then "Stop" + else "Go") = target diff --git a/test/python_unit_tests/features/collections/Collections.rosetta b/test/python_unit_tests/features/collections/Collections.rosetta new file mode 100644 index 0000000..35aaa17 --- /dev/null +++ b/test/python_unit_tests/features/collections/Collections.rosetta @@ -0,0 +1,100 @@ +namespace rosetta_dsl.test.semantic.collections : <"generate Python unit tests from Rosetta."> + +type CountItem: + name string (0..1) + value int (0..1) + +type CountContainer: + field1 int (0..*) + field2 CountItem (0..*) + +type CountTest: + bValue CountContainer (1..*) + condition TestCond: + if bValue -> field2 count > 0 + then True + else False + +type SumTest: + aValue int (1..1) + bValue int (1..1) + target int (1..1) + condition TestCond: + if [aValue, bValue] sum = target + then True + else False + +type MinTest: + a int (1..1) + condition Test: + if [a, 1] min = 1 + then True + else False + +type MaxTest: + a int (1..1) + condition Test: + if [a, 1, 20] max = 20 + then True + else False + +type LastTest: + aValue int (1..1) + bValue int (1..1) + cValue int (1..1) + target int (1..1) + condition TestCond: + if [aValue, bValue, cValue] last = target + then True + else False + +type SortTest: + condition Test: + if [3, 2, 1] sort first = 1 + then True + else False + +type JoinTest: + field1 string (1..1) + field2 string (1..1) + delimiter string (1..1) + target string (1..1) + condition TestCond: + if ([field1, field2] join delimiter) = target + then True + else False + +type FlattenTest: + fc FlattenContainer (1..*) + target int (0..*) + condition TestCond: + if (fc extract items then extract items then flatten) all = target + then True + else False + +type FilterItem: + fi int (1..1) + +type FilterTest: + fis FilterItem (1..*) + target int (1..1) + condition TestCondFilter: + if [fis filter i [i->fi = target]] count = 1 + then True + else False + +type FlattenBar: + numbers int (0..*) + +type FlattenFoo: + bars FlattenBar (0..*) + condition TestCondFoo: + if [1, 2, 3] all = (bars extract numbers then flatten) + then True + else False + +type FlattenItem: + items int (1..*) + +type FlattenContainer: + items FlattenItem (1..*) diff --git a/test/python_unit_tests/features/collections/ListExtensions.rosetta b/test/python_unit_tests/features/collections/ListExtensions.rosetta new file mode 100644 index 0000000..cd67e9a --- /dev/null +++ b/test/python_unit_tests/features/collections/ListExtensions.rosetta @@ -0,0 +1,37 @@ +namespace rosetta_dsl.test.semantic.collections.extensions : <"generate Python unit tests from Rosetta."> + +type ListFirstTest: + items int (0..*) + target int (0..1) + condition TestCond: + items first = target + +type ListLastTest: + items int (0..*) + target int (0..1) + condition TestCond: + items last = target + +type ListDistinctTest: + items int (0..*) + target int (0..*) + condition TestCond: + (items distinct) all = (target distinct) + +type ListSumTest: + items int (0..*) + target int (1..1) + condition TestCond: + items sum = target + +type ListOnlyElementTest: + items int (0..*) + target int (0..1) + condition TestCond: + items only-element = target + +type ListReverseTest: + items int (0..*) + target int (0..*) + condition TestCond: + (items reverse) all = target diff --git a/test/python_unit_tests/features/collections/test_list_extensions.py b/test/python_unit_tests/features/collections/test_list_extensions.py new file mode 100644 index 0000000..47435b1 --- /dev/null +++ b/test/python_unit_tests/features/collections/test_list_extensions.py @@ -0,0 +1,61 @@ +"""List extensions unit tests""" + +import pytest +from rosetta_dsl.test.semantic.collections.extensions.ListFirstTest import ( + ListFirstTest, +) +from rosetta_dsl.test.semantic.collections.extensions.ListLastTest import ListLastTest +from rosetta_dsl.test.semantic.collections.extensions.ListDistinctTest import ( + ListDistinctTest, +) +from rosetta_dsl.test.semantic.collections.extensions.ListSumTest import ListSumTest +from rosetta_dsl.test.semantic.collections.extensions.ListOnlyElementTest import ( + ListOnlyElementTest, +) +from rosetta_dsl.test.semantic.collections.extensions.ListReverseTest import ( + ListReverseTest, +) + + +def test_list_first(): + """Test 'first' list operator.""" + ListFirstTest(items=[1, 2, 3], target=1).validate_model() + # Current implementation raises IndexError for empty list + with pytest.raises(Exception): + ListFirstTest(items=[], target=None).validate_model() + + +def test_list_last(): + """Test 'last' list operator.""" + ListLastTest(items=[1, 2, 3], target=3).validate_model() + # Current implementation raises IndexError for empty list + with pytest.raises(Exception): + ListLastTest(items=[], target=None).validate_model() + + +def test_list_distinct(): + """Test 'distinct' list operator.""" + ListDistinctTest(items=[1, 2, 2, 3], target=[1, 2, 3]).validate_model() + + +def test_list_sum(): + """Test 'sum' list operator.""" + ListSumTest(items=[1, 2, 3], target=6).validate_model() + ListSumTest(items=[], target=0).validate_model() + + +def test_list_only_element(): + """Test 'only-element' list operator.""" + ListOnlyElementTest(items=[1], target=1).validate_model() + + # Returns None if multiple elements exist + ListOnlyElementTest(items=[1, 2], target=None).validate_model() + + # Returns None if empty + ListOnlyElementTest(items=[], target=None).validate_model() + + +def test_list_reverse(): + """Test 'reverse' list operator.""" + ListReverseTest(items=[1, 2, 3], target=[3, 2, 1]).validate_model() + ListReverseTest(items=[], target=[]).validate_model() diff --git a/test/python_unit_tests/features/collections/test_list_operators.py b/test/python_unit_tests/features/collections/test_list_operators.py new file mode 100644 index 0000000..eea3168 --- /dev/null +++ b/test/python_unit_tests/features/collections/test_list_operators.py @@ -0,0 +1,104 @@ +"""list operator unit tests""" + +import pytest + +from rosetta_dsl.test.semantic.collections.CountItem import CountItem +from rosetta_dsl.test.semantic.collections.CountContainer import CountContainer +from rosetta_dsl.test.semantic.collections.CountTest import CountTest +from rosetta_dsl.test.semantic.collections.SumTest import SumTest +from rosetta_dsl.test.semantic.collections.MinTest import MinTest +from rosetta_dsl.test.semantic.collections.MaxTest import MaxTest +from rosetta_dsl.test.semantic.collections.LastTest import LastTest +from rosetta_dsl.test.semantic.collections.SortTest import SortTest +from rosetta_dsl.test.semantic.collections.JoinTest import JoinTest +from rosetta_dsl.test.semantic.collections.FlattenItem import FlattenItem +from rosetta_dsl.test.semantic.collections.FlattenContainer import FlattenContainer +from rosetta_dsl.test.semantic.collections.FlattenTest import FlattenTest +from rosetta_dsl.test.semantic.collections.FlattenBar import FlattenBar +from rosetta_dsl.test.semantic.collections.FlattenFoo import FlattenFoo +from rosetta_dsl.test.semantic.collections.FilterItem import FilterItem +from rosetta_dsl.test.semantic.collections.FilterTest import FilterTest + + +def test_count_passes(): + """count tests""" + item1 = CountItem(name="item1", value=1) + container = CountContainer(field1=[1, 2], field2=[item1]) + count_test = CountTest(bValue=[container]) + count_test.validate_model() + + +def test_sum_passes(): + """sum tests""" + sum_test = SumTest(aValue=2, bValue=3, target=5) + sum_test.validate_model() + + +def test_min_passes(): + """min tests passes""" + min_test = MinTest(a=10) + min_test.validate_model() + + +def test_min_fails(): + """min tests fails""" + min_test = MinTest(a=-1) + with pytest.raises(Exception): + min_test.validate_model() + + +def test_max_passes(): + """max tests passes""" + max_test = MaxTest(a=1) + max_test.validate_model() + + +def test_max_fails(): + """max tests fails""" + max_test = MaxTest(a=100) + with pytest.raises(Exception): + max_test.validate_model() + + +def test_last_passes(): + """last tests passes""" + last_test = LastTest(aValue=1, bValue=2, cValue=3, target=3) + last_test.validate_model() + + +def test_sort_passes(): + """sort tests passes""" + sort_test = SortTest() + sort_test.validate_model() + + +def test_join_passes(): + """join tests passes""" + JoinTest(field1="a", field2="b", delimiter="", target="ab").validate_model() + + +def test_flatten_passes(): + """flatten tests passes""" + flatten_item = FlattenItem(items=[1, 2, 3]) + flatten_container = FlattenContainer( + items=[flatten_item, flatten_item, flatten_item] + ) + FlattenTest( + fc=[flatten_container], target=[1, 2, 3, 1, 2, 3, 1, 2, 3] + ).validate_model() + + +def test_flatten_foo_passes(): + """flatten foo tests passes""" + bar1 = FlattenBar(numbers=[1, 2]) + bar2 = FlattenBar(numbers=[3]) + foo = FlattenFoo(bars=[bar1, bar2]) + foo.validate_model() + + +# filter tests +def test_filter_passes(): + item1 = FilterItem(fi=1) + item2 = FilterItem(fi=2) + filter_test = FilterTest(fis=[item1, item2], target=1) + filter_test.validate_model() diff --git a/test/python_unit_tests/features/conversions/Conversions.rosetta b/test/python_unit_tests/features/conversions/Conversions.rosetta new file mode 100644 index 0000000..13489b4 --- /dev/null +++ b/test/python_unit_tests/features/conversions/Conversions.rosetta @@ -0,0 +1,31 @@ +namespace rosetta_dsl.test.semantic.conversions : <"generate Python unit tests from Rosetta."> + +type IntOperatorTest: + a string (1..1) + b int (0..1) + condition Test: + b = a to-int + +type DateOperatorTest: + a string (1..1) + b date (0..1) + condition Test: + b = a to-date + +type DateTimeOperatorTest: + a string (1..1) + b dateTime (0..1) + condition Test: + b = a to-date-time + +type TimeOperatorTest: + a string (1..1) + b time (0..1) + condition Test: + b = a to-time + +type ZonedDateTimeOperatorTest: + a string (1..1) + b zonedDateTime (0..1) + condition Test: + b = a to-zoned-date-time diff --git a/test/python_unit_tests/features/conversions/test_conversions.py b/test/python_unit_tests/features/conversions/test_conversions.py new file mode 100644 index 0000000..b8975d2 --- /dev/null +++ b/test/python_unit_tests/features/conversions/test_conversions.py @@ -0,0 +1,135 @@ +"""conversion unit tests""" + +import datetime +from datetime import date +from zoneinfo import ZoneInfo +import pytest + +from rosetta_dsl.test.semantic.conversions.DateOperatorTest import DateOperatorTest +from rosetta_dsl.test.semantic.conversions.DateTimeOperatorTest import ( + DateTimeOperatorTest, +) +from rosetta_dsl.test.semantic.conversions.IntOperatorTest import IntOperatorTest +from rosetta_dsl.test.semantic.conversions.TimeOperatorTest import TimeOperatorTest +from rosetta_dsl.test.semantic.conversions.ZonedDateTimeOperatorTest import ( + ZonedDateTimeOperatorTest, +) + + +# to-int tests +def test_to_int_passes(): + to_int_test = IntOperatorTest(a="1", b=1) + to_int_test.validate_model() + + +def test_to_int_fails(): + to_int_test = IntOperatorTest(a="a", b=1) + with pytest.raises(Exception): + to_int_test.validate_model() + + +# to-date tests +def test_to_date_passes(): + to_date_test = DateOperatorTest(a="2025-05-26", b=date(2025, 5, 26)) + to_date_test.validate_model() + + +def test_to_date_invalid_format_fails(): + to_date_test = DateOperatorTest(a="2025/05/26", b=date(2025, 5, 26)) + with pytest.raises(Exception): + to_date_test.validate_model() + + +# to-date-time tests +def test_to_date_time_passes(): + to_date_time_test = DateTimeOperatorTest( + a="2025-05-26 14:30:00", b=datetime.datetime(2025, 5, 26, 14, 30, 0) + ) + to_date_time_test.validate_model() + + +def test_to_date_time_fails(): + to_date_time_test = DateTimeOperatorTest( + a="2025-05-26 14-30-00", b=datetime.datetime(2025, 5, 26, 14, 30, 0) + ) + with pytest.raises(Exception): + to_date_time_test.validate_model() + + +# to-time tests +def test_to_time_passes(): + to_time_test = TimeOperatorTest(a="11:45:23", b=datetime.time(11, 45, 23)) + to_time_test.validate_model() + + +def test_to_time_fails(): + to_time_test = TimeOperatorTest(a="14-30-00", b=datetime.time(14, 30, 0)) + with pytest.raises(Exception): + to_time_test.validate_model() + + +# to-zoned-date-time tests +def test_to_zoned_date_time_offset_and_time_zone(): + to_zoned_date_time_test = ZonedDateTimeOperatorTest( + a="2025-07-07 15:30:00 +0100 Europe/Lisbon", + b=datetime.datetime(2025, 7, 7, 15, 30, 0, tzinfo=ZoneInfo("Europe/Lisbon")), + ) + to_zoned_date_time_test.validate_model() + + +def test_to_zoned_date_time_only_time_zone(): + to_zoned_date_time_test = ZonedDateTimeOperatorTest( + a="2025-07-07 15:30:00 Europe/Lisbon", + b=datetime.datetime(2025, 7, 7, 15, 30, 0, tzinfo=ZoneInfo("Europe/Lisbon")), + ) + to_zoned_date_time_test.validate_model() + + +def test_to_zoned_date_time_only_time_zone2(): + to_zoned_date_time_test = ZonedDateTimeOperatorTest( + a="2025-03-15 12:00:00 Zulu", + b=datetime.datetime(2025, 3, 15, 12, 0, 0, tzinfo=ZoneInfo("Zulu")), + ) + to_zoned_date_time_test.validate_model() + + +def test_to_zoned_date_time_only_offset(): + to_zoned_date_time_test = ZonedDateTimeOperatorTest( + a="2025-05-26 14:30:00 +0900", + b=datetime.datetime( + 2025, + 5, + 26, + 14, + 30, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=9)), + ), + ) + to_zoned_date_time_test.validate_model() + + +def test_to_zoned_date_time_fails(): + to_zoned_date_time_test = ZonedDateTimeOperatorTest( + a="2025-12-15 15:30:00 +0100 Europe/Lisbon", + b=datetime.datetime(2025, 5, 26, 14, 30, 0, tzinfo=ZoneInfo("Europe/Lisbon")), + ) + with pytest.raises(Exception): + to_zoned_date_time_test.validate_model() + + +def test_to_zoned_date_time_fails_format(): + to_zoned_date_time_test = ZonedDateTimeOperatorTest( + a="2025-05-26 14:30:00 +09x0", + b=datetime.datetime( + 2025, + 5, + 26, + 14, + 30, + 0, + tzinfo=datetime.timezone(datetime.timedelta(hours=9)), + ), + ) + with pytest.raises(Exception): + to_zoned_date_time_test.validate_model() diff --git a/test/python_unit_tests/features/expressions/ConditionalExpression.rosetta b/test/python_unit_tests/features/expressions/ConditionalExpression.rosetta new file mode 100644 index 0000000..ef1b933 --- /dev/null +++ b/test/python_unit_tests/features/expressions/ConditionalExpression.rosetta @@ -0,0 +1,19 @@ +namespace rosetta_dsl.test.semantic.expressions.conditional : <"generate Python unit tests from Rosetta."> + +type ConditionalValueTest: + param int(1..1) + target string(1..1) + condition TestCond: + (if param > 10 + then "High" + else "Low") = target + +type ConditionalNestedTest: + param int(1..1) + target string(1..1) + condition TestCond: + (if param > 10 + then "High" + else if param > 5 + then "Medium" + else "Low") = target diff --git a/test/python_unit_tests/features/expressions/SwitchOp.rosetta b/test/python_unit_tests/features/expressions/SwitchOp.rosetta new file mode 100644 index 0000000..f8a2cd0 --- /dev/null +++ b/test/python_unit_tests/features/expressions/SwitchOp.rosetta @@ -0,0 +1,12 @@ +namespace rosetta_dsl.test.semantic.expressions.switch_op + +type SwitchTest: <"Test switch operation"> + x int (1..1) + target string (1..1) + condition TestCond: + if (x switch + 1 then "One", + 2 then "Two", + default "Other") = target + then True + else False diff --git a/test/python_unit_tests/features/expressions/TypeConversion.rosetta b/test/python_unit_tests/features/expressions/TypeConversion.rosetta new file mode 100644 index 0000000..ac5eb41 --- /dev/null +++ b/test/python_unit_tests/features/expressions/TypeConversion.rosetta @@ -0,0 +1,13 @@ +namespace rosetta_dsl.test.semantic.expressions.type_conversion : <"generate Python unit tests from Rosetta."> + +type StringToIntTest: + s string(1..1) + target int(1..1) + condition TestCond: + (s to-int) = target + +type IntToStringTest: + i int(1..1) + target string(1..1) + condition TestCond: + (i to-string) = target diff --git a/test/python_unit_tests/features/expressions/test_conditional_expression.py b/test/python_unit_tests/features/expressions/test_conditional_expression.py new file mode 100644 index 0000000..15e0043 --- /dev/null +++ b/test/python_unit_tests/features/expressions/test_conditional_expression.py @@ -0,0 +1,26 @@ +"""Conditional expression unit tests""" + +from rosetta_dsl.test.semantic.expressions.conditional.ConditionalValueTest import ( + ConditionalValueTest, +) +from rosetta_dsl.test.semantic.expressions.conditional.ConditionalNestedTest import ( + ConditionalNestedTest, +) + + +def test_conditional_value(): + """Test simple if-then-else expression.""" + ConditionalValueTest(param=20, target="High").validate_model() + ConditionalValueTest(param=5, target="Low").validate_model() + + +def test_conditional_nested(): + """Test nested if-then-else expression.""" + ConditionalNestedTest(param=20, target="High").validate_model() + ConditionalNestedTest(param=8, target="Medium").validate_model() + ConditionalNestedTest(param=2, target="Low").validate_model() + + +if __name__ == "__main__": + test_conditional_value() + test_conditional_nested() diff --git a/test/python_unit_tests/features/expressions/test_switch_op.py b/test/python_unit_tests/features/expressions/test_switch_op.py new file mode 100644 index 0000000..c5eb14a --- /dev/null +++ b/test/python_unit_tests/features/expressions/test_switch_op.py @@ -0,0 +1,17 @@ +"""Switch expression unit tests""" + +from rosetta_dsl.test.semantic.expressions.switch_op.SwitchTest import SwitchTest + + +def test_switch_op(): + """Test switch operation.""" + # Test valid cases + SwitchTest(x=1, target="One").validate_model() + SwitchTest(x=2, target="Two").validate_model() + + # Test default case + SwitchTest(x=3, target="Other").validate_model() + + +if __name__ == "__main__": + test_switch_op() diff --git a/test/python_unit_tests/features/expressions/test_type_conversion.py b/test/python_unit_tests/features/expressions/test_type_conversion.py new file mode 100644 index 0000000..50a4c85 --- /dev/null +++ b/test/python_unit_tests/features/expressions/test_type_conversion.py @@ -0,0 +1,18 @@ +"""Type conversion unit tests""" + +from rosetta_dsl.test.semantic.expressions.type_conversion.StringToIntTest import ( + StringToIntTest, +) +from rosetta_dsl.test.semantic.expressions.type_conversion.IntToStringTest import ( + IntToStringTest, +) + + +def test_string_to_int(): + """Test 'to-int' conversion.""" + StringToIntTest(s="123", target=123).validate_model() + + +def test_int_to_string(): + """Test 'to-string' conversion.""" + IntToStringTest(i=456, target="456").validate_model() diff --git a/test/python_unit_tests/rosetta/ConditionsTests.rosetta b/test/python_unit_tests/features/language/ConditionsTests.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/ConditionsTests.rosetta rename to test/python_unit_tests/features/language/ConditionsTests.rosetta diff --git a/test/python_unit_tests/rosetta/Multiline.rosetta b/test/python_unit_tests/features/language/Multiline.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/Multiline.rosetta rename to test/python_unit_tests/features/language/Multiline.rosetta diff --git a/test/python_unit_tests/rosetta/PythonNameMangling.rosetta b/test/python_unit_tests/features/language/PythonNameMangling.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/PythonNameMangling.rosetta rename to test/python_unit_tests/features/language/PythonNameMangling.rosetta diff --git a/test/python_unit_tests/rosetta/Switch.rosetta b/test/python_unit_tests/features/language/Switch.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/Switch.rosetta rename to test/python_unit_tests/features/language/Switch.rosetta diff --git a/test/python_unit_tests/rosetta/TestEnumQualifiedPathName.rosetta b/test/python_unit_tests/features/language/TestEnumQualifiedPathName.rosetta similarity index 56% rename from test/python_unit_tests/rosetta/TestEnumQualifiedPathName.rosetta rename to test/python_unit_tests/features/language/TestEnumQualifiedPathName.rosetta index badb15f..edcc5bc 100644 --- a/test/python_unit_tests/rosetta/TestEnumQualifiedPathName.rosetta +++ b/test/python_unit_tests/features/language/TestEnumQualifiedPathName.rosetta @@ -1,4 +1,4 @@ -namespace rosetta_dsl.test.semantic.enum : <"generate Python unit tests from Rosetta."> +namespace rosetta_dsl.test.semantic.ENUM : <"generate Python unit tests from Rosetta."> enum TestEnum: A @@ -7,4 +7,4 @@ enum TestEnum: type EnumTestType: ett TestEnum(1..1) condition Test: - ett = TestEnum.B \ No newline at end of file + ett = TestEnum.B diff --git a/test/python_unit_tests/semantics/test_conditions.py b/test/python_unit_tests/features/language/test_conditions.py similarity index 100% rename from test/python_unit_tests/semantics/test_conditions.py rename to test/python_unit_tests/features/language/test_conditions.py diff --git a/test/python_unit_tests/features/language/test_enum_usage.py b/test/python_unit_tests/features/language/test_enum_usage.py new file mode 100644 index 0000000..417c3d7 --- /dev/null +++ b/test/python_unit_tests/features/language/test_enum_usage.py @@ -0,0 +1,17 @@ +"""Enum usage unit tests""" + +from rosetta_dsl.test.semantic.test_enum_usage.TrafficLight import TrafficLight +from rosetta_dsl.test.semantic.test_enum_usage.CheckLightTest import CheckLightTest + + +def test_enum_values(): + """Test enum member access and values.""" + # Generator uppercases enum members + assert TrafficLight.RED.name == "RED" + assert TrafficLight.YELLOW.name == "YELLOW" + + +def test_enum_function(): + """Test passing enum as input.""" + CheckLightTest(color=TrafficLight.RED, target="Stop").validate_model() + CheckLightTest(color=TrafficLight.GREEN, target="Go").validate_model() diff --git a/test/python_unit_tests/semantics/test_name_python_mangling.py b/test/python_unit_tests/features/language/test_name_python_mangling.py similarity index 100% rename from test/python_unit_tests/semantics/test_name_python_mangling.py rename to test/python_unit_tests/features/language/test_name_python_mangling.py diff --git a/test/python_unit_tests/semantics/test_switch_operator.py b/test/python_unit_tests/features/language/test_switch_operator.py similarity index 100% rename from test/python_unit_tests/semantics/test_switch_operator.py rename to test/python_unit_tests/features/language/test_switch_operator.py diff --git a/test/python_unit_tests/rosetta/CardinalityTests.rosetta b/test/python_unit_tests/features/model_structure/CardinalityTests.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/CardinalityTests.rosetta rename to test/python_unit_tests/features/model_structure/CardinalityTests.rosetta diff --git a/test/python_unit_tests/rosetta/ChoiceDeepPath.rosetta b/test/python_unit_tests/features/model_structure/ChoiceDeepPath.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/ChoiceDeepPath.rosetta rename to test/python_unit_tests/features/model_structure/ChoiceDeepPath.rosetta diff --git a/test/python_unit_tests/rosetta/CircularDependency.rosetta b/test/python_unit_tests/features/model_structure/CircularDependency.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/CircularDependency.rosetta rename to test/python_unit_tests/features/model_structure/CircularDependency.rosetta diff --git a/test/python_unit_tests/rosetta/ClassMemberAccess.rosetta b/test/python_unit_tests/features/model_structure/ClassMemberAccess.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/ClassMemberAccess.rosetta rename to test/python_unit_tests/features/model_structure/ClassMemberAccess.rosetta diff --git a/test/python_unit_tests/features/model_structure/EnumMetadata.rosetta b/test/python_unit_tests/features/model_structure/EnumMetadata.rosetta new file mode 100644 index 0000000..3e444dd --- /dev/null +++ b/test/python_unit_tests/features/model_structure/EnumMetadata.rosetta @@ -0,0 +1,16 @@ +namespace rosetta_dsl.test.model.enum_metadata : <"test"> + +enum CurrencyEnum: + USD + EUR + GBP + +type CashTransfer: + amount number (1..1) + currency CurrencyEnum (1..1) + [metadata id] + +type TransferReport: + transfers CashTransfer (1..*) + primaryCurrency CurrencyEnum (1..1) + [metadata reference] diff --git a/test/python_unit_tests/features/model_structure/Inheritance.rosetta b/test/python_unit_tests/features/model_structure/Inheritance.rosetta new file mode 100644 index 0000000..7081c88 --- /dev/null +++ b/test/python_unit_tests/features/model_structure/Inheritance.rosetta @@ -0,0 +1,13 @@ +namespace rosetta_dsl.test.semantic.model_structure.inheritance : <"generate Python unit tests from Rosetta."> + +type Super: + superAttr string (1..1) + +type Sub extends Super: + subAttr int (1..1) + +type InheritanceTest: + s Super (1..1) + target string (1..1) + condition TestCond: + s -> superAttr = target diff --git a/test/python_unit_tests/rosetta/KeyRefTest.rosetta b/test/python_unit_tests/features/model_structure/KeyRefTest.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/KeyRefTest.rosetta rename to test/python_unit_tests/features/model_structure/KeyRefTest.rosetta diff --git a/test/python_unit_tests/features/model_structure/ReuseType.rosetta b/test/python_unit_tests/features/model_structure/ReuseType.rosetta new file mode 100644 index 0000000..59495e7 --- /dev/null +++ b/test/python_unit_tests/features/model_structure/ReuseType.rosetta @@ -0,0 +1,11 @@ +namespace rosetta_dsl.test.model.reuse_type : <"generate Python unit tests from Rosetta."> + +type BaseEntity: + [metadata key] + number int(1..1) +type BarNoRef: + bar BaseEntity(1..1) +type BarRef: + bar BaseEntity(1..1) + [metadata reference] + diff --git a/test/python_unit_tests/features/model_structure/StructuralValidation.rosetta b/test/python_unit_tests/features/model_structure/StructuralValidation.rosetta new file mode 100644 index 0000000..6ca0f6f --- /dev/null +++ b/test/python_unit_tests/features/model_structure/StructuralValidation.rosetta @@ -0,0 +1,16 @@ +namespace rosetta_dsl.test.semantic.model_structure.structural_validation : <"generate Python unit tests from Rosetta."> + +type OneOfTest: + attr1 string (0..1) + attr2 int (0..1) + condition: one-of + +type RequiredChoiceTest: + attr1 string (0..1) + attr2 int (0..1) + condition: required choice attr1, attr2 + +type OptionalChoiceTest: + attr1 string (0..1) + attr2 int (0..1) + condition: optional choice attr1, attr2 diff --git a/test/python_unit_tests/semantics/test_cardinality.py b/test/python_unit_tests/features/model_structure/test_cardinality.py similarity index 100% rename from test/python_unit_tests/semantics/test_cardinality.py rename to test/python_unit_tests/features/model_structure/test_cardinality.py diff --git a/test/python_unit_tests/model/test_circular_dependency.py b/test/python_unit_tests/features/model_structure/test_circular_dependency.py similarity index 100% rename from test/python_unit_tests/model/test_circular_dependency.py rename to test/python_unit_tests/features/model_structure/test_circular_dependency.py diff --git a/test/python_unit_tests/features/model_structure/test_class_member_access_operator.py b/test/python_unit_tests/features/model_structure/test_class_member_access_operator.py new file mode 100644 index 0000000..1d3e429 --- /dev/null +++ b/test/python_unit_tests/features/model_structure/test_class_member_access_operator.py @@ -0,0 +1,57 @@ +from rune.runtime.metadata import * +from rune.runtime.utils import * +from rune.runtime.conditions import * +from rosetta_dsl.test.model.class_member_access.ClassMemberAccess import ( + ClassMemberAccess, +) + +class_member_access = ClassMemberAccess(one=42, three=[1, 2, 3]) + + +def test_attribute_single(): + """Test single attribute access""" + assert rune_resolve_attr(class_member_access, "one") == 42 + + +def test_attribute_optional(): + """Test optional attribute access""" + assert rune_resolve_attr(class_member_access, "two") is None + + +def test_attribute_multi(): + """Test multi attribute access""" + assert rune_resolve_attr(class_member_access, "three") == [1, 2, 3] + + +def test_attribute_single_collection(): + """Test single attribute access collection""" + assert rune_resolve_attr([class_member_access, class_member_access], "one") == [ + 42, + 42, + ] + + +def test_attribute_optional_collection(): + """Test optional attribute access collection""" + assert rune_resolve_attr([class_member_access, class_member_access], "two") is None + + +def test_attribute_multi_collection(): + """Test multi attribute access collection""" + assert rune_resolve_attr([class_member_access, class_member_access], "three") == [ + 1, + 2, + 3, + 1, + 2, + 3, + ] + + +if __name__ == "__main__": + test_attribute_single() + test_attribute_optional() + test_attribute_multi() + test_attribute_single_collection() + test_attribute_optional_collection() + test_attribute_multi_collection() diff --git a/test/python_unit_tests/semantics/test_deep_path.py b/test/python_unit_tests/features/model_structure/test_deep_path.py similarity index 100% rename from test/python_unit_tests/semantics/test_deep_path.py rename to test/python_unit_tests/features/model_structure/test_deep_path.py diff --git a/test/python_unit_tests/features/model_structure/test_entity_reuse.py b/test/python_unit_tests/features/model_structure/test_entity_reuse.py new file mode 100644 index 0000000..10f0282 --- /dev/null +++ b/test/python_unit_tests/features/model_structure/test_entity_reuse.py @@ -0,0 +1,21 @@ +"""Test entity reuse.""" + +import pytest + +from pydantic import ValidationError + +from rosetta_dsl.test.model.reuse_type.BaseEntity import BaseEntity +from rosetta_dsl.test.model.reuse_type.BarNoRef import BarNoRef +from rosetta_dsl.test.model.reuse_type.BarRef import BarRef + + +def test_entity_reuse(): + """Test entity reuse.""" + base_entity = BaseEntity(number=1) + BarRef(bar=base_entity) + with pytest.raises(ValidationError): + BarNoRef(bar=base_entity) + + +if __name__ == "__main__": + test_entity_reuse() diff --git a/test/python_unit_tests/features/model_structure/test_enum_metadata.py b/test/python_unit_tests/features/model_structure/test_enum_metadata.py new file mode 100644 index 0000000..cedc099 --- /dev/null +++ b/test/python_unit_tests/features/model_structure/test_enum_metadata.py @@ -0,0 +1,37 @@ +"""ENUM metadata test- parked until backlog addressed""" + +import pytest +from decimal import Decimal +from rosetta_dsl.test.model.enum_metadata.CurrencyEnum import CurrencyEnum +from rosetta_dsl.test.model.enum_metadata.CashTransfer import CashTransfer +from rune.runtime.metadata import _EnumWrapper + + +@pytest.mark.skip(reason="Blocked by Enum Wrapper implementation - see Backlog") +def test_enum_metadata_behavior(): + """ + Test that Enums are wrapped at runtime to support metadata, + while preserving equality with the raw Enum member. + """ + # Create a transfer with an enum value + transfer = CashTransfer(amount=Decimal("100.00"), currency=CurrencyEnum.USD) + + wrapped_currency = transfer.currency + + # 1. Assert it is wrapped in the runtime _EnumWrapper (to hold metadata) + assert isinstance(wrapped_currency, _EnumWrapper), ( + "Enum usage should be wrapped in _EnumWrapper at runtime" + ) + + # 2. Assert metadata access exists (even if empty initially) + # The wrapper should expose a 'meta' attribute for keys/references + assert hasattr(wrapped_currency, "meta"), "Wrapper should expose metadata attribute" + + # 3. Assert equality with the raw Enum member + # The wrapper should proxy equality checks to the underlying enum + assert wrapped_currency == CurrencyEnum.USD, ( + "Wrapped enum should be equal to the raw Enum member" + ) + assert wrapped_currency.value == CurrencyEnum.USD.value, ( + "Wrapped value should match raw enum value" + ) diff --git a/test/python_unit_tests/features/model_structure/test_inheritance.py b/test/python_unit_tests/features/model_structure/test_inheritance.py new file mode 100644 index 0000000..e5216fe --- /dev/null +++ b/test/python_unit_tests/features/model_structure/test_inheritance.py @@ -0,0 +1,30 @@ +"""Inheritance unit tests""" + +import pytest +from rosetta_dsl.test.semantic.model_structure.inheritance.Sub import Sub +from rosetta_dsl.test.semantic.model_structure.inheritance.InheritanceTest import ( + InheritanceTest, +) + + +def test_inheritance_structure(): + """Test that Sub has both superAttr and subAttr""" + sub = Sub(superAttr="parent", subAttr=10) + assert sub.superAttr == "parent" + assert sub.subAttr == 10 + + # check if superAttr is part of validation + with pytest.raises(Exception): # ValidationError or strict check + sub_fail = Sub(subAttr=10) # Missing superAttr + sub_fail.validate_model() + + +def test_polymorphism(): + """Test passing Sub to a field expecting Super""" + sub = Sub(superAttr="hello", subAttr=20) + InheritanceTest(s=sub, target="hello").validate_model() + + +if __name__ == "__main__": + test_inheritance_structure() + test_polymorphism() diff --git a/test/python_unit_tests/model/test_key_ref.py b/test/python_unit_tests/features/model_structure/test_key_ref.py similarity index 68% rename from test/python_unit_tests/model/test_key_ref.py rename to test/python_unit_tests/features/model_structure/test_key_ref.py index 4d876b5..49ce5d2 100644 --- a/test/python_unit_tests/model/test_key_ref.py +++ b/test/python_unit_tests/features/model_structure/test_key_ref.py @@ -1,4 +1,5 @@ -'''key ref unit test''' +"""key ref unit test""" + from rune.runtime.metadata import IntWithMeta, Reference from rosetta_dsl.test.model.key_ref.ARef import ARef @@ -6,34 +7,38 @@ from rosetta_dsl.test.model.key_ref.BRef import BRef from rosetta_dsl.test.model.key_ref.BNoRefARef import BNoRefARef from rosetta_dsl.test.model.key_ref.BNoRefANoRef import BNoRefANoRef - -def test_resuse_ref(): - '''test key ref''' + + +def test_reuse_ref(): + """test key ref""" a = ARef(aValue=IntWithMeta(value=1, key="key-123")) b1 = BRef(aReference=a) - assert(len(b1.validate_model())== 0) + assert len(b1.validate_model()) == 0 b2 = BRef(aReference=Reference(target=a, ext_key="key-123")) - assert(len(b2.validate_model())== 0) + assert len(b2.validate_model()) == 0 + def test_reuse_a_ref(): - '''test key ref''' + """test key ref""" a = ARef(aValue=IntWithMeta(value=1)) b1 = BNoRefARef(aReference=a) - assert(len(b1.validate_model())== 0) + assert len(b1.validate_model()) == 0 b2 = BNoRefARef(aReference=a) - assert(len(b2.validate_model()) == 0) + assert len(b2.validate_model()) == 0 + def test_reuse_a_no_ref(): - '''test key ref''' + """test key ref""" a = ANoRef(aValue=IntWithMeta(value=1)) b1 = BNoRefANoRef(aReference=a) - assert(len(b1.validate_model())== 0) + assert len(b1.validate_model()) == 0 b2 = BNoRefANoRef(aReference=a) - assert(len(b2.validate_model()) == 0) + assert len(b2.validate_model()) == 0 + if __name__ == "__main__": - test_resuse_ref() + test_reuse_ref() test_reuse_a_ref() test_reuse_a_no_ref() -#EOF \ No newline at end of file +# EOF diff --git a/test/python_unit_tests/features/model_structure/test_structural_validation.py b/test/python_unit_tests/features/model_structure/test_structural_validation.py new file mode 100644 index 0000000..f794ca2 --- /dev/null +++ b/test/python_unit_tests/features/model_structure/test_structural_validation.py @@ -0,0 +1,73 @@ +"""Structural validation unit tests""" + +import pytest +from rune.runtime.conditions import ConditionViolationError +from rosetta_dsl.test.semantic.model_structure.structural_validation.OneOfTest import ( + OneOfTest, +) +from rosetta_dsl.test.semantic.model_structure.structural_validation.RequiredChoiceTest import ( + RequiredChoiceTest, +) +from rosetta_dsl.test.semantic.model_structure.structural_validation.OptionalChoiceTest import ( + OptionalChoiceTest, +) + + +def test_one_of_valid(): + """Test one-of valid cases""" + # Only one set + t1 = OneOfTest(attr1="a") + t1.validate_model() + t2 = OneOfTest(attr2=1) + t2.validate_model() + + +def test_one_of_invalid(): + """Test one-of invalid cases""" + # Both set + with pytest.raises(ConditionViolationError): + t = OneOfTest(attr1="a", attr2=1) + t.validate_model() + + # None set (standard one-of implies required choice among all fields) + with pytest.raises(ConditionViolationError): + t = OneOfTest() + t.validate_model() + + +def test_required_choice_valid(): + """Test required choice valid cases""" + t1 = RequiredChoiceTest(attr1="a") + t1.validate_model() + + +def test_required_choice_invalid(): + """Test required choice invalid cases""" + # Both set (choice allows only one) + with pytest.raises(ConditionViolationError): + t = RequiredChoiceTest(attr1="a", attr2=1) + t.validate_model() + + # None set (required means must have one) + with pytest.raises(ConditionViolationError): + t = RequiredChoiceTest() + t.validate_model() + + +def test_optional_choice_valid(): + """Test optional choice valid cases""" + # None set (optional allows empty) + t0 = OptionalChoiceTest() + t0.validate_model() + + # One set + t1 = OptionalChoiceTest(attr1="a") + t1.validate_model() + + +def test_optional_choice_invalid(): + """Test optional choice invalid cases""" + # Both set + with pytest.raises(ConditionViolationError): + t = OptionalChoiceTest(attr1="a", attr2=1) + t.validate_model() diff --git a/test/python_unit_tests/features/operators/ArithmeticOp.rosetta b/test/python_unit_tests/features/operators/ArithmeticOp.rosetta new file mode 100644 index 0000000..9835c3d --- /dev/null +++ b/test/python_unit_tests/features/operators/ArithmeticOp.rosetta @@ -0,0 +1,41 @@ +namespace rosetta_dsl.test.semantic.arithmetic_op : <"generate Python unit tests from Rosetta."> + +type AddTest: <"Test add operation condition"> + aValue int (1..1) + bValue int (1..1) + target int (1..1) + condition TestCond: <"Test condition"> + if aValue + bValue = target + then True + else + False + +type SubtractTest: <"Test subtract operation condition"> + aValue int (1..1) + bValue int (1..1) + target int (1..1) + condition TestCond: <"Test condition"> + if aValue - bValue = target + then True + else + False + +type MultiplyTest: <"Test multiply operation condition"> + aValue int (1..1) + bValue int (1..1) + target int (1..1) + condition TestCond: <"Test condition"> + if aValue * bValue = target + then True + else + False + +type DivideTest: <"Test divide operation condition"> + aValue int (1..1) + bValue int (1..1) + target int (1..1) + condition TestCond: <"Test condition"> + if aValue / bValue = target + then True + else + False diff --git a/test/python_unit_tests/rosetta/BinaryOp.rosetta b/test/python_unit_tests/features/operators/BinaryOp.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/BinaryOp.rosetta rename to test/python_unit_tests/features/operators/BinaryOp.rosetta diff --git a/test/python_unit_tests/features/operators/ComparisonOp.rosetta b/test/python_unit_tests/features/operators/ComparisonOp.rosetta new file mode 100644 index 0000000..145ee6b --- /dev/null +++ b/test/python_unit_tests/features/operators/ComparisonOp.rosetta @@ -0,0 +1,13 @@ +namespace rosetta_dsl.test.semantic.comparison_op : <"generate Python unit tests from Rosetta."> + +type ComparisonTest: + a int(1..1) + b int(1..1) + op string(1..1) + target boolean(1..1) + condition TestCond: + if op = "LT" then (a < b) = target + else if op = "LE" then (a <= b) = target + else if op = "GT" then (a > b) = target + else if op = "GE" then (a >= b) = target + else False diff --git a/test/python_unit_tests/features/operators/ComplexBooleanLogic.rosetta b/test/python_unit_tests/features/operators/ComplexBooleanLogic.rosetta new file mode 100644 index 0000000..6b1c7c7 --- /dev/null +++ b/test/python_unit_tests/features/operators/ComplexBooleanLogic.rosetta @@ -0,0 +1,18 @@ +namespace rosetta_dsl.test.semantic.operators.complex_boolean_logic : <"generate Python unit tests from Rosetta."> + +type NotOpTest: + a boolean(1..1) + target boolean(1..1) + condition TestCond: + if (a = False) = target + then True + else False + +type ComplexLogicTest: + a boolean(1..1) + b boolean(1..1) + target boolean(1..1) + condition TestCond: + if ((a or b) and (a = False)) = target + then True + else False diff --git a/test/python_unit_tests/rosetta/LogicalOp.rosetta b/test/python_unit_tests/features/operators/LogicalOp.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/LogicalOp.rosetta rename to test/python_unit_tests/features/operators/LogicalOp.rosetta diff --git a/test/python_unit_tests/features/operators/test_arithmetic_operators.py b/test/python_unit_tests/features/operators/test_arithmetic_operators.py new file mode 100644 index 0000000..e305063 --- /dev/null +++ b/test/python_unit_tests/features/operators/test_arithmetic_operators.py @@ -0,0 +1,31 @@ +from rosetta_dsl.test.semantic.arithmetic_op.AddTest import AddTest +from rosetta_dsl.test.semantic.arithmetic_op.SubtractTest import SubtractTest +from rosetta_dsl.test.semantic.arithmetic_op.MultiplyTest import MultiplyTest +from rosetta_dsl.test.semantic.arithmetic_op.DivideTest import DivideTest + + +def test_add(): + """Test add operator""" + add_test = AddTest(aValue=5, bValue=10, target=15) + add_test.validate_model() + + +def test_subtract(): + """Test subtract operator""" + subtract_test = SubtractTest(aValue=5, bValue=10, target=-5) + subtract_test.validate_model() + + +def test_multiply(): + """Test multiply operator""" + multiply_test = MultiplyTest(aValue=5, bValue=10, target=50) + multiply_test.validate_model() + + +def test_divide(): + """Test divide operator""" + divide_test = DivideTest(aValue=10, bValue=2, target=5) + divide_test.validate_model() + + +# EOF diff --git a/test/python_unit_tests/features/operators/test_binary_operators.py b/test/python_unit_tests/features/operators/test_binary_operators.py new file mode 100644 index 0000000..389e889 --- /dev/null +++ b/test/python_unit_tests/features/operators/test_binary_operators.py @@ -0,0 +1,27 @@ +from rosetta_dsl.test.semantic.binary_op.ContainsTest import ContainsTest +from rosetta_dsl.test.semantic.binary_op.DisjointTest import DisjointTest +from rosetta_dsl.test.semantic.binary_op.EqualsTest import EqualsTest +from rosetta_dsl.test.semantic.binary_op.NotEqualsTest import NotEqualsTest + + +def test_equals(): + equals_test = EqualsTest(aValue=5, target=5) + equals_test.validate_model() + + +def test_not_equals(): + not_equals_test = NotEqualsTest(aValue=5, target=15) + not_equals_test.validate_model() + + +def test_contains(): + contains_test = ContainsTest(aValue=["a", "b", "c"], target="c") + contains_test.validate_model() + + +def test_disjoint(): + disjoint_test = DisjointTest(aValue=["a", "b", "c"], target=["d", "e", "f"]) + disjoint_test.validate_model() + + +# EOF diff --git a/test/python_unit_tests/features/operators/test_comparison_operators.py b/test/python_unit_tests/features/operators/test_comparison_operators.py new file mode 100644 index 0000000..0ab495c --- /dev/null +++ b/test/python_unit_tests/features/operators/test_comparison_operators.py @@ -0,0 +1,31 @@ +"""Comparison operators unit tests""" + +from rosetta_dsl.test.semantic.comparison_op.ComparisonTest import ComparisonTest + + +def test_less_than(): + """Test '<' operator.""" + ComparisonTest(a=5, b=10, op="LT", target=True).validate_model() + ComparisonTest(a=10, b=5, op="LT", target=False).validate_model() + ComparisonTest(a=5, b=5, op="LT", target=False).validate_model() + + +def test_less_than_or_equal(): + """Test '<=' operator.""" + ComparisonTest(a=5, b=10, op="LE", target=True).validate_model() + ComparisonTest(a=5, b=5, op="LE", target=True).validate_model() + ComparisonTest(a=10, b=5, op="LE", target=False).validate_model() + + +def test_greater_than(): + """Test '>' operator.""" + ComparisonTest(a=10, b=5, op="GT", target=True).validate_model() + ComparisonTest(a=5, b=10, op="GT", target=False).validate_model() + ComparisonTest(a=5, b=5, op="GT", target=False).validate_model() + + +def test_greater_than_or_equal(): + """Test '>=' operator.""" + ComparisonTest(a=10, b=5, op="GE", target=True).validate_model() + ComparisonTest(a=5, b=5, op="GE", target=True).validate_model() + ComparisonTest(a=5, b=10, op="GE", target=False).validate_model() diff --git a/test/python_unit_tests/features/operators/test_complex_boolean_logic.py b/test/python_unit_tests/features/operators/test_complex_boolean_logic.py new file mode 100644 index 0000000..198a531 --- /dev/null +++ b/test/python_unit_tests/features/operators/test_complex_boolean_logic.py @@ -0,0 +1,25 @@ +"""Complex boolean logic unit tests""" + +from rosetta_dsl.test.semantic.operators.complex_boolean_logic.NotOpTest import ( + NotOpTest, +) +from rosetta_dsl.test.semantic.operators.complex_boolean_logic.ComplexLogicTest import ( + ComplexLogicTest, +) + + +def test_not_op(): + """Test negation by equality.""" + NotOpTest(a=True, target=False).validate_model() + NotOpTest(a=False, target=True).validate_model() + + +def test_complex_logic(): + """Test complex boolean expression with negation by equality.""" + # (a or b) and (not a) + # T, T -> (T or T) and F -> T and F -> F + ComplexLogicTest(a=True, b=True, target=False).validate_model() + # F, T -> (F or T) and T -> T and T -> T + ComplexLogicTest(a=False, b=True, target=True).validate_model() + # F, F -> (F or F) and T -> F and T -> F + ComplexLogicTest(a=False, b=False, target=False).validate_model() diff --git a/test/python_unit_tests/semantics/test_logical_operators.py b/test/python_unit_tests/features/operators/test_logical_operators.py similarity index 100% rename from test/python_unit_tests/semantics/test_logical_operators.py rename to test/python_unit_tests/features/operators/test_logical_operators.py diff --git a/test/python_unit_tests/features/robustness/NullHandling.rosetta b/test/python_unit_tests/features/robustness/NullHandling.rosetta new file mode 100644 index 0000000..d1e2120 --- /dev/null +++ b/test/python_unit_tests/features/robustness/NullHandling.rosetta @@ -0,0 +1,13 @@ +namespace rosetta_dsl.test.semantic.robustness.null_handling : <"generate Python unit tests from Rosetta."> + +type IsAbsentTest: + val string(0..1) + target boolean(1..1) + condition TestCond: + (val is absent) = target + +type IsAbsentListTest: + items int (0..*) + target boolean(1..1) + condition TestCond: + (items is absent) = target diff --git a/test/python_unit_tests/features/robustness/test_null_handling.py b/test/python_unit_tests/features/robustness/test_null_handling.py new file mode 100644 index 0000000..f45f638 --- /dev/null +++ b/test/python_unit_tests/features/robustness/test_null_handling.py @@ -0,0 +1,23 @@ +"""Null handling unit tests""" + +from rosetta_dsl.test.semantic.robustness.null_handling.IsAbsentTest import ( + IsAbsentTest, +) +from rosetta_dsl.test.semantic.robustness.null_handling.IsAbsentListTest import ( + IsAbsentListTest, +) + + +def test_is_absent(): + """Test 'is absent' check on scalar value.""" + IsAbsentTest(val=None, target=True).validate_model() + IsAbsentTest(val="foo", target=False).validate_model() + + +def test_is_absent_list(): + """Test 'is absent' check on list of values.""" + # Renamed field from 'list' to 'items' to avoid name collision with built-in list type + IsAbsentListTest(items=[], target=True).validate_model() + # If list is explicit None? + IsAbsentListTest(items=None, target=True).validate_model() + IsAbsentListTest(items=[1], target=False).validate_model() diff --git a/test/python_unit_tests/rosetta/DateSerialization.rosetta b/test/python_unit_tests/features/serialization/DateSerialization.rosetta similarity index 100% rename from test/python_unit_tests/rosetta/DateSerialization.rosetta rename to test/python_unit_tests/features/serialization/DateSerialization.rosetta diff --git a/test/python_unit_tests/model/test_date_serialization.py b/test/python_unit_tests/features/serialization/test_date_serialization.py similarity index 100% rename from test/python_unit_tests/model/test_date_serialization.py rename to test/python_unit_tests/features/serialization/test_date_serialization.py diff --git a/test/python_unit_tests/rosetta/Count.rosetta b/test/python_unit_tests/rosetta/Count.rosetta deleted file mode 100644 index f5ad9ba..0000000 --- a/test/python_unit_tests/rosetta/Count.rosetta +++ /dev/null @@ -1,17 +0,0 @@ -namespace rosetta_dsl.test.semantic.count_operator : <"generate Python unit tests from Rosetta."> - -type ClassA: <"Test class A"> - name string (0..1) <"Name"> - value int (0..1) <"Value"> - -type ClassB: <"Test class B"> - field1 int (0..*) <"Test int field 1"> - field2 ClassA (0..*) <"Test classA field 2"> - -type CountTest: <"Test count operation in a condition"> - bValue ClassB (1..*) <"Test B type bValue"> - - condition TestCond: <"Test condition"> - if bValue -> field2 count > 0 - then True - else False \ No newline at end of file diff --git a/test/python_unit_tests/rosetta/Filter.rosetta b/test/python_unit_tests/rosetta/Filter.rosetta deleted file mode 100644 index 7b1e624..0000000 --- a/test/python_unit_tests/rosetta/Filter.rosetta +++ /dev/null @@ -1,13 +0,0 @@ -namespace rosetta_dsl.test.semantic.filter_operator : <"generate Python unit tests from Rosetta."> - -type FilterItem: - fi int (1..1) - -type FilterTest : <"Test filter operation condition"> - fis FilterItem (1..*) - target int (1..1) - condition Test: - if [fis filter i [i->fi = target]] count = 1 - then True - else - False diff --git a/test/python_unit_tests/rosetta/Flatten.rosetta b/test/python_unit_tests/rosetta/Flatten.rosetta deleted file mode 100644 index df4f940..0000000 --- a/test/python_unit_tests/rosetta/Flatten.rosetta +++ /dev/null @@ -1,21 +0,0 @@ -namespace rosetta_dsl.test.semantic.flatten_operator : <"generate Python unit tests from Rosetta."> - -type ClassA: <"Test type A"> - field1 int (0..1) <"test field 1"> - field2 int (0..1) <"test field 2"> - field3 int (0..1) <"test field 3"> - -type ClassB: <""> - fieldList ClassA (0..*) <""> - -type FlattenTest: <"Test flatten operation in a condition"> - bValue ClassB (0..*) <"Test value"> - field3 int (1..1) <"Test field 3"> - condition FlattenTestCond: <"Test condition"> - if field3>0 - then bValue - extract fieldList - then flatten - else - False - \ No newline at end of file diff --git a/test/python_unit_tests/rosetta/FlattenWithComparison.rosetta b/test/python_unit_tests/rosetta/FlattenWithComparison.rosetta deleted file mode 100644 index 4e2cdbf..0000000 --- a/test/python_unit_tests/rosetta/FlattenWithComparison.rosetta +++ /dev/null @@ -1,11 +0,0 @@ -namespace rosetta_dsl.test.semantic.flatten_with_comparison : <"generate Python unit tests from Rosetta."> - -type Bar: - numbers int (0..*) -type Foo: <"Test flatten operation condition"> - bars Bar (0..*) <"test bar"> - condition TestCondition: <"Test Condition"> - [1, 2, 3] = - (bars - extract numbers - then flatten) diff --git a/test/python_unit_tests/rosetta/Join.rosetta b/test/python_unit_tests/rosetta/Join.rosetta deleted file mode 100644 index e62ebf4..0000000 --- a/test/python_unit_tests/rosetta/Join.rosetta +++ /dev/null @@ -1,12 +0,0 @@ -namespace rosetta_dsl.test.semantic.join_operator : <"generate Python unit tests from Rosetta."> - -type JoinTest: <"Test join binary expression condition"> - field1 string (1..1) - field2 string (1..1) - condition TestCond: <"Test condition"> - if "" join [field1, field2] = "ab" - then - True - else - False - diff --git a/test/python_unit_tests/rosetta/Last.rosetta b/test/python_unit_tests/rosetta/Last.rosetta deleted file mode 100644 index df9c5a8..0000000 --- a/test/python_unit_tests/rosetta/Last.rosetta +++ /dev/null @@ -1,12 +0,0 @@ -namespace rosetta_dsl.test.semantic.last_operator : <"generate Python unit tests from Rosetta."> - -type LastTest: <" Test last operation condition"> - aValue int (1..1) - bValue int (1..1) - cValue int (1..1) - target int (1..1) - condition TestCond: <"Test condition"> - if [aValue, bValue, cValue] last = target - then True - else - False diff --git a/test/python_unit_tests/rosetta/MinMax.rosetta b/test/python_unit_tests/rosetta/MinMax.rosetta deleted file mode 100644 index bce9fc8..0000000 --- a/test/python_unit_tests/rosetta/MinMax.rosetta +++ /dev/null @@ -1,17 +0,0 @@ -namespace rosetta_dsl.test.semantic.min_max : <"generate Python unit tests from Rosetta."> - -type MinTest: - a int (1..1) - condition Test: - if [a,1]min = 1 - then True - else - False - -type MaxTest: - a int (1..1) - condition Test: - if [a,1,20]max = 20 - then True - else - False \ No newline at end of file diff --git a/test/python_unit_tests/rosetta/Sort.rosetta b/test/python_unit_tests/rosetta/Sort.rosetta deleted file mode 100644 index ee85a17..0000000 --- a/test/python_unit_tests/rosetta/Sort.rosetta +++ /dev/null @@ -1,8 +0,0 @@ -namespace rosetta_dsl.test.semantic.sort_operator : <"generate Python unit tests from Rosetta."> - -type SortTest: - condition Test: - if [3, 2, 1] sort first = 1 - then True - else - False diff --git a/test/python_unit_tests/rosetta/Sum.rosetta b/test/python_unit_tests/rosetta/Sum.rosetta deleted file mode 100644 index 87b8b8a..0000000 --- a/test/python_unit_tests/rosetta/Sum.rosetta +++ /dev/null @@ -1,11 +0,0 @@ -namespace rosetta_dsl.test.semantic.sum_operator : <"generate Python unit tests from Rosetta."> - -type SumTest: <" Test sum operation condition"> - aValue int (1..1) - bValue int (1..1) - target int (1..1) - condition TestCond: <"Test condition"> - if [aValue, bValue] sum = target - then True - else - False diff --git a/test/python_unit_tests/rosetta/ToDate.rosetta b/test/python_unit_tests/rosetta/ToDate.rosetta deleted file mode 100644 index b49e69c..0000000 --- a/test/python_unit_tests/rosetta/ToDate.rosetta +++ /dev/null @@ -1,7 +0,0 @@ -namespace rosetta_dsl.test.semantic.date_operator : <"generate Python unit tests from Rosetta."> - -type DateOperatorTest: - a string (1..1) - b date (0..1) - condition Test: - b = a to-date \ No newline at end of file diff --git a/test/python_unit_tests/rosetta/ToDateTime.rosetta b/test/python_unit_tests/rosetta/ToDateTime.rosetta deleted file mode 100644 index b5e41b1..0000000 --- a/test/python_unit_tests/rosetta/ToDateTime.rosetta +++ /dev/null @@ -1,7 +0,0 @@ -namespace rosetta_dsl.test.semantic.date_time_operator : <"generate Python unit tests from Rosetta."> - -type DateTimeOperatorTest: - a string (1..1) - b dateTime (0..1) - condition Test: - b = a to-date-time \ No newline at end of file diff --git a/test/python_unit_tests/rosetta/ToInt.rosetta b/test/python_unit_tests/rosetta/ToInt.rosetta deleted file mode 100644 index 8f7b60d..0000000 --- a/test/python_unit_tests/rosetta/ToInt.rosetta +++ /dev/null @@ -1,7 +0,0 @@ -namespace rosetta_dsl.test.semantic.int_operator : <"generate Python unit tests from Rosetta."> - -type IntOperatorTest: - a string (1..1) - b int (0..1) - condition Test: - b = a to-int diff --git a/test/python_unit_tests/rosetta/ToTime.rosetta b/test/python_unit_tests/rosetta/ToTime.rosetta deleted file mode 100644 index b02cd21..0000000 --- a/test/python_unit_tests/rosetta/ToTime.rosetta +++ /dev/null @@ -1,7 +0,0 @@ -namespace rosetta_dsl.test.semantic.time_operator : <"generate Python unit tests from Rosetta."> - -type TimeOperatorTest: - a string (1..1) - b time (0..1) - condition Test: - b = a to-time \ No newline at end of file diff --git a/test/python_unit_tests/rosetta/ToZonedDateTime.rosetta b/test/python_unit_tests/rosetta/ToZonedDateTime.rosetta deleted file mode 100644 index 7bdef4c..0000000 --- a/test/python_unit_tests/rosetta/ToZonedDateTime.rosetta +++ /dev/null @@ -1,7 +0,0 @@ -namespace rosetta_dsl.test.semantic.zoned_date_time_operator : <"generate Python unit tests from Rosetta."> - -type ZonedDateTimeOperatorTest: - a string (1..1) - b zonedDateTime (0..1) - condition Test: - b = a to-zoned-date-time \ No newline at end of file diff --git a/test/python_unit_tests/run_python_unit_tests.sh b/test/python_unit_tests/run_python_unit_tests.sh index fe32657..4b218fb 100755 --- a/test/python_unit_tests/run_python_unit_tests.sh +++ b/test/python_unit_tests/run_python_unit_tests.sh @@ -2,13 +2,19 @@ function usage { cat </dev/null && PYEXE=python || PYEXE=python3 +# If a virtual environment is active, or if .pyenv/bin is in PATH, scrub it +# This ensures we use a system python to create the new venv +VENV_NAME=".pyenv" +CLEAN_PATH=$(echo "$PATH" | sed -E "s|[^:]*/$VENV_NAME/[^:]*:?||g") + +if command -v python3 &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python3) +elif command -v python &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python) +else + echo "Python is not installed." + error +fi + if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' >/dev/null 2>&1; then echo "Found $($PYEXE -V)" echo "Expecting at least python 3.11 - exiting!" @@ -63,14 +105,31 @@ PROJECT_ROOT_PATH="$MY_PATH/../.." PYTHON_SETUP_PATH="$MY_PATH/../python_setup" JAR_PATH="$PROJECT_ROOT_PATH/target/python-0.0.0.main-SNAPSHOT.jar" -INPUT_ROSETTA_PATH="$PROJECT_ROOT_PATH/test/python_unit_tests/rosetta" + +if [[ -n "$TEST_SUBDIR" ]]; then + INPUT_ROSETTA_PATH="$MY_PATH/$TEST_SUBDIR" + if [[ ! -d "$INPUT_ROSETTA_PATH" ]]; then + echo "Directory not found: $INPUT_ROSETTA_PATH" + exit 1 + fi +else + INPUT_ROSETTA_PATH="$MY_PATH/features" +fi + PYTHON_TESTS_TARGET_PATH="$PROJECT_ROOT_PATH/target/python-tests/unit_tests" # Validate inputs/existence if [[ ! -f "$JAR_PATH" ]]; then echo "Could not find generator jar at: $JAR_PATH" - echo "Build the jar first (e.g., mvn -q -DskipTests package) and re-run." - exit 1 + echo "Building with maven..." + if ! (cd "$PROJECT_ROOT_PATH" && mvn clean package); then + echo "Maven build failed - exiting." + exit 1 + fi + if [[ ! -f "$JAR_PATH" ]]; then + echo "Maven build completed but $JAR_PATH still missing - exiting." + exit 1 + fi fi if [[ ! -d "$INPUT_ROSETTA_PATH" ]]; then echo "Input Rune sources not found at: $INPUT_ROSETTA_PATH" @@ -89,24 +148,42 @@ if [[ $JAVA_EXIT_CODE -ne 0 ]]; then exit 1 fi -echo "***** setting up common environment" -# shellcheck disable=SC1090 -source "$PYTHON_SETUP_PATH/setup_python_env.sh" - -echo "***** activating virtual environment" VENV_NAME=".pyenv" -# shellcheck disable=SC1090 -source "$PROJECT_ROOT_PATH/$VENV_NAME/${PY_SCRIPTS}/activate" || error +VENV_PATH="$PROJECT_ROOT_PATH/$VENV_NAME" + +if [[ $REUSE_ENV -eq 1 && -d "$VENV_PATH" ]]; then + echo "***** reusing virtual environment" + if [ -z "${WINDIR}" ]; then PY_SCRIPTS='bin'; else PY_SCRIPTS='Scripts'; fi + # shellcheck disable=SC1090 + source "$VENV_PATH/${PY_SCRIPTS}/activate" || error + echo "***** removing existing python_rosetta_dsl package" + python -m pip uninstall -y python_rosetta_dsl || true +else + echo "***** setting up common environment" + # shellcheck disable=SC1090 + source "$PYTHON_SETUP_PATH/setup_python_env.sh" + + echo "***** activating virtual environment" + # shellcheck disable=SC1090 + source "$VENV_PATH/${PY_SCRIPTS}/activate" || error +fi # package and install generated Python cd "$PYTHON_TESTS_TARGET_PATH" || error -$PYEXE -m pip wheel --no-deps --only-binary :all: . || error -$PYEXE -m pip install python_rosetta_dsl-0.0.0-py3-none-any.whl +python -m pip wheel --no-deps --only-binary :all: . || error +python -m pip install python_rosetta_dsl-0.0.0-py3-none-any.whl # run tests echo "***** run unit tests" cd "$MY_PATH" || error -$PYEXE -m pytest -p no:cacheprovider "$MY_PATH" + +if [[ -n "$TEST_SUBDIR" ]]; then + TEST_TARGET="$MY_PATH/$TEST_SUBDIR" + echo "Running tests in: $TEST_TARGET" + python -m pytest -p no:cacheprovider "$TEST_TARGET" +else + python -m pytest -p no:cacheprovider "$MY_PATH" +fi if (( CLEANUP )); then echo "***** cleanup" diff --git a/test/python_unit_tests/semantics/test_binary_operators.py b/test/python_unit_tests/semantics/test_binary_operators.py deleted file mode 100644 index 96c167f..0000000 --- a/test/python_unit_tests/semantics/test_binary_operators.py +++ /dev/null @@ -1,22 +0,0 @@ -from rosetta_dsl.test.semantic.binary_op.ContainsTest import ContainsTest -from rosetta_dsl.test.semantic.binary_op.DisjointTest import DisjointTest -from rosetta_dsl.test.semantic.binary_op.EqualsTest import EqualsTest -from rosetta_dsl.test.semantic.binary_op.NotEqualsTest import NotEqualsTest - - -def test_equals(): - equalsTest=EqualsTest(aValue=5,target=5) - equalsTest.validate_model() - -def test_not_equals(): - notEqualsTets = NotEqualsTest(aValue=5, target=15) - notEqualsTets.validate_model() - -def test_contains(): - containsTest=ContainsTest(aValue=["a","b","c"],target="c") - containsTest.validate_model() - -def test_disjoint(): - disjointTest=DisjointTest(aValue=["a","b","c"], target=["d","e","f"]) - disjointTest.validate_model() -#EOF \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_class_member_access_operator.py b/test/python_unit_tests/semantics/test_class_member_access_operator.py deleted file mode 100644 index 7aa5724..0000000 --- a/test/python_unit_tests/semantics/test_class_member_access_operator.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -from rune.runtime.base_data_class import BaseDataClass -from rune.runtime.metadata import * -from rune.runtime.utils import * -from rune.runtime.conditions import * -from rosetta_dsl.test.model.class_member_access.ClassMemberAccess import ClassMemberAccess - -class_member_access = ClassMemberAccess(one=42, three=[1, 2, 3]) - -def test_attribute_single (): - assert rune_resolve_attr(class_member_access, 'one') == 42 -def test_attribute_optional (): - assert rune_resolve_attr(class_member_access, 'two') is None -def test_attribute_multi (): - assert rune_resolve_attr(class_member_access, 'three') == [1, 2, 3] -def test_attribute_single_collection (): - assert rune_resolve_attr([class_member_access, class_member_access], 'one') == [42, 42] -def test_attribute_optional_collection (): - assert rune_resolve_attr([class_member_access, class_member_access], 'two') is None -def test_attribute_multi_collection (): - assert rune_resolve_attr([class_member_access, class_member_access], 'three') == [1, 2, 3, 1, 2, 3] - -if __name__ == "__main__": - test_attribute_single () - test_attribute_optional () - test_attribute_multi () - test_attribute_single_collection () - test_attribute_optional_collection () - test_attribute_multi_collection () \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_count_operator.py b/test/python_unit_tests/semantics/test_count_operator.py deleted file mode 100644 index 79a8d16..0000000 --- a/test/python_unit_tests/semantics/test_count_operator.py +++ /dev/null @@ -1,27 +0,0 @@ -'''count operator unit tests''' -import pytest - -from rosetta_dsl.test.semantic.count_operator.ClassA import ClassA -from rosetta_dsl.test.semantic.count_operator.ClassB import ClassB -from rosetta_dsl.test.semantic.count_operator.CountTest import CountTest - -def create_classA(name:str,value:int): - return ClassA(name=name,value=value) -def create_classB (field1:list[int],field2:ClassA): - return ClassB(field1=field1,field2=field2) -def test_count_operator_passes (): - a1=create_classA("value1",1) - a2=create_classA("value2",2) - b=create_classB([],[a1,a2]) - ctest=CountTest(bValue=[b]) - ctest.validate_model() - -def test_count_operator_fails (): - b=create_classB([0,1],[]) - ctest=CountTest(bValue=[b]) - with pytest.raises(Exception): - ctest.validate_model() - -if __name__ == "__main__": - test_count_operator_passes() - test_count_operator_fails() \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_filter.py b/test/python_unit_tests/semantics/test_filter.py deleted file mode 100644 index 4e1ac18..0000000 --- a/test/python_unit_tests/semantics/test_filter.py +++ /dev/null @@ -1,13 +0,0 @@ -'''filter unit test''' -import pytest - -from rosetta_dsl.test.semantic.filter_operator.FilterItem import FilterItem -from rosetta_dsl.test.semantic.filter_operator.FilterTest import FilterTest - -def test_filter_passes(): - target = 5 - filter_test = FilterTest(fis=[FilterItem(fi=target)], target=target) - filter_test.validate_model() - -if __name__ == "__main__": - test_filter_passes() \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_flatten_operator.py b/test/python_unit_tests/semantics/test_flatten_operator.py deleted file mode 100644 index fdb4590..0000000 --- a/test/python_unit_tests/semantics/test_flatten_operator.py +++ /dev/null @@ -1,17 +0,0 @@ -'''flatten operator unit tests''' -import pytest -from rosetta_dsl.test.semantic.flatten_operator.ClassA import ClassA -from rosetta_dsl.test.semantic.flatten_operator.ClassB import ClassB -from rosetta_dsl.test.semantic.flatten_operator.FlattenTest import FlattenTest -def test_flatten_operator_passes(): - a1=ClassA(field1=0,field2=1,field3=2) - a2 = ClassA(field1=3, field2=4, field3=5) - a3= ClassA(field1=6,field2=7,field3=8) - a4=ClassA(field1=9,field2=10,field3=11) - ab1= ClassB(fieldList=[a1,a2]) - ab2=ClassB(fieldList=[a3,a4]) - ftest=FlattenTest(bValue=[ab1,ab2],field3=2) - ftest.validate_model() -# EOF - - diff --git a/test/python_unit_tests/semantics/test_flatten_operator_with_comparison.py b/test/python_unit_tests/semantics/test_flatten_operator_with_comparison.py deleted file mode 100644 index 342cb0f..0000000 --- a/test/python_unit_tests/semantics/test_flatten_operator_with_comparison.py +++ /dev/null @@ -1,12 +0,0 @@ -'''flatten operator unit tests''' -import pytest -from rosetta_dsl.test.semantic.flatten_with_comparison.Bar import Bar -from rosetta_dsl.test.semantic.flatten_with_comparison.Foo import Foo - -def test_flatten_operator_passes(): - '''Test flatten operator passes''' - bar = Bar(numbers=[1, 2, 3]) - foo = Foo(bars=[bar]) - foo.validate_model() - -# EOF \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_join_operator.py b/test/python_unit_tests/semantics/test_join_operator.py deleted file mode 100644 index 97078a9..0000000 --- a/test/python_unit_tests/semantics/test_join_operator.py +++ /dev/null @@ -1,16 +0,0 @@ -'''join unit test''' -import pytest - -from rosetta_dsl.test.semantic.join_operator.JoinTest import JoinTest - -def test_join_passes(): - sort_test= JoinTest(field1="a", field2="b") - print(sort_test) - sort_test.validate_model() - -#def test_join_fails(): -# sort_test= JoinTest(field1="c", field2="b") -# sort_test.validate_model() - -if __name__ == "__main__": - test_join_passes() \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_last_operator.py b/test/python_unit_tests/semantics/test_last_operator.py deleted file mode 100644 index 3bd7de1..0000000 --- a/test/python_unit_tests/semantics/test_last_operator.py +++ /dev/null @@ -1,12 +0,0 @@ -'''last unit test''' -import pytest - -from rosetta_dsl.test.semantic.last_operator.LastTest import LastTest - -def test_last_passes(): - last = 5 - last_test = LastTest(aValue=2, bValue=3, cValue=last, target=last) - last_test.validate_model() - -if __name__ == "__main__": - test_last_passes() diff --git a/test/python_unit_tests/semantics/test_local_conditions.py b/test/python_unit_tests/semantics/test_local_conditions.py deleted file mode 100644 index 940205d..0000000 --- a/test/python_unit_tests/semantics/test_local_conditions.py +++ /dev/null @@ -1,51 +0,0 @@ -'''Tests of the local registration of conditions''' -import inspect -import pytest -from rune.runtime.conditions import rune_local_condition -from rune.runtime.conditions import rune_execute_local_conditions -from rune.runtime.conditions import ConditionViolationError - - -def test_pre_post_conditions(): - '''Tests the registration of functions in two different registries''' - _pre_registry = {} - _post_registry = {} - self = inspect.currentframe() - - # A local PRE condition - @rune_local_condition(_pre_registry) - def some_local_condition(): - print(f'Pre {self}') - return True - - # A local POST condition - @rune_local_condition(_post_registry) - def some_local_post_condition(): - print(f'Post {self}') - return True - - # Check all PRE conditions - rune_execute_local_conditions(_pre_registry, 'Pre-condition') - - print('Some Code....') - - # Check all POST conditions - rune_execute_local_conditions(_post_registry, 'Post-condition') - - -def test_raise_local_cond(): - '''checks if exception is raised and it is of the correct type''' - _registry = {} - @rune_local_condition(_registry) - def some_failing_local_post_condition(): - return False - - with pytest.raises(ConditionViolationError): - rune_execute_local_conditions(_registry, 'condition') - - -if __name__ == '__main__': - test_pre_post_conditions() - test_raise_local_cond() - -# EOF diff --git a/test/python_unit_tests/semantics/test_min_max.py b/test/python_unit_tests/semantics/test_min_max.py deleted file mode 100644 index 700a356..0000000 --- a/test/python_unit_tests/semantics/test_min_max.py +++ /dev/null @@ -1,27 +0,0 @@ -'''min max unit tests''' -import pytest - -from rosetta_dsl.test.semantic.min_max.MaxTest import MaxTest -from rosetta_dsl.test.semantic.min_max.MinTest import MinTest - -def test_min_passes(): - min_test = MinTest(a=10) - min_test.validate_model() - -def test_min_fails (): - min_test = MinTest(a=-1) - with pytest.raises(Exception): - min_test.validate_model() - -def test_max_passes(): - max_test = MaxTest(a=1) - max_test.validate_model() - -def test_max_fails (): - max_test = MaxTest(a=100) - with pytest.raises(Exception): - max_test.validate_model() - -if __name__ == "__main__": - test_min_passes() - test_min_fails() \ No newline at end of file diff --git a/test/python_unit_tests/semantics/test_sort_operator.py b/test/python_unit_tests/semantics/test_sort_operator.py deleted file mode 100644 index 547367d..0000000 --- a/test/python_unit_tests/semantics/test_sort_operator.py +++ /dev/null @@ -1,11 +0,0 @@ -'''sort unit test''' -import pytest - -from rosetta_dsl.test.semantic.sort_operator.SortTest import SortTest - -def test_switch_passes(): - sort_test= SortTest() - sort_test.validate_model() - -if __name__ == "__main__": - test_switch_passes() diff --git a/test/python_unit_tests/semantics/test_sum_operator.py b/test/python_unit_tests/semantics/test_sum_operator.py deleted file mode 100644 index d8545ca..0000000 --- a/test/python_unit_tests/semantics/test_sum_operator.py +++ /dev/null @@ -1,11 +0,0 @@ -'''sort unit test''' -import pytest - -from rosetta_dsl.test.semantic.sum_operator.SumTest import SumTest - -def test_sum_passes(): - sum_test = SumTest(aValue=2, bValue=3, target=5) - sum_test.validate_model() - -if __name__ == "__main__": - test_sum_passes() diff --git a/test/python_unit_tests/semantics/test_to_date_operator.py b/test/python_unit_tests/semantics/test_to_date_operator.py deleted file mode 100644 index ff0fea5..0000000 --- a/test/python_unit_tests/semantics/test_to_date_operator.py +++ /dev/null @@ -1,20 +0,0 @@ -'''to-date unit tests''' -from datetime import date - -import pytest - -from rosetta_dsl.test.semantic.date_operator.DateOperatorTest import DateOperatorTest - -def test_to_date_passes(): - '''no doc''' - to_date_test= DateOperatorTest(a="2025-05-26",b=date(2025, 5, 26)) - to_date_test.validate_model() -def test_to_date_invalid_format_fails(): - '''no doc''' - to_date_test= DateOperatorTest(a="2025/05/26", b=date(2025,5,26)) - with pytest.raises(Exception): - to_date_test.validate_model() - -if __name__ == "__main__": - test_to_date_passes() - test_to_date_invalid_format_fails() diff --git a/test/python_unit_tests/semantics/test_to_date_time.py b/test/python_unit_tests/semantics/test_to_date_time.py deleted file mode 100644 index 239e921..0000000 --- a/test/python_unit_tests/semantics/test_to_date_time.py +++ /dev/null @@ -1,22 +0,0 @@ -'''to-date-time unit tests''' - -import datetime - -import pytest - -from rosetta_dsl.test.semantic.date_time_operator.DateTimeOperatorTest import DateTimeOperatorTest - -def test_to_date_time_passes(): - '''no doc''' - to_date_time_test= DateTimeOperatorTest(a="2025-05-26 14:30:00",b=datetime.datetime(2025, 5, 26,14,30,0)) - to_date_time_test.validate_model() - -def test_to_date_time_fails(): - '''no doc''' - to_date_time_test=DateTimeOperatorTest(a="2025-05-26 14-30-00",b=datetime.datetime(2025, 5, 26,14,30,0)) - with pytest.raises(Exception): - to_date_time_test.validate_model() - -if __name__ == "__main__": - test_to_date_time_passes() - test_to_date_time_fails() diff --git a/test/python_unit_tests/semantics/test_to_int.py b/test/python_unit_tests/semantics/test_to_int.py deleted file mode 100644 index b8b8319..0000000 --- a/test/python_unit_tests/semantics/test_to_int.py +++ /dev/null @@ -1,21 +0,0 @@ -'''to-int unit tests''' - -import pytest - -from rosetta_dsl.test.semantic.int_operator.IntOperatorTest import IntOperatorTest - - -def test_to_int_passes(): - '''no doc''' - to_int_test = IntOperatorTest(a="1",b=1) - to_int_test.validate_model() - -def test_to_int_fails(): - '''no doc''' - to_int_test= IntOperatorTest(a="a",b=1) - with pytest.raises(Exception): - to_int_test.validate_model() - -if __name__ == "__main__": - test_to_int_passes() - test_to_int_fails() diff --git a/test/python_unit_tests/semantics/test_to_time.py b/test/python_unit_tests/semantics/test_to_time.py deleted file mode 100644 index cb808a0..0000000 --- a/test/python_unit_tests/semantics/test_to_time.py +++ /dev/null @@ -1,23 +0,0 @@ -'''to-date-time unit tests''' - -import datetime -import time - -import pytest - -from rosetta_dsl.test.semantic.time_operator.TimeOperatorTest import TimeOperatorTest - -def test_to_time_passes(): - '''no doc''' - to_time_test= TimeOperatorTest(a="11:45:23",b=datetime.time(11,45,23)) - to_time_test.validate_model() - -def test_to_time_fails(): - '''no doc''' - to_date_time_test=TimeOperatorTest(a="14-30-00",b=datetime.time(14,30,0)) - with pytest.raises(Exception): - to_date_time_test.validate_model() - -if __name__ == "__main__": - test_to_time_passes() - test_to_time_fails() diff --git a/test/python_unit_tests/semantics/test_to_zoned_date_time.py b/test/python_unit_tests/semantics/test_to_zoned_date_time.py deleted file mode 100644 index a8303fa..0000000 --- a/test/python_unit_tests/semantics/test_to_zoned_date_time.py +++ /dev/null @@ -1,42 +0,0 @@ -'''to-zoned-date-time unit tests''' -import datetime -from zoneinfo import ZoneInfo - -import pytest - -from rosetta_dsl.test.semantic.zoned_date_time_operator.ZonedDateTimeOperatorTest import ZonedDateTimeOperatorTest - - -def test_to_zoned_date_time_offset_and_time_zone(): - to_zoned_date_time_test= ZonedDateTimeOperatorTest(a="2025-07-07 15:30:00 +0100 Europe/Lisbon",b=datetime.datetime(2025,7, 7, 15, 30, 0, tzinfo=ZoneInfo("Europe/Lisbon"))) - to_zoned_date_time_test.validate_model() - -def test_to_zoned_date_time_only_time_zone(): - to_zoned_date_time_test= ZonedDateTimeOperatorTest(a="2025-07-07 15:30:00 Europe/Lisbon",b=datetime.datetime(2025,7, 7, 15, 30, 0, tzinfo=ZoneInfo("Europe/Lisbon"))) - to_zoned_date_time_test.validate_model() - -def test_to_zoned_date_time_only_time_zone2(): - to_zoned_date_time_test= ZonedDateTimeOperatorTest(a="2025-03-15 12:00:00 Zulu",b=datetime.datetime(2025,3, 15, 12, 0, 0, tzinfo=ZoneInfo("Zulu"))) - to_zoned_date_time_test.validate_model() - -def test_to_zoned_date_time_only_offset(): - to_zoned_date_time_test= ZonedDateTimeOperatorTest(a="2025-05-26 14:30:00 +0900",b=datetime.datetime(2025,5, 26, 14, 30, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=9)))) - to_zoned_date_time_test.validate_model() - -def test_to_zoned_date_time_fails(): - to_zoned_date_time_test= ZonedDateTimeOperatorTest(a="2025-12-15 15:30:00 +0100 Europe/Lisbon",b=datetime.datetime(2025, 5, 26, 14, 30, 0, tzinfo=ZoneInfo("Europe/Lisbon"))) - with pytest.raises(Exception): - to_zoned_date_time_test.validate_model() - -def test_to_zoned_date_time_fails_format(): - to_zoned_date_time_test= ZonedDateTimeOperatorTest(a="2025-05-26 14:30:00 +09x0",b=datetime.datetime(2025, 5, 26, 14, 30, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=9)))) - with pytest.raises(Exception): - to_zoned_date_time_test.validate_model() - -if __name__ == "__main__": - test_to_zoned_date_time_offset_and_time_zone() - test_to_zoned_date_time_only_time_zone() - test_to_zoned_date_time_only_time_zone2() - test_to_zoned_date_time_only_offset() - test_to_zoned_date_time_fails() - test_to_zoned_date_time_fails_format() diff --git a/test/serialization_tests/run_serialization_tests.sh b/test/serialization_tests/run_serialization_tests.sh index c1baa28..1c34044 100755 --- a/test/serialization_tests/run_serialization_tests.sh +++ b/test/serialization_tests/run_serialization_tests.sh @@ -13,11 +13,24 @@ function error export PYTHONDONTWRITEBYTECODE=1 -type -P python > /dev/null && PYEXE=python || PYEXE=python3 -if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' > /dev/null 2>&1; then - echo "Found $($PYEXE -V)" - echo "Expecting at least python 3.11 - exiting!" - exit 1 +# If a virtual environment is active, or if .pyenv/bin is in PATH, scrub it +# This ensures we use a system python to create the new venv +VENV_NAME=".pyenv" +CLEAN_PATH=$(echo "$PATH" | sed -E "s|[^:]*/$VENV_NAME/[^:]*:?||g") + +if command -v python3 &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python3) +elif command -v python &>/dev/null; then + PYEXE=$(PATH="$CLEAN_PATH" command -v python) +else + echo "Python is not installed." + error +fi + +if ! $PYEXE -c 'import sys; assert sys.version_info >= (3,11)' >/dev/null 2>&1; then + echo "Found $($PYEXE -V)" + echo "Expecting at least python 3.11 - exiting!" + exit 1 fi # Parse args: default is skip; pass 'getsource' to fetch sources @@ -55,8 +68,15 @@ fi # Validate inputs/existence if [[ ! -f "$JAR_PATH" ]]; then echo "Could not find generator jar at: $JAR_PATH" - echo "Build the jar first (e.g., mvn -q -DskipTests package) and re-run." - exit 1 + echo "Building with maven..." + if ! (cd "$PROJECT_ROOT_PATH" && mvn clean package); then + echo "Maven build failed - exiting." + exit 1 + fi + if [[ ! -f "$JAR_PATH" ]]; then + echo "Maven build completed but $JAR_PATH still missing - exiting." + exit 1 + fi fi if [[ ! -d "$SERIALIZATION_SOURCE_ROOT" ]]; then echo "Serialization sources not found at: $SERIALIZATION_SOURCE_ROOT" @@ -81,19 +101,19 @@ source "$PYTHON_SETUP_PATH/setup_python_env.sh" echo "***** activating virtual environment" VENV_NAME=".pyenv" -# PY_SCRIPTS is set by setup_python_env.sh +if [ -z "${WINDIR}" ]; then PY_SCRIPTS='bin'; else PY_SCRIPTS='Scripts'; fi source "$PROJECT_ROOT_PATH/$VENV_NAME/${PY_SCRIPTS}/activate" || error echo "***** Build and Install Helper" cd "$MY_PATH/test_helper" || error -$PYEXE -m pip wheel --no-deps --only-binary :all: . || error -$PYEXE -m pip install test_helper-0.0.0-py3-none-any.whl || error +python -m pip wheel --no-deps --only-binary :all: . || error +python -m pip install test_helper-0.0.0-py3-none-any.whl || error rm -f test_helper-0.0.0-py3-none-any.whl echo "***** Build and Install Generated Unit Tests" cd "$PYTHON_TESTS_TARGET_PATH" || error rm -f ./*.whl -$PYEXE -m pip wheel --no-deps --only-binary :all: . || error +python -m pip wheel --no-deps --only-binary :all: . || error WHEEL_FILE=$(ls -1 ./*.whl 2>/dev/null | head -n 1) if [[ -z "$WHEEL_FILE" ]]; then echo "No wheel produced in $PYTHON_TESTS_TARGET_PATH" @@ -101,12 +121,12 @@ if [[ -z "$WHEEL_FILE" ]]; then source "$PYTHON_SETUP_PATH/cleanup_python_env.sh" exit 1 fi -$PYEXE -m pip install "$WHEEL_FILE" || error +python -m pip install "$WHEEL_FILE" || error # run tests echo "***** run tests" cd "$MY_PATH" || error -$PYEXE -m pytest -p no:cacheprovider . || error +python -m pytest -p no:cacheprovider . || error echo "***** cleanup" deactivate