From b9c914452b05ff205e8d093ed983a59a1e0ece1c Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:31:34 -0700 Subject: [PATCH 01/12] Adds new model changes for `Structure` (#138) * adds `Display` for `FullyQualifiedTypeReference` (For Java templates) * adds `TryFrom` for `FullyQualifiedTypeReference` (Used ijn templates) * adds utilities for model changes * adds a `data_model_store` which stores all the data model nodes based on their `FullyQualifiedTypeReference` * adds changes for using the new `DataModelNode` * Adds `From` impls for builder errors to be converted to `CodeGenError` * Modifies test cases for TODOs and removes Rust support * Adds template changes for `Struct` --- .../mismatched_sequence_type.ion | 9 - .../bad/nested_struct/mismatched_type.ion | 1 - .../input/bad/scalar/mismatched_type.ion | 1 - .../sequence/mismatched_sequence_element.ion | 1 - .../bad/sequence/mismatched_sequence_type.ion | 1 - .../mismatched_sequence_element.ion | 8 - .../mismatched_sequence_type.ion | 7 - .../struct_with_fields/mismatched_type.ion | 1 - .../input/good/nested_struct/empty_values.ion | 1 - .../input/good/nested_struct/valid_fields.ion | 1 - .../nested_struct/valid_unordered_fields.ion | 1 - .../input/good/scalar/empty_value.ion | 2 - .../input/good/scalar/valid_value.ion | 2 - .../input/good/sequence/empty_sequence.ion | 1 - .../input/good/sequence/valid_elements.ion | 1 - .../good/struct_with_fields/empty_values.ion | 1 - .../good/struct_with_fields/valid_fields.ion | 1 - .../valid_unordered_fields.ion | 1 - .../test/java/org/example/CodeGenTest.java | 121 +-- code-gen-projects/schema/nested_struct.isl | 1 - code-gen-projects/schema/scalar.isl | 4 - code-gen-projects/schema/sequence.isl | 5 - .../schema/struct_with_fields.isl | 1 - src/bin/ion/commands/generate/context.rs | 112 +-- src/bin/ion/commands/generate/generator.rs | 840 ++++++++---------- src/bin/ion/commands/generate/mod.rs | 18 +- src/bin/ion/commands/generate/model.rs | 167 +++- src/bin/ion/commands/generate/result.rs | 45 + .../generate/templates/java/class.templ | 108 ++- .../generate/templates/java/nested_type.templ | 239 +---- src/bin/ion/commands/generate/utils.rs | 163 ++-- tests/cli.rs | 162 +--- tests/code-gen-tests.rs | 49 +- 33 files changed, 791 insertions(+), 1285 deletions(-) delete mode 100644 code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion delete mode 100644 code-gen-projects/input/bad/scalar/mismatched_type.ion delete mode 100644 code-gen-projects/input/bad/sequence/mismatched_sequence_element.ion delete mode 100644 code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion delete mode 100644 code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion delete mode 100644 code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion delete mode 100644 code-gen-projects/input/good/scalar/empty_value.ion delete mode 100644 code-gen-projects/input/good/scalar/valid_value.ion delete mode 100644 code-gen-projects/input/good/sequence/empty_sequence.ion delete mode 100644 code-gen-projects/input/good/sequence/valid_elements.ion delete mode 100644 code-gen-projects/schema/scalar.isl delete mode 100644 code-gen-projects/schema/sequence.isl diff --git a/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion b/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion deleted file mode 100644 index aa1c3078..00000000 --- a/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion +++ /dev/null @@ -1,9 +0,0 @@ -// nested struct with mismatched sequence type -{ - A: "hello", - B: 12, - C: { - D: false, - E: (1 2 3) // expected list - } -} \ No newline at end of file diff --git a/code-gen-projects/input/bad/nested_struct/mismatched_type.ion b/code-gen-projects/input/bad/nested_struct/mismatched_type.ion index 30ae03b8..e8c48dba 100644 --- a/code-gen-projects/input/bad/nested_struct/mismatched_type.ion +++ b/code-gen-projects/input/bad/nested_struct/mismatched_type.ion @@ -4,6 +4,5 @@ B: 12, C: { D: 1e0, // expected type: bool - E: [1, 2, 3] } } diff --git a/code-gen-projects/input/bad/scalar/mismatched_type.ion b/code-gen-projects/input/bad/scalar/mismatched_type.ion deleted file mode 100644 index 39c18a90..00000000 --- a/code-gen-projects/input/bad/scalar/mismatched_type.ion +++ /dev/null @@ -1 +0,0 @@ -12 // expected string \ No newline at end of file diff --git a/code-gen-projects/input/bad/sequence/mismatched_sequence_element.ion b/code-gen-projects/input/bad/sequence/mismatched_sequence_element.ion deleted file mode 100644 index c9736c10..00000000 --- a/code-gen-projects/input/bad/sequence/mismatched_sequence_element.ion +++ /dev/null @@ -1 +0,0 @@ -[1, 2, 3] // expected string \ No newline at end of file diff --git a/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion b/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion deleted file mode 100644 index bd2fc7e6..00000000 --- a/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion +++ /dev/null @@ -1 +0,0 @@ -("foo" "bar" "baz") // expected list \ No newline at end of file diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion deleted file mode 100644 index bcb578df..00000000 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion +++ /dev/null @@ -1,8 +0,0 @@ -// struct with mismatched sequence element -{ - A: "hello", - B: 12, - C: (1 2 3), - D: 10e2 -} - diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion deleted file mode 100644 index 300a99f5..00000000 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion +++ /dev/null @@ -1,7 +0,0 @@ -// simple struct with type mismatched sequence type -{ - A: "hello", - B: 12, - C: ["foo", "bar", "baz"], // expected sexp - D: 10e2 -} diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion index 3cad5335..9ff644d8 100644 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion @@ -2,7 +2,6 @@ { A: "hello", B: false, // expected field type: int - C: ("foo" "bar" "baz"), D: 10e2 } diff --git a/code-gen-projects/input/good/nested_struct/empty_values.ion b/code-gen-projects/input/good/nested_struct/empty_values.ion index 798cc96d..7bca3c4f 100644 --- a/code-gen-projects/input/good/nested_struct/empty_values.ion +++ b/code-gen-projects/input/good/nested_struct/empty_values.ion @@ -2,7 +2,6 @@ { C: { D: false, - E: [], }, A: "", B: 0, diff --git a/code-gen-projects/input/good/nested_struct/valid_fields.ion b/code-gen-projects/input/good/nested_struct/valid_fields.ion index f63ab8ab..391c8cd1 100644 --- a/code-gen-projects/input/good/nested_struct/valid_fields.ion +++ b/code-gen-projects/input/good/nested_struct/valid_fields.ion @@ -4,6 +4,5 @@ B: 12, C: { D: false, - E: [1, 2, 3] } } diff --git a/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion index cca47176..da21667b 100644 --- a/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion +++ b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion @@ -4,7 +4,6 @@ A: "hello", C: { D: false, - E: [1, 2, 3] } } diff --git a/code-gen-projects/input/good/scalar/empty_value.ion b/code-gen-projects/input/good/scalar/empty_value.ion deleted file mode 100644 index b4fa0125..00000000 --- a/code-gen-projects/input/good/scalar/empty_value.ion +++ /dev/null @@ -1,2 +0,0 @@ -// empty string -"" \ No newline at end of file diff --git a/code-gen-projects/input/good/scalar/valid_value.ion b/code-gen-projects/input/good/scalar/valid_value.ion deleted file mode 100644 index 554b705d..00000000 --- a/code-gen-projects/input/good/scalar/valid_value.ion +++ /dev/null @@ -1,2 +0,0 @@ -// a scalar value of string type -"Hello World!" \ No newline at end of file diff --git a/code-gen-projects/input/good/sequence/empty_sequence.ion b/code-gen-projects/input/good/sequence/empty_sequence.ion deleted file mode 100644 index 0637a088..00000000 --- a/code-gen-projects/input/good/sequence/empty_sequence.ion +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/code-gen-projects/input/good/sequence/valid_elements.ion b/code-gen-projects/input/good/sequence/valid_elements.ion deleted file mode 100644 index 43892360..00000000 --- a/code-gen-projects/input/good/sequence/valid_elements.ion +++ /dev/null @@ -1 +0,0 @@ -["foo", "bar", "baz"] \ No newline at end of file diff --git a/code-gen-projects/input/good/struct_with_fields/empty_values.ion b/code-gen-projects/input/good/struct_with_fields/empty_values.ion index a5c13861..2f0f96c4 100644 --- a/code-gen-projects/input/good/struct_with_fields/empty_values.ion +++ b/code-gen-projects/input/good/struct_with_fields/empty_values.ion @@ -1,6 +1,5 @@ // struct with empty list, empty string and zeros { - C: (), A: "", B: 0, D: 0e0, diff --git a/code-gen-projects/input/good/struct_with_fields/valid_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_fields.ion index 1ccd7777..80842565 100644 --- a/code-gen-projects/input/good/struct_with_fields/valid_fields.ion +++ b/code-gen-projects/input/good/struct_with_fields/valid_fields.ion @@ -2,6 +2,5 @@ { A: "hello", B: 12, - C: ("foo" "bar" "baz"), D: 10e2 } diff --git a/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion index 62d974f9..33106965 100644 --- a/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion +++ b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion @@ -1,7 +1,6 @@ // struct with unordered fields { - C: ("foo" "bar" "baz"), A: "hello", B: 12, D: 10e2, diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 8519ac99..f1d2323d 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -22,22 +22,16 @@ class CodeGenTest { private static final IonLoader ionLoader = ionSystem.getLoader(); @Test void getterAndSetterTestForStructWithFields() { - ArrayList a = new ArrayList(); - a.add("foo"); - a.add("bar"); - a.add("baz"); StructWithFields s = new StructWithFields(); // set all the fields of `StructWithFields` s.setA("hello"); s.setB(12); - s.setC(a); s.setD(10e2); // getter tests for `StructWithFields` assertEquals("hello", s.getA(), "s.getA() should return \"hello\""); assertEquals(12, s.getB(), "s.getB() should return `12`"); - assertEquals(3, s.getC().size(), "s.getC() should return ArrayList fo size 3"); assertEquals(10e2, s.getD(), "s.getD() should return `10e2`"); // setter tests for `StructWithFields` @@ -45,30 +39,23 @@ class CodeGenTest { assertEquals("hi", s.getA(), "s.getA() should return \"hi\""); s.setB(6); assertEquals(6, s.getB(), "s.getB() should return `6`"); - s.setC(new ArrayList()); - assertEquals(true, s.getC().isEmpty(), "s.getC().isEmpty() should return `true`"); s.setD(11e3); assertEquals(11e3 ,s.getD(), "s.getD() should return `11e3`"); } @Test void getterAndSetterTestForNestedStruct() { // getter tests for `NestedStruct` - ArrayList a = new ArrayList(); - a.add(1); - a.add(2); - a.add(3); NestedStruct n = new NestedStruct(); // set all the fields of `NestedStruct` n.setA("hello"); n.setB(12); - n.setC(false, a); + n.setC(false); // getter tests for `NestedStruct` assertEquals("hello", n.getA(), "n.getA() should return \"hello\""); assertEquals(12, n.getB(), "n.getB() should return `12`"); assertEquals(false, n.getC().getD(), "n.getC().getD() should return `false`"); - assertEquals(3, n.getC().getE().size(), "n.getC().getE().size() should return ArrayList fo size 3"); // setter tests for `NestedStruct` n.setA("hi"); @@ -77,40 +64,6 @@ class CodeGenTest { assertEquals(6, n.getB(), "s.getB() should return `6`"); n.getC().setD(true); assertEquals(true, n.getC().getD(), "s.getC().getD() should return `true`"); - n.getC().setE(new ArrayList()); - assertEquals(0, n.getC().getE().size(), "s.getC().getE().size() should return ArrayList fo size 0"); - } - -@Test void getterAndSetterTestForSequence() { - ArrayList a = new ArrayList(); - a.add("foo"); - a.add("bar"); - a.add("baz"); - Sequence s = new Sequence(); - - // set all the fields of `Sequence` - s.setValue(a); - - // getter tests for `Sequence` - assertEquals(3, s.getValue().size(), "s.getValue().size() should return ArrayList fo size 3"); - - // setter tests for `Sequence` - s.setValue(new ArrayList()); - assertEquals(true, s.getValue().isEmpty(), "s.getValue().isEmpty() should return `true`"); - } - - @Test void getterAndSetterTestForScalar() { - Scalar s = new Scalar(); - - // set all the fields of `Scalar` - s.setValue("hello"); - - // getter tests for `Scalar` - assertEquals("hello", s.getValue(), "s.getValue() should return \"hello\""); - - // setter tests for `Scalar` - s.setValue("hi"); - assertEquals("hi", s.getValue(), "s.getValue() should return \"hi\""); } @Test void roundtripGoodTestForStructWithFields() throws IOException { @@ -184,76 +137,4 @@ class CodeGenTest { } } } - - @Test void roundtripGoodTestForSequence() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/good/sequence"); - String[] fileNames = dir.list(); - for (String fileName : fileNames) { - File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { - reader.next(); - Sequence s = Sequence.readFrom(reader); - IonWriter writer = b.build(out); - s.writeTo(writer); - writer.close(); - assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray())); - } - } - } - - @Test void roundtripBadTestForSequence() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/bad/sequence"); - String[] fileNames = dir.list(); - for (String fileName : fileNames) { - File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { - reader.next(); - assertThrows(Throwable.class, () -> { Sequence s = Sequence.readFrom(reader); }); - } - } - } - - @Test void roundtripGoodTestForScalar() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/good/scalar"); - String[] fileNames = dir.list(); - for (String fileName : fileNames) { - File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { - reader.next(); - Scalar s = Scalar.readFrom(reader); - IonWriter writer = b.build(out); - s.writeTo(writer); - writer.close(); - assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray())); - } - } - } - - @Test void roundtripBadTestForScalar() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/bad/scalar"); - String[] fileNames = dir.list(); - for (String fileName : fileNames) { - File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { - reader.next(); - assertThrows(Throwable.class, () -> { Scalar s = Scalar.readFrom(reader); }); - } - } - } } diff --git a/code-gen-projects/schema/nested_struct.isl b/code-gen-projects/schema/nested_struct.isl index 39664ea9..cceac91c 100644 --- a/code-gen-projects/schema/nested_struct.isl +++ b/code-gen-projects/schema/nested_struct.isl @@ -8,7 +8,6 @@ type::{ type: struct, fields: { D: bool, - E: { type: list, element: int } } } } diff --git a/code-gen-projects/schema/scalar.isl b/code-gen-projects/schema/scalar.isl deleted file mode 100644 index cc3957aa..00000000 --- a/code-gen-projects/schema/scalar.isl +++ /dev/null @@ -1,4 +0,0 @@ -type::{ - name: scalar, - type: string -} \ No newline at end of file diff --git a/code-gen-projects/schema/sequence.isl b/code-gen-projects/schema/sequence.isl deleted file mode 100644 index 23affb38..00000000 --- a/code-gen-projects/schema/sequence.isl +++ /dev/null @@ -1,5 +0,0 @@ -type::{ - name: sequence, - type: list, - element: string -} \ No newline at end of file diff --git a/code-gen-projects/schema/struct_with_fields.isl b/code-gen-projects/schema/struct_with_fields.isl index 40a4d371..870e10c0 100644 --- a/code-gen-projects/schema/struct_with_fields.isl +++ b/code-gen-projects/schema/struct_with_fields.isl @@ -4,7 +4,6 @@ type::{ fields: { A: string, B: int, - C: { element: string, type: sexp }, D: float, } } diff --git a/src/bin/ion/commands/generate/context.rs b/src/bin/ion/commands/generate/context.rs index a6e538fa..3eec1fa7 100644 --- a/src/bin/ion/commands/generate/context.rs +++ b/src/bin/ion/commands/generate/context.rs @@ -1,126 +1,24 @@ +use crate::commands::generate::model::DataModelNode; use serde::Serialize; -use std::fmt::{Display, Formatter}; /// Represents a context that will be used for code generation pub struct CodeGenContext { - // Initially the abstract_data_type field is set to None. - // Once an ISL type definition is mapped to an abstract data type this will have Some value. - pub(crate) abstract_data_type: Option, + // Represents the nested types for the current abstract data type + pub(crate) nested_types: Vec, } impl CodeGenContext { pub fn new() -> Self { Self { - abstract_data_type: None, + nested_types: vec![], } } - - pub fn with_abstract_data_type(&mut self, abstract_data_type: AbstractDataType) { - self.abstract_data_type = Some(abstract_data_type); - } -} - -/// A target-language-agnostic data type that determines which template(s) to use for code generation. -#[derive(Debug, Clone, PartialEq, Serialize)] -pub enum AbstractDataType { - // A scalar value (e.g. a string or integer or user defined type) - // e.g. Given below ISL, - // ``` - // type::{ - // name: value_type, - // type: int - // } - // ``` - // Corresponding abstract type in Rust would look like following: - // ``` - // struct ValueType { - // value: i64 - // } - // ``` - Value, - // A series of zero or more values whose type is described by the nested `element_type` - // and sequence type is described by nested `sequence_type` (e.g. List or SExp). - // If there is no `element` constraint present in schema type then `element_type` will be None. - // If there is no `type` constraint present in schema type then `sequence_type` will be None. - // e.g. Given below ISL, - // ``` - // type::{ - // name: sequence_type, - // element: int - // } - // ``` - // Corresponding abstract type in Rust would look like following: - // ``` - // struct SequenceType { - // value: Vec - // } - // ``` - Sequence { - element_type: Option, - sequence_type: Option, - }, - // A collection of field name/value pairs (e.g. a map) - // the nested boolean represents whether the struct has closed fields or not - // e.g. Given below ISL, - // ``` - // type::{ - // name: struct_type, - // fields: { - // a: int, - // b: string, - // } - // } - // ``` - // Corresponding abstract type in Rust would look like following: - // ``` - // struct StructType { - // a: i64, - // b: String, - // } - // ``` - Structure(bool), -} - -impl AbstractDataType { - pub fn element_type(&self) -> Option { - match self { - AbstractDataType::Sequence { element_type, .. } => element_type.to_owned(), - _ => None, - } - } - - pub fn sequence_type(&self) -> Option { - match self { - AbstractDataType::Sequence { sequence_type, .. } => sequence_type.to_owned(), - _ => None, - } - } - - pub fn is_content_closed(&self) -> Option { - match self { - AbstractDataType::Structure(content_closed) => Some(*content_closed), - _ => None, - } - } -} - -impl Display for AbstractDataType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - AbstractDataType::Value => "single value struct", - AbstractDataType::Sequence { .. } => "sequence value struct", - AbstractDataType::Structure(_) => "struct", - } - ) - } } /// Represents a sequenced type which could either be a list or s-expression. /// This is used by `AbstractDataType` to represent sequence type for `Sequence` variant. #[derive(Debug, Clone, PartialEq, Serialize)] +#[allow(dead_code)] pub enum SequenceType { List, SExp, diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 41f44791..658518c4 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,8 +1,14 @@ -use crate::commands::generate::context::{AbstractDataType, CodeGenContext, SequenceType}; -use crate::commands::generate::result::{invalid_abstract_data_type_error, CodeGenResult}; +use crate::commands::generate::context::CodeGenContext; +use crate::commands::generate::model::{ + AbstractDataType, DataModelNode, FieldPresence, FieldReference, FullyQualifiedTypeReference, + StructureBuilder, +}; +use crate::commands::generate::result::{ + invalid_abstract_data_type_error, invalid_abstract_data_type_raw_error, CodeGenResult, +}; use crate::commands::generate::templates; -use crate::commands::generate::utils::{Field, JavaLanguage, Language, NestedType, RustLanguage}; use crate::commands::generate::utils::{IonSchemaType, Template}; +use crate::commands::generate::utils::{JavaLanguage, Language, RustLanguage}; use convert_case::{Case, Casing}; use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue}; use ion_schema::isl::isl_type::IslType; @@ -23,14 +29,15 @@ pub(crate) struct CodeGenerator<'a, L: Language> { pub(crate) tera: Tera, output: &'a Path, // This field is used by Java code generation to get the namespace for generated code. - // For Rust code generation, this will be set to None. - namespace: Option<&'a str>, + current_type_fully_qualified_name: Vec, // Represents a counter for naming nested type definitions pub(crate) nested_type_counter: usize, + pub(crate) data_model_store: HashMap, phantom: PhantomData, } impl<'a> CodeGenerator<'a, RustLanguage> { + #[allow(dead_code)] pub fn new(output: &'a Path) -> CodeGenerator { let mut tera = Tera::default(); // Add all templates using `rust_templates` module constants @@ -63,16 +70,18 @@ impl<'a> CodeGenerator<'a, RustLanguage> { Self { output, - namespace: None, + // Currently Rust code generation doesn't have a `--namespace` option available on the CLI, hence this is default set as an empty vector. + current_type_fully_qualified_name: vec![], nested_type_counter: 0, tera, phantom: PhantomData, + data_model_store: HashMap::new(), } } } impl<'a> CodeGenerator<'a, JavaLanguage> { - pub fn new(output: &'a Path, namespace: &'a str) -> CodeGenerator<'a, JavaLanguage> { + pub fn new(output: &'a Path, namespace: Vec) -> CodeGenerator<'a, JavaLanguage> { let mut tera = Tera::default(); // Add all templates using `java_templates` module constants // This allows packaging binary without the need of template resources. @@ -86,16 +95,17 @@ impl<'a> CodeGenerator<'a, JavaLanguage> { .unwrap(); Self { output, - namespace: Some(namespace), + current_type_fully_qualified_name: namespace, nested_type_counter: 0, tera, phantom: PhantomData, + data_model_store: HashMap::new(), } } } impl<'a, L: Language + 'static> CodeGenerator<'a, L> { - /// Represents a [tera] filter that converts given tera string value to [upper camel case]. + /// A [tera] filter that converts given tera string value to [upper camel case]. /// Returns error if the given value is not a string. /// /// For more information: @@ -135,7 +145,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { )) } - /// Represents a [tera] filter that converts given tera string value to [snake case]. + /// A [tera] filter that converts given tera string value to [snake case]. /// Returns error if the given value is not a string. /// /// For more information: @@ -154,7 +164,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { )) } - /// Represents a [tera] filter that return true if the value is a built in type, otherwise returns false. + /// A [tera] filter that return true if the value is a built in type, otherwise returns false. /// /// For more information: /// @@ -164,12 +174,47 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { _map: &HashMap, ) -> Result { Ok(tera::Value::Bool(L::is_built_in_type( - value.as_str().ok_or(tera::Error::msg( - "`is_built_in_type` called with non-String Value", - ))?, + value + .as_str() + .ok_or(tera::Error::msg( + "Required string for the `is_built_in_type` filter", + ))? + .to_string(), ))) } + /// A [tera] filter that return field names for the given object. + /// + /// For more information: + /// + /// [tera]: + pub fn field_names( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + Ok(tera::Value::Array( + value + .as_object() + .ok_or(tera::Error::msg("Required object for `keys` filter"))? + .keys() + .map(|k| tera::Value::String(k.to_string())) + .collect(), + )) + } + + /// A [tera] filter that returns a string representation of a tera object i.e. `FullyQualifiedTypeReference`. + /// + /// For more information: + /// + /// [tera]: + pub fn fully_qualified_type_name( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + let fully_qualified_type_ref: &FullyQualifiedTypeReference = &value.try_into()?; + Ok(tera::Value::String(fully_qualified_type_ref.to_string())) + } + /// Generates code for all the schemas in given authorities pub fn generate_code_for_authorities( &mut self, @@ -212,12 +257,18 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // Register a tera filter that can be used to see if a type is built in data type or not self.tera .register_filter("is_built_in_type", Self::is_built_in_type); + self.tera.register_filter("field_names", Self::field_names); + self.tera + .register_filter("fully_qualified_type_name", Self::fully_qualified_type_name); // Iterate through the ISL types, generate an abstract data type for each for isl_type in schema.types() { // unwrap here is safe because all the top-level type definition always has a name - let isl_type_name = isl_type.name().unwrap(); - self.generate_abstract_data_type(isl_type_name, isl_type)?; + let isl_type_name = isl_type.name().unwrap().to_string(); + self.generate_abstract_data_type(&isl_type_name, isl_type)?; + // Since the fully qualified name of this generator represents the current fully qualified name, + // remove it before generating code for the next ISL type. + self.current_type_fully_qualified_name.pop(); } Ok(()) @@ -229,101 +280,116 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { &mut self, type_name: &String, isl_type: &IslType, - nested_types: &mut Vec, - ) -> CodeGenResult<()> { - // Add an object called `nested_types` in tera context - // This will have a list of `nested_type` where each will include fields, a target_kind_name and abstract_data_type - let mut tera_fields = vec![]; + parent_code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult { let mut code_gen_context = CodeGenContext::new(); - let mut nested_anonymous_types = vec![]; - let constraints = isl_type.constraints(); - for constraint in constraints { - self.map_constraint_to_abstract_data_type( - &mut nested_anonymous_types, - &mut tera_fields, - constraint, - &mut code_gen_context, - )?; - } - - // TODO: verify the `occurs` value within a field, by default the fields are optional. - if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { - // Add the nested type into parent type's tera context - nested_types.push(NestedType { - target_kind_name: type_name.to_case(Case::UpperCamel), - fields: tera_fields, - abstract_data_type: abstract_data_type.to_owned(), - nested_types: nested_anonymous_types, - }); - } else { - return invalid_abstract_data_type_error( - "Can not determine abstract data type, specified constraints do not map to an abstract data type.", - ); - } - - Ok(()) + let mut data_model_node = self.convert_isl_type_def_to_data_model_node( + type_name, + isl_type, + &mut code_gen_context, + )?; + + // add this nested type to parent code gene context's current list of nested types + parent_code_gen_context + .nested_types + .push(data_model_node.to_owned()); + + // pop out the nested type name from the fully qualified namespace as it has been already added to the type store and to nested types + self.current_type_fully_qualified_name.pop(); + data_model_node + .fully_qualified_type_ref() + .ok_or(invalid_abstract_data_type_raw_error( + "Can not determine fully qualified name for the data model", + )) } fn generate_abstract_data_type( &mut self, - isl_type_name: &str, + isl_type_name: &String, isl_type: &IslType, ) -> CodeGenResult<()> { let mut context = Context::new(); - let mut tera_fields = vec![]; let mut code_gen_context = CodeGenContext::new(); - let mut nested_types = vec![]; - // Set the ISL type name for the generated abstract data type - context.insert("target_kind_name", &isl_type_name.to_case(Case::UpperCamel)); + let data_model_node = self.convert_isl_type_def_to_data_model_node( + isl_type_name, + isl_type, + &mut code_gen_context, + )?; + + // add the entire type store and the data model node into tera's context to be used to render template + context.insert( + "type_store", + &self + .data_model_store + .iter() + .map(|(k, v)| (format!("{}", k), v)) + .collect::>(), + ); + context.insert("model", &data_model_node); + + self.render_generated_code(isl_type_name, &mut context, &data_model_node) + } + + fn convert_isl_type_def_to_data_model_node( + &mut self, + isl_type_name: &String, + isl_type: &IslType, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult { + self.current_type_fully_qualified_name + .push(isl_type_name.to_case(Case::UpperCamel)); let constraints = isl_type.constraints(); - for constraint in constraints { - self.map_constraint_to_abstract_data_type( - &mut nested_types, - &mut tera_fields, - constraint, - &mut code_gen_context, - )?; - } - // if any field in `tera_fields` contains a `None` `value_type` then it means there is a constraint that leads to open ended types. - // Return error in such case. - if tera_fields + // Initialize `AbstractDataType` according to the first constraint in the list of constraints + let abstract_data_type = if constraints .iter() - .any(|Field { value_type, .. }| value_type.is_none()) + .any(|it| matches!(it.constraint(), IslConstraintValue::Fields(_, _))) { - return invalid_abstract_data_type_error("Currently code generation does not support open ended types. \ - Error can be due to a missing `type` or `fields` or `element` constraint in the type definition."); - } - - // add fields for template - // TODO: verify the `occurs` value within a field, by default the fields are optional. - if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { - context.insert("fields", &tera_fields); - context.insert("abstract_data_type", abstract_data_type); - context.insert("nested_types", &nested_types); + self.build_structure_from_constraints(constraints, code_gen_context, isl_type)? } else { - return invalid_abstract_data_type_error( - "Can not determine abstract data type, specified constraints do not map to an abstract data type.", - ); - } + todo!("Support for sequences, maps, scalars, and tuples not implemented yet.") + }; - self.render_generated_code(isl_type_name, &mut context, &mut code_gen_context) + let data_model_node = DataModelNode { + name: isl_type_name.to_case(Case::UpperCamel), + code_gen_type: Some(abstract_data_type.to_owned()), + nested_types: code_gen_context.nested_types.to_owned(), + }; + + // TODO: verify the `occurs` value within a field, by default the fields are optional. + // add current data model node into the data model store + self.data_model_store.insert( + abstract_data_type.fully_qualified_type_ref().ok_or( + invalid_abstract_data_type_raw_error( + "Can not determine fully qualified name for the data model", + ), + )?, + data_model_node.to_owned(), + ); + Ok(data_model_node) } fn render_generated_code( &mut self, type_name: &str, context: &mut Context, - code_gen_context: &mut CodeGenContext, + data_model_node: &DataModelNode, ) -> CodeGenResult<()> { // Add namespace to tera context - if let Some(namespace) = self.namespace { - context.insert("namespace", namespace); - } + let mut import_context = Context::new(); + let namespace_ref = self.current_type_fully_qualified_name.as_slice(); + context.insert("namespace", &namespace_ref[0..namespace_ref.len() - 1]); + import_context.insert("namespace", &namespace_ref[0..namespace_ref.len() - 1]); + // Render or generate file for the template with the given context - let template: &Template = &code_gen_context.abstract_data_type.as_ref().try_into()?; + let template: &Template = &data_model_node.try_into()?; + + // This will be used by Java templates. Since `java` templates use recursion(i.e. use the same template for nested types) when rendering nested types, + // We need to tune the `is_nested` flag to allow static classes being added inside a parent class + context.insert("is_nested", &false); + let rendered = self .tera .render(&format!("{}.templ", L::template_name(template)), context) @@ -347,25 +413,29 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(()) } - /// Provides name of the type reference that will be used for generated abstract data type - fn type_reference_name( + /// Provides the `FullyQualifiedTypeReference` to be used for the `AbstractDataType` in the data model. + /// Returns None when the given ISL type is `struct`, `list` or `sexp` as open-ended types are not supported currently. + fn fully_qualified_type_ref_name( &mut self, isl_type_ref: &IslTypeRef, - nested_types: &mut Vec, - ) -> CodeGenResult> { + parent_code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult> { Ok(match isl_type_ref { IslTypeRef::Named(name, _) => { let schema_type: IonSchemaType = name.into(); L::target_type(&schema_type) + .as_ref() + .map(|type_name| FullyQualifiedTypeReference { + type_name: vec![type_name.to_string()], + parameters: vec![], + }) } IslTypeRef::TypeImport(_, _) => { unimplemented!("Imports in schema are not supported yet!"); } IslTypeRef::Anonymous(type_def, _) => { let name = self.next_nested_type_name(); - self.generate_nested_type(&name, type_def, nested_types)?; - - Some(name) + Some(self.generate_nested_type(&name, type_def, parent_code_gen_context)?) } }) } @@ -377,379 +447,251 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { name } - /// Maps the given constraint value to an abstract data type - fn map_constraint_to_abstract_data_type( + /// Build structure from constraints + fn build_structure_from_constraints( &mut self, - nested_types: &mut Vec, - tera_fields: &mut Vec, - constraint: &IslConstraint, + constraints: &[IslConstraint], code_gen_context: &mut CodeGenContext, - ) -> CodeGenResult<()> { - match constraint.constraint() { - IslConstraintValue::Element(isl_type, _) => { - let type_name = self.type_reference_name(isl_type, nested_types)?; - - self.verify_and_update_abstract_data_type( - AbstractDataType::Sequence { - element_type: type_name.to_owned(), - sequence_type: None, - }, - tera_fields, - code_gen_context, - )?; - - // Verify that the current type doesn't contains any nested types and that they are also of sequence or scalar type. - // if found nested sequence/scalar types then remove them from nested types and set the sequence or scalar as a field in current class/struct. - if let Some(type_reference_name) = &type_name { - if type_reference_name.contains("NestedType") { - // This is a nested type. Check for the abstract data type. If it is sequence type or scalar type, - // then add them into the current tera fields and remove them from `nested_types`. Scalar and sequence types - // doesn't need to have a separate class/struct created for them. - if let Some(nested_type) = nested_types.get_mut(0) { - if matches!( - nested_type.abstract_data_type, - AbstractDataType::Sequence { .. } - ) || nested_type.abstract_data_type == AbstractDataType::Value - { - // scalar and sequence types will only have 1 field. The field name here would be - // replaced with current `fields` constraint's field name. - // But `value_type` and ` isl_type_name` would be based on what we have in the `nested_type`. - let field = nested_type.fields.pop().unwrap(); - self.generate_struct_field( - tera_fields, - L::target_type_as_sequence(&field.value_type), - field.isl_type_name, - "value", - Some(nested_type.abstract_data_type.to_owned()), - )?; - - // change the `element_type` of current AbstractDataType::Sequence { .. }. This should be the type of nested type. - if let Some(AbstractDataType::Sequence { sequence_type, .. }) = - &code_gen_context.abstract_data_type - { - code_gen_context.abstract_data_type = - Some(AbstractDataType::Sequence { - element_type: field.value_type, - sequence_type: sequence_type.to_owned(), - }); - } - - // remove this nested type from the list as it will now be part of this field without generating separate nested type. - nested_types.pop(); - return Ok(()); - } - } + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut structure_builder = StructureBuilder::default(); + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Fields(struct_fields, is_closed) => { + // TODO: Check for `closed` annotation on fields and based on that return error while reading if there are extra fields. + let mut fields = HashMap::new(); + for (name, value) in struct_fields.iter() { + let type_name = self + .fully_qualified_type_ref_name( + value.type_reference(), + code_gen_context, + )? + .ok_or(invalid_abstract_data_type_raw_error( + "Given type doesn't have a name", + ))?; + + // TODO: change the field presence based on occurs constraint + // by default the field presence is optional + fields.insert( + name.to_string(), + FieldReference(type_name.to_owned(), FieldPresence::Optional), + ); } + // unwrap here is safe as the `current_abstract_data_type_builder` will either be initialized with default implementation + // or already initialized with a previous structure related constraint at this point. + structure_builder + .fields(fields) + .source(parent_isl_type.to_owned()) + .is_closed(*is_closed) + .name(self.current_type_fully_qualified_name.to_owned()); } - - // if the abstract data type is a sequence then pass the type name as the updated `element_type`. - if let Some(AbstractDataType::Sequence { - element_type, - sequence_type: Some(_), - }) = &code_gen_context.abstract_data_type - { - self.generate_struct_field( - tera_fields, - L::target_type_as_sequence(element_type), - isl_type.name(), - "value", - None, - )?; - } else { - self.generate_struct_field(tera_fields, None, isl_type.name(), "value", None)?; + IslConstraintValue::Type(_) => { + // by default fields aren't closed + structure_builder + .is_closed(false) + .source(parent_isl_type.to_owned()); } - } - IslConstraintValue::Fields(fields, content_closed) => { - // TODO: Check for `closed` annotation on fields and based on that return error while reading if there are extra fields. - self.verify_and_update_abstract_data_type( - AbstractDataType::Structure(*content_closed), - tera_fields, - code_gen_context, - )?; - for (name, value) in fields.iter() { - let mut type_name = - self.type_reference_name(value.type_reference(), nested_types)?; - let mut abstract_data_type = None; - let mut isl_type_name = value.type_reference().name(); - - if let Some(type_reference_name) = &type_name { - if type_reference_name.contains("NestedType") { - // This is a nested type. Check for the abstract data type. If it is sequence type or scalar type, - // then add them into the current tera fields and remove them from `nested_types`. Scalar and sequence types - // doesn't need to have a separate class/struct created for them. - if let Some(nested_type) = nested_types.get_mut(0) { - if matches!( - nested_type.abstract_data_type, - AbstractDataType::Sequence { .. } - ) || nested_type.abstract_data_type == AbstractDataType::Value - { - // scalar and sequence types will only have 1 field. The field name here would be - // replaced with current `fields` constraint's field name. - // But `value_type` and ` isl_type_name` would be based on what we have in the `nested_type`. - let field = nested_type.fields.pop().unwrap(); - abstract_data_type = - Some(nested_type.abstract_data_type.to_owned()); - isl_type_name = field.isl_type_name; - type_name = field.value_type; - - // remove this nested type from the list as it will now be part of this field without generating separate nested type. - nested_types.pop(); - } - } - } - } - self.generate_struct_field( - tera_fields, - type_name, - isl_type_name, - name, - abstract_data_type, - )?; + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ) } } - IslConstraintValue::Type(isl_type) => { - let type_name = self.type_reference_name(isl_type, nested_types)?; - - self.verify_and_update_abstract_data_type( - if isl_type.name() == "list" { - AbstractDataType::Sequence { - element_type: type_name.clone(), - sequence_type: Some(SequenceType::List), - } - } else if isl_type.name() == "sexp" { - AbstractDataType::Sequence { - element_type: type_name.clone(), - sequence_type: Some(SequenceType::SExp), - } - } else if isl_type.name() == "struct" { - AbstractDataType::Structure(false) // by default contents aren't closed - } else { - AbstractDataType::Value - }, - tera_fields, - code_gen_context, - )?; - - // Verify that the current type doesn't contains any nested types and that they are of sequence or scalar type. - // if found nested sequence/scalar types then remove them from `nested_types` and set the sequence or scalar as a field in current class/struct. - if let Some(type_reference_name) = &type_name { - if type_reference_name.contains("NestedType") { - // This is a nested type. Check for the abstract data type. If it is sequence type or scalar type, - // then add them into the current tera fields and remove them from `nested_types`. Scalar and sequence types - // doesn't need to have a separate class/struct created for them. - if let Some(nested_type) = nested_types.get_mut(0) { - if matches!( - nested_type.abstract_data_type, - AbstractDataType::Sequence { .. } - ) || nested_type.abstract_data_type == AbstractDataType::Value - { - // scalar and sequence types will only have 1 field. The field name here would be - // replaced with current `fields` constraint's field name. - // But `value_type` and ` isl_type_name` would be based on what we have in the `nested_type`. - let field = nested_type.fields.pop().unwrap(); - self.generate_struct_field( - tera_fields, - field.value_type, - field.isl_type_name, - "value", - Some(nested_type.abstract_data_type.to_owned()), - )?; - - // Update current `abstract_data_type` according to nested type - if let AbstractDataType::Sequence { - element_type: nested_element_type, - sequence_type: nested_sequence_type, - } = &nested_type.abstract_data_type - { - code_gen_context.abstract_data_type = - Some(AbstractDataType::Sequence { - element_type: nested_element_type.to_owned(), - sequence_type: nested_sequence_type.to_owned(), - }); - } - - // remove this nested type from the list as it will now be part of this field without generating separate nested type. - nested_types.pop(); - return Ok(()); - } - } - } - } + } - // if the abstract data type is a sequence then pass the type name as the updated `element_type`. - if let Some(AbstractDataType::Sequence { element_type, .. }) = - &code_gen_context.abstract_data_type - { - self.generate_struct_field( - tera_fields, - L::target_type_as_sequence(element_type), - isl_type.name(), - "value", - None, - )?; - } else { - self.generate_struct_field( - tera_fields, - type_name, - isl_type.name(), - "value", - None, - )?; + Ok(AbstractDataType::Structure(structure_builder.build()?)) + } +} + +#[cfg(test)] +mod isl_to_model_tests { + use super::*; + use crate::commands::generate::model::AbstractDataType; + use ion_schema::isl; + + #[test] + fn isl_to_model_test_for_struct() -> CodeGenResult<()> { + let isl_type = isl::isl_type::v_2_0::load_isl_type( + r#" + // ISL type definition with `fields` constraint + type:: { + name: my_struct, + type: struct, + fields: { + foo: string, + bar: int + }, } + "# + .as_bytes(), + )?; + + // Initialize code generator for Java + let mut java_code_generator = CodeGenerator::::new( + Path::new("./"), + vec!["org".to_string(), "example".to_string()], + ); + let data_model_node = java_code_generator.convert_isl_type_def_to_data_model_node( + &"my_struct".to_string(), + &isl_type, + &mut CodeGenContext::new(), + )?; + let abstract_data_type = data_model_node.code_gen_type.unwrap(); + assert_eq!( + abstract_data_type.fully_qualified_type_ref().unwrap(), + FullyQualifiedTypeReference { + type_name: vec![ + "org".to_string(), + "example".to_string(), + "MyStruct".to_string() + ], + parameters: vec![] } - _ => {} + ); + assert!(matches!(abstract_data_type, AbstractDataType::Structure(_))); + if let AbstractDataType::Structure(structure) = abstract_data_type { + assert_eq!( + structure.name, + vec![ + "org".to_string(), + "example".to_string(), + "MyStruct".to_string() + ] + ); + assert!(!structure.is_closed); + assert_eq!(structure.source, isl_type); + assert_eq!( + structure.fields, + HashMap::from_iter(vec![ + ( + "foo".to_string(), + FieldReference( + FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![] + }, + FieldPresence::Optional + ) + ), + ( + "bar".to_string(), + FieldReference( + FullyQualifiedTypeReference { + type_name: vec!["int".to_string()], + parameters: vec![] + }, + FieldPresence::Optional + ) + ) + ]) + ) } Ok(()) } - /// Generates a struct field based on field name and value(data type) - fn generate_struct_field( - &mut self, - tera_fields: &mut Vec, - abstract_data_type_name: Option, - isl_type_name: String, - field_name: &str, - // This argument is used only for nested sequence type, - // it will be `None` in all other cases. - abstract_data_type: Option, - ) -> CodeGenResult<()> { - tera_fields.push(Field { - name: field_name.to_string(), - value_type: abstract_data_type_name, - isl_type_name, - abstract_data_type, - }); - Ok(()) - } - - /// Verify that the current abstract data type is same as previously determined abstract data type - /// This is referring to abstract data type determined with each constraint that is verifies - /// that all the constraints map to a single abstract data type and not different abstract data types. - /// Also, updates the underlying `element_type` for List and SExp. - /// e.g. - /// ``` - /// type::{ - /// name: foo, - /// type: string, - /// fields:{ - /// source: String, - /// destination: String - /// } - /// } - /// ``` - /// For the above schema, both `fields` and `type` constraints map to different abstract data types - /// respectively Struct(with given fields `source` and `destination`) and Value(with a single field that has String data type). - fn verify_and_update_abstract_data_type( - &mut self, - current_abstract_data_type: AbstractDataType, - tera_fields: &mut Vec, - code_gen_context: &mut CodeGenContext, - ) -> CodeGenResult<()> { - if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { - match abstract_data_type { - // In the case when a `type` constraint occurs before `element` constraint. The element type for the sequence - // needs to be updated based on `element` constraint whereas sequence type will be used as per `type` constraint. - // e.g. For a schema as below: - // ``` - // type::{ - // name: sequence_type, - // type: sexp, - // element: string, - // } - // ``` - // Here, first `type` constraint would set the `AbstractDataType::Sequence{ element_type: T, sequence_type: "sexp"}` - // which uses generic type T and sequence type is sexp. Next `element` constraint would - // set the `AbstractDataType::Sequence{ element_type: String, sequence_type: "list"}`. - // Now this method performs verification that if the above described case occurs - // then it updates the `element_type` as per `element` constraint - // and `sequence_type` as per `type` constraint. - AbstractDataType::Sequence { - element_type, - sequence_type, - } if abstract_data_type != ¤t_abstract_data_type - && (element_type.is_none()) - && matches!( - ¤t_abstract_data_type, - &AbstractDataType::Sequence { .. } - ) => - { - // if current abstract data type is sequence and element_type is generic T or Object, - // then this was set by a `type` constraint in sequence field, - // so remove all previous fields that allows `Object` and update with current abstract_data_type. - tera_fields.pop(); - code_gen_context.with_abstract_data_type(AbstractDataType::Sequence { - element_type: current_abstract_data_type.element_type(), - sequence_type: sequence_type.to_owned(), - }); - } - // In the case when a `type` constraint occurs before `element` constraint. The element type for the sequence - // needs to be updated based on `element` constraint whereas sequence type will be used as per `type` constraint. - // e.g. For a schema as below: - // ``` - // type::{ - // name: sequence_type, - // element: string, - // type: sexp, - // } - // ``` - // Here, first `element` constraint would set the `AbstractDataType::Sequence{ element_type: String, sequence_type: "list"}` , - // Next `type` constraint would set the `AbstractDataType::Sequence{ element_type: T, sequence_type: "sexp"}` - // which uses generic type `T` and sequence type is sexp. Now this method performs verification that - // if the above described case occurs then it updates the `element_type` as per `element` constraint - // and `sequence_type` as per `type` constraint. - AbstractDataType::Sequence { element_type, .. } - if abstract_data_type != ¤t_abstract_data_type - && (current_abstract_data_type.element_type().is_none()) - && matches!( - ¤t_abstract_data_type, - &AbstractDataType::Sequence { .. } - ) => - { - // if `element` constraint has already set the abstract data_type to `Sequence` - // then remove previous fields as new fields will be added again after updating `element_type`. - // `type` constraint does update the ISL type name to either `list` or `sexp`, - // which needs to be updated within `abstract_data_type` as well. - tera_fields.pop(); - code_gen_context.with_abstract_data_type(AbstractDataType::Sequence { - element_type: element_type.to_owned(), - sequence_type: current_abstract_data_type.sequence_type(), - }) - } - // In the case when a `type` constraint occurs before `fields` constraint. The `content_closed` property for the struct - // needs to be updated based on `fields` constraint. - // e.g. For a schema as below: - // ``` - // type::{ - // name: struct_type, - // type: struct, - // fields: {} - // foo: string - // }, - // } - // ``` - // Here, first `type` constraint would set tera_fields with `value_type: None` and with `fields` constraint this field should be popped, - // and modify the `content_closed` property as per `fields` constraint. - AbstractDataType::Structure(_) - if !tera_fields.is_empty() - && tera_fields[0].value_type.is_none() - && matches!( - ¤t_abstract_data_type, - &AbstractDataType::Structure(_) - ) => - { - tera_fields.pop(); - // unwrap here is safe because we know the current_abstract_data_type is a `Structure` - code_gen_context.with_abstract_data_type(AbstractDataType::Structure( - current_abstract_data_type.is_content_closed().unwrap(), - )) - } - _ if abstract_data_type != ¤t_abstract_data_type => { - return invalid_abstract_data_type_error(format!("Can not determine abstract data type as current constraint {} conflicts with prior constraints for {}.", current_abstract_data_type, abstract_data_type)); + #[test] + fn isl_to_model_test_for_nested_struct() -> CodeGenResult<()> { + let isl_type = isl::isl_type::v_2_0::load_isl_type( + r#" + // ISL type definition with nested `fields` constraint + type:: { + name: my_nested_struct, + type: struct, + fields: { + foo: { + fields: { + baz: bool + }, + type: struct + }, + bar: int + }, } - _ => {} + "# + .as_bytes(), + )?; + + // Initialize code generator for Java + let mut java_code_generator = CodeGenerator::::new( + Path::new("./"), + vec!["org".to_string(), "example".to_string()], + ); + let data_model_node = java_code_generator.convert_isl_type_def_to_data_model_node( + &"my_nested_struct".to_string(), + &isl_type, + &mut CodeGenContext::new(), + )?; + let abstract_data_type = data_model_node.code_gen_type.unwrap(); + assert_eq!( + abstract_data_type.fully_qualified_type_ref().unwrap(), + FullyQualifiedTypeReference { + type_name: vec![ + "org".to_string(), + "example".to_string(), + "MyNestedStruct".to_string() + ], + parameters: vec![] } - } else { - code_gen_context.with_abstract_data_type(current_abstract_data_type); + ); + assert!(matches!(abstract_data_type, AbstractDataType::Structure(_))); + if let AbstractDataType::Structure(structure) = abstract_data_type { + assert_eq!( + structure.name, + vec![ + "org".to_string(), + "example".to_string(), + "MyNestedStruct".to_string() + ] + ); + assert!(!structure.is_closed); + assert_eq!(structure.source, isl_type); + assert_eq!( + structure.fields, + HashMap::from_iter(vec![ + ( + "foo".to_string(), + FieldReference( + FullyQualifiedTypeReference { + type_name: vec![ + "org".to_string(), + "example".to_string(), + "MyNestedStruct".to_string(), + "NestedType1".to_string() + ], + parameters: vec![] + }, + FieldPresence::Optional + ) + ), + ( + "bar".to_string(), + FieldReference( + FullyQualifiedTypeReference { + type_name: vec!["int".to_string()], + parameters: vec![] + }, + FieldPresence::Optional + ) + ) + ]) + ); + assert_eq!(data_model_node.nested_types.len(), 1); + assert_eq!( + data_model_node.nested_types[0] + .code_gen_type + .as_ref() + .unwrap() + .fully_qualified_type_ref(), + Some(FullyQualifiedTypeReference { + type_name: vec![ + "org".to_string(), + "example".to_string(), + "MyNestedStruct".to_string(), + "NestedType1".to_string() + ], + parameters: vec![] + }) + ); } Ok(()) } diff --git a/src/bin/ion/commands/generate/mod.rs b/src/bin/ion/commands/generate/mod.rs index b8357256..528a2dd0 100644 --- a/src/bin/ion/commands/generate/mod.rs +++ b/src/bin/ion/commands/generate/mod.rs @@ -7,7 +7,7 @@ mod utils; mod model; use crate::commands::generate::generator::CodeGenerator; -use crate::commands::generate::utils::{JavaLanguage, RustLanguage}; +use crate::commands::generate::utils::JavaLanguage; use crate::commands::IonCliCommand; use anyhow::{bail, Result}; use clap::{Arg, ArgAction, ArgMatches, Command}; @@ -121,11 +121,12 @@ impl IonCliCommand for GenerateCommand { // generate code based on schema and programming language match language { "java" => - CodeGenerator::::new(output, namespace.unwrap().as_str()) - .generate_code_for_authorities(&authorities, &mut schema_system)?, - "rust" => - CodeGenerator::::new(output) + CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()) .generate_code_for_authorities(&authorities, &mut schema_system)?, + "rust" => { + // TODO: Initialize and run code generator for `rust`, once the rust templates are modified based on new code generation model + todo!("Rust support is disabled until this is resolved: https://github.com/amazon-ion/ion-cli/issues/136") + } _ => bail!( "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", language @@ -135,8 +136,11 @@ impl IonCliCommand for GenerateCommand { Some(schema_id) => { // generate code based on schema and programming language match language { - "java" => CodeGenerator::::new(output, namespace.unwrap().as_str()).generate_code_for_schema(&mut schema_system, schema_id)?, - "rust" => CodeGenerator::::new(output).generate_code_for_schema(&mut schema_system, schema_id)?, + "java" => CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()).generate_code_for_schema(&mut schema_system, schema_id)?, + "rust" => { + // TODO: Initialize and run code generator for `rust`, once the rust templates are modified based on new code generation model + todo!() + } _ => bail!( "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", language diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index 01c13d8d..7dd1280c 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -1,6 +1,7 @@ use derive_builder::Builder; use ion_schema::isl::isl_type::IslType; use std::collections::HashMap; +use std::fmt::{Display, Formatter}; // This module contains a data model that the code generator can use to render a template based on the type of the model. // Currently, this same data model is represented by `AbstractDataType` but it doesn't hold all the information for the template. // e.g. currently there are different fields in the template that hold this information like fields, target_kind_name, abstract_data_type. @@ -10,25 +11,42 @@ use std::collections::HashMap; // TODO: This is not yet used in the implementation, modify current implementation to use this data model. use crate::commands::generate::context::SequenceType; use serde::Serialize; +use serde_json::Value; /// Represent a node in the data model tree of the generated code. /// Each node in this tree could either be a module/package or a concrete data structure(class, struct, enum etc.). /// This tree structure will be used by code generator and templates to render the generated code as per given ISL type definition hierarchy. -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub struct DataModelNode { // Represents the name of this data model // Note: It doesn't point to the fully qualified name. To get fully qualified name use `fully_qualified_name()` from `AbstractDataType`. - name: String, + // e.g. For a given schema as below: + // ``` + // type::{ + // name: foo, + // type: struct, + // fields: { + // a: int, + // b: string + // } + // } + // ``` + // The name of the abstract data type would be `Foo` where `Foo` will represent a Java class or Rust struct. + pub(crate) name: String, // Represents the type of the data model // It can be `None` for modules or packages. - code_gen_type: Option, + pub(crate) code_gen_type: Option, // Represents the nested types for this data model - nested_types: Vec, + pub(crate) nested_types: Vec, } impl DataModelNode { - #![allow(dead_code)] + #[allow(dead_code)] + pub fn name(&self) -> &str { + &self.name + } + + #[allow(dead_code)] pub fn is_scalar(&self) -> bool { if let Some(code_gen_type) = &self.code_gen_type { return matches!(code_gen_type, AbstractDataType::Scalar(_)); @@ -36,6 +54,7 @@ impl DataModelNode { false } + #[allow(dead_code)] pub fn is_sequence(&self) -> bool { if let Some(code_gen_type) = &self.code_gen_type { return matches!(code_gen_type, AbstractDataType::Sequence(_)); @@ -43,12 +62,19 @@ impl DataModelNode { false } + #[allow(dead_code)] pub fn is_structure(&self) -> bool { if let Some(code_gen_type) = &self.code_gen_type { return matches!(code_gen_type, AbstractDataType::Structure(_)); } false } + + pub fn fully_qualified_type_ref(&mut self) -> Option { + self.code_gen_type + .as_ref() + .and_then(|t| t.fully_qualified_type_ref()) + } } /// Represents a fully qualified type name for a type definition @@ -58,35 +84,106 @@ impl DataModelNode { type FullyQualifiedTypeName = Vec; /// Represents a fully qualified type name for a type reference -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Hash, Eq)] pub struct FullyQualifiedTypeReference { // Represents fully qualified name of the type // e.g. In Java, `org.example.Foo` // In Rust, `crate::org::example::Foo` - type_name: FullyQualifiedTypeName, + pub(crate) type_name: FullyQualifiedTypeName, // For types with parameters this will represent the nested parameters - parameters: Vec, + pub(crate) parameters: Vec, +} + +impl From for FullyQualifiedTypeReference { + fn from(value: FullyQualifiedTypeName) -> Self { + Self { + type_name: value, + parameters: vec![], + } + } +} + +impl Display for FullyQualifiedTypeReference { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.parameters.is_empty() { + return write!(f, "{}", self.type_name.join(".")); + } + write!(f, "{}<", self.type_name.join("."))?; + + for (i, parameter) in self.parameters.iter().enumerate() { + if i == self.parameters.len() - 1 { + write!(f, "{}", parameter)?; + } else { + write!(f, "{},", parameter)?; + } + } + write!(f, ">") + } +} + +// This is useful for code generator to convert input `serde_json::Value` coming from tera(template engine) into `FullyQualifiedTypeReference` +impl TryFrom<&Value> for FullyQualifiedTypeReference { + type Error = tera::Error; + + fn try_from(v: &Value) -> Result { + let obj = v.as_object().ok_or(tera::Error::msg( + "Tera value can not be converted to an object", + ))?; + let mut type_name = vec![]; + let mut parameters: Vec = vec![]; + for (key, value) in obj { + if key == "type_name" { + type_name = value + .as_array() + .unwrap() + .iter() + .map(|s| s.as_str().unwrap().to_string()) + .collect(); + } else { + let parameters_result: Result, tera::Error> = + value + .as_array() + .unwrap() + .iter() + .map(|v| v.try_into()) + .collect(); + parameters = parameters_result?; + } + } + Ok(FullyQualifiedTypeReference { + type_name, + parameters, + }) + } +} + +impl FullyQualifiedTypeReference { + #[allow(dead_code)] + pub fn with_parameters(&mut self, parameters: Vec) { + self.parameters = parameters; + } } /// A target-language-agnostic data type that determines which template(s) to use for code generation. -#[allow(dead_code)] // TODO: Add more code gen types like sum/discriminated union, enum and map. #[non_exhaustive] #[derive(Debug, Clone, PartialEq, Serialize)] pub enum AbstractDataType { // Represents a scalar type which also has a name attached to it and is nominally distinct from its base type. + #[allow(dead_code)] WrappedScalar(WrappedScalar), // Represents a scalar value (e.g. a string or integer or user defined type) + #[allow(dead_code)] Scalar(Scalar), // A series of zero or more values whose type is described by the nested `element_type` + #[allow(dead_code)] Sequence(Sequence), // A collection of field name/value pairs (e.g. a map) Structure(Structure), } impl AbstractDataType { - #![allow(dead_code)] + #[allow(dead_code)] pub fn doc_comment(&self) -> Option<&str> { match self { AbstractDataType::WrappedScalar(WrappedScalar { doc_comment, .. }) => { @@ -95,17 +192,21 @@ impl AbstractDataType { AbstractDataType::Scalar(Scalar { doc_comment, .. }) => { doc_comment.as_ref().map(|s| s.as_str()) } - AbstractDataType::Sequence(Sequence { doc_comment, .. }) => Some(doc_comment), - AbstractDataType::Structure(Structure { doc_comment, .. }) => Some(doc_comment), + AbstractDataType::Sequence(Sequence { doc_comment, .. }) => Some(doc_comment.as_str()), + AbstractDataType::Structure(Structure { doc_comment, .. }) => { + doc_comment.as_ref().map(|s| s.as_str()) + } } } - pub fn fully_qualified_name(&self) -> FullyQualifiedTypeName { + pub fn fully_qualified_type_ref(&self) -> Option { match self { - AbstractDataType::WrappedScalar(w) => w.fully_qualified_type_name().to_owned(), - AbstractDataType::Scalar(s) => s.name.to_owned(), - AbstractDataType::Sequence(seq) => seq.name.to_owned(), - AbstractDataType::Structure(structure) => structure.name.to_owned(), + AbstractDataType::WrappedScalar(w) => { + Some(w.fully_qualified_type_name().to_owned().into()) + } + AbstractDataType::Scalar(s) => Some(s.name.to_owned().into()), + AbstractDataType::Sequence(seq) => Some(seq.element_type.to_owned()), + AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()), } } } @@ -197,7 +298,8 @@ impl WrappedScalar { /// ``` /// type::{ /// name: sequence_type, -/// element: int +/// element: int, +/// type: list /// } /// ``` /// Corresponding generated code in Rust would look like following: @@ -250,35 +352,38 @@ pub struct Sequence { #[builder(setter(into))] pub struct Structure { // Represents the fully qualified name for this data model - name: FullyQualifiedTypeName, + pub(crate) name: FullyQualifiedTypeName, // Represents doc comment for the generated code - doc_comment: String, + #[builder(default)] + pub(crate) doc_comment: Option, // Represents whether the struct has closed fields or not - is_closed: bool, + pub(crate) is_closed: bool, // Represents the fields of the struct i.e. (field_name, field_value) pairs // field_value represents `FieldReference` i.e. the type of the value field as fully qualified name and the presence for this field. // _Note: that a hashmap with (FullQualifiedTypeReference, DataModel) pairs will be stored in code generator to get information on the field_value name used here._ - fields: HashMap, + pub(crate) fields: HashMap, // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. // This will also be useful for `text` type to verify if this is a `string` or `symbol`. // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. #[serde(skip_serializing)] - source: IslType, + pub(crate) source: IslType, } /// Represents whether the field is required or not -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq, Serialize)] -enum FieldPresence { +pub enum FieldPresence { + #[allow(dead_code)] Required, Optional, } /// Represents a reference to the field with its fully qualified name and its presence (i.e. required or optional) -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq, Serialize)] -struct FieldReference(FullyQualifiedTypeReference, FieldPresence); +pub struct FieldReference( + pub(crate) FullyQualifiedTypeReference, + pub(crate) FieldPresence, +); #[cfg(test)] mod model_tests { @@ -389,7 +494,7 @@ mod model_tests { fn struct_builder_test() { let expected_struct = Structure { name: vec!["org".to_string(), "example".to_string(), "Foo".to_string()], - doc_comment: "This is a structure".to_string(), + doc_comment: Some("This is a structure".to_string()), is_closed: false, fields: HashMap::from_iter(vec![ ( @@ -447,7 +552,7 @@ mod model_tests { "example".to_string(), "Foo".to_string(), ]) - .doc_comment("This is a structure") + .doc_comment(Some("This is a structure".to_string())) .is_closed(false) .fields(HashMap::from_iter(vec![ ( diff --git a/src/bin/ion/commands/generate/result.rs b/src/bin/ion/commands/generate/result.rs index 16b9ea73..d18dd19b 100644 --- a/src/bin/ion/commands/generate/result.rs +++ b/src/bin/ion/commands/generate/result.rs @@ -1,3 +1,6 @@ +use crate::commands::generate::model::{ + ScalarBuilderError, SequenceBuilderError, StructureBuilderError, WrappedScalarBuilderError, +}; use ion_schema::result::IonSchemaError; use thiserror::Error; @@ -24,6 +27,8 @@ pub enum CodeGenError { }, #[error("{description}")] InvalidDataModel { description: String }, + #[error("{description}")] + DataModelBuilderError { description: String }, } /// A convenience method for creating an CodeGen containing an CodeGenError::InvalidDataModel @@ -33,3 +38,43 @@ pub fn invalid_abstract_data_type_error>(description: S) -> Cod description: description.as_ref().to_string(), }) } + +/// A convenience method for creating an CodeGenError::InvalidDataModel +/// with the provided description text. +pub fn invalid_abstract_data_type_raw_error>(description: S) -> CodeGenError { + CodeGenError::InvalidDataModel { + description: description.as_ref().to_string(), + } +} + +impl From for CodeGenError { + fn from(value: WrappedScalarBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} + +impl From for CodeGenError { + fn from(value: ScalarBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} + +impl From for CodeGenError { + fn from(value: SequenceBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} + +impl From for CodeGenError { + fn from(value: StructureBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} diff --git a/src/bin/ion/commands/generate/templates/java/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index 048f96cf..ec7f8cbe 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -2,38 +2,47 @@ {% import "nested_type.templ" as macros %} {% import "util_macros.templ" as util_macros %} +{% macro class(model, is_nested) %} -package {{ namespace }}; -import java.util.ArrayList; +{% if is_nested == false %} +{% set full_namespace = namespace | join(sep=".") %} + +package {{ full_namespace }}; import com.amazon.ion.IonReader; import com.amazon.ion.IonException; import com.amazon.ion.IonWriter; import com.amazon.ion.IonType; import java.io.IOException; +{% endif %} + + +{# Verify that the abstract data type is a structure and store information for this structure #} +{% set struct_info = model.code_gen_type["Structure"] %} -public class {{ target_kind_name }} { - {% for field in fields -%} - private {{ field.value_type }} {{ field.name | camel }}; +{% if is_nested == true %} static {% endif %} class {{ model.name }} { + {% for field_name, field_value in struct_info["fields"] -%} + private {{ field_value.0 | fully_qualified_type_name }} {{ field_name | camel }}; {% endfor %} - public {{ target_kind_name }}() {} + public {{ model.name }}() {} - {% for field in fields -%}public {{ field.value_type }} get{% filter upper_camel %}{{ field.name }}{% endfilter %}() { - return this.{{ field.name | camel }}; + {% for field_name, field_value in struct_info["fields"] -%}public {{ field_value.0 | fully_qualified_type_name }} get{% filter upper_camel %}{{ field_name }}{% endfilter %}() { + return this.{{ field_name | camel }}; } {% endfor %} - {% for field in fields %} - {% if field.value_type is containing("NestedType") %} - public void set{% filter upper_camel %}{{ field.name }}{% endfilter %}( - {{ macros::define_params_for_anonymous_type(nested_types=nested_types, field=field, abstract_data_type=abstract_data_type, initial_field_name=field.name) }} + {% for field_name, field_value in struct_info["fields"] %} + {% set val = field_value.0 | fully_qualified_type_name %} + {% if val is containing("NestedType") %} + public void set{% filter upper_camel %}{{ field_name }}{% endfilter %}( + {{ macros::define_params_for_anonymous_type(nested_type=field_value.0, type_store=type_store, field_name=field_name, initial_field_name=field_name) }} ) { - {{ macros::initialize_anonymous_type(nested_types=nested_types, field=field, abstract_data_type=abstract_data_type) }} - this.{{ field.name | camel }} = {{ field.name | camel }}; + {{ macros::initialize_anonymous_type(nested_type=field_value.0, type_store=type_store, field_name=field_name) }} + this.{{ field_name | camel }} = {{ field_name | camel }}; return; {% else %} - public void set{% filter upper_camel %}{{ field.name }}{% endfilter %}({{ field.value_type }} {{ field.name | camel }}) { - this.{{ field.name | camel }} = {{ field.name | camel }}; + public void set{% filter upper_camel %}{{ field_name }}{% endfilter %}({{ val }} {{ field_name | camel }}) { + this.{{ field_name | camel }} = {{ field_name | camel }}; return; {% endif %} } @@ -41,18 +50,19 @@ public class {{ target_kind_name }} { /** - * Reads a {{ target_kind_name }} from an {@link IonReader}. + * Reads a {{ model.name }} from an {@link IonReader}. * * This method does not advance the reader at the current level. * The caller is responsible for positioning the reader on the value to read. */ - public static {{ target_kind_name }} readFrom(IonReader reader) { + public static {{ model.name }} readFrom(IonReader reader) { {# Initializes all the fields of this class #} - {% for field in fields -%} - {{ field.value_type }} {{ field.name | camel }} = - {% if field.value_type == "boolean" %} + {% for field_name, field_val in struct_info["fields"] -%} + {% set field_value = field_val.0 | fully_qualified_type_name %} + {{ field_value }} {{ field_name | camel }} = + {% if field_value == "boolean" %} false - {% elif field.value_type == "int" or field.value_type == "double" %} + {% elif field_value == "int" or field_value == "double" %} 0 {% else %} null @@ -65,37 +75,39 @@ public class {{ target_kind_name }} { reader.next(); String fieldName = reader.getFieldName(); switch(fieldName) { - {% for field in fields %} - case "{{ field.name }}": - {{ field.name | camel }} = {% if field.value_type | is_built_in_type %} - {% if field.value_type == "bytes[]" %} + {% for field_name, field_val in struct_info["fields"] %} + {% set field_value = field_val.0 | fully_qualified_type_name %} + + case "{{ field_name }}": + {{ field_name | camel }} = {% if field_value | is_built_in_type %} + {% if field_value == "bytes[]" %} reader.newBytes(); {% else %} - reader.{{ field.value_type | camel }}Value(); + reader.{{ field_value | camel }}Value(); {% endif %} - {% elif field.value_type is containing("ArrayList") %} - {{ util_macros::read_as_sequence(field=field) }} + {% elif field_value is containing("ArrayList") %} + {{ util_macros::read_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} {% else %} - {{ field.value_type }}.readFrom(reader); + {{ field_value }}.readFrom(reader); {% endif %} break; {% endfor %} default: - throw new IonException("Can not read field name:" + fieldName + " for {{ target_kind_name }} as it doesn't exist in the given schema type definition."); + throw new IonException("Can not read field name:" + fieldName + " for {{ model.name }} as it doesn't exist in the given schema type definition."); } } reader.stepOut(); - {{ target_kind_name }} {{ target_kind_name | camel }} = new {{ target_kind_name }}(); - {% for field in fields -%} - {{ target_kind_name | camel }}.{{ field.name | camel }} = {{ field.name | camel }}; + {{ model.name }} {{ model.name | camel }} = new {{ model.name }}(); + {% for field_name, field_value in struct_info["fields"] -%} + {{ model.name | camel }}.{{ field_name | camel }} = {{ field_name | camel }}; {% endfor %} - return {{ target_kind_name | camel }}; + return {{ model.name | camel }}; } /** - * Writes a {{ target_kind_name }} as Ion from an {@link IonWriter}. + * Writes a {{ model.name }} as Ion from an {@link IonWriter}. * * This method does not close the writer after writing is complete. * The caller is responsible for closing the stream associated with the writer. @@ -103,22 +115,26 @@ public class {{ target_kind_name }} { public void writeTo(IonWriter writer) throws IOException { {# Writes `Structure` class with multiple fields based on `field.name` as an Ion struct #} writer.stepIn(IonType.STRUCT); - {% for field in fields %} - writer.setFieldName("{{ field.name }}"); - {% if field.value_type | is_built_in_type == false %} - {% if field.value_type is containing("ArrayList") %} - {{ util_macros::write_as_sequence(field=field) }} + {% for field_name, field_val in struct_info["fields"] %} + {% set field_value = field_val.0 | fully_qualified_type_name %} + writer.setFieldName("{{ field_name }}"); + {% if field_value | is_built_in_type == false %} + this.{{ field_name | camel }}.writeTo(writer); + {% else %} + {% if field_value is containing("ArrayList") %} + {{ util_macros::write_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} {% else %} - this.{{ field.name | camel }}.writeTo(writer); + writer.write{{ field_value | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.{{ field_name | camel }}); {% endif %} - {% else %} - writer.write{{ field.isl_type_name | upper_camel }}(this.{{ field.name | camel }}); {% endif %} {% endfor %} writer.stepOut(); } - {% for inline_type in nested_types -%} - {{ macros::nested_type(target_kind_name=inline_type.target_kind_name, fields=inline_type.fields, abstract_data_type=inline_type.abstract_data_type, nested_anonymous_types=inline_type.nested_types) }} + {% for inline_type in model.nested_types -%} + {% set is_nested = true %} + {{ macros::nested_type(model=inline_type, is_nested=is_nested) }} {% endfor -%} } +{% endmacro model %} +{{ self::class(model=model, is_nested=is_nested) }} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/java/nested_type.templ b/src/bin/ion/commands/generate/templates/java/nested_type.templ index 99efe133..299a6085 100644 --- a/src/bin/ion/commands/generate/templates/java/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/java/nested_type.templ @@ -1,206 +1,55 @@ {% import "util_macros.templ" as util_macros %} {# following macro defines an anonymous type as children class for its parent type definition #} -{% macro nested_type(target_kind_name, fields, abstract_data_type, nested_anonymous_types) -%} - public static class {{ target_kind_name }} { - {% for field in fields -%} - private {{ field.value_type }} {{ field.name | camel }}; - {% endfor -%} - - public {{ target_kind_name }}() {} - - {% for field in fields %}public {{ field.value_type }} get{% filter upper_camel %}{{ field.name }}{% endfilter %}() { - return this.{{ field.name | camel }}; - } - {% endfor %} - - - {% for field in fields %} - {% if field.value_type is containing("NestedType") -%} - public void set{% filter upper_camel -%}{{ field.name }}{% endfilter -%}( - {{ self::define_params_for_anonymous_type(nested_types=nested_anonymous_types, field=field, abstract_data_type=abstract_data_type, initial_field_name=field.name) }} - ) { - {{ self::initialize_anonymous_type(nested_types=nested_anonymous_types, field=field, abstract_data_type=abstract_data_type) }} - this.{{ field.name | camel }} = {{ field.name | camel }}; - return; - {% else -%} - public void set{% filter upper_camel -%}{{ field.name }}{% endfilter -%}({{ field.value_type }} {{ field.name | camel }}) { - this.{{ field.name | camel }} = {{ field.name | camel }}; - return; - {% endif -%} - } - {% endfor %} - - /** - * Reads a {{ target_kind_name }} from an {@link IonReader}. - * - * This method does not advance the reader at the current level. - * The caller is responsible for positioning the reader on the value to read. - */ - public static {{ target_kind_name }} readFrom(IonReader reader) { - {# Initializes all the fields of this class #} - {% for field in fields -%} - {{ field.value_type }} {{ field.name | camel }} = - {% if field.value_type == "boolean" -%} - false - {% elif field.value_type == "int" or field.value_type == "double" -%} - 0 - {% else -%} - null - {% endif -%}; - {% endfor -%} - {% if abstract_data_type == "Value"-%} - {# Reads `Value` class with a single field `value` #} - value = {% if fields[0].value_type | is_built_in_type -%} - {% if fields[0].value_type == "bytes[]" -%} - reader.newBytes(); - {% else -%} - reader.{{ fields[0].value_type | camel }}Value(); - {% endif -%} - {% else -%} - {{ fields[0].value_type }}.readFrom(reader); - {% endif -%} - {% elif abstract_data_type is object and abstract_data_type is containing("Structure") -%} - {# Reads `Structure` class with multiple fields based on `field.name` #} - reader.stepIn(); - while (reader.hasNext()) { - reader.next(); - String fieldName = reader.getFieldName(); - switch(fieldName) { - {% for field in fields -%} - case "{{ field.name }}": - {{ field.name | camel }} = {% if field.value_type | is_built_in_type %} - {% if field.value_type == "bytes[]" %} - reader.newBytes(); - {% else %} - reader.{{ field.value_type | camel }}Value(); - {% endif %} - {% elif field.value_type is containing("ArrayList") %} - {{ util_macros::read_as_sequence(field=field) }} - {% else %} - {{ field.value_type }}.readFrom(reader); - {% endif %} - break; - {% endfor %} - default: - throw new IonException("Can not read field name:" + fieldName + " for {{ target_kind_name }} as it doesn't exist in the given schema type definition."); - } - } - reader.stepOut(); - {% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %} - {# Reads `Sequence` class with a single field `value` that is an `ArrayList` #} - if(reader.getType() != IonType.{{ abstract_data_type["Sequence"].sequence_type | upper }}) { - throw new IonException("Expected {{ abstract_data_type["Sequence"].sequence_type }}, found " + reader.getType() + " while reading {{ field.name | camel }}."); - } - reader.stepIn(); - value = new {{ fields[0].value_type }}(); - {# Iterate through the `ArrayList` and read each element in it based on the data type provided in `abstract_data_type[Sequence]` #} - while (reader.hasNext()) { - reader.next(); - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - value.add({{ abstract_data_type["Sequence"].element_type }}.readFrom(reader)); - {% elif abstract_data_type["Sequence"].element_type == "bytes[]" %} - value.add(reader.newBytes()); - {% else %} - value.add(reader.{{ abstract_data_type["Sequence"].element_type | camel }}Value()); - {% endif %} - } - reader.stepOut(); - {% endif %} - {{ target_kind_name }} {{ target_kind_name | camel }} = new {{ target_kind_name }}(); - {% for field in fields -%} - {{ target_kind_name | camel }}.{{ field.name | camel }} = {{ field.name | camel }}; - {% endfor %} - - return {{ target_kind_name | camel }}; - } - - - /** - * Writes a {{ target_kind_name }} as Ion from an {@link IonWriter}. - * - * This method does not close the writer after writing is complete. - * The caller is responsible for closing the stream associated with the writer. - */ - public void writeTo(IonWriter writer) throws IOException { - {% if abstract_data_type == "Value" %} - {# Writes `Value` class with a single field `value` as an Ion value #} - {% for field in fields %} - {% if field.value_type | is_built_in_type == false %} - this.{{ field.name | camel }}.writeTo(writer)?; - {% else %} - writer.write{{ field.isl_type_name | upper_camel }}(this.value); - {% endif %} - {% endfor %} - {% elif abstract_data_type is object and abstract_data_type is containing("Structure") %} - {# Writes `Structure` class with multiple fields based on `field.name` as an Ion struct #} - writer.stepIn(IonType.STRUCT); - {% for field in fields %} - writer.setFieldName("{{ field.name }}"); - {% if field.value_type | is_built_in_type == false %} - - {% if field.value_type is containing("ArrayList") %} - {{ util_macros::write_as_sequence(field=field) }} - {% else %} - this.{{ field.name | camel }}.writeTo(writer); - {% endif %} - {% else %} - writer.write{{ field.isl_type_name | upper_camel }}(this.{{ field.name | camel }}); - {% endif %} - {% endfor %} - writer.stepOut(); - {% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %} - {# Writes `Sequence` class with a single field `value` that is an `ArrayList` as an Ion sequence #} - writer.stepIn(IonType.{{ field.abstract_data_type["Sequence"].sequence_type | upper }}); - for ({{ abstract_data_type["Sequence"].element_type }} value: this.value) { - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - value.writeTo(writer); - {% else %} - writer.write{{ abstract_data_type["Sequence"].element_type | upper_camel }}(value); - {% endif %} - } - writer.stepOut(); - {% endif %} - } - - {% for inline_type in nested_anonymous_types -%} - {{ self::nested_type(target_kind_name=inline_type.target_kind_name, fields=inline_type.fields, abstract_data_type=inline_type.abstract_data_type, nested_anonymous_types=inline_type.nested_types) }} - {% endfor -%} - } +{% macro nested_type(model, is_nested) -%} + {% if model.code_gen_type is containing("Structure")%} + {% include "class.templ" %} + {% endif %} {% endmacro nested_type -%} {# following macro defines statements to initialize anonymous types for setter methods #} -{% macro initialize_anonymous_type(nested_types, field, abstract_data_type) %} - {% set map = nested_types | group_by(attribute="target_kind_name") %} - {% if abstract_data_type is object and abstract_data_type is containing("Sequence") %} - {% set inline_type = map[abstract_data_type["Sequence"].element_type][0] %} - {% else %} - {% set inline_type = map[field.value_type][0] %} +{% macro initialize_anonymous_type(nested_type, type_store, field_name) %} + {% set key = nested_type | fully_qualified_type_name %} + {% set inline_type = type_store[key] %} + {{ key }} {{ field_name | camel }} = new {{ key }}(); + {% if inline_type.code_gen_type is containing("Sequence") %} + {{ field_name | camel }}.setValue(value); + {% elif inline_type.code_gen_type is containing("Scalar") %} + {{ field_name | camel }}.setValue(value); + {% elif inline_type.code_gen_type is containing("Structure")%} + {% for inline_type_field_name, inline_type_field_value in inline_type.code_gen_type["Structure"].fields %} + {{ field_name | camel }}.set{{ inline_type_field_name | upper_camel }}({{ inline_type_field_name | camel }}); + {% endfor %} {% endif %} - {{ inline_type.target_kind_name }} {{ field.name | camel }} = new {{ inline_type.target_kind_name }}(); - {% for inline_type_field in inline_type.fields %} - {{ field.name | camel }}.set{{ inline_type_field.name | upper_camel }}({{ inline_type_field.name | camel }}); - {% endfor %} {% endmacro %} {# following macro defines arguments to setter methods for anonymous types #} -{% macro define_params_for_anonymous_type(nested_types, field, abstract_data_type, initial_field_name) %} - {% set map = nested_types | group_by(attribute="target_kind_name") %} - {% if abstract_data_type is object and abstract_data_type is containing("Sequence") %} - {% set inline_type = map[abstract_data_type["Sequence"].element_type][0] %} - {% else -%} - {% set inline_type = map[field.value_type][0] %} - {% endif -%} - {% for inline_type_field in inline_type.fields | sort(attribute="name") %} - {% if inline_type_field.value_type is containing("NestedType") %} - {{ self::define_params_for_anonymous_type(nested_types=inline_type.nested_types, field=inline_type_field, abstract_data_type=inline_type.abstract_data_type, initial_field_name=initial_field_name) }} - {% else %} - {% if inline_type_field.name == "value" and not initial_field_name == field.name %} - {{ inline_type_field.value_type }} {{ field.name | camel }} - {% else %} - {{ inline_type_field.value_type }} {{ inline_type_field.name | camel }} - {% endif %} - {% endif %} - {% if not loop.last -%},{% endif -%} - {% endfor %} +{% macro define_params_for_anonymous_type(nested_type, field_name, initial_field_name) %} + {% set field_name = nested_type | fully_qualified_type_name %} + {% set inline_type = type_store[field_name] %} + {% if inline_type.code_gen_type is containing("Structure") %} + {% set inline_struct_type_info = inline_type.code_gen_type["Structure"] %} + {% set field_names = inline_struct_type_info["fields"] | field_names %} + {% for inline_type_field_name in field_names | sort %} + {% set inline_type_field_value = inline_struct_type_info["fields"][inline_type_field_name] %} + {% set value_type = inline_type_field_value.0 | fully_qualified_type_name %} + {% if value_type is containing("NestedType") %} + {{ self::define_params_for_anonymous_type(nested_type=inline_type_field_value.0, field_name=inline_type_field_name, initial_field_name=initial_field_name) }} + {% else %} + {% if inline_type_field_name == "value" and not initial_field_name == field_name %} + {{ value_type }} {{ field_name | camel }} + {% else %} + {{ value_type }} {{ inline_type_field_name | camel }} + {% endif %} + {% endif %} + {% if not loop.last -%},{% endif -%} + {% endfor %} + {% elif inline_type.code_gen_type is containing("Sequence") %} + {% set sequence_info = model.code_gen_type["WrappedSequence"] %} + {{ sequence_info["element_type"] | fully_qualified_type_name }} value + {% elif inline_type.code_gen_type is containing("Scalar") %} + {% set scalar_info = model.code_gen_type["WrappedScalar"] %} + {% set base_type = scalar_info["name"]["parameters"][0] | fully_qualified_type_name %} + {{ base_type }} value + {% endif %} {% endmacro %} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/utils.rs b/src/bin/ion/commands/generate/utils.rs index a182c140..dca7bf46 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -1,49 +1,10 @@ -use crate::commands::generate::context::AbstractDataType; +use crate::commands::generate::model::{ + AbstractDataType, DataModelNode, FullyQualifiedTypeReference, +}; use crate::commands::generate::result::{invalid_abstract_data_type_error, CodeGenError}; use convert_case::{Case, Casing}; -use serde::Serialize; use std::fmt::{Display, Formatter}; -/// Represents a field that will be added to generated data model. -/// This will be used by the template engine to fill properties of a struct/class. -#[derive(Serialize)] -pub struct Field { - pub(crate) name: String, - // The value_type represents the AbstractDatType for given field. When given ISL has constraints, that lead to open ended types, - // this will be ste to None, Otherwise set to Some(ABSTRACT_DATA_TYPE_NAME). - // e.g For below ISL type: - // ``` - // type::{ - // name: list_type, - // type: list // since this doesn't have `element` constraint defined it will be set `value_type` to None - // } - // ``` - // Following will be the `Field` value for this ISL type: - // Field { - // name: value, - // value_type: None, - // isl_type_name: "list" - // abstract_data_type: None - // } - // Code generation process results into an Error when `value_type` is set to `None` - pub(crate) value_type: Option, - pub(crate) isl_type_name: String, - // `abstract_data_type` is only used for sequence type fields. This value provides `element_type` - // and `sequence_type` information for this sequence type field. - pub(crate) abstract_data_type: Option, -} - -/// Represents an nested type that can be a part of another type definition. -/// This will be used by the template engine to add these intermediate data models for nested types -/// in to the parent type definition's module/namespace. -#[derive(Serialize)] -pub struct NestedType { - pub(crate) target_kind_name: String, - pub(crate) fields: Vec, - pub(crate) abstract_data_type: AbstractDataType, - pub(crate) nested_types: Vec, -} - pub trait Language { /// Provides a file extension based on programming language fn file_extension() -> String; @@ -58,16 +19,31 @@ pub trait Language { fn file_name_for_type(name: &str) -> String; /// Maps the given ISL type to a target type name + /// Returns None when the given ISL type is `struct`, `list` or `sexp` as open-ended types are not supported currently. fn target_type(ion_schema_type: &IonSchemaType) -> Option; /// Provides given target type as sequence /// e.g. - /// target_type = "Foo" returns "ArrayList" + /// target_type = "Foo" returns "java.util.ArrayList" /// target_type = "Foo" returns "Vec" - fn target_type_as_sequence(target_type: &Option) -> Option; - - /// Returns true if the type name specified is provided by the target language implementation - fn is_built_in_type(name: &str) -> bool; + #[allow(dead_code)] + fn target_type_as_sequence( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference; + + /// Returns true if the type `String` specified is provided by the target language implementation + fn is_built_in_type(type_name: String) -> bool; + + /// Returns a fully qualified type reference name as per the programming language + /// e.g. For a fully qualified type reference as below: + /// FullyQualifiedTypeReference { + /// type_name: vec!["org", "example", "Foo"], + /// parameters: vec![] // type ref with no parameters + /// } + /// In Java, `org.example.Foo` + /// In Rust, `org::example::Foo` + #[allow(dead_code)] + fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String; /// Returns the template as string based on programming language /// e.g. @@ -107,17 +83,41 @@ impl Language for JavaLanguage { ) } - fn target_type_as_sequence(target_type: &Option) -> Option { - target_type.as_ref().map(|target_type_name| { - match JavaLanguage::wrapper_class(target_type_name) { - Some(wrapper_name) => format!("ArrayList<{}>", wrapper_name), - None => format!("ArrayList<{}>", target_type_name), - } - }) + fn target_type_as_sequence( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference { + match JavaLanguage::wrapper_class(&format!("{}", target_type)) { + Some(wrapper_name) => FullyQualifiedTypeReference { + type_name: vec![ + "java".to_string(), + "util".to_string(), + "ArrayList".to_string(), + ], + parameters: vec![FullyQualifiedTypeReference { + type_name: vec![wrapper_name], + parameters: vec![], + }], + }, + None => FullyQualifiedTypeReference { + type_name: vec![ + "java".to_string(), + "util".to_string(), + "ArrayList".to_string(), + ], + parameters: vec![target_type], + }, + } } - fn is_built_in_type(name: &str) -> bool { - matches!(name, "int" | "String" | "boolean" | "byte[]" | "double") + fn is_built_in_type(type_name: String) -> bool { + matches!( + type_name.as_str(), + "int" | "String" | "boolean" | "byte[]" | "double" + ) + } + + fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String { + name.type_name.join(".") } fn template_name(template: &Template) -> String { @@ -181,14 +181,32 @@ impl Language for RustLanguage { ) } - fn target_type_as_sequence(target_type: &Option) -> Option { - target_type - .as_ref() - .map(|target_type_name| format!("Vec<{}>", target_type_name)) + fn target_type_as_sequence( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference { + FullyQualifiedTypeReference { + type_name: vec!["Vec".to_string()], + parameters: vec![target_type], + } } - fn is_built_in_type(name: &str) -> bool { - matches!(name, "i64" | "String" | "bool" | "Vec" | "f64") + fn is_built_in_type(type_name: String) -> bool { + matches!( + type_name.as_str(), + "i64" + | "String" + | "bool" + | "Vec" + | "f64" + | "Vec" + | "Vec" + | "Vec" + | "Vec" + ) + } + + fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String { + name.type_name.join("::") } fn template_name(template: &Template) -> String { @@ -218,19 +236,22 @@ pub enum Template { Scalar, // Represents a template for a Rust struct or Java class with Ion scalar value } -impl TryFrom> for Template { +impl TryFrom<&DataModelNode> for Template { type Error = CodeGenError; - fn try_from(value: Option<&AbstractDataType>) -> Result { - match value { - Some(abstract_data_type) => match abstract_data_type { - AbstractDataType::Value => Ok(Template::Scalar), - AbstractDataType::Sequence { .. } => Ok(Template::Sequence), + fn try_from(value: &DataModelNode) -> Result { + if let Some(abstract_data_type) = &value.code_gen_type { + match abstract_data_type { + AbstractDataType::Scalar(_) | AbstractDataType::WrappedScalar(_) => { + Ok(Template::Scalar) + } + AbstractDataType::Sequence(_) => Ok(Template::Sequence), AbstractDataType::Structure(_) => Ok(Template::Struct), - }, - None => invalid_abstract_data_type_error( + } + } else { + invalid_abstract_data_type_error( "Can not get a template without determining data model first.", - ), + ) } } } diff --git a/tests/cli.rs b/tests/cli.rs index 53bf96ae..d9d4de00 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -222,111 +222,7 @@ mod code_gen_tests { use super::*; use std::fs; - #[rstest] - #[case::simple_struct( - r#" - type::{ - name: simple_struct, - fields: { - name: string, - id: int, - }, - } - "#, - & ["id: i64", "name: String"], - & ["pub fn name(&self) -> &String {", "pub fn id(&self) -> &i64 {"] - )] - #[case::value_struct( - r#" - type::{ - name: value_struct, - type: int // this will be a field in struct - } - "#, - & ["value: i64"], - & ["pub fn value(&self) -> &i64 {"] - )] - #[case::sequence_struct( - r#" - type::{ - name: sequence_struct, - element: string, // this will be a sequence field in struct - type: list - } - "#, - & ["value: Vec"], - & ["pub fn value(&self) -> &Vec {"] - )] - #[case::struct_with_reference_field( - r#" - type::{ - name: struct_with_reference_field, - fields: { - reference: other_type - } - } - - type::{ - name: other_type, - type: int - } - "#, - & ["reference: OtherType"], - & ["pub fn reference(&self) -> &OtherType {"] - )] - #[case::struct_with_nested_type( - r#" - type::{ - name: struct_with_nested_type, - fields: { - nested_type: { type: int } - } - } - "#, - & ["nested_type: i64"], - & ["pub fn nested_type(&self) -> &i64 {"] - )] - /// Calls ion-cli generate with different schema file. Pass the test if the return value contains the expected properties and accessors. - fn test_code_generation_in_rust( - #[case] test_schema: &str, - #[case] expected_properties: &[&str], - #[case] expected_accessors: &[&str], - ) -> Result<()> { - let mut cmd = Command::cargo_bin("ion")?; - let temp_dir = TempDir::new()?; - let input_schema_path = temp_dir.path().join("test_schema.isl"); - let mut input_schema_file = File::create(input_schema_path)?; - input_schema_file.write(test_schema.as_bytes())?; - input_schema_file.flush()?; - cmd.args([ - "-X", - "generate", - "--schema", - "test_schema.isl", - "--output", - temp_dir.path().to_str().unwrap(), - "--language", - "rust", - "--directory", - temp_dir.path().to_str().unwrap(), - ]); - let command_assert = cmd.assert(); - let output_file_path = temp_dir.path().join("ion_generated_code.rs"); - command_assert.success(); - let contents = - fs::read_to_string(output_file_path).expect("Should have been able to read the file"); - for expected_property in expected_properties { - assert!(contents.contains(expected_property)); - } - for expected_accessor in expected_accessors { - assert!(contents.contains(expected_accessor)); - } - // verify that it generates read-write APIs - assert!(contents.contains("pub fn read_from(reader: &mut Reader) -> SerdeResult {")); - assert!(contents - .contains("pub fn write_to(&self, writer: &mut W) -> SerdeResult<()> {")); - Ok(()) - } + //TODO: Add cargo roundtrip tests once the rust templates are modified based on new code generation model #[rstest] #[case( @@ -343,60 +239,6 @@ mod code_gen_tests { & ["private int id;", "private String name;"], & ["public String getName() {", "public int getId() {"] )] - #[case( - "ValueStruct", - r#" - type::{ - name: value_struct, - type: int // this will be a field in struct - } - "#, - & ["private int value;"], - & ["public int getValue() {"] - )] - #[case( - "SequenceStruct", - r#" - type::{ - name: sequence_struct, - element: string, // this will be a sequence field in struct - type: list - } - "#, - & ["private ArrayList value;"], - & ["public ArrayList getValue() {"] - )] - #[case( - "StructWithReferenceField", - r#" - type::{ - name: struct_with_reference_field, - fields: { - reference: other_type - } - } - - type::{ - name: other_type, - type: int - } - "#, - & ["private OtherType reference;"], - & ["public OtherType getReference() {"] - )] - #[case( - "StructWithNestedType", - r#" - type::{ - name: struct_with_nested_type, - fields: { - nested_type: { type: int } - } - } - "#, - & ["private int nestedType;"], - & ["public int getNestedType() {"] - )] /// Calls ion-cli generate with different schema file. Pass the test if the return value contains the expected properties and accessors. fn test_code_generation_in_java( #[case] test_name: &str, @@ -408,7 +250,7 @@ mod code_gen_tests { let temp_dir = TempDir::new()?; let input_schema_path = temp_dir.path().join("test_schema.isl"); let mut input_schema_file = File::create(input_schema_path)?; - input_schema_file.write(test_schema.as_bytes())?; + input_schema_file.write_all(test_schema.as_bytes())?; input_schema_file.flush()?; cmd.args([ "-X", diff --git a/tests/code-gen-tests.rs b/tests/code-gen-tests.rs index 90c63c62..54c60a57 100644 --- a/tests/code-gen-tests.rs +++ b/tests/code-gen-tests.rs @@ -49,52 +49,7 @@ fn roundtrip_tests_for_generated_code_gradle() -> Result<()> { Ok(()) } -#[test] -fn roundtrip_tests_for_generated_code_cargo() -> Result<()> { - // run the cargo project defined under `code-gen-projects`, - // this project runs the code generator in its build process and generates code, - // this project also has some predefined tests for the generated code, - // so simply running the tests on this project builds the project, generates code and runs tests - - // absolute paths for crate and executables - let ion_executable = env!("CARGO_BIN_EXE_ion"); - let test_project_path = code_gen_projects_path().join("rust").join("code-gen-demo"); - let cargo_executable = env!("CARGO"); - - // Clean - let cargo_clean_output = std::process::Command::new(cargo_executable) - .current_dir(&test_project_path) - .arg("clean") - .output() - .expect("failed to execute 'cargo clean'"); - - println!("Cargo clean status: {}", cargo_clean_output.status); - std::io::stdout() - .write_all(&cargo_clean_output.stdout) - .unwrap(); - std::io::stderr() - .write_all(&cargo_clean_output.stderr) - .unwrap(); - - // Test - let cargo_test_output = std::process::Command::new(cargo_executable) - .current_dir(&test_project_path) - .arg("test") - .env("ION_CLI", ion_executable) - .output() - .expect("failed to execute 'cargo test'"); - - println!("Cargo test status: {}", cargo_test_output.status); - std::io::stdout() - .write_all(&cargo_test_output.stdout) - .unwrap(); - std::io::stderr() - .write_all(&cargo_test_output.stderr) - .unwrap(); - - assert!(cargo_test_output.status.success()); - Ok(()) -} +//TODO: Add cargo roundtrip tests once the rust templates are modified based on new code generation model #[rstest] #[case::any_element_list( @@ -128,7 +83,7 @@ fn test_unsupported_schema_types_failures(#[case] test_schema: &str) -> Result<( let temp_dir = TempDir::new()?; let input_schema_path = temp_dir.path().join("test_schema.isl"); let mut input_schema_file = File::create(input_schema_path)?; - input_schema_file.write(test_schema.as_bytes())?; + input_schema_file.write_all(test_schema.as_bytes())?; input_schema_file.flush()?; cmd.args([ "-X", From f3ae683151d1f87aa163bb24abbe236340686984 Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:52:15 -0700 Subject: [PATCH 02/12] Adds new model changes for scalar (#143) --- code-gen-projects/schema/scalar.isl | 4 + src/bin/ion/commands/generate/generator.rs | 177 +++++++++++++++++- src/bin/ion/commands/generate/model.rs | 48 +++-- .../generate/templates/java/scalar.templ | 52 ++--- tests/cli.rs | 11 ++ 5 files changed, 234 insertions(+), 58 deletions(-) create mode 100644 code-gen-projects/schema/scalar.isl diff --git a/code-gen-projects/schema/scalar.isl b/code-gen-projects/schema/scalar.isl new file mode 100644 index 00000000..cc3957aa --- /dev/null +++ b/code-gen-projects/schema/scalar.isl @@ -0,0 +1,4 @@ +type::{ + name: scalar, + type: string +} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 658518c4..ba9b773f 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,7 +1,7 @@ use crate::commands::generate::context::CodeGenContext; use crate::commands::generate::model::{ AbstractDataType, DataModelNode, FieldPresence, FieldReference, FullyQualifiedTypeReference, - StructureBuilder, + ScalarBuilder, StructureBuilder, WrappedScalarBuilder, }; use crate::commands::generate::result::{ invalid_abstract_data_type_error, invalid_abstract_data_type_raw_error, CodeGenResult, @@ -287,6 +287,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { type_name, isl_type, &mut code_gen_context, + true, )?; // add this nested type to parent code gene context's current list of nested types @@ -315,6 +316,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_name, isl_type, &mut code_gen_context, + false, )?; // add the entire type store and the data model node into tera's context to be used to render template @@ -336,6 +338,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_name: &String, isl_type: &IslType, code_gen_context: &mut CodeGenContext, + is_nested_type: bool, ) -> CodeGenResult { self.current_type_fully_qualified_name .push(isl_type_name.to_case(Case::UpperCamel)); @@ -348,6 +351,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { .any(|it| matches!(it.constraint(), IslConstraintValue::Fields(_, _))) { self.build_structure_from_constraints(constraints, code_gen_context, isl_type)? + } else if Self::contains_scalar_constraints(constraints) { + if is_nested_type { + self.build_scalar_from_constraints(constraints, code_gen_context, isl_type)? + } else { + self.build_wrapped_scalar_from_constraints(constraints, code_gen_context, isl_type)? + } } else { todo!("Support for sequences, maps, scalars, and tuples not implemented yet.") }; @@ -371,6 +380,13 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(data_model_node) } + /// Verifies if the given constraints contain a `type` constraint without any container type references. (e.g. `sexp`, `list`, `struct`) + fn contains_scalar_constraints(constraints: &[IslConstraint]) -> bool { + constraints.iter().any(|it| matches!(it.constraint(), IslConstraintValue::Type(isl_type_ref) if isl_type_ref.name().as_str() != "list" + && isl_type_ref.name().as_str() != "sexp" + && isl_type_ref.name().as_str() != "struct")) + } + fn render_generated_code( &mut self, type_name: &str, @@ -447,7 +463,33 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { name } - /// Build structure from constraints + /// Builds `AbstractDataType::Structure` from the given constraints. + /// e.g. for a given type definition as below: + /// ``` + /// type::{ + /// name: Foo, + /// type: struct, + /// fields: { + /// a: string, + /// b: int, + /// } + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::Structure( + /// Structure { + /// name: vec!["org", "example", "Foo"], // assuming the namespace is `org.example` + /// fields: { + /// a: FieldReference { FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] }, FieldPresence::Optional }, + /// b: FieldReference { FullyQualifiedTypeReference { type_name: vec!["int"], parameters: vec![] }, FieldPresence::Optional }, + /// }, // HashMap with fields defined through `fields` constraint above + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType {name: "foo", .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// is_closed: false, // If the fields constraint was annotated with `closed` then this would be true. + /// } + /// ) + /// ``` fn build_structure_from_constraints( &mut self, constraints: &[IslConstraint], @@ -455,6 +497,9 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { parent_isl_type: &IslType, ) -> CodeGenResult { let mut structure_builder = StructureBuilder::default(); + structure_builder + .name(self.current_type_fully_qualified_name.to_owned()) + .source(parent_isl_type.to_owned()); for constraint in constraints { match constraint.constraint() { IslConstraintValue::Fields(struct_fields, is_closed) => { @@ -479,17 +524,11 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { } // unwrap here is safe as the `current_abstract_data_type_builder` will either be initialized with default implementation // or already initialized with a previous structure related constraint at this point. - structure_builder - .fields(fields) - .source(parent_isl_type.to_owned()) - .is_closed(*is_closed) - .name(self.current_type_fully_qualified_name.to_owned()); + structure_builder.fields(fields).is_closed(*is_closed); } IslConstraintValue::Type(_) => { // by default fields aren't closed - structure_builder - .is_closed(false) - .source(parent_isl_type.to_owned()); + structure_builder.is_closed(false); } _ => { return invalid_abstract_data_type_error( @@ -501,6 +540,122 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(AbstractDataType::Structure(structure_builder.build()?)) } + + /// Builds `AbstractDataType::WrappedScalar` from the given constraints. + /// ``` + /// type::{ + /// name: Foo, + /// type: string, + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::WrappedScalar( + /// WrappedScalar { + /// name: vec!["org", "example", "Foo"], // assuming the namespace is `org.example` + /// base_type: FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] } + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType {name: "foo", .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + /// + /// _Note: Currently code generator would return an error when there are multiple `type` constraints in the type definition. + /// This avoids providing conflicting type constraints in the type definition._ + fn build_wrapped_scalar_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut wrapped_scalar_builder = WrappedScalarBuilder::default(); + wrapped_scalar_builder + .name(self.current_type_fully_qualified_name.to_owned()) + .source(parent_isl_type.to_owned()); + + let mut found_base_type = false; + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Type(isl_type) => { + if found_base_type { + return invalid_abstract_data_type_error("Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types."); + } + let type_name = self + .fully_qualified_type_ref_name(isl_type, code_gen_context)? + .ok_or(invalid_abstract_data_type_raw_error(format!( + "Could not determine `FullQualifiedTypeReference` for type {:?}", + isl_type + )))?; + + // by default fields aren't closed + wrapped_scalar_builder.base_type(type_name); + found_base_type = true; + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); + } + } + } + + Ok(AbstractDataType::WrappedScalar( + wrapped_scalar_builder.build()?, + )) + } + + /// Builds `AbstractDataType::Scalar` from the given constraints. + /// ``` + /// { type: string } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::Scalar( + /// Scalar { + /// base_type: FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] } + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType { .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + /// + /// _Note: Currently code generator would return an error when there are multiple `type` constraints in the type definition. + /// This avoids providing conflicting type constraints in the type definition._ + fn build_scalar_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut scalar_builder = ScalarBuilder::default(); + scalar_builder.source(parent_isl_type.to_owned()); + + let mut found_base_type = false; + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Type(isl_type) => { + if found_base_type { + return invalid_abstract_data_type_error("Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types."); + } + let type_name = self + .fully_qualified_type_ref_name(isl_type, code_gen_context)? + .ok_or(invalid_abstract_data_type_raw_error( + "Could not determine `FullQualifiedTypeReference` for `struct`, `list` or `sexp` as open ended container types aren't supported." + ))?; + + scalar_builder.base_type(type_name); + found_base_type = true; + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); + } + } + } + + Ok(AbstractDataType::Scalar(scalar_builder.build()?)) + } } #[cfg(test)] @@ -535,6 +690,7 @@ mod isl_to_model_tests { &"my_struct".to_string(), &isl_type, &mut CodeGenContext::new(), + false, )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( @@ -620,6 +776,7 @@ mod isl_to_model_tests { &"my_nested_struct".to_string(), &isl_type, &mut CodeGenContext::new(), + false, )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index 7dd1280c..eccd2b4a 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -204,7 +204,7 @@ impl AbstractDataType { AbstractDataType::WrappedScalar(w) => { Some(w.fully_qualified_type_name().to_owned().into()) } - AbstractDataType::Scalar(s) => Some(s.name.to_owned().into()), + AbstractDataType::Scalar(s) => Some(s.base_type.to_owned()), AbstractDataType::Sequence(seq) => Some(seq.element_type.to_owned()), AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()), } @@ -226,10 +226,11 @@ pub struct Scalar { // element: string // this is a nested scalar type // } // ``` - // Corresponding `FullyQualifiedName` would be `vec!["String"]`. - name: FullyQualifiedTypeName, + // Corresponding `FullyQualifiedReference` would be `FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] }`. + base_type: FullyQualifiedTypeReference, // Represents doc comment for the generated code // If the doc comment is provided for this scalar type then this is `Some(doc_comment)`, other it is None. + #[builder(default)] doc_comment: Option, // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. @@ -265,16 +266,12 @@ pub struct WrappedScalar { // type: string // } // ``` - // Corresponding `FullyQualifiedTypeReference` would be as following: - // ``` - // FullyQualifiedTypeReference { - // type_name: vec!["Foo"], // name of the wrapped scalar type - // parameters: vec![FullyQualifiedTypeReference {type_name: vec!["String"] }] // base type name for the scalar value - // } - // ``` - name: FullyQualifiedTypeReference, + // Corresponding `name` would be `vec!["Foo"]` and `base_type` would be `FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] }`. + name: FullyQualifiedTypeName, + base_type: FullyQualifiedTypeReference, // Represents doc comment for the generated code // If the doc comment is provided for this scalar type then this is `Some(doc_comment)`, other it is None. + #[builder(default)] doc_comment: Option, // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. @@ -286,7 +283,7 @@ pub struct WrappedScalar { impl WrappedScalar { pub fn fully_qualified_type_name(&self) -> &FullyQualifiedTypeName { - &self.name.type_name + &self.name } } @@ -396,7 +393,10 @@ mod model_tests { #[test] fn scalar_builder_test() { let expected_scalar = Scalar { - name: vec![], + base_type: FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![], + }, doc_comment: Some("This is scalar type".to_string()), source: anonymous_type(vec![type_constraint(named_type_ref("string"))]), }; @@ -405,7 +405,7 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .name(vec![]) + .base_type(vec!["String".to_string()]) .doc_comment(Some("This is scalar type".to_string())) .source(anonymous_type(vec![type_constraint(named_type_ref( "string", @@ -418,12 +418,10 @@ mod model_tests { #[test] fn wrapped_scalar_builder_test() { let expected_scalar = WrappedScalar { - name: FullyQualifiedTypeReference { - type_name: vec!["Foo".to_string()], - parameters: vec![FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], - parameters: vec![], - }], + name: vec!["Foo".to_string()], + base_type: FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![], }, doc_comment: Some("This is scalar type".to_string()), source: anonymous_type(vec![type_constraint(named_type_ref("string"))]), @@ -433,12 +431,10 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .name(FullyQualifiedTypeReference { - type_name: vec!["Foo".to_string()], - parameters: vec![FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], - parameters: vec![], - }], + .name(vec!["Foo".to_string()]) + .base_type(FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![], }) .doc_comment(Some("This is scalar type".to_string())) .source(anonymous_type(vec![type_constraint(named_type_ref( diff --git a/src/bin/ion/commands/generate/templates/java/scalar.templ b/src/bin/ion/commands/generate/templates/java/scalar.templ index f5c7954c..c71f5f52 100644 --- a/src/bin/ion/commands/generate/templates/java/scalar.templ +++ b/src/bin/ion/commands/generate/templates/java/scalar.templ @@ -1,69 +1,77 @@ -package {{ namespace }}; -import java.util.ArrayList; +{% macro scalar(model) %} +{% set full_namespace = namespace | join(sep=".") %} + +package {{ full_namespace }}; import com.amazon.ion.IonReader; import com.amazon.ion.IonException; import com.amazon.ion.IonWriter; import com.amazon.ion.IonType; import java.io.IOException; -public class {{ target_kind_name }} { - private {{ fields[0].value_type }} value; +{# Verify that the abstract data type is a scalar type and store information for this scalar value #} +{% set scalar_info = model.code_gen_type["WrappedScalar"] %} +{% set base_type = scalar_info["base_type"] | fully_qualified_type_name %} + +class {{ model.name }} { + private {{ base_type }} value; - public {{ target_kind_name }}() {} + public {{ model.name }}() {} - public {{ fields[0].value_type }} getValue() { + public {{ base_type }} getValue() { return this.value; } - public void setValue({{ fields[0].value_type }} value) { + public void setValue({{ base_type }} value) { this.value = value; return; } /** - * Reads a {{ target_kind_name }} from an {@link IonReader}. + * Reads a {{ model.name }} from an {@link IonReader}. * * This method does not advance the reader at the current level. * The caller is responsible for positioning the reader on the value to read. */ - public static {{ target_kind_name }} readFrom(IonReader reader) { + public static {{ model.name }} readFrom(IonReader reader) { {# Initializes all the fields of this class #} - {{ fields[0].value_type }} value = - {% if fields[0].value_type == "boolean" %} + {{ base_type }} value = + {% if base_type == "boolean" %} false - {% elif fields[0].value_type == "int" or fields[0].value_type == "double" %} + {% elif base_type == "int" or base_type == "double" %} 0 {% else %} null {% endif %}; {# Reads `Value` class with a single field `value` #} - value = {% if fields[0].value_type | is_built_in_type %} - {% if fields[0].value_type == "bytes[]" %} + value = {% if base_type | is_built_in_type %} + {% if base_type == "bytes[]" %} reader.newBytes(); {% else %} - reader.{{ fields[0].value_type | camel }}Value(); + reader.{{ base_type | camel }}Value(); {% endif %} {% else %} - {{ fields[0].value_type }}.readFrom(reader); + {{ base_type }}.readFrom(reader); {% endif %} - {{ target_kind_name }} {{ target_kind_name | camel }} = new {{ target_kind_name }}(); - {{ target_kind_name | camel }}.value = value; + {{ model.name }} {{ model.name | camel }} = new {{ model.name }}(); + {{ model.name | camel }}.value = value; - return {{ target_kind_name | camel }}; + return {{ model.name | camel }}; } /** - * Writes a {{ target_kind_name }} as Ion from an {@link IonWriter}. + * Writes a {{ model.name }} as Ion from an {@link IonWriter}. * * This method does not close the writer after writing is complete. * The caller is responsible for closing the stream associated with the writer. */ public void writeTo(IonWriter writer) throws IOException { {# Writes `Value` class with a single field `value` as an Ion value #} - {% if fields[0].value_type | is_built_in_type == false %} + {% if base_type | is_built_in_type == false %} this.value.writeTo(writer)?; {% else %} - writer.write{{ fields[0].isl_type_name | upper_camel }}(this.value); + writer.write{{ base_type | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.value); {% endif %} } } +{% endmacro %} +{{ self::scalar(model=model) }} \ No newline at end of file diff --git a/tests/cli.rs b/tests/cli.rs index d9d4de00..b55aa031 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -239,6 +239,17 @@ mod code_gen_tests { & ["private int id;", "private String name;"], & ["public String getName() {", "public int getId() {"] )] + #[case( + "Scalar", + r#" + type::{ + name: scalar, + type: string + } + "#, + & ["private String value;"], + & ["public String getValue() {"] + )] /// Calls ion-cli generate with different schema file. Pass the test if the return value contains the expected properties and accessors. fn test_code_generation_in_java( #[case] test_name: &str, From c67f936790761fd080b3469bd3004ff0dc006d32 Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:22:15 -0700 Subject: [PATCH 03/12] Adds changes for sequence data type (#144) --- code-gen-projects/schema/sequence.isl | 5 + src/bin/ion/commands/generate/generator.rs | 236 +++++++++++++++--- src/bin/ion/commands/generate/model.rs | 74 ++++-- src/bin/ion/commands/generate/result.rs | 9 + .../generate/templates/java/class.templ | 10 +- .../generate/templates/java/nested_type.templ | 2 +- .../generate/templates/java/sequence.templ | 68 ++--- .../generate/templates/java/util_macros.templ | 32 +-- src/bin/ion/commands/generate/utils.rs | 4 +- 9 files changed, 340 insertions(+), 100 deletions(-) create mode 100644 code-gen-projects/schema/sequence.isl diff --git a/code-gen-projects/schema/sequence.isl b/code-gen-projects/schema/sequence.isl new file mode 100644 index 00000000..8331affa --- /dev/null +++ b/code-gen-projects/schema/sequence.isl @@ -0,0 +1,5 @@ +type::{ + name: sequence, + type: list, + element: string +} diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index ba9b773f..410798f9 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,7 +1,7 @@ -use crate::commands::generate::context::CodeGenContext; +use crate::commands::generate::context::{CodeGenContext, SequenceType}; use crate::commands::generate::model::{ AbstractDataType, DataModelNode, FieldPresence, FieldReference, FullyQualifiedTypeReference, - ScalarBuilder, StructureBuilder, WrappedScalarBuilder, + ScalarBuilder, SequenceBuilder, StructureBuilder, WrappedScalarBuilder, WrappedSequenceBuilder, }; use crate::commands::generate::result::{ invalid_abstract_data_type_error, invalid_abstract_data_type_raw_error, CodeGenResult, @@ -298,7 +298,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // pop out the nested type name from the fully qualified namespace as it has been already added to the type store and to nested types self.current_type_fully_qualified_name.pop(); data_model_node - .fully_qualified_type_ref() + .fully_qualified_type_ref::() .ok_or(invalid_abstract_data_type_raw_error( "Can not determine fully qualified name for the data model", )) @@ -345,12 +345,36 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { let constraints = isl_type.constraints(); - // Initialize `AbstractDataType` according to the first constraint in the list of constraints + // Initialize `AbstractDataType` according to the list of constraints + // Below are some checks to verify which AbstractDatatype variant should be constructed based on given ISL constraints: + // * If given list of constraints has any `fields` constraint then `AbstractDataType::Structure` needs to be constructed. + // * Since currently, code generation doesn't support open ended types having `type: struct` alone is not enough for constructing + // `AbstractDataType::Structure`. + // * If given list of constraints has any `element` constraint then `AbstractDataType::Sequence` needs to be constructed. + // * Since currently, code generation doesn't support open ended types having `type: list` or `type: sexp` alone is not enough for constructing + // `AbstractDataType::Sequence`. + // * The sequence type for `Sequence` will be stored based on `type` constraint with either `list` or `sexp`. + // * If given list of constraints has any `type` constraint except `type: list`, `type: struct` and `type: sexp`, then `AbstractDataType::Scalar` needs to be constructed. + // * The `base_type` for `Scalar` will be stored based on `type` constraint. + // * All the other constraints except the above ones are not yet supported by code generator. let abstract_data_type = if constraints .iter() .any(|it| matches!(it.constraint(), IslConstraintValue::Fields(_, _))) { self.build_structure_from_constraints(constraints, code_gen_context, isl_type)? + } else if constraints + .iter() + .any(|it| matches!(it.constraint(), IslConstraintValue::Element(_, _))) + { + if is_nested_type { + self.build_sequence_from_constraints(constraints, code_gen_context, isl_type)? + } else { + self.build_wrapped_sequence_from_constraints( + constraints, + code_gen_context, + isl_type, + )? + } } else if Self::contains_scalar_constraints(constraints) { if is_nested_type { self.build_scalar_from_constraints(constraints, code_gen_context, isl_type)? @@ -358,7 +382,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { self.build_wrapped_scalar_from_constraints(constraints, code_gen_context, isl_type)? } } else { - todo!("Support for sequences, maps, scalars, and tuples not implemented yet.") + todo!("Support for maps and tuples not implemented yet.") }; let data_model_node = DataModelNode { @@ -370,7 +394,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // TODO: verify the `occurs` value within a field, by default the fields are optional. // add current data model node into the data model store self.data_model_store.insert( - abstract_data_type.fully_qualified_type_ref().ok_or( + abstract_data_type.fully_qualified_type_ref::().ok_or( invalid_abstract_data_type_raw_error( "Can not determine fully qualified name for the data model", ), @@ -463,6 +487,27 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { name } + /// Returns error if duplicate constraints are present based `found_constraint` flag + fn handle_duplicate_constraint( + &mut self, + found_constraint: bool, + constraint_name: &str, + isl_type: &IslTypeRef, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult { + if found_constraint { + return invalid_abstract_data_type_error(format!( + "Multiple `{}` constraints in the type definitions are not supported in code generation as it can lead to conflicting types.", constraint_name + )); + } + + self.fully_qualified_type_ref_name(isl_type, code_gen_context)? + .ok_or(invalid_abstract_data_type_raw_error(format!( + "Could not determine `FullQualifiedTypeReference` for type {:?}", + isl_type + ))) + } + /// Builds `AbstractDataType::Structure` from the given constraints. /// e.g. for a given type definition as below: /// ``` @@ -577,20 +622,19 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { for constraint in constraints { match constraint.constraint() { IslConstraintValue::Type(isl_type) => { - if found_base_type { - return invalid_abstract_data_type_error("Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types."); - } - let type_name = self - .fully_qualified_type_ref_name(isl_type, code_gen_context)? - .ok_or(invalid_abstract_data_type_raw_error(format!( - "Could not determine `FullQualifiedTypeReference` for type {:?}", - isl_type - )))?; - - // by default fields aren't closed + let type_name = self.handle_duplicate_constraint( + found_base_type, + "type", + isl_type, + code_gen_context, + )?; wrapped_scalar_builder.base_type(type_name); found_base_type = true; } + IslConstraintValue::ContainerLength(_) => { + // TODO: add support for container length + // this is currently not supported and is a no-op + } _ => { return invalid_abstract_data_type_error( "Could not determine the abstract data type due to conflicting constraints", @@ -634,15 +678,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { for constraint in constraints { match constraint.constraint() { IslConstraintValue::Type(isl_type) => { - if found_base_type { - return invalid_abstract_data_type_error("Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types."); - } - let type_name = self - .fully_qualified_type_ref_name(isl_type, code_gen_context)? - .ok_or(invalid_abstract_data_type_raw_error( - "Could not determine `FullQualifiedTypeReference` for `struct`, `list` or `sexp` as open ended container types aren't supported." - ))?; - + let type_name = self.handle_duplicate_constraint( + found_base_type, + "type", + isl_type, + code_gen_context, + )?; scalar_builder.base_type(type_name); found_base_type = true; } @@ -656,6 +697,139 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(AbstractDataType::Scalar(scalar_builder.build()?)) } + + /// Builds `AbstractDataType::WrappedSequence` from the given constraints. + /// ``` + /// type::{ + /// name: foo, + /// type: list, + /// element: string, + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::WrappedSequence( + /// WrappedSequence { + /// name: vec!["org", "example", "Foo"] // assuming the namespace here is `org.example` + /// element_type: FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] } // Represents the element type for the list + /// sequence_type: SequenceType::List, // Represents list type for the given sequence + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType { .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + fn build_wrapped_sequence_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut wrapped_sequence_builder = WrappedSequenceBuilder::default(); + wrapped_sequence_builder + .name(self.current_type_fully_qualified_name.to_owned()) + .source(parent_isl_type.to_owned()); + let mut found_base_type = false; + let mut found_element_constraint = false; + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Element(isl_type_ref, _) => { + let type_name = self.handle_duplicate_constraint( + found_element_constraint, + "type", + isl_type_ref, + code_gen_context, + )?; + + wrapped_sequence_builder.element_type(type_name); + found_element_constraint = true; + } + IslConstraintValue::Type(isl_type_ref) => { + if found_base_type { + return invalid_abstract_data_type_error( + "Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types." + ); + } + if isl_type_ref.name() == "sexp" { + wrapped_sequence_builder.sequence_type(SequenceType::SExp); + } else if isl_type_ref.name() == "list" { + wrapped_sequence_builder.sequence_type(SequenceType::List); + } + found_base_type = true; + } + IslConstraintValue::ContainerLength(_) => { + // TODO: add support for container length + // this is currently not supported and is a no-op + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); + } + } + } + Ok(AbstractDataType::WrappedSequence( + wrapped_sequence_builder.build()?, + )) + } + + /// Builds `AbstractDataType::Sequence` from the given constraints. + /// ``` + /// { + /// type: list, + /// element: string, + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::Sequence( + /// Sequence { + /// element_type: FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] } // Represents the element type for the list + /// sequence_type: SequenceType::List, // Represents list type for the given sequence + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType { .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + fn build_sequence_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut sequence_builder = SequenceBuilder::default(); + sequence_builder.source(parent_isl_type.to_owned()); + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Element(isl_type_ref, _) => { + let type_name = self + .fully_qualified_type_ref_name(isl_type_ref, code_gen_context)? + .ok_or(invalid_abstract_data_type_raw_error(format!( + "Could not determine `FullQualifiedTypeReference` for type {:?}", + isl_type_ref + )))?; + + sequence_builder.element_type(type_name); + } + IslConstraintValue::Type(isl_type_ref) => { + if isl_type_ref.name() == "sexp" { + sequence_builder.sequence_type(SequenceType::SExp); + } else if isl_type_ref.name() == "list" { + sequence_builder.sequence_type(SequenceType::List); + } + } + IslConstraintValue::ContainerLength(_) => { + // TODO: add support for container length + // this is currently not supported and is a no-op + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); + } + } + } + Ok(AbstractDataType::Sequence(sequence_builder.build()?)) + } } #[cfg(test)] @@ -694,7 +868,9 @@ mod isl_to_model_tests { )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( - abstract_data_type.fully_qualified_type_ref().unwrap(), + abstract_data_type + .fully_qualified_type_ref::() + .unwrap(), FullyQualifiedTypeReference { type_name: vec![ "org".to_string(), @@ -780,7 +956,9 @@ mod isl_to_model_tests { )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( - abstract_data_type.fully_qualified_type_ref().unwrap(), + abstract_data_type + .fully_qualified_type_ref::() + .unwrap(), FullyQualifiedTypeReference { type_name: vec![ "org".to_string(), @@ -838,7 +1016,7 @@ mod isl_to_model_tests { .code_gen_type .as_ref() .unwrap() - .fully_qualified_type_ref(), + .fully_qualified_type_ref::(), Some(FullyQualifiedTypeReference { type_name: vec![ "org".to_string(), diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index eccd2b4a..10017bee 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -10,6 +10,7 @@ use std::fmt::{Display, Formatter}; // _Note: This model will eventually use a map (FullQualifiedTypeReference, DataModel) to resolve some the references in container types(sequence or structure)._ // TODO: This is not yet used in the implementation, modify current implementation to use this data model. use crate::commands::generate::context::SequenceType; +use crate::commands::generate::utils::Language; use serde::Serialize; use serde_json::Value; @@ -70,10 +71,10 @@ impl DataModelNode { false } - pub fn fully_qualified_type_ref(&mut self) -> Option { + pub fn fully_qualified_type_ref(&mut self) -> Option { self.code_gen_type .as_ref() - .and_then(|t| t.fully_qualified_type_ref()) + .and_then(|t| t.fully_qualified_type_ref::()) } } @@ -170,14 +171,13 @@ impl FullyQualifiedTypeReference { #[derive(Debug, Clone, PartialEq, Serialize)] pub enum AbstractDataType { // Represents a scalar type which also has a name attached to it and is nominally distinct from its base type. - #[allow(dead_code)] WrappedScalar(WrappedScalar), // Represents a scalar value (e.g. a string or integer or user defined type) - #[allow(dead_code)] Scalar(Scalar), // A series of zero or more values whose type is described by the nested `element_type` - #[allow(dead_code)] Sequence(Sequence), + // Represents a sequence type which also has name attached to it and is nominally distinct from its enclosed type. + WrappedSequence(WrappedSequence), // A collection of field name/value pairs (e.g. a map) Structure(Structure), } @@ -192,20 +192,30 @@ impl AbstractDataType { AbstractDataType::Scalar(Scalar { doc_comment, .. }) => { doc_comment.as_ref().map(|s| s.as_str()) } - AbstractDataType::Sequence(Sequence { doc_comment, .. }) => Some(doc_comment.as_str()), + AbstractDataType::Sequence(Sequence { doc_comment, .. }) => { + doc_comment.as_ref().map(|s| s.as_str()) + } + AbstractDataType::WrappedSequence(WrappedSequence { doc_comment, .. }) => { + doc_comment.as_ref().map(|s| s.as_str()) + } AbstractDataType::Structure(Structure { doc_comment, .. }) => { doc_comment.as_ref().map(|s| s.as_str()) } } } - pub fn fully_qualified_type_ref(&self) -> Option { + pub fn fully_qualified_type_ref(&self) -> Option { match self { AbstractDataType::WrappedScalar(w) => { Some(w.fully_qualified_type_name().to_owned().into()) } AbstractDataType::Scalar(s) => Some(s.base_type.to_owned()), - AbstractDataType::Sequence(seq) => Some(seq.element_type.to_owned()), + AbstractDataType::Sequence(seq) => { + Some(L::target_type_as_sequence(seq.element_type.to_owned())) + } + AbstractDataType::WrappedSequence(seq) => { + Some(L::target_type_as_sequence(seq.element_type.to_owned())) + } AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()), } } @@ -289,8 +299,6 @@ impl WrappedScalar { /// Represents series of zero or more values whose type is described by the nested `element_type` /// and sequence type is described by nested `sequence_type` (e.g. List or SExp). -/// If there is no `element` constraint present in schema type then `element_type` will be None. -/// If there is no `type` constraint present in schema type then `sequence_type` will be None. /// e.g. Given below ISL, /// ``` /// type::{ @@ -308,11 +316,12 @@ impl WrappedScalar { #[allow(dead_code)] #[derive(Debug, Clone, Builder, PartialEq, Serialize)] #[builder(setter(into))] -pub struct Sequence { +pub struct WrappedSequence { // Represents the fully qualified name for this data model name: FullyQualifiedTypeName, // Represents doc comment for the generated code - doc_comment: String, + #[builder(default)] + doc_comment: Option, // Represents the fully qualified name with namespace where each element of vector stores a module name or class/struct name. // _Note: that a hashmap with (FullQualifiedTypeReference, DataModel) pairs will be stored in code generator to get information on the element_type name used here._ element_type: FullyQualifiedTypeReference, @@ -326,6 +335,41 @@ pub struct Sequence { source: IslType, } +/// Represents series of zero or more values whose type is described by the nested `element_type` +/// and sequence type is described by nested `sequence_type` (e.g. List or SExp). +/// e.g. Given below ISL, +/// ``` +/// type::{ +/// name: sequence_type, +/// element: int, +/// type: list +/// } +/// ``` +/// Corresponding generated code in Rust would look like following: +/// ``` +/// struct SequenceType { +/// value: Vec +/// } +/// ``` +#[derive(Debug, Clone, Builder, PartialEq, Serialize)] +#[builder(setter(into))] +pub struct Sequence { + // Represents doc comment for the generated code + #[builder(default)] + pub(crate) doc_comment: Option, + // Represents the fully qualified name with namespace where each element of vector stores a module name or class/struct name. + // _Note: that a hashmap with (FullQualifiedTypeReference, DataModel) pairs will be stored in code generator to get information on the element_type name used here._ + pub(crate) element_type: FullyQualifiedTypeReference, + // Represents the type of the sequence which is either `sexp` or `list`. + pub(crate) sequence_type: SequenceType, + // Represents the source ISL type which can be used to get other constraints useful for this type. + // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. + // This will also be useful for `text` type to verify if this is a `string` or `symbol`. + // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. + #[serde(skip_serializing)] + pub(crate) source: IslType, +} + /// Represents a collection of field name/value pairs (e.g. a map) /// e.g. Given below ISL, /// ``` @@ -448,8 +492,7 @@ mod model_tests { #[test] fn sequence_builder_test() { let expected_seq = Sequence { - name: vec![], - doc_comment: "This is sequence type of strings".to_string(), + doc_comment: Some("This is sequence type of strings".to_string()), element_type: FullyQualifiedTypeReference { type_name: vec!["String".to_string()], parameters: vec![], @@ -465,8 +508,7 @@ mod model_tests { // sets all the information about the sequence except the `element_type` seq_builder - .name(vec![]) - .doc_comment("This is sequence type of strings") + .doc_comment(Some("This is sequence type of strings".to_string())) .sequence_type(SequenceType::List) .source(anonymous_type(vec![ type_constraint(named_type_ref("list")), diff --git a/src/bin/ion/commands/generate/result.rs b/src/bin/ion/commands/generate/result.rs index d18dd19b..8c6dcd6c 100644 --- a/src/bin/ion/commands/generate/result.rs +++ b/src/bin/ion/commands/generate/result.rs @@ -1,5 +1,6 @@ use crate::commands::generate::model::{ ScalarBuilderError, SequenceBuilderError, StructureBuilderError, WrappedScalarBuilderError, + WrappedSequenceBuilderError, }; use ion_schema::result::IonSchemaError; use thiserror::Error; @@ -71,6 +72,14 @@ impl From for CodeGenError { } } +impl From for CodeGenError { + fn from(value: WrappedSequenceBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} + impl From for CodeGenError { fn from(value: StructureBuilderError) -> Self { CodeGenError::DataModelBuilderError { diff --git a/src/bin/ion/commands/generate/templates/java/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index ec7f8cbe..085f2a24 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -119,13 +119,13 @@ import java.io.IOException; {% set field_value = field_val.0 | fully_qualified_type_name %} writer.setFieldName("{{ field_name }}"); {% if field_value | is_built_in_type == false %} - this.{{ field_name | camel }}.writeTo(writer); - {% else %} - {% if field_value is containing("ArrayList") %} + {% if field_value is containing("ArrayList") %} {{ util_macros::write_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} - {% else %} + {% else %} + this.{{ field_name | camel }}.writeTo(writer); + {% endif %} + {% else %} writer.write{{ field_value | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.{{ field_name | camel }}); - {% endif %} {% endif %} {% endfor %} writer.stepOut(); diff --git a/src/bin/ion/commands/generate/templates/java/nested_type.templ b/src/bin/ion/commands/generate/templates/java/nested_type.templ index 299a6085..d59c0155 100644 --- a/src/bin/ion/commands/generate/templates/java/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/java/nested_type.templ @@ -49,7 +49,7 @@ {{ sequence_info["element_type"] | fully_qualified_type_name }} value {% elif inline_type.code_gen_type is containing("Scalar") %} {% set scalar_info = model.code_gen_type["WrappedScalar"] %} - {% set base_type = scalar_info["name"]["parameters"][0] | fully_qualified_type_name %} + {% set base_type = scalar_info["base_type"] | fully_qualified_type_name %} {{ base_type }} value {% endif %} {% endmacro %} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/java/sequence.templ b/src/bin/ion/commands/generate/templates/java/sequence.templ index 0f4c8a7f..15378305 100644 --- a/src/bin/ion/commands/generate/templates/java/sequence.templ +++ b/src/bin/ion/commands/generate/templates/java/sequence.templ @@ -1,81 +1,83 @@ -package {{ namespace }}; -import java.util.ArrayList; +{% macro sequence(model) %} + +{% if is_nested == false %} +{% set full_namespace = namespace | join(sep=".") %} + +package {{ full_namespace }}; import com.amazon.ion.IonReader; import com.amazon.ion.IonException; import com.amazon.ion.IonWriter; import com.amazon.ion.IonType; import java.io.IOException; +{% endif %} + +{# Verify that the abstract data type is a sequence type and store information for this sequence value #} +{% set sequence_info = model.code_gen_type["WrappedSequence"] %} -public class {{ target_kind_name }} { - private {{ fields[0].value_type }} value; +class {{ model.name }} { + private java.util.ArrayList<{{ sequence_info["element_type"] | fully_qualified_type_name }}> value; - public {{ target_kind_name }}() {} + public {{ model.name }}() {} - public {{ fields[0].value_type }} getValue() { + public java.util.ArrayList<{{ sequence_info["element_type"] | fully_qualified_type_name }}> getValue() { return this.value; } - public void setValue({{ fields[0].value_type }} value) { + public void setValue(java.util.ArrayList<{{ sequence_info["element_type"] | fully_qualified_type_name }}> value) { this.value = value; return; } /** - * Reads a {{ target_kind_name }} from an {@link IonReader}. + * Reads a {{ model.name }} from an {@link IonReader}. * * This method does not advance the reader at the current level. * The caller is responsible for positioning the reader on the value to read. */ - public static {{ target_kind_name }} readFrom(IonReader reader) { + public static {{ model.name }} readFrom(IonReader reader) { {# Initializes all the fields of this class #} - {{ fields[0].value_type }} value = - {% if fields[0].value_type == "boolean" %} - false - {% elif fields[0].value_type == "int" or fields[0].value_type == "double" %} - 0 - {% else %} - null - {% endif %}; + java.util.ArrayList<{{ sequence_info["element_type"] | fully_qualified_type_name }}> value = new java.util.ArrayList<{{ sequence_info["element_type"] | fully_qualified_type_name }}>(); {# Reads `Sequence` class with a single field `value` that is an `ArrayList` #} - if(reader.getType() != IonType.{{ abstract_data_type["Sequence"].sequence_type | upper }}) { - throw new IonException("Expected {{ abstract_data_type["Sequence"].sequence_type }}, found " + reader.getType() + " while reading {{ fields[0].name | camel }}."); + if(reader.getType() != IonType.{{ sequence_info["sequence_type"] | upper }}) { + throw new IonException("Expected {{ sequence_info["sequence_type"] }}, found " + reader.getType() + " while reading value."); } reader.stepIn(); - value = new {{ fields[0].value_type }}(); - {# Iterate through the `ArrayList` and read each element in it based on the data type provided in `abstract_data_type[Sequence]` #} + {# Iterate through the `ArrayList` and read each element in it based on the data type provided in `sequence_info["sequence_type"]` #} while (reader.hasNext()) { reader.next(); - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - value.add({{ abstract_data_type["Sequence"].element_type }}.readFrom(reader)); - {% elif abstract_data_type["Sequence"].element_type == "bytes[]" %} + {% if sequence_info["element_type"] |fully_qualified_type_name | is_built_in_type == false %} + value.add({{ sequence_info["element_type"] | fully_qualified_type_name }}.readFrom(reader)); + {% elif sequence_info["element_type"] | fully_qualified_type_name == "bytes[]" %} value.add(reader.newBytes()); {% else %} - value.add(reader.{{ abstract_data_type["Sequence"].element_type | camel }}Value()); + value.add(reader.{{ sequence_info["element_type"] | fully_qualified_type_name | camel }}Value()); {% endif %} } reader.stepOut(); - {{ target_kind_name }} {{ target_kind_name | camel }} = new {{ target_kind_name }}(); - {{ target_kind_name | camel }}.value = value; + {{ model.name }} {{ model.name | camel }} = new {{ model.name }}(); + {{ model.name | camel }}.value = value; - return {{ target_kind_name | camel }}; + return {{ model.name | camel }}; } /** - * Writes a {{ target_kind_name }} as Ion from an {@link IonWriter}. + * Writes a {{ model.name }} as Ion from an {@link IonWriter}. * * This method does not close the writer after writing is complete. * The caller is responsible for closing the stream associated with the writer. */ public void writeTo(IonWriter writer) throws IOException { {# Writes `Sequence` class with a single field `value` that is an `ArrayList` as an Ion sequence #} - writer.stepIn(IonType.{{ abstract_data_type["Sequence"].sequence_type | upper }}); - for ({{ abstract_data_type["Sequence"].element_type }} value: this.value) { - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} + writer.stepIn(IonType.{{ sequence_info["sequence_type"] | upper }}); + for ({{ sequence_info["element_type"] | fully_qualified_type_name }} value: this.value) { + {% if sequence_info["element_type"] | fully_qualified_type_name | is_built_in_type == false %} value.writeTo(writer); {% else %} - writer.write{{ abstract_data_type["Sequence"].element_type | upper_camel }}(value); + writer.write{{ sequence_info["element_type"] | fully_qualified_type_name | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(value); {% endif %} } writer.stepOut(); } } +{% endmacro %} +{{ self::sequence(model=model) }} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/java/util_macros.templ b/src/bin/ion/commands/generate/templates/java/util_macros.templ index c3c81d64..a03f0927 100644 --- a/src/bin/ion/commands/generate/templates/java/util_macros.templ +++ b/src/bin/ion/commands/generate/templates/java/util_macros.templ @@ -1,34 +1,36 @@ {# following macro defines statements to read a class field as sequence #} -{% macro read_as_sequence(field) %} - new {{ field.value_type }}(); +{% macro read_as_sequence(field_name, field_value, type_store) %} + {% set field_value_model = type_store[field_value] %} + new {{ field_value }}(); {# Reads `Sequence` field that is an `ArrayList` #} - if(reader.getType() != IonType.{{ field.abstract_data_type["Sequence"].sequence_type | upper }}) { - throw new IonException("Expected {{ field.abstract_data_type["Sequence"].sequence_type }}, found " + reader.getType() + " while reading {{ field.name | camel }}."); + if(reader.getType() != IonType.{{ field_value_model.code_gen_type["Sequence"].sequence_type | upper }}) { + throw new IonException("Expected {{ field_value_model.code_gen_type["Sequence"].sequence_type }}, found " + reader.getType() + " while reading {{ field_name | camel }}."); } reader.stepIn(); {# Iterate through the `ArrayList` and read each element in it based on the data type provided in `field.abstract_data_type[Sequence]` #} while (reader.hasNext()) { reader.next(); - {% if field.abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - {{ field.name | camel }}.add({{ field.abstract_data_type["Sequence"].element_type }}.readFrom(reader)); - {% elif field.abstract_data_type["Sequence"].element_type == "bytes[]" %} - {{ field.name | camel }}.add(reader.newBytes()); + {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} + {{ field_name | camel }}.add({{ field_value_model.code_gen_type["Sequence"].element_type }}.readFrom(reader)); + {% elif field_value_model.code_gen_type["Sequence"].element_type == "bytes[]" %} + {{ field_name | camel }}.add(reader.newBytes()); {% else %} - {{ field.name | camel }}.add(reader.{{ field.abstract_data_type["Sequence"].element_type | camel }}Value()); + {{ field_name | camel }}.add(reader.{{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | camel }}Value()); {% endif %} } reader.stepOut(); {% endmacro %} {# following macro defines statements to write a class field as sequence #} -{% macro write_as_sequence(field) %} +{% macro write_as_sequence(field_name, field_value, type_store) %} + {% set field_value_model = type_store[field_value] %} {# Writes `Sequence` field that is an `ArrayList` as an Ion sequence #} - writer.stepIn(IonType.{{ field.abstract_data_type["Sequence"].sequence_type | upper }}); - for ({{ field.abstract_data_type["Sequence"].element_type }} value: this.{{ field.name |camel }}) { - {% if field.abstract_data_type["Sequence"].element_type | is_built_in_type == false %} + writer.stepIn(IonType.{{ field_value_model.code_gen_type["Sequence"].sequence_type | upper }}); + for ({{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name }} value: this.{{ field_name |camel }}) { + {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} value.writeTo(writer); {% else %} - writer.write{{ field.abstract_data_type["Sequence"].element_type | upper_camel }}(value); + writer.write{{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(value); {% endif %} } writer.stepOut(); -{% endmacro %} +{% endmacro %} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/utils.rs b/src/bin/ion/commands/generate/utils.rs index dca7bf46..93647680 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -245,7 +245,9 @@ impl TryFrom<&DataModelNode> for Template { AbstractDataType::Scalar(_) | AbstractDataType::WrappedScalar(_) => { Ok(Template::Scalar) } - AbstractDataType::Sequence(_) => Ok(Template::Sequence), + AbstractDataType::Sequence(_) | AbstractDataType::WrappedSequence(_) => { + Ok(Template::Sequence) + } AbstractDataType::Structure(_) => Ok(Template::Struct), } } else { From 12a98fdc60bd06fe8a45459f90f6217bbc3d3158 Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:25:20 -0700 Subject: [PATCH 04/12] Adds tests for new model changes (#146) --- .../mismatched_sequence_type.ion | 9 + .../bad/nested_struct/mismatched_type.ion | 1 + .../input/bad/scalar/mismatched_type.ion | 1 + .../mismatched_sequence_element_type.ion | 1 + .../bad/sequence/mismatched_sequence_type.ion | 1 + .../mismatched_sequence_element_type.ion | 7 + .../mismatched_sequence_type.ion | 7 + .../struct_with_fields/mismatched_type.ion | 2 +- .../input/good/nested_struct/empty_values.ion | 1 + .../input/good/nested_struct/valid_fields.ion | 1 + .../nested_struct/valid_unordered_fields.ion | 2 +- .../input/good/scalar/empty_value.ion | 2 + .../input/good/scalar/valid_value.ion | 2 + .../input/good/sequence/empty_sequence.ion | 1 + .../input/good/sequence/valid_elements.ion | 1 + .../good/struct_with_fields/empty_values.ion | 1 + .../good/struct_with_fields/valid_fields.ion | 1 + .../valid_unordered_fields.ion | 2 +- .../test/java/org/example/CodeGenTest.java | 163 ++++++++++++------ code-gen-projects/schema/nested_struct.isl | 1 + .../schema/struct_with_fields.isl | 1 + 21 files changed, 150 insertions(+), 58 deletions(-) create mode 100644 code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion create mode 100644 code-gen-projects/input/bad/scalar/mismatched_type.ion create mode 100644 code-gen-projects/input/bad/sequence/mismatched_sequence_element_type.ion create mode 100644 code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion create mode 100644 code-gen-projects/input/good/scalar/empty_value.ion create mode 100644 code-gen-projects/input/good/scalar/valid_value.ion create mode 100644 code-gen-projects/input/good/sequence/empty_sequence.ion create mode 100644 code-gen-projects/input/good/sequence/valid_elements.ion diff --git a/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion b/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion new file mode 100644 index 00000000..6b244a95 --- /dev/null +++ b/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion @@ -0,0 +1,9 @@ +// nested struct with mismatched sequence type +{ + A: "hello", + B: 12, + C: { + D: false, + E: (1 2 3) // expected list + } +} diff --git a/code-gen-projects/input/bad/nested_struct/mismatched_type.ion b/code-gen-projects/input/bad/nested_struct/mismatched_type.ion index e8c48dba..30ae03b8 100644 --- a/code-gen-projects/input/bad/nested_struct/mismatched_type.ion +++ b/code-gen-projects/input/bad/nested_struct/mismatched_type.ion @@ -4,5 +4,6 @@ B: 12, C: { D: 1e0, // expected type: bool + E: [1, 2, 3] } } diff --git a/code-gen-projects/input/bad/scalar/mismatched_type.ion b/code-gen-projects/input/bad/scalar/mismatched_type.ion new file mode 100644 index 00000000..854c85ab --- /dev/null +++ b/code-gen-projects/input/bad/scalar/mismatched_type.ion @@ -0,0 +1 @@ +12 // expected string diff --git a/code-gen-projects/input/bad/sequence/mismatched_sequence_element_type.ion b/code-gen-projects/input/bad/sequence/mismatched_sequence_element_type.ion new file mode 100644 index 00000000..5926de22 --- /dev/null +++ b/code-gen-projects/input/bad/sequence/mismatched_sequence_element_type.ion @@ -0,0 +1 @@ +[1, 2, 3] // expected list of strings diff --git a/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion b/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion new file mode 100644 index 00000000..a3e42013 --- /dev/null +++ b/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion @@ -0,0 +1 @@ +("foo" "bar" "baz") // expected list diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element_type.ion new file mode 100644 index 00000000..723ed823 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element_type.ion @@ -0,0 +1,7 @@ +// struct with mismatched sequence element +{ + A: "hello", + B: 12, + C: (1 2 3), // expected sexpression of strings + D: 10e2 +} diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion new file mode 100644 index 00000000..300a99f5 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion @@ -0,0 +1,7 @@ +// simple struct with type mismatched sequence type +{ + A: "hello", + B: 12, + C: ["foo", "bar", "baz"], // expected sexp + D: 10e2 +} diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion index 9ff644d8..72253de9 100644 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion @@ -2,6 +2,6 @@ { A: "hello", B: false, // expected field type: int + C: ("foo" "bar" "baz"), D: 10e2 } - diff --git a/code-gen-projects/input/good/nested_struct/empty_values.ion b/code-gen-projects/input/good/nested_struct/empty_values.ion index 7bca3c4f..798cc96d 100644 --- a/code-gen-projects/input/good/nested_struct/empty_values.ion +++ b/code-gen-projects/input/good/nested_struct/empty_values.ion @@ -2,6 +2,7 @@ { C: { D: false, + E: [], }, A: "", B: 0, diff --git a/code-gen-projects/input/good/nested_struct/valid_fields.ion b/code-gen-projects/input/good/nested_struct/valid_fields.ion index 391c8cd1..f63ab8ab 100644 --- a/code-gen-projects/input/good/nested_struct/valid_fields.ion +++ b/code-gen-projects/input/good/nested_struct/valid_fields.ion @@ -4,5 +4,6 @@ B: 12, C: { D: false, + E: [1, 2, 3] } } diff --git a/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion index da21667b..70e6d7ef 100644 --- a/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion +++ b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion @@ -4,6 +4,6 @@ A: "hello", C: { D: false, + E: [1, 2, 3] } } - diff --git a/code-gen-projects/input/good/scalar/empty_value.ion b/code-gen-projects/input/good/scalar/empty_value.ion new file mode 100644 index 00000000..d5f18f8e --- /dev/null +++ b/code-gen-projects/input/good/scalar/empty_value.ion @@ -0,0 +1,2 @@ +// empty string +"" diff --git a/code-gen-projects/input/good/scalar/valid_value.ion b/code-gen-projects/input/good/scalar/valid_value.ion new file mode 100644 index 00000000..64e295b1 --- /dev/null +++ b/code-gen-projects/input/good/scalar/valid_value.ion @@ -0,0 +1,2 @@ +// a scalar value of string type +"Hello World!" diff --git a/code-gen-projects/input/good/sequence/empty_sequence.ion b/code-gen-projects/input/good/sequence/empty_sequence.ion new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/code-gen-projects/input/good/sequence/empty_sequence.ion @@ -0,0 +1 @@ +[] diff --git a/code-gen-projects/input/good/sequence/valid_elements.ion b/code-gen-projects/input/good/sequence/valid_elements.ion new file mode 100644 index 00000000..1935ae9c --- /dev/null +++ b/code-gen-projects/input/good/sequence/valid_elements.ion @@ -0,0 +1 @@ +["foo", "bar", "baz"] diff --git a/code-gen-projects/input/good/struct_with_fields/empty_values.ion b/code-gen-projects/input/good/struct_with_fields/empty_values.ion index 2f0f96c4..a5c13861 100644 --- a/code-gen-projects/input/good/struct_with_fields/empty_values.ion +++ b/code-gen-projects/input/good/struct_with_fields/empty_values.ion @@ -1,5 +1,6 @@ // struct with empty list, empty string and zeros { + C: (), A: "", B: 0, D: 0e0, diff --git a/code-gen-projects/input/good/struct_with_fields/valid_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_fields.ion index 80842565..1ccd7777 100644 --- a/code-gen-projects/input/good/struct_with_fields/valid_fields.ion +++ b/code-gen-projects/input/good/struct_with_fields/valid_fields.ion @@ -2,5 +2,6 @@ { A: "hello", B: 12, + C: ("foo" "bar" "baz"), D: 10e2 } diff --git a/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion index 33106965..36a1eb2d 100644 --- a/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion +++ b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion @@ -1,6 +1,6 @@ - // struct with unordered fields { + C: ("foo" "bar" "baz"), A: "hello", B: 12, D: 10e2, diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index f1d2323d..506b4db4 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.FileInputStream; +import java.io.BufferedInputStream; import java.io.File; class CodeGenTest { @@ -46,95 +47,147 @@ class CodeGenTest { @Test void getterAndSetterTestForNestedStruct() { // getter tests for `NestedStruct` NestedStruct n = new NestedStruct(); + ArrayList a = new ArrayList(); + a.add(1); + a.add(2); + a.add(3); // set all the fields of `NestedStruct` n.setA("hello"); n.setB(12); - n.setC(false); + n.setC(false, a); // getter tests for `NestedStruct` assertEquals("hello", n.getA(), "n.getA() should return \"hello\""); assertEquals(12, n.getB(), "n.getB() should return `12`"); assertEquals(false, n.getC().getD(), "n.getC().getD() should return `false`"); + assertEquals(3, n.getC().getE().size(), "n.getC().getE().size() should return ArrayList fo size 3"); - // setter tests for `NestedStruct` - n.setA("hi"); - assertEquals("hi", n.getA(), "s.getA() should return \"hi\""); - n.setB(6); - assertEquals(6, n.getB(), "s.getB() should return `6`"); - n.getC().setD(true); - assertEquals(true, n.getC().getD(), "s.getC().getD() should return `true`"); + // setter tests for `NestedStruct` + n.setA("hi"); + assertEquals("hi", n.getA(), "s.getA() should return \"hi\""); + n.setB(6); + assertEquals(6, n.getB(), "s.getB() should return `6`"); + n.getC().setD(true); + assertEquals(true, n.getC().getD(), "s.getC().getD() should return `true`"); + n.getC().setE(new ArrayList()); + assertEquals(0, n.getC().getE().size(), "s.getC().getE().size() should return ArrayList fo size 0"); } - @Test void roundtripGoodTestForStructWithFields() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/good/struct_with_fields"); - String[] fileNames = dir.list(); - for (String fileName : fileNames) { - File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { - reader.next(); - StructWithFields s = StructWithFields.readFrom(reader); - IonWriter writer = b.build(out); - s.writeTo(writer); - writer.close(); - assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray())); - } - } + @Test void getterAndSetterTestForSequence() { + ArrayList a = new ArrayList(); + a.add("foo"); + a.add("bar"); + a.add("baz"); + Sequence s = new Sequence(); + + // set all the fields of `Sequence` + s.setValue(a); + + // getter tests for `Sequence` + assertEquals(3, s.getValue().size(), "s.getValue().size() should return ArrayList fo size 3"); + + // setter tests for `Sequence` + s.setValue(new ArrayList()); + assertEquals(true, s.getValue().isEmpty(), "s.getValue().isEmpty() should return `true`"); + } + + @Test void getterAndSetterTestForScalar() { + Scalar s = new Scalar(); + + // set all the fields of `Scalar` + s.setValue("hello"); + + // getter tests for `Scalar` + assertEquals("hello", s.getValue(), "s.getValue() should return \"hello\""); + + // setter tests for `Scalar` + s.setValue("hi"); + assertEquals("hi", s.getValue(), "s.getValue() should return \"hi\""); + } + + @FunctionalInterface + interface ReaderFunction { + T read(IonReader reader) throws IOException; + } + + @FunctionalInterface + interface WriterFunction { + void write(T item, IonWriter writer) throws IOException; + } + + @Test + void roundtripBadTestForScalar() throws IOException { + runRoundtripBadTest("/bad/scalar", Scalar::readFrom); + } + + @Test + void roundtripBadTestForSequence() throws IOException { + runRoundtripBadTest("/bad/sequence", Sequence::readFrom); } - @Test void roundtripBadTestForStructWithFields() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/bad/struct_with_fields"); + @Test + void roundtripBadTestForStructWithFields() throws IOException { + runRoundtripBadTest("/bad/struct_with_fields", StructWithFields::readFrom); + } + + @Test + void roundtripBadTestForNestedStruct() throws IOException { + runRoundtripBadTest("/bad/nested_struct", NestedStruct::readFrom); + } + + private void runRoundtripBadTest(String path, ReaderFunction readerFunction) throws IOException { + File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); for (String fileName : fileNames) { File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { + try (InputStream inputStream = new FileInputStream(f); + BufferedInputStream bufferedStream = new BufferedInputStream(inputStream); + IonReader reader = IonReaderBuilder.standard().build(bufferedStream)) { reader.next(); - assertThrows(Throwable.class, () -> { StructWithFields s = StructWithFields.readFrom(reader); }); + assertThrows(Throwable.class, () -> readerFunction.read(reader)); } } } - @Test void roundtripGoodTestForNestedStruct() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/good/nested_struct"); + @Test + void roundtripGoodTestForScalar() throws IOException { + runRoundtripGoodTest("/good/scalar", Scalar::readFrom, (item, writer) -> item.writeTo(writer)); + } + + @Test + void roundtripGoodTestForSequence() throws IOException { + runRoundtripGoodTest("/good/sequence", Sequence::readFrom, (item, writer) -> item.writeTo(writer)); + } + + @Test + void roundtripGoodTestForStructWithFields() throws IOException { + runRoundtripGoodTest("/good/struct_with_fields", StructWithFields::readFrom, (item, writer) -> item.writeTo(writer)); + } + + @Test + void roundtripGoodTestForNestedStruct() throws IOException { + runRoundtripGoodTest("/good/nested_struct", NestedStruct::readFrom, (item, writer) -> item.writeTo(writer)); + } + + private void runRoundtripGoodTest(String path, ReaderFunction readerFunction, WriterFunction writerFunction) throws IOException { + File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); for (String fileName : fileNames) { File f = new File(dir, fileName); InputStream inputStream = new FileInputStream(f); + BufferedInputStream bufferedStream = new BufferedInputStream(inputStream); IonTextWriterBuilder b = IonTextWriterBuilder.standard(); ByteArrayOutputStream out = new ByteArrayOutputStream(); IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { + try (IonReader reader = readerBuilder.build(bufferedStream)) { reader.next(); - NestedStruct n = NestedStruct.readFrom(reader); IonWriter writer = b.build(out); - n.writeTo(writer); + T item = readerFunction.read(reader); + writerFunction.write(item, writer); writer.close(); assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray())); } } } - - @Test void roundtripBadTestForNestedStruct() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/bad/nested_struct"); - String[] fileNames = dir.list(); - for (String fileName : fileNames) { - File f = new File(dir, fileName); - InputStream inputStream = new FileInputStream(f); - IonTextWriterBuilder b = IonTextWriterBuilder.standard(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IonReaderBuilder readerBuilder = IonReaderBuilder.standard(); - try (IonReader reader = readerBuilder.build(inputStream)) { - reader.next(); - assertThrows(Throwable.class, () -> { NestedStruct n = NestedStruct.readFrom(reader); }); - } - } - } } diff --git a/code-gen-projects/schema/nested_struct.isl b/code-gen-projects/schema/nested_struct.isl index cceac91c..39664ea9 100644 --- a/code-gen-projects/schema/nested_struct.isl +++ b/code-gen-projects/schema/nested_struct.isl @@ -8,6 +8,7 @@ type::{ type: struct, fields: { D: bool, + E: { type: list, element: int } } } } diff --git a/code-gen-projects/schema/struct_with_fields.isl b/code-gen-projects/schema/struct_with_fields.isl index 870e10c0..40a4d371 100644 --- a/code-gen-projects/schema/struct_with_fields.isl +++ b/code-gen-projects/schema/struct_with_fields.isl @@ -4,6 +4,7 @@ type::{ fields: { A: string, B: int, + C: { element: string, type: sexp }, D: float, } } From e609634e44fc39f80a9ac920880695fca297ac2d Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:39:26 -0700 Subject: [PATCH 05/12] Adds new model changes for Rust code generation (#147) * Adds changes for Rust templates * Add `string_representation` on `FullyQualifiedTypeReference` which will have namespace represented based on programming language * Add tests for Rust code generation --- src/bin/ion/commands/generate/generator.rs | 13 +- src/bin/ion/commands/generate/mod.rs | 10 +- src/bin/ion/commands/generate/model.rs | 82 +++++++---- .../generate/templates/rust/nested_type.templ | 135 +----------------- .../generate/templates/rust/scalar.templ | 30 ++-- .../generate/templates/rust/sequence.templ | 35 ++--- .../generate/templates/rust/struct.templ | 64 +++++---- .../generate/templates/rust/util_macros.templ | 27 ++-- src/bin/ion/commands/generate/utils.rs | 76 ++++++++-- tests/code-gen-tests.rs | 47 ++++++ 10 files changed, 271 insertions(+), 248 deletions(-) diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 410798f9..3c1b7365 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -212,7 +212,9 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { _map: &HashMap, ) -> Result { let fully_qualified_type_ref: &FullyQualifiedTypeReference = &value.try_into()?; - Ok(tera::Value::String(fully_qualified_type_ref.to_string())) + Ok(tera::Value::String( + fully_qualified_type_ref.string_representation::(), + )) } /// Generates code for all the schemas in given authorities @@ -325,7 +327,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { &self .data_model_store .iter() - .map(|(k, v)| (format!("{}", k), v)) + .map(|(k, v)| (k.string_representation::(), v)) .collect::>(), ); context.insert("model", &data_model_node); @@ -340,8 +342,11 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { code_gen_context: &mut CodeGenContext, is_nested_type: bool, ) -> CodeGenResult { - self.current_type_fully_qualified_name - .push(isl_type_name.to_case(Case::UpperCamel)); + L::add_type_to_namespace( + is_nested_type, + isl_type_name, + &mut self.current_type_fully_qualified_name, + ); let constraints = isl_type.constraints(); diff --git a/src/bin/ion/commands/generate/mod.rs b/src/bin/ion/commands/generate/mod.rs index 528a2dd0..2c83515c 100644 --- a/src/bin/ion/commands/generate/mod.rs +++ b/src/bin/ion/commands/generate/mod.rs @@ -7,7 +7,7 @@ mod utils; mod model; use crate::commands::generate::generator::CodeGenerator; -use crate::commands::generate::utils::JavaLanguage; +use crate::commands::generate::utils::{JavaLanguage, RustLanguage}; use crate::commands::IonCliCommand; use anyhow::{bail, Result}; use clap::{Arg, ArgAction, ArgMatches, Command}; @@ -124,8 +124,8 @@ impl IonCliCommand for GenerateCommand { CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()) .generate_code_for_authorities(&authorities, &mut schema_system)?, "rust" => { - // TODO: Initialize and run code generator for `rust`, once the rust templates are modified based on new code generation model - todo!("Rust support is disabled until this is resolved: https://github.com/amazon-ion/ion-cli/issues/136") + CodeGenerator::::new(output) + .generate_code_for_authorities(&authorities, &mut schema_system)? } _ => bail!( "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", @@ -138,8 +138,8 @@ impl IonCliCommand for GenerateCommand { match language { "java" => CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()).generate_code_for_schema(&mut schema_system, schema_id)?, "rust" => { - // TODO: Initialize and run code generator for `rust`, once the rust templates are modified based on new code generation model - todo!() + CodeGenerator::::new(output) + .generate_code_for_authorities(&authorities, &mut schema_system)? } _ => bail!( "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index 10017bee..b78cabbe 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -1,17 +1,19 @@ use derive_builder::Builder; use ion_schema::isl::isl_type::IslType; use std::collections::HashMap; -use std::fmt::{Display, Formatter}; +use std::fmt::Debug; // This module contains a data model that the code generator can use to render a template based on the type of the model. // Currently, this same data model is represented by `AbstractDataType` but it doesn't hold all the information for the template. // e.g. currently there are different fields in the template that hold this information like fields, target_kind_name, abstract_data_type. // Also, the current approach doesn't allow having nested sequences in the generated code. Because the `element_type` in `AbstractDataType::Sequence` // doesn't have information on its nested types' `element_type`. This can be resolved with below defined new data model. // _Note: This model will eventually use a map (FullQualifiedTypeReference, DataModel) to resolve some the references in container types(sequence or structure)._ +// Any changes to the model will require subsequent changes to the templates which use this model. // TODO: This is not yet used in the implementation, modify current implementation to use this data model. use crate::commands::generate::context::SequenceType; use crate::commands::generate::utils::Language; -use serde::Serialize; +use serde::ser::Error; +use serde::{Serialize, Serializer}; use serde_json::Value; /// Represent a node in the data model tree of the generated code. @@ -104,24 +106,6 @@ impl From for FullyQualifiedTypeReference { } } -impl Display for FullyQualifiedTypeReference { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.parameters.is_empty() { - return write!(f, "{}", self.type_name.join(".")); - } - write!(f, "{}<", self.type_name.join("."))?; - - for (i, parameter) in self.parameters.iter().enumerate() { - if i == self.parameters.len() - 1 { - write!(f, "{}", parameter)?; - } else { - write!(f, "{},", parameter)?; - } - } - write!(f, ">") - } -} - // This is useful for code generator to convert input `serde_json::Value` coming from tera(template engine) into `FullyQualifiedTypeReference` impl TryFrom<&Value> for FullyQualifiedTypeReference { type Error = tera::Error; @@ -163,6 +147,24 @@ impl FullyQualifiedTypeReference { pub fn with_parameters(&mut self, parameters: Vec) { self.parameters = parameters; } + + /// Provides string representation of this `FullyQualifiedTypeReference` + pub fn string_representation(&self) -> String { + if self.parameters.is_empty() { + return format!("{}", self.type_name.join(&L::namespace_separator())); + } + let parameters = self + .parameters + .iter() + .map(|p| p.string_representation::()) + .collect::>() + .join(", "); + format!( + "{}<{}>", + self.type_name.join(&L::namespace_separator()), + parameters + ) + } } /// A target-language-agnostic data type that determines which template(s) to use for code generation. @@ -221,6 +223,26 @@ impl AbstractDataType { } } +/// Helper function for serializing abstract data type's `source` field that represents an ISL type. +/// This method returns the name for the given ISL type. +// TODO: `IslType` does not implement `Serialize`, once that is available this method can be removed. +fn serialize_type_name(isl_type: &IslType, serializer: S) -> Result +where + S: Serializer, +{ + isl_type + .name() + .as_ref() + .ok_or(S::Error::custom("Isl type doesn't have a name"))? + .serialize(serializer) +} + +/// Helper function for checking to skip or serialize `source` field in abstract data type that represents an ISL type. +/// This method returns true if the ISl type doesn't have a name, otherwise returns false. +fn is_anonymous(isl_type: &IslType) -> bool { + isl_type.name().is_none() +} + /// Represents a scalar type (e.g. a string or integer or user defined type) #[allow(dead_code)] #[derive(Debug, Clone, Builder, PartialEq, Serialize)] @@ -245,8 +267,8 @@ pub struct Scalar { // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. // This will also be useful for `text` type to verify if this is a `string` or `symbol`. - // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] source: IslType, } @@ -286,8 +308,8 @@ pub struct WrappedScalar { // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. // This will also be useful for `text` type to verify if this is a `string` or `symbol`. - // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] source: IslType, } @@ -330,8 +352,8 @@ pub struct WrappedSequence { // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. // This will also be useful for `text` type to verify if this is a `string` or `symbol`. - // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] source: IslType, } @@ -365,8 +387,8 @@ pub struct Sequence { // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. // This will also be useful for `text` type to verify if this is a `string` or `symbol`. - // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] pub(crate) source: IslType, } @@ -406,8 +428,8 @@ pub struct Structure { // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. // This will also be useful for `text` type to verify if this is a `string` or `symbol`. - // TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized. - #[serde(skip_serializing)] + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] pub(crate) source: IslType, } diff --git a/src/bin/ion/commands/generate/templates/rust/nested_type.templ b/src/bin/ion/commands/generate/templates/rust/nested_type.templ index 6caa5ee0..ec991a88 100644 --- a/src/bin/ion/commands/generate/templates/rust/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/rust/nested_type.templ @@ -1,133 +1,8 @@ {% import "util_macros.templ" as util_macros %} {# following macro defines an anonymous type as children class for its parent type definition #} -{% macro nested_type(target_kind_name, fields, abstract_data_type, nested_anonymous_types) -%} - #[derive(Debug, Clone, Default)] - pub struct {{ target_kind_name }} { - {% for field in fields -%} - {{ field.name | snake | indent(first = true) }}: {{ field.value_type }}, - {% endfor %} - } - - impl {{ target_kind_name }} { - pub fn new({% for field in fields | sort(attribute="name") -%}{{ field.name | snake }}: {{ field.value_type }},{% endfor %}) -> Self { - Self { - {% for field in fields -%} - {{ field.name | snake }}, - {% endfor %} - } - } - - - {% for field in fields -%}pub fn {{ field.name | snake }}(&self) -> &{{ field.value_type }} { - &self.{{ field.name | snake }} - } - {% endfor %} - - - pub fn read_from(reader: &mut Reader) -> SerdeResult { - let mut abstract_data_type = {{ target_kind_name }}::default(); - {% if abstract_data_type == "Value"%} - abstract_data_type.value = {% if fields[0].value_type | is_built_in_type == false %} - {{ fields[0].value_type }}::read_from(reader)?; - {% else %} - reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ fields[0].value_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if fields[0].value_type | lower == "string" %} .to_string() {% endif %}; - {% endif %} - {% elif abstract_data_type is object and abstract_data_type is containing("Structure") %} - reader.step_in()?; - while reader.next()? != StreamItem::Nothing { - if let Some(field_name) = reader.field_name()?.text() { - match field_name { - {% for field in fields -%} - {% if field.value_type | is_built_in_type == false %} - {% if field.value_type is containing("Vec") %} - "{{ field.name }}" => { {{ util_macros::read_as_sequence(field=field) }} } - {% else %} - "{{ field.name }}" => { abstract_data_type.{{ field.name | snake }} = {{ field.value_type }}::read_from(reader)?; } - {% endif %} - {% else %} - "{{ field.name }}" => { abstract_data_type.{{ field.name | snake}} = reader.read_{% if field.isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ field.value_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if field.value_type | lower== "string" %} .to_string() {% endif %}; } - {% endif %} - {% endfor %} - _ => { - {% if abstract_data_type["Structure"] %} - return validation_error( - "Can not read field name:{{ field.name }} for {{ target_kind_name }} as it doesn't exist in the given schema type definition." - ); - {% endif %} - } - } - } - } - reader.step_out()?; - {% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %} - if reader.ion_type() != Some(IonType::{{ abstract_data_type["Sequence"].sequence_type }}) { - return validation_error(format!( - "Expected {{ abstract_data_type["Sequence"].sequence_type }}, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() - )); - } - reader.step_in()?; - - abstract_data_type.value = { - let mut values = vec![]; - - while reader.next()? != StreamItem::Nothing { - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - values.push({{ abstract_data_type["Sequence"].element_type }}::read_from(reader)?); - {% else %} - values.push(reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ abstract_data_type["Sequence"].element_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if abstract_data_type["Sequence"].element_type | lower== "string" %} .to_string() {% endif %}); - {% endif %} - } - values - }; - reader.step_out()?; - {% else %} - return validation_error("Can not resolve read API template for {{ target_kind_name }}"); - {% endif %} - Ok(abstract_data_type) - } - - pub fn write_to(&self, writer: &mut W) -> SerdeResult<()> { - {% if abstract_data_type == "Value" %} - {% for field in fields %} - {% if field.value_type | is_built_in_type == false %} - self.{{ field.name | snake }}.write_to(writer)?; - {% else %} - writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.value_type | lower }}{% endif %}(self.value.to_owned())?; - {% endif %} - {% endfor %} - {% elif abstract_data_type is object and abstract_data_type is containing("Structure") %} - writer.step_in(IonType::Struct)?; - {% for field in fields %} - writer.set_field_name("{{ field.name }}"); - {% if field.value_type | is_built_in_type == false %} - {% if field.value_type is containing("Vec") %} - {{ util_macros::write_as_sequence(field=field) }} - {% else %} - self.{{ field.name | snake }}.write_to(writer)?; - {% endif %} - {% else %} - {# TODO: Change the following `to_owned` to only be used when writing i64,f32,f64,bool which require owned value as input #} - writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.value_type | lower }}{% endif %}(self.{{ field.name | snake }}.to_owned())?; - {% endif %} - {% endfor %} - writer.step_out()?; - {% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %} - writer.step_in(IonType::{{ abstract_data_type["Sequence"].sequence_type }})?; - for value in &self.value { - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - value.write_to(writer)?; - {% else %} - writer.write_{% if fields[0].isl_type_name == "symbol" %}symbol{% else %}{{ abstract_data_type["Sequence"].element_type | lower }}{% endif %}(value.to_owned())?; - {% endif %} - } - writer.step_out()?; - {% endif %} - Ok(()) - } - } - - {% for inline_type in nested_anonymous_types -%} - {{ self::nested_type(target_kind_name=inline_type.target_kind_name, fields=inline_type.fields, abstract_data_type=inline_type.abstract_data_type, nested_anonymous_types=inline_type.nested_types) }} - {% endfor -%} -{% endmacro %} \ No newline at end of file +{% macro nested_type(model, is_nested) -%} + {% if model.code_gen_type is containing("Structure")%} + {% include "struct.templ" %} + {% endif %} +{% endmacro nested_type -%} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/rust/scalar.templ b/src/bin/ion/commands/generate/templates/rust/scalar.templ index cd9d9d7d..c6c0b0ef 100644 --- a/src/bin/ion/commands/generate/templates/rust/scalar.templ +++ b/src/bin/ion/commands/generate/templates/rust/scalar.templ @@ -1,40 +1,44 @@ -use {{ target_kind_name | snake }}::{{ target_kind_name }}; -pub mod {{ target_kind_name | snake }} { +{# Verify that the abstract data type is a scalar type and store information for this scalar value #} +{% set scalar_info = model.code_gen_type["WrappedScalar"] %} +{% set base_type = scalar_info["base_type"] | fully_qualified_type_name %} + +use {{ model.name | snake }}::{{ model.name }}; +pub mod {{ model.name | snake }} { use super::*; #[derive(Debug, Clone, Default)] - pub struct {{ target_kind_name }} { - value: {{ fields[0].value_type }}, + pub struct {{ model.name }} { + value: {{ base_type }}, } - impl {{ target_kind_name }} { - pub fn new(value: {{ fields[0].value_type }}) -> Self { + impl {{ model.name }} { + pub fn new(value: {{ base_type }}) -> Self { Self { value, } } - pub fn value(&self) -> &{{ fields[0].value_type }} { + pub fn value(&self) -> &{{ base_type }} { &self.value } pub fn read_from(reader: &mut Reader) -> SerdeResult { - let mut abstract_data_type = {{ target_kind_name }}::default(); - abstract_data_type.value = {% if fields[0].value_type | is_built_in_type == false %} - {{ fields[0].value_type }}::read_from(reader)?; + let mut abstract_data_type = {{ model.name }}::default(); + abstract_data_type.value = {% if base_type | is_built_in_type == false %} + {{ base_type }}::read_from(reader)?; {% else %} - reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ fields[0].value_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if fields[0].value_type | lower == "string" %} .to_string() {% endif %}; + reader.read_{% if field.source is defined and field.source == "symbol" %}symbol()?.text().unwrap(){% else %}{{ base_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if base_type| lower == "string" %} .to_string() {% endif %}; {% endif %} Ok(abstract_data_type) } pub fn write_to(&self, writer: &mut W) -> SerdeResult<()> { - {% if fields[0].value_type | is_built_in_type == false %} + {% if base_type | is_built_in_type == false %} self.value.write_to(writer)?; {% else %} - writer.write_{% if fields[0].isl_type_name == "symbol" %}symbol{% else %}{{ fields[0].value_type | lower }}{% endif %}(self.value.to_owned())?; + writer.write_{% if field.source is defined and field.source == "symbol" %}symbol{% else %}{{ base_type | lower }}{% endif %}(self.value.to_owned())?; {% endif %} Ok(()) } diff --git a/src/bin/ion/commands/generate/templates/rust/sequence.templ b/src/bin/ion/commands/generate/templates/rust/sequence.templ index 4bfb37a8..4a4555d4 100644 --- a/src/bin/ion/commands/generate/templates/rust/sequence.templ +++ b/src/bin/ion/commands/generate/templates/rust/sequence.templ @@ -1,31 +1,34 @@ -use {{ target_kind_name | snake }}::{{ target_kind_name }}; -pub mod {{ target_kind_name | snake }} { +{% set sequence_info = model.code_gen_type["WrappedSequence"] %} + +use {{ model.name | snake }}::{{ model.name }}; + +pub mod {{ model.name | snake }} { use super::*; #[derive(Debug, Clone, Default)] - pub struct {{ target_kind_name }} { - value: {{ fields[0].value_type }}, + pub struct {{ model.name }} { + value: Vec<{{ sequence_info["element_type"] | fully_qualified_type_name }}>, } - impl {{ target_kind_name }} { - pub fn new(value: {{ fields[0].value_type }}) -> Self { + impl {{ model.name }} { + pub fn new(value: Vec<{{ sequence_info["element_type"] | fully_qualified_type_name }}>) -> Self { Self { value, } } - pub fn value(&self) -> &{{ fields[0].value_type }} { + pub fn value(&self) -> &Vec<{{ sequence_info["element_type"] | fully_qualified_type_name }}> { &self.value } pub fn read_from(reader: &mut Reader) -> SerdeResult { - let mut abstract_data_type = {{ target_kind_name }}::default(); + let mut abstract_data_type = {{ model.name }}::default(); - if reader.ion_type() != Some(IonType::{{ abstract_data_type["Sequence"].sequence_type }}) { + if reader.ion_type() != Some(IonType::{{ sequence_info["sequence_type"] }}) { return validation_error(format!( - "Expected {{ abstract_data_type["Sequence"].sequence_type }}, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() + "Expected {{ sequence_info["sequence_type"] }}, found {} while reading {{ model.name }}.", reader.ion_type().unwrap() )); } @@ -35,10 +38,10 @@ pub mod {{ target_kind_name | snake }} { let mut values = vec![]; while reader.next()? != StreamItem::Nothing { - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - values.push({{ abstract_data_type["Sequence"].element_type }}::read_from(reader)?); + {% if sequence_info["element_type"] | fully_qualified_type_name | is_built_in_type == false %} + values.push({{ sequence_info["element_type"] }}::read_from(reader)?); {% else %} - values.push(reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ abstract_data_type["Sequence"].element_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if abstract_data_type["Sequence"].element_type | lower== "string" %} .to_string() {% endif %}); + values.push(reader.read_{% if field.source is defined and field.source == "symbol" %}symbol()?.text().unwrap(){% else %}{{ sequence_info["element_type"] | fully_qualified_type_name | lower | replace(from="string", to ="str") }}()?{% endif %}{% if sequence_info["element_type"] | fully_qualified_type_name | lower== "string" %} .to_string() {% endif %}); {% endif %} } values @@ -48,12 +51,12 @@ pub mod {{ target_kind_name | snake }} { } pub fn write_to(&self, writer: &mut W) -> SerdeResult<()> { - writer.step_in(IonType::{{ abstract_data_type["Sequence"].sequence_type }})?; + writer.step_in(IonType::{{ sequence_info["sequence_type"] }})?; for value in &self.value { - {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} + {% if sequence_info["element_type"] | fully_qualified_type_name | is_built_in_type == false %} value.write_to(writer)?; {% else %} - writer.write_{% if fields[0].isl_type_name == "symbol" %}symbol{% else %}{{ abstract_data_type["Sequence"].element_type | lower }}{% endif %}(value.to_owned())?; + writer.write_{% if field.source is defined and field.source == "symbol" %}symbol{% else %}{{ sequence_info["element_type"] | fully_qualified_type_name | lower }}{% endif %}(value.to_owned())?; {% endif %} } writer.step_out()?; diff --git a/src/bin/ion/commands/generate/templates/rust/struct.templ b/src/bin/ion/commands/generate/templates/rust/struct.templ index 5c00c9e8..de14e79c 100644 --- a/src/bin/ion/commands/generate/templates/rust/struct.templ +++ b/src/bin/ion/commands/generate/templates/rust/struct.templ @@ -2,55 +2,59 @@ {% import "nested_type.templ" as macros %} {% import "util_macros.templ" as util_macros %} -use {{ target_kind_name | snake }}::{{ target_kind_name }}; -pub mod {{ target_kind_name | snake }} { +{% macro struct(model, is_nested) %} +{% set struct_info = model.code_gen_type["Structure"] %} + +use {{ model.name | snake }}::{{ model.name }}; +pub mod {{ model.name | snake }} { use super::*; #[derive(Debug, Clone, Default)] - pub struct {{ target_kind_name }} { - {% for field in fields -%} - {{ field.name | snake | indent(first = true) }}: {{ field.value_type }}, + pub struct {{ model.name }} { + {% for field_name, field_value in struct_info["fields"] -%} + {{ field_name | snake | indent(first = true) }}: {{ field_value.0 | fully_qualified_type_name }}, {% endfor %} } - impl {{ target_kind_name }} { - pub fn new({% for field in fields | sort(attribute="name") -%}{{ field.name | snake }}: {{ field.value_type }},{% endfor %}) -> Self { + impl {{ model.name }} { + pub fn new({% for field_name in struct_info["fields"] | field_names -%}{% set field_value = struct_info["fields"][field_name] %}{{ field_name | snake }}: {{ field_value.0 | fully_qualified_type_name }},{% endfor %}) -> Self { Self { - {% for field in fields -%} - {{ field.name | snake }}, + {% for field_name, field_value in struct_info["fields"] -%} + {{ field_name | snake }}, {% endfor %} } } - {% for field in fields -%}pub fn {{ field.name | snake }}(&self) -> &{{ field.value_type }} { - &self.{{ field.name | snake }} + {% for field_name, field_value in struct_info["fields"] -%}pub fn {{ field_name | snake }}(&self) -> &{{ field_value.0 | fully_qualified_type_name }} { + &self.{{ field_name | snake }} } {% endfor %} pub fn read_from(reader: &mut Reader) -> SerdeResult { - let mut abstract_data_type = {{ target_kind_name }}::default(); + let mut abstract_data_type = {{ model.name }}::default(); reader.step_in()?; while reader.next()? != StreamItem::Nothing { if let Some(field_name) = reader.field_name()?.text() { match field_name { - {% for field in fields -%} - {% if field.value_type | is_built_in_type == false %} - {% if field.value_type is containing("Vec") %} - "{{ field.name }}" => { {{ util_macros::read_as_sequence(field=field) }} } + {% for field_name, field_val in struct_info["fields"] -%} + {% set field_value = field_val.0 | fully_qualified_type_name %} + {% if field_value | is_built_in_type == false %} + {% if field_value is containing("Vec") %} + "{{ field_name }}" => { {{ util_macros::read_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} } {% else %} - "{{ field.name }}" => { abstract_data_type.{{ field.name | snake }} = {{ field.value_type }}::read_from(reader)?; } + "{{ field_name }}" => { abstract_data_type.{{ field_name | snake }} = {{ field_value }}::read_from(reader)?; } {% endif %} {% else %} - "{{ field.name }}" => { abstract_data_type.{{ field.name | snake}} = reader.read_{% if field.isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ field.value_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if field.value_type | lower== "string" %} .to_string() {% endif %}; } + "{{ field_name }}" => { abstract_data_type.{{ field_name | snake}} = reader.read_{% if field.source is defined and field.source == "symbol" %}symbol()?.text().unwrap(){% else %}{{ field_value | lower | replace(from="string", to ="str") }}()?{% endif %}{% if field_value | lower== "string" %} .to_string() {% endif %}; } {% endif %} {% endfor %} _ => { {% if abstract_data_type["Structure"] %} return validation_error( - "Can not read field name:{{ field.name }} for {{ target_kind_name }} as it doesn't exist in the given schema type definition." + "Can not read field name:{{ field_name }} for {{ model.name }} as it doesn't exist in the given schema type definition." ); {% endif %} } @@ -63,17 +67,18 @@ pub mod {{ target_kind_name | snake }} { pub fn write_to(&self, writer: &mut W) -> SerdeResult<()> { writer.step_in(IonType::Struct)?; - {% for field in fields %} - writer.set_field_name("{{ field.name }}"); - {% if field.value_type | is_built_in_type == false %} - {% if field.value_type is containing("Vec") %} - {{ util_macros::write_as_sequence(field=field) }} + {% for field_name, field_val in struct_info["fields"] %} + {% set field_value = field_val.0 | fully_qualified_type_name %} + writer.set_field_name("{{ field_name }}"); + {% if field_value | is_built_in_type == false %} + {% if field_value is containing("Vec") %} + {{ util_macros::write_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} {% else %} - self.{{ field.name | snake }}.write_to(writer)?; + self.{{ field_name | snake }}.write_to(writer)?; {% endif %} {% else %} {# TODO: Change the following `to_owned` to only be used when writing i64,f32,f64,bool which require owned value as input #} - writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.value_type | lower }}{% endif %}(self.{{ field.name | snake }}.to_owned())?; + writer.write_{% if field.source is defined and field.source == "symbol" %}symbol{% else %}{{ field_value | lower }}{% endif %}(self.{{ field_name | snake }}.to_owned())?; {% endif %} {% endfor %} writer.step_out()?; @@ -81,7 +86,10 @@ pub mod {{ target_kind_name | snake }} { } } - {% for inline_type in nested_types -%} - {{ macros::nested_type(target_kind_name=inline_type.target_kind_name, fields=inline_type.fields, abstract_data_type=inline_type.abstract_data_type, nested_anonymous_types=inline_type.nested_types) }} + {% for inline_type in model.nested_types -%} + {% set is_nested = true %} + {{ macros::nested_type(model=inline_type, is_nested=is_nested) }} {% endfor -%} } +{% endmacro struct %} +{{ self::struct(model=model, is_nested=is_nested) }} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/rust/util_macros.templ b/src/bin/ion/commands/generate/templates/rust/util_macros.templ index 3d2e3614..66f306c4 100644 --- a/src/bin/ion/commands/generate/templates/rust/util_macros.templ +++ b/src/bin/ion/commands/generate/templates/rust/util_macros.templ @@ -1,20 +1,22 @@ {# following macro defines statements to read a class field as sequence #} -{% macro read_as_sequence(field) %} - if reader.ion_type() != Some(IonType::{{ field.abstract_data_type["Sequence"].sequence_type }}) { +{% macro read_as_sequence(field_name, field_value, type_store) %} + {% set field_value_model = type_store[field_value] %} + + if reader.ion_type() != Some(IonType::{{ field_value_model.code_gen_type["Sequence"].sequence_type }}) { return validation_error(format!( - "Expected {{ field.abstract_data_type["Sequence"].sequence_type }}, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() + "Expected {{ field_value_model.code_gen_type["Sequence"].sequence_type }}, found {} while reading {{ field_name }}.", reader.ion_type().unwrap() )); } reader.step_in()?; - abstract_data_type.{{ field.name | snake }} = { + abstract_data_type.{{ field_name | snake }} = { let mut values = vec![]; while reader.next()? != StreamItem::Nothing { - {% if field.abstract_data_type["Sequence"].element_type | is_built_in_type == false %} - values.push({{ field.abstract_data_type["Sequence"].element_type }}::read_from(reader)?); + {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} + values.push({{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name }}::read_from(reader)?); {% else %} - values.push(reader.read_{% if field.isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ field.abstract_data_type["Sequence"].element_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if field.abstract_data_type["Sequence"].element_type | lower== "string" %} .to_string() {% endif %}); + values.push(reader.read_{% if field.source is defined and field.source == "symbol" %}symbol()?.text().unwrap(){% else %}{{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | lower | replace(from="string", to ="str") }}()?{% endif %}{% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | lower== "string" %} .to_string() {% endif %}); {% endif %} } values @@ -22,13 +24,14 @@ reader.step_out()?; {% endmacro %} {# following macro defines statements to write a class field as sequence #} -{% macro write_as_sequence(field) %} - writer.step_in(IonType::{{ field.abstract_data_type["Sequence"].sequence_type }}); - for value in &self.{{ field.name | snake }} { - {% if field.abstract_data_type["Sequence"].element_type | is_built_in_type == false %} +{% macro write_as_sequence(field_name, field_value, type_store) %} + {% set field_value_model = type_store[field_value] %} + writer.step_in(IonType::{{ field_value_model.code_gen_type["Sequence"].sequence_type }}); + for value in &self.{{ field_name | snake }} { + {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} value.write_to(writer)?; {% else %} - writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.abstract_data_type["Sequence"].element_type | lower }}{% endif %}(value.to_owned())?; + writer.write_{% if field.source is defined and field.source == "symbol" %}symbol{% else %}{{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | lower }}{% endif %}(value.to_owned())?; {% endif %} } writer.step_out()?; diff --git a/src/bin/ion/commands/generate/utils.rs b/src/bin/ion/commands/generate/utils.rs index 93647680..2f254de7 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -50,6 +50,26 @@ pub trait Language { /// In Rust, Template::Struct -> "struct" /// In Java, Template::Struct -> "class" fn template_name(template: &Template) -> String; + + /// Returns the namespace separator for programming language + /// e.g. In Java, it returns "::" + /// In Rust, it returns "." + fn namespace_separator() -> &'static str; + + /// Modifies the given namespace to add the given type to the namespace path. + /// _Note:_ For Rust, it uses the `is_nested_type` field to only get modules in the path name until the leaf type is reached. + /// e.g. given a module as below: + /// ``` + /// mod foo { + /// struct Foo { ... } + /// mod nested_type { + /// struct NestedType { ... } + /// } + /// } + /// ``` + /// To add `NestedType` into the namespace path, `is_nested_type` helps remove any prior types form the path and add this current type. + /// i.e. given namespace path as `foo::Foo`, it will first remove `Foo` and then add the current type as `foo::nested_type::NestedType`. + fn add_type_to_namespace(is_nested_type: bool, type_name: &String, namespace: &mut Vec); } pub struct JavaLanguage; @@ -86,7 +106,7 @@ impl Language for JavaLanguage { fn target_type_as_sequence( target_type: FullyQualifiedTypeReference, ) -> FullyQualifiedTypeReference { - match JavaLanguage::wrapper_class(&format!("{}", target_type)) { + match JavaLanguage::wrapper_class(&target_type.string_representation::()) { Some(wrapper_name) => FullyQualifiedTypeReference { type_name: vec![ "java".to_string(), @@ -127,6 +147,18 @@ impl Language for JavaLanguage { Template::Sequence => "sequence".to_string(), } } + + fn namespace_separator() -> &'static str { + "." + } + + fn add_type_to_namespace( + _is_nested_type: bool, + type_name: &String, + namespace: &mut Vec, + ) { + namespace.push(type_name.to_case(Case::UpperCamel)) + } } impl JavaLanguage { @@ -193,15 +225,7 @@ impl Language for RustLanguage { fn is_built_in_type(type_name: String) -> bool { matches!( type_name.as_str(), - "i64" - | "String" - | "bool" - | "Vec" - | "f64" - | "Vec" - | "Vec" - | "Vec" - | "Vec" + "i64" | "String" | "bool" | "Vec" | "f64" ) } @@ -216,6 +240,38 @@ impl Language for RustLanguage { Template::Sequence => "sequence".to_string(), } } + + fn namespace_separator() -> &'static str { + "::" + } + + fn add_type_to_namespace( + is_nested_type: bool, + type_name: &String, + namespace: &mut Vec, + ) { + // e.g. For example there is a `NestedType` inside `Foo` struct. Rust code generation also generates similar modules for the generated structs. + // ```rust + // mod foo { + // struct Foo { + // ... + // } + // mod nested_type { + // struct NestedType { + // ... + // } + // } + // } + // ``` + if is_nested_type { + // Assume we have the current namespace as `foo::Foo` + // then the following step will remove `Foo` from the path for nested type. + // So that the final namespace path for `NestedType` will become `foo::nested_type::NestedType` + namespace.pop(); // Remove the parent struct/enum + } + namespace.push(type_name.to_case(Case::Snake)); // Add this type's module name to the namespace path + namespace.push(type_name.to_case(Case::UpperCamel)) // Add this type itself to the namespace path + } } impl Display for RustLanguage { diff --git a/tests/code-gen-tests.rs b/tests/code-gen-tests.rs index 54c60a57..a7b8d559 100644 --- a/tests/code-gen-tests.rs +++ b/tests/code-gen-tests.rs @@ -49,6 +49,53 @@ fn roundtrip_tests_for_generated_code_gradle() -> Result<()> { Ok(()) } +#[test] +fn roundtrip_tests_for_generated_code_cargo() -> Result<()> { + // run the cargo project defined under `code-gen-projects`, + // this project runs the code generator in its build process and generates code, + // this project also has some predefined tests for the generated code, + // so simply running the tests on this project builds the project, generates code and runs tests + + // absolute paths for crate and executables + let ion_executable = env!("CARGO_BIN_EXE_ion"); + let test_project_path = code_gen_projects_path().join("rust").join("code-gen-demo"); + let cargo_executable = env!("CARGO"); + + // Clean + let cargo_clean_output = std::process::Command::new(cargo_executable) + .current_dir(&test_project_path) + .arg("clean") + .output() + .expect("failed to execute 'cargo clean'"); + + println!("Cargo clean status: {}", cargo_clean_output.status); + std::io::stdout() + .write_all(&cargo_clean_output.stdout) + .unwrap(); + std::io::stderr() + .write_all(&cargo_clean_output.stderr) + .unwrap(); + + // Test + let cargo_test_output = std::process::Command::new(cargo_executable) + .current_dir(&test_project_path) + .arg("test") + .env("ION_CLI", ion_executable) + .output() + .expect("failed to execute 'cargo test'"); + + println!("Cargo test status: {}", cargo_test_output.status); + std::io::stdout() + .write_all(&cargo_test_output.stdout) + .unwrap(); + std::io::stderr() + .write_all(&cargo_test_output.stderr) + .unwrap(); + + assert!(cargo_test_output.status.success()); + Ok(()) +} + //TODO: Add cargo roundtrip tests once the rust templates are modified based on new code generation model #[rstest] From 46864a65efd9afe66dfb39547310751a4dea835a Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:42:13 -0700 Subject: [PATCH 06/12] Adds changes for optional/required fields in Java code generation (#148) --- .../missing_required_fields.ion | 7 + .../nested_struct/valid_optional_fields.ion | 9 + .../valid_optional_fields.ion | 7 + .../rust/code-gen-demo/src/lib.rs | 28 ++++ .../schema/struct_with_fields.isl | 2 +- src/bin/ion/commands/generate/generator.rs | 155 +++++++++++++++--- src/bin/ion/commands/generate/mod.rs | 35 +++- src/bin/ion/commands/generate/model.rs | 6 +- .../generate/templates/java/class.templ | 57 ++++--- .../generate/templates/java/nested_type.templ | 8 +- .../generate/templates/java/util_macros.templ | 43 ++--- src/bin/ion/commands/generate/utils.rs | 80 +++++++-- tests/cli.rs | 2 +- 13 files changed, 350 insertions(+), 89 deletions(-) create mode 100644 code-gen-projects/input/bad/struct_with_fields/missing_required_fields.ion create mode 100644 code-gen-projects/input/good/nested_struct/valid_optional_fields.ion create mode 100644 code-gen-projects/input/good/struct_with_fields/valid_optional_fields.ion diff --git a/code-gen-projects/input/bad/struct_with_fields/missing_required_fields.ion b/code-gen-projects/input/bad/struct_with_fields/missing_required_fields.ion new file mode 100644 index 00000000..e1995de9 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_fields/missing_required_fields.ion @@ -0,0 +1,7 @@ +// simple struct with all valid fields +{ + A: "hello", + B: 12, + // C: ("foo" "bar" "baz"), // since `C` is a required field, this is an invalid struct + D: 10e2 +} diff --git a/code-gen-projects/input/good/nested_struct/valid_optional_fields.ion b/code-gen-projects/input/good/nested_struct/valid_optional_fields.ion new file mode 100644 index 00000000..8b3dca70 --- /dev/null +++ b/code-gen-projects/input/good/nested_struct/valid_optional_fields.ion @@ -0,0 +1,9 @@ +// nested struct with some optional fields that are not provided +{ + A: "hello", + // B: 12, // since `B` is optional field, this is a valid struct + C: { + D: false, + E: [1, 2, 3] + } +} diff --git a/code-gen-projects/input/good/struct_with_fields/valid_optional_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_optional_fields.ion new file mode 100644 index 00000000..657bd913 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_fields/valid_optional_fields.ion @@ -0,0 +1,7 @@ +// simple struct with all valid fields +{ + A: "hello", + B: 12, + C: ("foo" "bar" "baz"), + // D: 10e2, // since `D` is optional field, this is a valid struct +} diff --git a/code-gen-projects/rust/code-gen-demo/src/lib.rs b/code-gen-projects/rust/code-gen-demo/src/lib.rs index 5d735f4c..6e9f9f3e 100644 --- a/code-gen-projects/rust/code-gen-demo/src/lib.rs +++ b/code-gen-projects/rust/code-gen-demo/src/lib.rs @@ -10,10 +10,28 @@ mod tests { use ion_rs::ReaderBuilder; use ion_rs::TextWriterBuilder; use std::fs; + use std::path::MAIN_SEPARATOR_STR as PATH_SEPARATOR; use test_generator::test_resources; include!(concat!(env!("OUT_DIR"), "/ion_generated_code.rs")); + /// Determines if the given file name is in the ROUNDTRIP_TESTS_SKIP_LIST list. This deals with platform + /// path separator differences from '/' separators in the path list. + #[inline] + pub fn skip_list_contains_path(file_name: &str) -> bool { + ROUNDTRIP_TESTS_SKIP_LIST + .iter() + // TODO construct the paths in a not so hacky way + .map(|p| p.replace('/', PATH_SEPARATOR)) + .any(|p| p == file_name) + } + + pub const ROUNDTRIP_TESTS_SKIP_LIST: &[&str] = &[ + "../../input/good/nested_struct/valid_optional_fields.ion", + "../../input/good/struct_with_fields/valid_optional_fields.ion", + "../../input/bad/struct_with_fields/missing_required_fields.ion", + ]; + #[test] fn it_works() { let result = add(2, 2); @@ -22,6 +40,10 @@ mod tests { #[test_resources("../../input/good/struct_with_fields/**/*.ion")] fn roundtrip_good_test_generated_code_structs_with_fields(file_name: &str) -> SerdeResult<()> { + // if file name is under the ROUNDTRIP_TESTS_SKIP_LIST then do nothing. + if skip_list_contains_path(&file_name) { + return Ok(()); + } let ion_string = fs::read_to_string(file_name).unwrap(); let mut reader = ReaderBuilder::new().build(ion_string.clone())?; let mut buffer = Vec::new(); @@ -43,6 +65,9 @@ mod tests { #[test_resources("../../input/bad/struct_with_fields/**/*.ion")] fn roundtrip_bad_test_generated_code_structs_with_fields(file_name: &str) -> SerdeResult<()> { + if skip_list_contains_path(&file_name) { + return Ok(()); + } let ion_string = fs::read_to_string(file_name).unwrap(); let mut reader = ReaderBuilder::new().build(ion_string.clone())?; // read given Ion value using Ion reader @@ -55,6 +80,9 @@ mod tests { #[test_resources("../../input/good/nested_struct/**/*.ion")] fn roundtrip_good_test_generated_code_nested_structs(file_name: &str) -> SerdeResult<()> { + if skip_list_contains_path(&file_name) { + return Ok(()); + } let ion_string = fs::read_to_string(file_name).unwrap(); let mut reader = ReaderBuilder::new().build(ion_string.clone())?; let mut buffer = Vec::new(); diff --git a/code-gen-projects/schema/struct_with_fields.isl b/code-gen-projects/schema/struct_with_fields.isl index 40a4d371..78a036c1 100644 --- a/code-gen-projects/schema/struct_with_fields.isl +++ b/code-gen-projects/schema/struct_with_fields.isl @@ -4,7 +4,7 @@ type::{ fields: { A: string, B: int, - C: { element: string, type: sexp }, + C: { element: string, type: sexp, occurs: required }, D: float, } } diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 3c1b7365..a0c9cc37 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -202,6 +202,58 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { )) } + /// A [tera] filter that returns the parameter names for given fully qualified type name. + /// + /// For more information: + /// + /// [tera]: + pub fn parameters( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + let fully_qualified_type_ref: &FullyQualifiedTypeReference = &value.try_into()?; + Ok(tera::Value::Array( + fully_qualified_type_ref + .parameters + .iter() + .map(|p| tera::Value::String(p.string_representation::())) + .collect(), + )) + } + + /// A [tera] filter that return primitive data type name for given wrapper class name. + /// + /// For more information: + /// + /// [tera]: + pub fn primitive_data_type( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + Ok(tera::Value::String( + JavaLanguage::primitive_data_type(value.as_str().ok_or(tera::Error::msg( + "Required string for `primitive_data_type` filter", + ))?) + .to_string(), + )) + } + + /// A [tera] filter that return wrapper class name for a primitive data type. This filter is only used for Java templates. + /// + /// For more information: + /// + /// [tera]: + pub fn wrapper_class( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + Ok(tera::Value::String(JavaLanguage::wrapper_class( + value.as_str().ok_or(tera::Error::msg( + "Required string for `primitive_data_type` filter", + ))?, + ))) + } + /// A [tera] filter that returns a string representation of a tera object i.e. `FullyQualifiedTypeReference`. /// /// For more information: @@ -263,6 +315,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { self.tera .register_filter("fully_qualified_type_name", Self::fully_qualified_type_name); + self.tera.register_filter("parameters", Self::parameters); + self.tera + .register_filter("primitive_data_type", Self::primitive_data_type); + self.tera + .register_filter("wrapper_class", Self::wrapper_class); + // Iterate through the ISL types, generate an abstract data type for each for isl_type in schema.types() { // unwrap here is safe because all the top-level type definition always has a name @@ -278,15 +336,19 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { /// generates an nested type that can be part of another type definition. /// This will be used by the parent type to add this nested type in its namespace or module. + /// _Note: `field_presence` is only used ofr variably occurring type references and currently that is only supported with `fields` constraint. + /// For all other cases `field_presence` will be set as default `FieldPresence::Required`._ fn generate_nested_type( &mut self, type_name: &String, isl_type: &IslType, + field_presence: FieldPresence, parent_code_gen_context: &mut CodeGenContext, ) -> CodeGenResult { let mut code_gen_context = CodeGenContext::new(); let mut data_model_node = self.convert_isl_type_def_to_data_model_node( type_name, + field_presence, isl_type, &mut code_gen_context, true, @@ -299,11 +361,20 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // pop out the nested type name from the fully qualified namespace as it has been already added to the type store and to nested types self.current_type_fully_qualified_name.pop(); - data_model_node - .fully_qualified_type_ref::() - .ok_or(invalid_abstract_data_type_raw_error( - "Can not determine fully qualified name for the data model", - )) + match field_presence { + FieldPresence::Optional => Ok(L::target_type_as_optional( + data_model_node.fully_qualified_type_ref::().ok_or( + invalid_abstract_data_type_raw_error( + "Can not determine fully qualified name for the data model", + ), + )?, + )), + FieldPresence::Required => data_model_node.fully_qualified_type_ref::().ok_or( + invalid_abstract_data_type_raw_error( + "Can not determine fully qualified name for the data model", + ), + ), + } } fn generate_abstract_data_type( @@ -316,6 +387,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { let data_model_node = self.convert_isl_type_def_to_data_model_node( isl_type_name, + FieldPresence::Required, // Sets `field_presence` as `Required`, as the top level type definition can not be `Optional`. isl_type, &mut code_gen_context, false, @@ -335,9 +407,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { self.render_generated_code(isl_type_name, &mut context, &data_model_node) } + /// _Note: `field_presence` is only used for variably occurring type references and currently that is only supported with `fields` constraint. + /// For all other cases `field_presence` will be set as default `FieldPresence::Required`._ fn convert_isl_type_def_to_data_model_node( &mut self, isl_type_name: &String, + field_presence: FieldPresence, isl_type: &IslType, code_gen_context: &mut CodeGenContext, is_nested_type: bool, @@ -398,14 +473,23 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // TODO: verify the `occurs` value within a field, by default the fields are optional. // add current data model node into the data model store - self.data_model_store.insert( - abstract_data_type.fully_qualified_type_ref::().ok_or( + // verify if the field presence was provided as optional and set the type reference name as optional. + let type_name = match field_presence { + FieldPresence::Optional => abstract_data_type.fully_qualified_type_ref::().ok_or( invalid_abstract_data_type_raw_error( "Can not determine fully qualified name for the data model", ), )?, - data_model_node.to_owned(), - ); + FieldPresence::Required => abstract_data_type.fully_qualified_type_ref::().ok_or( + invalid_abstract_data_type_raw_error( + "Can not determine fully qualified name for the data model", + ), + )?, + }; + + self.data_model_store + .insert(type_name, data_model_node.to_owned()); + Ok(data_model_node) } @@ -459,10 +543,13 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { } /// Provides the `FullyQualifiedTypeReference` to be used for the `AbstractDataType` in the data model. - /// Returns None when the given ISL type is `struct`, `list` or `sexp` as open-ended types are not supported currently. + /// Returns `None` when the given ISL type is `struct`, `list` or `sexp` as open-ended types are not supported currently. + /// _Note: `field_presence` is only used for variably occurring type references and currently that is only supported with `fields` constraint. + /// For all other cases `field_presence` will be set as default `FieldPresence::Required`._ fn fully_qualified_type_ref_name( &mut self, isl_type_ref: &IslTypeRef, + field_presence: FieldPresence, parent_code_gen_context: &mut CodeGenContext, ) -> CodeGenResult> { Ok(match isl_type_ref { @@ -474,13 +561,25 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { type_name: vec![type_name.to_string()], parameters: vec![], }) + .map(|t| { + if field_presence == FieldPresence::Optional { + L::target_type_as_optional(t) + } else { + t + } + }) } IslTypeRef::TypeImport(_, _) => { unimplemented!("Imports in schema are not supported yet!"); } IslTypeRef::Anonymous(type_def, _) => { let name = self.next_nested_type_name(); - Some(self.generate_nested_type(&name, type_def, parent_code_gen_context)?) + Some(self.generate_nested_type( + &name, + type_def, + field_presence, + parent_code_gen_context, + )?) } }) } @@ -498,6 +597,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { found_constraint: bool, constraint_name: &str, isl_type: &IslTypeRef, + field_presence: FieldPresence, code_gen_context: &mut CodeGenContext, ) -> CodeGenResult { if found_constraint { @@ -506,7 +606,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { )); } - self.fully_qualified_type_ref_name(isl_type, code_gen_context)? + self.fully_qualified_type_ref_name(isl_type, field_presence, code_gen_context)? .ok_or(invalid_abstract_data_type_raw_error(format!( "Could not determine `FullQualifiedTypeReference` for type {:?}", isl_type @@ -556,20 +656,26 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // TODO: Check for `closed` annotation on fields and based on that return error while reading if there are extra fields. let mut fields = HashMap::new(); for (name, value) in struct_fields.iter() { + let field_presence = if value.occurs().inclusive_endpoints() == (0, 1) { + FieldPresence::Optional + } else if value.occurs().inclusive_endpoints() == (1, 1) { + FieldPresence::Required + } else { + // TODO: change the field presence based on occurs constraint + return invalid_abstract_data_type_error("Fields with occurs as a range aren't supported with code generation"); + }; let type_name = self .fully_qualified_type_ref_name( value.type_reference(), + field_presence, code_gen_context, )? .ok_or(invalid_abstract_data_type_raw_error( "Given type doesn't have a name", ))?; - - // TODO: change the field presence based on occurs constraint - // by default the field presence is optional fields.insert( name.to_string(), - FieldReference(type_name.to_owned(), FieldPresence::Optional), + FieldReference(type_name.to_owned(), field_presence), ); } // unwrap here is safe as the `current_abstract_data_type_builder` will either be initialized with default implementation @@ -631,6 +737,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { found_base_type, "type", isl_type, + FieldPresence::Required, code_gen_context, )?; wrapped_scalar_builder.base_type(type_name); @@ -687,6 +794,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { found_base_type, "type", isl_type, + FieldPresence::Required, code_gen_context, )?; scalar_builder.base_type(type_name); @@ -742,6 +850,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { found_element_constraint, "type", isl_type_ref, + FieldPresence::Required, code_gen_context, )?; @@ -807,7 +916,11 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { match constraint.constraint() { IslConstraintValue::Element(isl_type_ref, _) => { let type_name = self - .fully_qualified_type_ref_name(isl_type_ref, code_gen_context)? + .fully_qualified_type_ref_name( + isl_type_ref, + FieldPresence::Required, + code_gen_context, + )? .ok_or(invalid_abstract_data_type_raw_error(format!( "Could not determine `FullQualifiedTypeReference` for type {:?}", isl_type_ref @@ -867,6 +980,7 @@ mod isl_to_model_tests { ); let data_model_node = java_code_generator.convert_isl_type_def_to_data_model_node( &"my_struct".to_string(), + FieldPresence::Required, &isl_type, &mut CodeGenContext::new(), false, @@ -914,7 +1028,7 @@ mod isl_to_model_tests { "bar".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["int".to_string()], + type_name: vec!["Integer".to_string()], parameters: vec![] }, FieldPresence::Optional @@ -939,7 +1053,7 @@ mod isl_to_model_tests { fields: { baz: bool }, - type: struct + type: struct, }, bar: int }, @@ -955,6 +1069,7 @@ mod isl_to_model_tests { ); let data_model_node = java_code_generator.convert_isl_type_def_to_data_model_node( &"my_nested_struct".to_string(), + FieldPresence::Required, &isl_type, &mut CodeGenContext::new(), false, @@ -1007,7 +1122,7 @@ mod isl_to_model_tests { "bar".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["int".to_string()], + type_name: vec!["Integer".to_string()], parameters: vec![] }, FieldPresence::Optional diff --git a/src/bin/ion/commands/generate/mod.rs b/src/bin/ion/commands/generate/mod.rs index 2c83515c..b234e57e 100644 --- a/src/bin/ion/commands/generate/mod.rs +++ b/src/bin/ion/commands/generate/mod.rs @@ -11,10 +11,12 @@ use crate::commands::generate::utils::{JavaLanguage, RustLanguage}; use crate::commands::IonCliCommand; use anyhow::{bail, Result}; use clap::{Arg, ArgAction, ArgMatches, Command}; +use colored::Colorize; use ion_schema::authority::{DocumentAuthority, FileSystemDocumentAuthority}; use ion_schema::system::SchemaSystem; use std::fs; use std::path::{Path, PathBuf}; + pub struct GenerateCommand; impl IonCliCommand for GenerateCommand { @@ -120,10 +122,13 @@ impl IonCliCommand for GenerateCommand { None => { // generate code based on schema and programming language match language { - "java" => + "java" => { + Self::print_java_code_gen_warnings(); CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()) - .generate_code_for_authorities(&authorities, &mut schema_system)?, + .generate_code_for_authorities(&authorities, &mut schema_system)? + }, "rust" => { + Self::print_rust_code_gen_warnings(); CodeGenerator::::new(output) .generate_code_for_authorities(&authorities, &mut schema_system)? } @@ -136,8 +141,12 @@ impl IonCliCommand for GenerateCommand { Some(schema_id) => { // generate code based on schema and programming language match language { - "java" => CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()).generate_code_for_schema(&mut schema_system, schema_id)?, + "java" => { + Self::print_java_code_gen_warnings(); + CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()).generate_code_for_schema(&mut schema_system, schema_id)? + }, "rust" => { + Self::print_rust_code_gen_warnings(); CodeGenerator::::new(output) .generate_code_for_authorities(&authorities, &mut schema_system)? } @@ -154,3 +163,23 @@ impl IonCliCommand for GenerateCommand { Ok(()) } } + +impl GenerateCommand { + // Prints warning messages for Java code generation + fn print_java_code_gen_warnings() { + println!("{}","WARNING: Code generation in Java does not support any `$NOMINAL_ION_TYPES` data type.(For more information: https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#built-in-types) Reference issue: https://github.com/amazon-ion/ion-cli/issues/101".yellow().bold()); + println!( + "{}", + "Optional fields in generated code are represented with the wrapper class of that primitive data type and are set to `null` when missing." + .yellow() + .bold() + ); + println!("{}", "When the `writeTo` method is used on an optional field and if the field value is set as null then it would skip serializing that field.".yellow().bold()); + } + + // Prints warning messages for Rust code generation + fn print_rust_code_gen_warnings() { + println!("{}","WARNING: Code generation in Rust does not yet support any `$NOMINAL_ION_TYPES` data type.(For more information: https://amazon-ion.github.io/ion-schema/docs/isl-2-0/spec#built-in-types) Reference issue: https://github.com/amazon-ion/ion-cli/issues/101".yellow().bold()); + println!("{}","Code generation in Rust does not yet support optional/required fields. It does not have any checks added for this on read or write methods. Reference issue: https://github.com/amazon-ion/ion-cli/issues/106".yellow().bold()); + } +} diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index b78cabbe..1d0d99ba 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -151,7 +151,7 @@ impl FullyQualifiedTypeReference { /// Provides string representation of this `FullyQualifiedTypeReference` pub fn string_representation(&self) -> String { if self.parameters.is_empty() { - return format!("{}", self.type_name.join(&L::namespace_separator())); + return self.type_name.join(L::namespace_separator()).to_string(); } let parameters = self .parameters @@ -161,7 +161,7 @@ impl FullyQualifiedTypeReference { .join(", "); format!( "{}<{}>", - self.type_name.join(&L::namespace_separator()), + self.type_name.join(L::namespace_separator()), parameters ) } @@ -434,7 +434,7 @@ pub struct Structure { } /// Represents whether the field is required or not -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Copy)] pub enum FieldPresence { #[allow(dead_code)] Required, diff --git a/src/bin/ion/commands/generate/templates/java/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index 085f2a24..4f8ab727 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -31,13 +31,14 @@ import java.io.IOException; } {% endfor %} - {% for field_name, field_value in struct_info["fields"] %} - {% set val = field_value.0 | fully_qualified_type_name %} + {% for field_name, field_val in struct_info["fields"] %} + {% set field_value = field_val.0 %} + {% set val = field_value | fully_qualified_type_name %} {% if val is containing("NestedType") %} public void set{% filter upper_camel %}{{ field_name }}{% endfilter %}( - {{ macros::define_params_for_anonymous_type(nested_type=field_value.0, type_store=type_store, field_name=field_name, initial_field_name=field_name) }} + {{ macros::define_params_for_anonymous_type(nested_type=val, type_store=type_store, field_name=field_name, initial_field_name=field_name) }} ) { - {{ macros::initialize_anonymous_type(nested_type=field_value.0, type_store=type_store, field_name=field_name) }} + {{ macros::initialize_anonymous_type(nested_type=val, type_store=type_store, field_name=field_name) }} this.{{ field_name | camel }} = {{ field_name | camel }}; return; {% else %} @@ -59,14 +60,9 @@ import java.io.IOException; {# Initializes all the fields of this class #} {% for field_name, field_val in struct_info["fields"] -%} {% set field_value = field_val.0 | fully_qualified_type_name %} - {{ field_value }} {{ field_name | camel }} = - {% if field_value == "boolean" %} - false - {% elif field_value == "int" or field_value == "double" %} - 0 - {% else %} - null - {% endif %}; + {% set field_occurrence = field_val.1 %} + {# Initialize all the required fields as `java.util.Optional` to verify if the required fields were missing or not #} + java.util.Optional<{{ field_value | wrapper_class }}> {{ field_name | camel }} = java.util.Optional.empty(); {% endfor %} {# Reads `Structure` class with multiple fields based on `field.name` #} @@ -77,19 +73,21 @@ import java.io.IOException; switch(fieldName) { {% for field_name, field_val in struct_info["fields"] %} {% set field_value = field_val.0 | fully_qualified_type_name %} - + {% set field_occurrence = field_val.1 %} + {% if field_occurrence == "Optional" %} {% set field_value = field_value | primitive_data_type %} {% endif %} case "{{ field_name }}": - {{ field_name | camel }} = {% if field_value | is_built_in_type %} + {{ field_name | camel }} = java.util.Optional.of( + {% if field_value | is_built_in_type %} {% if field_value == "bytes[]" %} - reader.newBytes(); + reader.newBytes() {% else %} - reader.{{ field_value | camel }}Value(); + reader.{{ field_value | camel }}Value() {% endif %} {% elif field_value is containing("ArrayList") %} - {{ util_macros::read_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} + {{ util_macros::read_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store, field_occurrence=field_occurrence) }} {% else %} - {{ field_value }}.readFrom(reader); - {% endif %} + {{ field_value }}.readFrom(reader) + {% endif %}); break; {% endfor %} default: @@ -99,8 +97,14 @@ import java.io.IOException; reader.stepOut(); {{ model.name }} {{ model.name | camel }} = new {{ model.name }}(); - {% for field_name, field_value in struct_info["fields"] -%} - {{ model.name | camel }}.{{ field_name | camel }} = {{ field_name | camel }}; + {% for field_name, field_val in struct_info["fields"] -%} + {# field_val.1 is the field occurrence #} + {% if field_val.1 == "Required" %} + if (!{{ field_name | camel }}.isPresent()) { + throw new IonException("Can not find field name: {{ field_name }} for {{ model.name }} while reading Ion data."); + } + {% endif %} + {{ model.name | camel }}.{{ field_name | camel }} = {{ field_name | camel }}.orElse(null); {% endfor %} return {{ model.name | camel }}; @@ -111,12 +115,18 @@ import java.io.IOException; * * This method does not close the writer after writing is complete. * The caller is responsible for closing the stream associated with the writer. + * This method skips writing a field when it's null. */ public void writeTo(IonWriter writer) throws IOException { {# Writes `Structure` class with multiple fields based on `field.name` as an Ion struct #} writer.stepIn(IonType.STRUCT); {% for field_name, field_val in struct_info["fields"] %} {% set field_value = field_val.0 | fully_qualified_type_name %} + {% set field_occurrence = field_val.1 %} + {% if field_occurrence == "Optional" %} + {% set field_value = field_value | primitive_data_type %} + if (this.{{ field_name | camel }} != null) { + {% endif %} writer.setFieldName("{{ field_name }}"); {% if field_value | is_built_in_type == false %} {% if field_value is containing("ArrayList") %} @@ -125,7 +135,10 @@ import java.io.IOException; this.{{ field_name | camel }}.writeTo(writer); {% endif %} {% else %} - writer.write{{ field_value | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.{{ field_name | camel }}); + writer.write{{ field_value | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.{{ field_name | camel }}); + {% endif %} + {% if field_occurrence == "Optional" %} + } {% endif %} {% endfor %} writer.stepOut(); diff --git a/src/bin/ion/commands/generate/templates/java/nested_type.templ b/src/bin/ion/commands/generate/templates/java/nested_type.templ index d59c0155..04829ffb 100644 --- a/src/bin/ion/commands/generate/templates/java/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/java/nested_type.templ @@ -9,9 +9,8 @@ {# following macro defines statements to initialize anonymous types for setter methods #} {% macro initialize_anonymous_type(nested_type, type_store, field_name) %} - {% set key = nested_type | fully_qualified_type_name %} - {% set inline_type = type_store[key] %} - {{ key }} {{ field_name | camel }} = new {{ key }}(); + {% set inline_type = type_store[nested_type] %} + {{ nested_type }} {{ field_name | camel }} = new {{ nested_type }}(); {% if inline_type.code_gen_type is containing("Sequence") %} {{ field_name | camel }}.setValue(value); {% elif inline_type.code_gen_type is containing("Scalar") %} @@ -25,8 +24,7 @@ {# following macro defines arguments to setter methods for anonymous types #} {% macro define_params_for_anonymous_type(nested_type, field_name, initial_field_name) %} - {% set field_name = nested_type | fully_qualified_type_name %} - {% set inline_type = type_store[field_name] %} + {% set inline_type = type_store[nested_type] %} {% if inline_type.code_gen_type is containing("Structure") %} {% set inline_struct_type_info = inline_type.code_gen_type["Structure"] %} {% set field_names = inline_struct_type_info["fields"] | field_names %} diff --git a/src/bin/ion/commands/generate/templates/java/util_macros.templ b/src/bin/ion/commands/generate/templates/java/util_macros.templ index a03f0927..731f9671 100644 --- a/src/bin/ion/commands/generate/templates/java/util_macros.templ +++ b/src/bin/ion/commands/generate/templates/java/util_macros.templ @@ -1,24 +1,27 @@ {# following macro defines statements to read a class field as sequence #} -{% macro read_as_sequence(field_name, field_value, type_store) %} - {% set field_value_model = type_store[field_value] %} - new {{ field_value }}(); - {# Reads `Sequence` field that is an `ArrayList` #} - if(reader.getType() != IonType.{{ field_value_model.code_gen_type["Sequence"].sequence_type | upper }}) { - throw new IonException("Expected {{ field_value_model.code_gen_type["Sequence"].sequence_type }}, found " + reader.getType() + " while reading {{ field_name | camel }}."); - } - reader.stepIn(); - {# Iterate through the `ArrayList` and read each element in it based on the data type provided in `field.abstract_data_type[Sequence]` #} - while (reader.hasNext()) { - reader.next(); - {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} - {{ field_name | camel }}.add({{ field_value_model.code_gen_type["Sequence"].element_type }}.readFrom(reader)); - {% elif field_value_model.code_gen_type["Sequence"].element_type == "bytes[]" %} - {{ field_name | camel }}.add(reader.newBytes()); - {% else %} - {{ field_name | camel }}.add(reader.{{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | camel }}Value()); - {% endif %} - } - reader.stepOut(); +{% macro read_as_sequence(field_name, field_value, type_store, field_occurrence) %} + ((java.util.function.Supplier<{{ field_value }}>) () -> { + {% set field_value_model = type_store[field_value] %} + {{ field_value }} {{ field_name | camel }}List = new {{ field_value }}(); + {# Reads `Sequence` field that is an `ArrayList` #} + if(reader.getType() != IonType.{{ field_value_model.code_gen_type["Sequence"].sequence_type | upper }}) { + throw new IonException("Expected {{ field_value_model.code_gen_type["Sequence"].sequence_type }}, found " + reader.getType() + " while reading {{ field_name | camel }}."); + } + reader.stepIn(); + {# Iterate through the `ArrayList` and read each element in it based on the data type provided in `field.abstract_data_type[Sequence]` #} + while (reader.hasNext()) { + reader.next(); + {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} + {{ field_name | camel }}List.add({{ field_value_model.code_gen_type["Sequence"].element_type }}.readFrom(reader)); + {% elif field_value_model.code_gen_type["Sequence"].element_type == "bytes[]" %} + {{ field_name | camel }}List.add(reader.newBytes()); + {% else %} + {{ field_name | camel }}List.add(reader.{{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | camel }}Value()); + {% endif %} + } + reader.stepOut(); + return {{ field_name | camel }}List; + }).get() {% endmacro %} {# following macro defines statements to write a class field as sequence #} {% macro write_as_sequence(field_name, field_value, type_store) %} diff --git a/src/bin/ion/commands/generate/utils.rs b/src/bin/ion/commands/generate/utils.rs index 2f254de7..897418e2 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -69,7 +69,14 @@ pub trait Language { /// ``` /// To add `NestedType` into the namespace path, `is_nested_type` helps remove any prior types form the path and add this current type. /// i.e. given namespace path as `foo::Foo`, it will first remove `Foo` and then add the current type as `foo::nested_type::NestedType`. - fn add_type_to_namespace(is_nested_type: bool, type_name: &String, namespace: &mut Vec); + fn add_type_to_namespace(is_nested_type: bool, type_name: &str, namespace: &mut Vec); + + /// Returns the `FullyQualifiedReference` that represents the target type as optional in the given programming language + /// e.g. In Java, it will return "java.util.Optional" + /// In Rust, it will return "Option" + fn target_type_as_optional( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference; } pub struct JavaLanguage; @@ -106,7 +113,9 @@ impl Language for JavaLanguage { fn target_type_as_sequence( target_type: FullyQualifiedTypeReference, ) -> FullyQualifiedTypeReference { - match JavaLanguage::wrapper_class(&target_type.string_representation::()) { + match JavaLanguage::wrapper_class_or_none( + &target_type.string_representation::(), + ) { Some(wrapper_name) => FullyQualifiedTypeReference { type_name: vec![ "java".to_string(), @@ -152,20 +161,31 @@ impl Language for JavaLanguage { "." } - fn add_type_to_namespace( - _is_nested_type: bool, - type_name: &String, - namespace: &mut Vec, - ) { + fn add_type_to_namespace(_is_nested_type: bool, type_name: &str, namespace: &mut Vec) { namespace.push(type_name.to_case(Case::UpperCamel)) } + + fn target_type_as_optional( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference { + match JavaLanguage::wrapper_class_or_none( + &target_type.string_representation::(), + ) { + Some(wrapper_name) => FullyQualifiedTypeReference { + type_name: vec![wrapper_name], + parameters: vec![], + }, + None => target_type, + } + } } impl JavaLanguage { - fn wrapper_class(primitive_data_type: &str) -> Option { + /// Returns the wrapper class for the given primitive data type, otherwise returns None. + fn wrapper_class_or_none(primitive_data_type: &str) -> Option { match primitive_data_type { "int" => Some("Integer".to_string()), - "bool" => Some("Boolean".to_string()), + "boolean" => Some("Boolean".to_string()), "double" => Some("Double".to_string()), "long" => Some("Long".to_string()), _ => { @@ -174,6 +194,31 @@ impl JavaLanguage { } } } + + /// Returns the wrapper class for the given primitive data type + /// If `data_type` is a primitive, returns the boxed equivalent, otherwise returns `data_type`. + pub fn wrapper_class(primitive_data_type: &str) -> String { + match Self::wrapper_class_or_none(primitive_data_type) { + None => primitive_data_type.to_string(), + Some(wrapper_class) => wrapper_class, + } + } + + /// Returns the primitive data type for the given wrapper class, or `wrapper_class` + /// if it does not have an equivalent primitive type. + pub fn primitive_data_type(wrapper_class: &str) -> &str { + match wrapper_class { + "Integer" => "int", + "Boolean" => "boolean", + "Double" => "double", + "Long" => "long", + "Short" => "short", + "Byte" => "byte", + "Float" => "float", + "Character" => "char", + _ => wrapper_class, + } + } } impl Display for JavaLanguage { @@ -245,11 +290,7 @@ impl Language for RustLanguage { "::" } - fn add_type_to_namespace( - is_nested_type: bool, - type_name: &String, - namespace: &mut Vec, - ) { + fn add_type_to_namespace(is_nested_type: bool, type_name: &str, namespace: &mut Vec) { // e.g. For example there is a `NestedType` inside `Foo` struct. Rust code generation also generates similar modules for the generated structs. // ```rust // mod foo { @@ -272,6 +313,17 @@ impl Language for RustLanguage { namespace.push(type_name.to_case(Case::Snake)); // Add this type's module name to the namespace path namespace.push(type_name.to_case(Case::UpperCamel)) // Add this type itself to the namespace path } + + fn target_type_as_optional( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference { + // TODO: un-comment following block for optional support in Rust, once the templates are changes accordingly + // FullyQualifiedTypeReference { + // type_name: vec!["Option".to_string()], + // parameters: vec![target_type], + // } + target_type + } } impl Display for RustLanguage { diff --git a/tests/cli.rs b/tests/cli.rs index b55aa031..881640c3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -232,7 +232,7 @@ mod code_gen_tests { name: simple_struct, fields: { name: string, - id: int, + id: { type: int, occurs: required }, } } "#, From 624044b78e86234e774386c3e3e43ab19499d84b Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:55:14 -0700 Subject: [PATCH 07/12] Modifies generated setter API to take nested types instead of nested properties (#153) Fixes #152 --- .../test/java/org/example/CodeGenTest.java | 13 +++-- .../generate/templates/java/class.templ | 14 +----- .../generate/templates/java/nested_type.templ | 47 +------------------ 3 files changed, 11 insertions(+), 63 deletions(-) diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 506b4db4..24a3c508 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -47,15 +47,18 @@ class CodeGenTest { @Test void getterAndSetterTestForNestedStruct() { // getter tests for `NestedStruct` NestedStruct n = new NestedStruct(); - ArrayList a = new ArrayList(); - a.add(1); - a.add(2); - a.add(3); + ArrayList e = new ArrayList(); + e.add(1); + e.add(2); + e.add(3); // set all the fields of `NestedStruct` n.setA("hello"); n.setB(12); - n.setC(false, a); + NestedStruct.NestedType1 n1 = new NestedStruct.NestedType1(); + n1.setD(false); + n1.setE(e); + n.setC(n1); // getter tests for `NestedStruct` assertEquals("hello", n.getA(), "n.getA() should return \"hello\""); diff --git a/src/bin/ion/commands/generate/templates/java/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index 4f8ab727..0a91a0e5 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -32,21 +32,11 @@ import java.io.IOException; {% endfor %} {% for field_name, field_val in struct_info["fields"] %} - {% set field_value = field_val.0 %} - {% set val = field_value | fully_qualified_type_name %} - {% if val is containing("NestedType") %} - public void set{% filter upper_camel %}{{ field_name }}{% endfilter %}( - {{ macros::define_params_for_anonymous_type(nested_type=val, type_store=type_store, field_name=field_name, initial_field_name=field_name) }} - ) { - {{ macros::initialize_anonymous_type(nested_type=val, type_store=type_store, field_name=field_name) }} - this.{{ field_name | camel }} = {{ field_name | camel }}; - return; - {% else %} + {% set val = field_val.0 | fully_qualified_type_name %} public void set{% filter upper_camel %}{{ field_name }}{% endfilter %}({{ val }} {{ field_name | camel }}) { this.{{ field_name | camel }} = {{ field_name | camel }}; return; - {% endif %} - } + } {% endfor %} diff --git a/src/bin/ion/commands/generate/templates/java/nested_type.templ b/src/bin/ion/commands/generate/templates/java/nested_type.templ index 04829ffb..be4c0b26 100644 --- a/src/bin/ion/commands/generate/templates/java/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/java/nested_type.templ @@ -5,49 +5,4 @@ {% if model.code_gen_type is containing("Structure")%} {% include "class.templ" %} {% endif %} -{% endmacro nested_type -%} - -{# following macro defines statements to initialize anonymous types for setter methods #} -{% macro initialize_anonymous_type(nested_type, type_store, field_name) %} - {% set inline_type = type_store[nested_type] %} - {{ nested_type }} {{ field_name | camel }} = new {{ nested_type }}(); - {% if inline_type.code_gen_type is containing("Sequence") %} - {{ field_name | camel }}.setValue(value); - {% elif inline_type.code_gen_type is containing("Scalar") %} - {{ field_name | camel }}.setValue(value); - {% elif inline_type.code_gen_type is containing("Structure")%} - {% for inline_type_field_name, inline_type_field_value in inline_type.code_gen_type["Structure"].fields %} - {{ field_name | camel }}.set{{ inline_type_field_name | upper_camel }}({{ inline_type_field_name | camel }}); - {% endfor %} - {% endif %} -{% endmacro %} - -{# following macro defines arguments to setter methods for anonymous types #} -{% macro define_params_for_anonymous_type(nested_type, field_name, initial_field_name) %} - {% set inline_type = type_store[nested_type] %} - {% if inline_type.code_gen_type is containing("Structure") %} - {% set inline_struct_type_info = inline_type.code_gen_type["Structure"] %} - {% set field_names = inline_struct_type_info["fields"] | field_names %} - {% for inline_type_field_name in field_names | sort %} - {% set inline_type_field_value = inline_struct_type_info["fields"][inline_type_field_name] %} - {% set value_type = inline_type_field_value.0 | fully_qualified_type_name %} - {% if value_type is containing("NestedType") %} - {{ self::define_params_for_anonymous_type(nested_type=inline_type_field_value.0, field_name=inline_type_field_name, initial_field_name=initial_field_name) }} - {% else %} - {% if inline_type_field_name == "value" and not initial_field_name == field_name %} - {{ value_type }} {{ field_name | camel }} - {% else %} - {{ value_type }} {{ inline_type_field_name | camel }} - {% endif %} - {% endif %} - {% if not loop.last -%},{% endif -%} - {% endfor %} - {% elif inline_type.code_gen_type is containing("Sequence") %} - {% set sequence_info = model.code_gen_type["WrappedSequence"] %} - {{ sequence_info["element_type"] | fully_qualified_type_name }} value - {% elif inline_type.code_gen_type is containing("Scalar") %} - {% set scalar_info = model.code_gen_type["WrappedScalar"] %} - {% set base_type = scalar_info["base_type"] | fully_qualified_type_name %} - {{ base_type }} value - {% endif %} -{% endmacro %} \ No newline at end of file +{% endmacro nested_type -%} \ No newline at end of file From 49a957e52c891d74a328f4ef4670c5862d6b7cce Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:55:30 -0700 Subject: [PATCH 08/12] Adds support for builder API generation in Java (#154) --- .../test/java/org/example/CodeGenTest.java | 38 +++++-------- .../generate/templates/java/class.templ | 54 +++++++++++-------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 24a3c508..6206ab26 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -22,43 +22,35 @@ class CodeGenTest { private static final IonSystem ionSystem = IonSystemBuilder.standard().build(); private static final IonLoader ionLoader = ionSystem.getLoader(); - @Test void getterAndSetterTestForStructWithFields() { - StructWithFields s = new StructWithFields(); + @Test void builderTestForStructWithFields() { + StructWithFields.Builder sb = new StructWithFields.Builder(); + ArrayList c = new ArrayList(); + c.add("foo"); + c.add("bar"); + c.add("baz"); // set all the fields of `StructWithFields` - s.setA("hello"); - s.setB(12); - s.setD(10e2); + StructWithFields s = sb.a("hello").b(12).c(c).d(10e2).build(); // getter tests for `StructWithFields` assertEquals("hello", s.getA(), "s.getA() should return \"hello\""); assertEquals(12, s.getB(), "s.getB() should return `12`"); + assertEquals(3, s.getC().size(), "s.getC() should return ArrayList fo size 3"); assertEquals(10e2, s.getD(), "s.getD() should return `10e2`"); - - // setter tests for `StructWithFields` - s.setA("hi"); - assertEquals("hi", s.getA(), "s.getA() should return \"hi\""); - s.setB(6); - assertEquals(6, s.getB(), "s.getB() should return `6`"); - s.setD(11e3); - assertEquals(11e3 ,s.getD(), "s.getD() should return `11e3`"); } - @Test void getterAndSetterTestForNestedStruct() { + @Test void builderTestForNestedStruct() { // getter tests for `NestedStruct` - NestedStruct n = new NestedStruct(); + NestedStruct.Builder nb = new NestedStruct.Builder(); ArrayList e = new ArrayList(); e.add(1); e.add(2); e.add(3); // set all the fields of `NestedStruct` - n.setA("hello"); - n.setB(12); - NestedStruct.NestedType1 n1 = new NestedStruct.NestedType1(); - n1.setD(false); - n1.setE(e); - n.setC(n1); + NestedStruct.NestedType1.Builder nb1 = new NestedStruct.NestedType1.Builder(); + NestedStruct.NestedType1 c = nb1.d(false).e(e).build(); + NestedStruct n = nb.a("hello").b(12).c(c).build(); // getter tests for `NestedStruct` assertEquals("hello", n.getA(), "n.getA() should return \"hello\""); @@ -89,10 +81,6 @@ class CodeGenTest { // getter tests for `Sequence` assertEquals(3, s.getValue().size(), "s.getValue().size() should return ArrayList fo size 3"); - - // setter tests for `Sequence` - s.setValue(new ArrayList()); - assertEquals(true, s.getValue().isEmpty(), "s.getValue().isEmpty() should return `true`"); } @Test void getterAndSetterTestForScalar() { diff --git a/src/bin/ion/commands/generate/templates/java/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index 0a91a0e5..f0663f73 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -24,7 +24,7 @@ import java.io.IOException; private {{ field_value.0 | fully_qualified_type_name }} {{ field_name | camel }}; {% endfor %} - public {{ model.name }}() {} + private {{ model.name }}() {} {% for field_name, field_value in struct_info["fields"] -%}public {{ field_value.0 | fully_qualified_type_name }} get{% filter upper_camel %}{{ field_name }}{% endfilter %}() { return this.{{ field_name | camel }}; @@ -39,6 +39,34 @@ import java.io.IOException; } {% endfor %} + public static class Builder { + {% for field_name, field_val in struct_info["fields"] -%} + {% set propertyName = field_name | camel %} + {% set PropertyType = field_val.0 | fully_qualified_type_name | wrapper_class %} + + private {{ PropertyType }} {{ propertyName }}; + + public Builder {{ propertyName }}({{ PropertyType }} value) { + this.{{ propertyName }} = value; + return this; + } + {% endfor %} + + public {{ model.name }} build() { + {{ model.name }} instance = new {{ model.name }}(); + {% for field_name, field_val in struct_info["fields"] -%} + {% set propertyName = field_name | camel %} + {# field_val.1 is the field occurrence #} + {% if field_val.1 == "Required" %} + if ({{propertyName}} == null) { + throw new IllegalArgumentException("Missing required field {{propertyName}}"); + } + {% endif %} + instance.{{ propertyName }} = {{ propertyName }}; + {% endfor %} + return instance; + } + } /** * Reads a {{ model.name }} from an {@link IonReader}. @@ -47,13 +75,8 @@ import java.io.IOException; * The caller is responsible for positioning the reader on the value to read. */ public static {{ model.name }} readFrom(IonReader reader) { - {# Initializes all the fields of this class #} - {% for field_name, field_val in struct_info["fields"] -%} - {% set field_value = field_val.0 | fully_qualified_type_name %} - {% set field_occurrence = field_val.1 %} - {# Initialize all the required fields as `java.util.Optional` to verify if the required fields were missing or not #} - java.util.Optional<{{ field_value | wrapper_class }}> {{ field_name | camel }} = java.util.Optional.empty(); - {% endfor %} + {# Initializes the builder for this class #} + Builder builder = new Builder(); {# Reads `Structure` class with multiple fields based on `field.name` #} reader.stepIn(); @@ -66,7 +89,7 @@ import java.io.IOException; {% set field_occurrence = field_val.1 %} {% if field_occurrence == "Optional" %} {% set field_value = field_value | primitive_data_type %} {% endif %} case "{{ field_name }}": - {{ field_name | camel }} = java.util.Optional.of( + builder.{{ field_name | camel }}( {% if field_value | is_built_in_type %} {% if field_value == "bytes[]" %} reader.newBytes() @@ -86,18 +109,7 @@ import java.io.IOException; } reader.stepOut(); - {{ model.name }} {{ model.name | camel }} = new {{ model.name }}(); - {% for field_name, field_val in struct_info["fields"] -%} - {# field_val.1 is the field occurrence #} - {% if field_val.1 == "Required" %} - if (!{{ field_name | camel }}.isPresent()) { - throw new IonException("Can not find field name: {{ field_name }} for {{ model.name }} while reading Ion data."); - } - {% endif %} - {{ model.name | camel }}.{{ field_name | camel }} = {{ field_name | camel }}.orElse(null); - {% endfor %} - - return {{ model.name | camel }}; + return builder.build(); } /** From 116fe0af1d4d1b7c49cd72daa0ab4ca2d4db666e Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:33:33 -0700 Subject: [PATCH 09/12] Adds support for Java enum generation (#158) * adds changes for enum support * adds tests for enum generation * Adds changes for placeholder Rust enum template * Fixes bug for Rust namespace in generated code --- .../enum_type/invalid_case_enum_varaint.ion | 1 + .../bad/enum_type/invalid_enum_variant.ion | 1 + .../bad/enum_type/mismatched_enum_type.ion | 1 + .../mismatched_sequence_element_type.ion | 7 ++ .../mismatched_sequence_type.ion | 7 ++ .../mismatched_type.ion | 7 ++ .../missing_required_fields.ion | 7 ++ .../input/good/enum_type/valid_value_1.ion | 1 + .../input/good/enum_type/valid_value_2.ion | 1 + .../input/good/enum_type/valid_value_3.ion | 1 + .../input/good/enum_type/valid_value_4.ion | 1 + .../struct_with_enum_fields/empty_values.ion | 7 ++ .../struct_with_enum_fields/valid_fields.ion | 8 ++ .../valid_optional_fields.ion | 8 ++ .../valid_unordered_fields.ion | 8 ++ .../test/java/org/example/CodeGenTest.java | 21 ++++ code-gen-projects/schema/enum_type.isl | 4 + .../schema/struct_with_enum_fields.isl | 12 ++ src/bin/ion/commands/generate/generator.rs | 113 +++++++++++++++++- src/bin/ion/commands/generate/model.rs | 43 ++++++- src/bin/ion/commands/generate/result.rs | 12 +- .../generate/templates/java/enum.templ | 57 +++++++++ .../generate/templates/java/nested_type.templ | 2 + .../ion/commands/generate/templates/mod.rs | 2 + .../generate/templates/rust/enum.templ | 22 ++++ .../generate/templates/rust/nested_type.templ | 4 +- src/bin/ion/commands/generate/utils.rs | 22 ++++ 27 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 code-gen-projects/input/bad/enum_type/invalid_case_enum_varaint.ion create mode 100644 code-gen-projects/input/bad/enum_type/invalid_enum_variant.ion create mode 100644 code-gen-projects/input/bad/enum_type/mismatched_enum_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_enum_fields/mismatched_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_enum_fields/missing_required_fields.ion create mode 100644 code-gen-projects/input/good/enum_type/valid_value_1.ion create mode 100644 code-gen-projects/input/good/enum_type/valid_value_2.ion create mode 100644 code-gen-projects/input/good/enum_type/valid_value_3.ion create mode 100644 code-gen-projects/input/good/enum_type/valid_value_4.ion create mode 100644 code-gen-projects/input/good/struct_with_enum_fields/empty_values.ion create mode 100644 code-gen-projects/input/good/struct_with_enum_fields/valid_fields.ion create mode 100644 code-gen-projects/input/good/struct_with_enum_fields/valid_optional_fields.ion create mode 100644 code-gen-projects/input/good/struct_with_enum_fields/valid_unordered_fields.ion create mode 100644 code-gen-projects/schema/enum_type.isl create mode 100644 code-gen-projects/schema/struct_with_enum_fields.isl create mode 100644 src/bin/ion/commands/generate/templates/java/enum.templ create mode 100644 src/bin/ion/commands/generate/templates/rust/enum.templ diff --git a/code-gen-projects/input/bad/enum_type/invalid_case_enum_varaint.ion b/code-gen-projects/input/bad/enum_type/invalid_case_enum_varaint.ion new file mode 100644 index 00000000..587dfa51 --- /dev/null +++ b/code-gen-projects/input/bad/enum_type/invalid_case_enum_varaint.ion @@ -0,0 +1 @@ +FoobarBaz // expected FooBarBaz, found FoobarBaz \ No newline at end of file diff --git a/code-gen-projects/input/bad/enum_type/invalid_enum_variant.ion b/code-gen-projects/input/bad/enum_type/invalid_enum_variant.ion new file mode 100644 index 00000000..06a63d62 --- /dev/null +++ b/code-gen-projects/input/bad/enum_type/invalid_enum_variant.ion @@ -0,0 +1 @@ +hello // expected (foo, bar, baz or FooBarBaz) found hello \ No newline at end of file diff --git a/code-gen-projects/input/bad/enum_type/mismatched_enum_type.ion b/code-gen-projects/input/bad/enum_type/mismatched_enum_type.ion new file mode 100644 index 00000000..65c2d6b7 --- /dev/null +++ b/code-gen-projects/input/bad/enum_type/mismatched_enum_type.ion @@ -0,0 +1 @@ +"foo" // expected a symbol value foo for enum, found string "foo" \ No newline at end of file diff --git a/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion new file mode 100644 index 00000000..723ed823 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion @@ -0,0 +1,7 @@ +// struct with mismatched sequence element +{ + A: "hello", + B: 12, + C: (1 2 3), // expected sexpression of strings + D: 10e2 +} diff --git a/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_type.ion b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_type.ion new file mode 100644 index 00000000..300a99f5 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_type.ion @@ -0,0 +1,7 @@ +// simple struct with type mismatched sequence type +{ + A: "hello", + B: 12, + C: ["foo", "bar", "baz"], // expected sexp + D: 10e2 +} diff --git a/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_type.ion b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_type.ion new file mode 100644 index 00000000..72253de9 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_type.ion @@ -0,0 +1,7 @@ +// simple struct with type mismatched fields +{ + A: "hello", + B: false, // expected field type: int + C: ("foo" "bar" "baz"), + D: 10e2 +} diff --git a/code-gen-projects/input/bad/struct_with_enum_fields/missing_required_fields.ion b/code-gen-projects/input/bad/struct_with_enum_fields/missing_required_fields.ion new file mode 100644 index 00000000..e1995de9 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_enum_fields/missing_required_fields.ion @@ -0,0 +1,7 @@ +// simple struct with all valid fields +{ + A: "hello", + B: 12, + // C: ("foo" "bar" "baz"), // since `C` is a required field, this is an invalid struct + D: 10e2 +} diff --git a/code-gen-projects/input/good/enum_type/valid_value_1.ion b/code-gen-projects/input/good/enum_type/valid_value_1.ion new file mode 100644 index 00000000..19102815 --- /dev/null +++ b/code-gen-projects/input/good/enum_type/valid_value_1.ion @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/code-gen-projects/input/good/enum_type/valid_value_2.ion b/code-gen-projects/input/good/enum_type/valid_value_2.ion new file mode 100644 index 00000000..ba0e162e --- /dev/null +++ b/code-gen-projects/input/good/enum_type/valid_value_2.ion @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/code-gen-projects/input/good/enum_type/valid_value_3.ion b/code-gen-projects/input/good/enum_type/valid_value_3.ion new file mode 100644 index 00000000..3f953866 --- /dev/null +++ b/code-gen-projects/input/good/enum_type/valid_value_3.ion @@ -0,0 +1 @@ +baz \ No newline at end of file diff --git a/code-gen-projects/input/good/enum_type/valid_value_4.ion b/code-gen-projects/input/good/enum_type/valid_value_4.ion new file mode 100644 index 00000000..7eff0b46 --- /dev/null +++ b/code-gen-projects/input/good/enum_type/valid_value_4.ion @@ -0,0 +1 @@ +FooBarBaz \ No newline at end of file diff --git a/code-gen-projects/input/good/struct_with_enum_fields/empty_values.ion b/code-gen-projects/input/good/struct_with_enum_fields/empty_values.ion new file mode 100644 index 00000000..a5c13861 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_enum_fields/empty_values.ion @@ -0,0 +1,7 @@ +// struct with empty list, empty string and zeros +{ + C: (), + A: "", + B: 0, + D: 0e0, +} diff --git a/code-gen-projects/input/good/struct_with_enum_fields/valid_fields.ion b/code-gen-projects/input/good/struct_with_enum_fields/valid_fields.ion new file mode 100644 index 00000000..a32da8b6 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_enum_fields/valid_fields.ion @@ -0,0 +1,8 @@ +// simple struct with all valid fields +{ + A: "hello", + B: 12, + C: ("foo" "bar" "baz"), + D: 10e2, + E: foo +} diff --git a/code-gen-projects/input/good/struct_with_enum_fields/valid_optional_fields.ion b/code-gen-projects/input/good/struct_with_enum_fields/valid_optional_fields.ion new file mode 100644 index 00000000..e423b499 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_enum_fields/valid_optional_fields.ion @@ -0,0 +1,8 @@ +// simple struct with all valid fields +{ + A: "hello", + B: 12, + C: ("foo" "bar" "baz"), + // D: 10e2, // since `D` is optional field, this is a valid struct + E: foo, +} diff --git a/code-gen-projects/input/good/struct_with_enum_fields/valid_unordered_fields.ion b/code-gen-projects/input/good/struct_with_enum_fields/valid_unordered_fields.ion new file mode 100644 index 00000000..36a2d971 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_enum_fields/valid_unordered_fields.ion @@ -0,0 +1,8 @@ +// struct with unordered fields +{ + C: ("foo" "bar" "baz"), + A: "hello", + B: 12, + E: foo, + D: 10e2, +} diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 6206ab26..318403a0 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -127,6 +127,16 @@ void roundtripBadTestForNestedStruct() throws IOException { runRoundtripBadTest("/bad/nested_struct", NestedStruct::readFrom); } + @Test + void roundtripBadTestForStructWithEnumFields() throws IOException { + runRoundtripBadTest("/bad/struct_with_enum_fields", StructWithEnumFields::readFrom); + } + + @Test + void roundtripBadTestForEnumType() throws IOException { + runRoundtripBadTest("/bad/enum_type", EnumType::readFrom); + } + private void runRoundtripBadTest(String path, ReaderFunction readerFunction) throws IOException { File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); @@ -161,6 +171,17 @@ void roundtripGoodTestForNestedStruct() throws IOException { runRoundtripGoodTest("/good/nested_struct", NestedStruct::readFrom, (item, writer) -> item.writeTo(writer)); } + @Test + void roundtripGoodTestForStructWithEnumFields() throws IOException { + runRoundtripGoodTest("/good/struct_with_enum_fields", StructWithEnumFields::readFrom, (item, writer) -> item.writeTo(writer)); + } + + + @Test + void roundtripGoodTestForEnumType() throws IOException { + runRoundtripGoodTest("/good/enum_type", EnumType::readFrom, (item, writer) -> item.writeTo(writer)); + } + private void runRoundtripGoodTest(String path, ReaderFunction readerFunction, WriterFunction writerFunction) throws IOException { File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); diff --git a/code-gen-projects/schema/enum_type.isl b/code-gen-projects/schema/enum_type.isl new file mode 100644 index 00000000..5801db19 --- /dev/null +++ b/code-gen-projects/schema/enum_type.isl @@ -0,0 +1,4 @@ +type::{ + name: enum_type, + valid_values: [foo, bar, baz, FooBarBaz] +} \ No newline at end of file diff --git a/code-gen-projects/schema/struct_with_enum_fields.isl b/code-gen-projects/schema/struct_with_enum_fields.isl new file mode 100644 index 00000000..3ef77dbd --- /dev/null +++ b/code-gen-projects/schema/struct_with_enum_fields.isl @@ -0,0 +1,12 @@ +type::{ + name: struct_with_enum_fields, + type: struct, + fields: { + A: string, + B: int, + C: { element: string, type: sexp, occurs: required }, + D: float, + E: { valid_values: [foo, bar, baz] } + } +} + diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index a0c9cc37..7ab73883 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,7 +1,8 @@ use crate::commands::generate::context::{CodeGenContext, SequenceType}; use crate::commands::generate::model::{ - AbstractDataType, DataModelNode, FieldPresence, FieldReference, FullyQualifiedTypeReference, - ScalarBuilder, SequenceBuilder, StructureBuilder, WrappedScalarBuilder, WrappedSequenceBuilder, + AbstractDataType, DataModelNode, EnumBuilder, FieldPresence, FieldReference, + FullyQualifiedTypeReference, ScalarBuilder, SequenceBuilder, StructureBuilder, + WrappedScalarBuilder, WrappedSequenceBuilder, }; use crate::commands::generate::result::{ invalid_abstract_data_type_error, invalid_abstract_data_type_raw_error, CodeGenResult, @@ -10,12 +11,14 @@ use crate::commands::generate::templates; use crate::commands::generate::utils::{IonSchemaType, Template}; use crate::commands::generate::utils::{JavaLanguage, Language, RustLanguage}; use convert_case::{Case, Casing}; +use ion_rs::Value; use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue}; use ion_schema::isl::isl_type::IslType; use ion_schema::isl::isl_type_reference::IslTypeRef; +use ion_schema::isl::util::ValidValue; use ion_schema::isl::IslSchema; use ion_schema::system::SchemaSystem; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fs; use std::fs::OpenOptions; use std::io::Write; @@ -46,6 +49,7 @@ impl<'a> CodeGenerator<'a, RustLanguage> { ("struct.templ", templates::rust::STRUCT), ("scalar.templ", templates::rust::SCALAR), ("sequence.templ", templates::rust::SEQUENCE), + ("enum.templ", templates::rust::ENUM), ("util_macros.templ", templates::rust::UTIL_MACROS), ("import.templ", templates::rust::IMPORT), ("nested_type.templ", templates::rust::NESTED_TYPE), @@ -89,6 +93,7 @@ impl<'a> CodeGenerator<'a, JavaLanguage> { ("class.templ", templates::java::CLASS), ("scalar.templ", templates::java::SCALAR), ("sequence.templ", templates::java::SEQUENCE), + ("enum.templ", templates::java::ENUM), ("util_macros.templ", templates::java::UTIL_MACROS), ("nested_type.templ", templates::java::NESTED_TYPE), ]) @@ -328,7 +333,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { self.generate_abstract_data_type(&isl_type_name, isl_type)?; // Since the fully qualified name of this generator represents the current fully qualified name, // remove it before generating code for the next ISL type. - self.current_type_fully_qualified_name.pop(); + L::reset_namespace(&mut self.current_type_fully_qualified_name); } Ok(()) @@ -436,6 +441,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // * The sequence type for `Sequence` will be stored based on `type` constraint with either `list` or `sexp`. // * If given list of constraints has any `type` constraint except `type: list`, `type: struct` and `type: sexp`, then `AbstractDataType::Scalar` needs to be constructed. // * The `base_type` for `Scalar` will be stored based on `type` constraint. + // * If given list of constraints has any `valid_values` constraint which contains exclusively symbol values, then `AbstractDataType::Enum` needs to be constructed. // * All the other constraints except the above ones are not yet supported by code generator. let abstract_data_type = if constraints .iter() @@ -455,6 +461,8 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type, )? } + } else if Self::contains_enum_constraints(constraints) { + self.build_enum_from_constraints(constraints, code_gen_context, isl_type)? } else if Self::contains_scalar_constraints(constraints) { if is_nested_type { self.build_scalar_from_constraints(constraints, code_gen_context, isl_type)? @@ -500,6 +508,20 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { && isl_type_ref.name().as_str() != "struct")) } + /// Verifies if the given constraints contain a `valid_values` constraint with only symbol values. + fn contains_enum_constraints(constraints: &[IslConstraint]) -> bool { + constraints.iter().any(|it| { + if let IslConstraintValue::ValidValues(valid_values) = it.constraint() { + valid_values + .values() + .iter() + .all(|val| matches!(val, ValidValue::Element(Value::Symbol(_)))) + } else { + false + } + }) + } + fn render_generated_code( &mut self, type_name: &str, @@ -697,6 +719,89 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(AbstractDataType::Structure(structure_builder.build()?)) } + /// Builds `AbstractDataType::Enum` from the given constraints. + /// e.g. for a given type definition as below: + /// ``` + /// type::{ + /// name: Foo, + /// type: symbol, + /// valid_values: [foo, bar, baz] + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::Enum( + /// Enum { + /// name: vec!["org", "example", "Foo"], // assuming the namespace is `org.example` + /// variants: HashSet::from_iter( + /// vec![ + /// "foo", + /// "bar", + /// "baz" + /// ].iter()) // Represents enum variants + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType {name: "foo", .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + fn build_enum_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut enum_builder = EnumBuilder::default(); + enum_builder + .name(self.current_type_fully_qualified_name.to_owned()) + .source(parent_isl_type.to_owned()); + let mut found_base_type = false; + + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::ValidValues(valid_values_constraint) => { + let valid_values = valid_values_constraint + .values() + .iter() + .map(|v| match v { + ValidValue::Element(Value::Symbol(symbol_val) ) => { + symbol_val.text().map(|s| s.to_string()).ok_or(invalid_abstract_data_type_raw_error( + "Could not determine enum variant name", + )) + } + _ => invalid_abstract_data_type_error( + "Only `valid_values` constraint with values of type `symbol` are supported yet!" + ), + }) + .collect::>>()?; + enum_builder.variants(BTreeSet::from_iter(valid_values)); + } + IslConstraintValue::Type(isl_type_ref) => { + if isl_type_ref.name() != "symbol" { + return invalid_abstract_data_type_error( + "Only `valid_values` constraint with values of type `symbol` are supported yet!" + ); + } + + let _type_name = self.handle_duplicate_constraint( + found_base_type, + "type", + isl_type_ref, + FieldPresence::Required, + code_gen_context, + )?; + found_base_type = true; + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ) + } + } + } + + Ok(AbstractDataType::Enum(enum_builder.build()?)) + } + /// Builds `AbstractDataType::WrappedScalar` from the given constraints. /// ``` /// type::{ diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index 1d0d99ba..e218240c 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -1,6 +1,6 @@ use derive_builder::Builder; use ion_schema::isl::isl_type::IslType; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fmt::Debug; // This module contains a data model that the code generator can use to render a template based on the type of the model. // Currently, this same data model is represented by `AbstractDataType` but it doesn't hold all the information for the template. @@ -182,6 +182,8 @@ pub enum AbstractDataType { WrappedSequence(WrappedSequence), // A collection of field name/value pairs (e.g. a map) Structure(Structure), + // Represents an enum type + Enum(Enum), } impl AbstractDataType { @@ -203,6 +205,9 @@ impl AbstractDataType { AbstractDataType::Structure(Structure { doc_comment, .. }) => { doc_comment.as_ref().map(|s| s.as_str()) } + AbstractDataType::Enum(Enum { doc_comment, .. }) => { + doc_comment.as_ref().map(|s| s.as_str()) + } } } @@ -219,6 +224,7 @@ impl AbstractDataType { Some(L::target_type_as_sequence(seq.element_type.to_owned())) } AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()), + AbstractDataType::Enum(enum_type) => Some(enum_type.name.to_owned().into()), } } } @@ -448,6 +454,41 @@ pub struct FieldReference( pub(crate) FieldPresence, ); +/// Represents an enum type +/// e.g. Given below ISL, +/// ``` +/// type::{ +/// name: enum_type, +/// valid_values: [foo, bar, baz] +/// } +/// ``` +/// Corresponding generated code in Rust would look like following: +/// ``` +/// enum EnumType { +/// Foo, +/// Bar, +/// Baz +/// } +/// ``` +#[allow(dead_code)] +#[derive(Debug, Clone, Builder, PartialEq, Serialize)] +#[builder(setter(into))] +pub struct Enum { + // Represents the fully qualified name for this data model + pub(crate) name: FullyQualifiedTypeName, + // The variants of this enum + variants: BTreeSet, + // Represents doc comment for the generated code + #[builder(default)] + doc_comment: Option, + // Represents the source ISL type which can be used to get other constraints useful for this type. + // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. + // This will also be useful for `text` type to verify if this is a `string` or `symbol`. + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] + source: IslType, +} + #[cfg(test)] mod model_tests { use super::*; diff --git a/src/bin/ion/commands/generate/result.rs b/src/bin/ion/commands/generate/result.rs index 8c6dcd6c..d3ba0826 100644 --- a/src/bin/ion/commands/generate/result.rs +++ b/src/bin/ion/commands/generate/result.rs @@ -1,6 +1,6 @@ use crate::commands::generate::model::{ - ScalarBuilderError, SequenceBuilderError, StructureBuilderError, WrappedScalarBuilderError, - WrappedSequenceBuilderError, + EnumBuilderError, ScalarBuilderError, SequenceBuilderError, StructureBuilderError, + WrappedScalarBuilderError, WrappedSequenceBuilderError, }; use ion_schema::result::IonSchemaError; use thiserror::Error; @@ -87,3 +87,11 @@ impl From for CodeGenError { } } } + +impl From for CodeGenError { + fn from(value: EnumBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} diff --git a/src/bin/ion/commands/generate/templates/java/enum.templ b/src/bin/ion/commands/generate/templates/java/enum.templ new file mode 100644 index 00000000..c3ea2119 --- /dev/null +++ b/src/bin/ion/commands/generate/templates/java/enum.templ @@ -0,0 +1,57 @@ +{% set full_namespace = namespace | join(sep=".") %} +{% if is_nested == false %} +package {{ full_namespace }}; +import com.amazon.ion.IonReader; +import com.amazon.ion.IonException; +import com.amazon.ion.IonWriter; +import com.amazon.ion.IonType; +import java.io.IOException; +{% endif %} + +{# Verify that the abstract data type is a enum and store information for this enum #} +{% set enum_info = model.code_gen_type["Enum"] %} + +public {% if is_nested == true %} static {% endif %} enum {{ model.name }} { + {% for variant in enum_info["variants"] -%} + {{ variant | snake | upper }}("{{variant}}"), + {% endfor %}; + + private String textValue; + + {{model.name}}(String textValue) { + this.textValue = textValue; + } + + /** + * Writes a {{ model.name }} as Ion from an {@link IonWriter}. + * + * This method does not close the writer after writing is complete. + * The caller is responsible for closing the stream associated with the writer. + */ + public void writeTo(IonWriter writer) throws IOException { + writer.writeSymbol(this.textValue); + } + + /** + * Reads a {{ model.name }} from an {@link IonReader}. + * + * This method does not advance the reader at the current level. + * The caller is responsible for positioning the reader on the value to read. + */ + public static {{ model.name }} readFrom(IonReader reader) { + {# Enums are only supported for symbol types #} + if (reader.getType() != IonType.SYMBOL) { + throw new IonException("Expected symbol, found " + reader.getType() + " while reading {{ model.name }}"); + } + {# Reads given value as a string #} + String value = reader.stringValue(); + switch(value) { + {% for variant in enum_info["variants"] %} + case "{{ variant }}": + return {{ variant | snake | upper }}; + {% endfor %} + default: + throw new IonException(value + "is not a valid value for {{ model.name }}"); + } + } +} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/java/nested_type.templ b/src/bin/ion/commands/generate/templates/java/nested_type.templ index be4c0b26..644f0d28 100644 --- a/src/bin/ion/commands/generate/templates/java/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/java/nested_type.templ @@ -4,5 +4,7 @@ {% macro nested_type(model, is_nested) -%} {% if model.code_gen_type is containing("Structure")%} {% include "class.templ" %} + {% elif model.code_gen_type is containing("Enum")%} + {% include "enum.templ" %} {% endif %} {% endmacro nested_type -%} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/mod.rs b/src/bin/ion/commands/generate/templates/mod.rs index ff60f9aa..22edcac6 100644 --- a/src/bin/ion/commands/generate/templates/mod.rs +++ b/src/bin/ion/commands/generate/templates/mod.rs @@ -17,6 +17,7 @@ pub(crate) mod java { pub(crate) const CLASS: &str = include_template!("java/class.templ"); pub(crate) const SCALAR: &str = include_template!("java/scalar.templ"); pub(crate) const SEQUENCE: &str = include_template!("java/sequence.templ"); + pub(crate) const ENUM: &str = include_template!("java/enum.templ"); pub(crate) const UTIL_MACROS: &str = include_template!("java/util_macros.templ"); pub(crate) const NESTED_TYPE: &str = include_template!("java/nested_type.templ"); } @@ -26,6 +27,7 @@ pub(crate) mod rust { pub(crate) const STRUCT: &str = include_template!("rust/struct.templ"); pub(crate) const SCALAR: &str = include_template!("rust/scalar.templ"); pub(crate) const SEQUENCE: &str = include_template!("rust/sequence.templ"); + pub(crate) const ENUM: &str = include_template!("rust/enum.templ"); pub(crate) const UTIL_MACROS: &str = include_template!("rust/util_macros.templ"); pub(crate) const RESULT: &str = include_template!("rust/result.templ"); pub(crate) const NESTED_TYPE: &str = include_template!("rust/nested_type.templ"); diff --git a/src/bin/ion/commands/generate/templates/rust/enum.templ b/src/bin/ion/commands/generate/templates/rust/enum.templ new file mode 100644 index 00000000..95ded38d --- /dev/null +++ b/src/bin/ion/commands/generate/templates/rust/enum.templ @@ -0,0 +1,22 @@ +// Enum support is not yet completed for Rust code generation +// This template is just used as placeholder for enums. + + +use {{ model.name | snake }}::{{ model.name }}; +pub mod {{ model.name | snake }} { + use super::*; + #[derive(Debug, Clone, Default)] + pub enum {{ model.name }} { + #[default] + Unit // This is just a placeholder variant for enum generation + } + impl {{ model.name }} { + pub fn read_from(reader: &mut Reader) -> SerdeResult { + todo!("Enums are not supported with code generation yet!") + } + + pub fn write_to(&self, writer: &mut W) -> SerdeResult<()> { + todo!("Enums are not supported with code generation yet!") + } + } +} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/rust/nested_type.templ b/src/bin/ion/commands/generate/templates/rust/nested_type.templ index ec991a88..ddaeb497 100644 --- a/src/bin/ion/commands/generate/templates/rust/nested_type.templ +++ b/src/bin/ion/commands/generate/templates/rust/nested_type.templ @@ -2,7 +2,9 @@ {# following macro defines an anonymous type as children class for its parent type definition #} {% macro nested_type(model, is_nested) -%} - {% if model.code_gen_type is containing("Structure")%} + {% if model.code_gen_type is containing("Structure")%} {% include "struct.templ" %} + {% elif model.code_gen_type is containing("Enum") %} + {% include "enum.templ" %} {% endif %} {% endmacro nested_type -%} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/utils.rs b/src/bin/ion/commands/generate/utils.rs index 897418e2..b5038880 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -71,6 +71,9 @@ pub trait Language { /// i.e. given namespace path as `foo::Foo`, it will first remove `Foo` and then add the current type as `foo::nested_type::NestedType`. fn add_type_to_namespace(is_nested_type: bool, type_name: &str, namespace: &mut Vec); + /// Resets the namespace when code generation is complete for a single ISL type + fn reset_namespace(namespace: &mut Vec); + /// Returns the `FullyQualifiedReference` that represents the target type as optional in the given programming language /// e.g. In Java, it will return "java.util.Optional" /// In Rust, it will return "Option" @@ -154,6 +157,7 @@ impl Language for JavaLanguage { Template::Struct => "class".to_string(), Template::Scalar => "scalar".to_string(), Template::Sequence => "sequence".to_string(), + Template::Enum => "enum".to_string(), } } @@ -165,6 +169,11 @@ impl Language for JavaLanguage { namespace.push(type_name.to_case(Case::UpperCamel)) } + fn reset_namespace(namespace: &mut Vec) { + // resets the namespace by removing current abstract dta type name + namespace.pop(); + } + fn target_type_as_optional( target_type: FullyQualifiedTypeReference, ) -> FullyQualifiedTypeReference { @@ -283,6 +292,11 @@ impl Language for RustLanguage { Template::Struct => "struct".to_string(), Template::Scalar => "scalar".to_string(), Template::Sequence => "sequence".to_string(), + Template::Enum => { + //TODO: Rust enums are not supported yet + // The template `enum.templ` is just a placeholder + "enum".to_string() + } } } @@ -314,6 +328,12 @@ impl Language for RustLanguage { namespace.push(type_name.to_case(Case::UpperCamel)) // Add this type itself to the namespace path } + fn reset_namespace(namespace: &mut Vec) { + // Resets the namespace by removing current abstract data type name and module name + namespace.pop(); + namespace.pop(); + } + fn target_type_as_optional( target_type: FullyQualifiedTypeReference, ) -> FullyQualifiedTypeReference { @@ -342,6 +362,7 @@ pub enum Template { Struct, // Represents a template for a Rust struct or Java class with Ion struct value Sequence, // Represents a template for a Rust struct or Java class with Ion sequence value Scalar, // Represents a template for a Rust struct or Java class with Ion scalar value + Enum, // Represents a template for a Rust or Java enum } impl TryFrom<&DataModelNode> for Template { @@ -357,6 +378,7 @@ impl TryFrom<&DataModelNode> for Template { Ok(Template::Sequence) } AbstractDataType::Structure(_) => Ok(Template::Struct), + AbstractDataType::Enum(_) => Ok(Template::Enum), } } else { invalid_abstract_data_type_error( From e521e179712fa9baad2e9f84bee2f78529e35694 Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:52:33 -0700 Subject: [PATCH 10/12] Adds namespace fix for nested sequence and adds support for nested types in Sequence and Scalar ADT (#163) * Modified generator to fix bug for nested sequence namespace * Modified `fully_qualified_type_ref` method to return `FullyQualifiedTypeReference` Previously this returned `Option` * Modified templates for nested types in sequence * Adds tests for nested enums * Adds changes for using enum in namespace * Adds `NamespaceNode` for `FullyQualifiedName` --- Cargo.lock | 12 +- Cargo.toml | 1 + .../invalid_value.ion | 1 + .../valid_value.ion | 1 + .../test/java/org/example/CodeGenTest.java | 11 ++ .../schema/sequence_with_enum_element.isl | 5 + src/bin/ion/commands/generate/generator.rs | 147 ++++++++++-------- src/bin/ion/commands/generate/mod.rs | 5 +- src/bin/ion/commands/generate/model.rs | 118 ++++++++++---- .../generate/templates/java/class.templ | 5 +- .../generate/templates/java/enum.templ | 2 +- .../generate/templates/java/scalar.templ | 4 +- .../generate/templates/java/sequence.templ | 8 +- .../generate/templates/java/util_macros.templ | 2 +- .../generate/templates/rust/scalar.templ | 2 + .../generate/templates/rust/sequence.templ | 10 +- .../generate/templates/rust/struct.templ | 3 +- src/bin/ion/commands/generate/utils.rs | 83 ++++++---- 18 files changed, 287 insertions(+), 133 deletions(-) create mode 100644 code-gen-projects/input/bad/sequence_with_enum_element/invalid_value.ion create mode 100644 code-gen-projects/input/good/sequence_with_enum_element/valid_value.ion create mode 100644 code-gen-projects/schema/sequence_with_enum_element.isl diff --git a/Cargo.lock b/Cargo.lock index 63c045b6..085ac291 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -928,6 +928,7 @@ dependencies = [ "infer", "ion-rs", "ion-schema", + "itertools 0.13.0", "lowcharts", "matches", "pager", @@ -995,6 +996,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1328,7 +1338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", - "itertools", + "itertools 0.10.5", "predicates-core", ] diff --git a/Cargo.toml b/Cargo.toml index 63b859cf..d8abd91f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ thiserror = "1.0.50" zstd = "0.13.0" termcolor = "1.4.1" derive_builder = "0.20.0" +itertools = "0.13.0" [target.'cfg(not(target_os = "windows"))'.dependencies] pager = "0.16.1" diff --git a/code-gen-projects/input/bad/sequence_with_enum_element/invalid_value.ion b/code-gen-projects/input/bad/sequence_with_enum_element/invalid_value.ion new file mode 100644 index 00000000..c336718a --- /dev/null +++ b/code-gen-projects/input/bad/sequence_with_enum_element/invalid_value.ion @@ -0,0 +1 @@ +[foobar] // expected values are either foo , bar or baz, found foobar. \ No newline at end of file diff --git a/code-gen-projects/input/good/sequence_with_enum_element/valid_value.ion b/code-gen-projects/input/good/sequence_with_enum_element/valid_value.ion new file mode 100644 index 00000000..3470110c --- /dev/null +++ b/code-gen-projects/input/good/sequence_with_enum_element/valid_value.ion @@ -0,0 +1 @@ +[foo, bar, baz] \ No newline at end of file diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 318403a0..5511b3cf 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -137,6 +137,12 @@ void roundtripBadTestForEnumType() throws IOException { runRoundtripBadTest("/bad/enum_type", EnumType::readFrom); } + + @Test + void roundtripBadTestForSequenceWithEnumElement() throws IOException { + runRoundtripBadTest("/bad/sequence_with_enum_element", SequenceWithEnumElement::readFrom); + } + private void runRoundtripBadTest(String path, ReaderFunction readerFunction) throws IOException { File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); @@ -182,6 +188,11 @@ void roundtripGoodTestForEnumType() throws IOException { runRoundtripGoodTest("/good/enum_type", EnumType::readFrom, (item, writer) -> item.writeTo(writer)); } + @Test + void roundtripGoodTestForSequenceWithEnumElement() throws IOException { + runRoundtripGoodTest("/good/sequence_with_enum_element", SequenceWithEnumElement::readFrom, (item, writer) -> item.writeTo(writer)); + } + private void runRoundtripGoodTest(String path, ReaderFunction readerFunction, WriterFunction writerFunction) throws IOException { File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); diff --git a/code-gen-projects/schema/sequence_with_enum_element.isl b/code-gen-projects/schema/sequence_with_enum_element.isl new file mode 100644 index 00000000..bf095236 --- /dev/null +++ b/code-gen-projects/schema/sequence_with_enum_element.isl @@ -0,0 +1,5 @@ +type::{ + name: sequence_with_enum_element, + type: list, + element: { valid_values: [foo, bar, baz] } +} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 7ab73883..92b4b2bf 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,7 +1,7 @@ use crate::commands::generate::context::{CodeGenContext, SequenceType}; use crate::commands::generate::model::{ AbstractDataType, DataModelNode, EnumBuilder, FieldPresence, FieldReference, - FullyQualifiedTypeReference, ScalarBuilder, SequenceBuilder, StructureBuilder, + FullyQualifiedTypeReference, NamespaceNode, ScalarBuilder, SequenceBuilder, StructureBuilder, WrappedScalarBuilder, WrappedSequenceBuilder, }; use crate::commands::generate::result::{ @@ -32,7 +32,7 @@ pub(crate) struct CodeGenerator<'a, L: Language> { pub(crate) tera: Tera, output: &'a Path, // This field is used by Java code generation to get the namespace for generated code. - current_type_fully_qualified_name: Vec, + current_type_fully_qualified_name: Vec, // Represents a counter for naming nested type definitions pub(crate) nested_type_counter: usize, pub(crate) data_model_store: HashMap, @@ -85,7 +85,7 @@ impl<'a> CodeGenerator<'a, RustLanguage> { } impl<'a> CodeGenerator<'a, JavaLanguage> { - pub fn new(output: &'a Path, namespace: Vec) -> CodeGenerator<'a, JavaLanguage> { + pub fn new(output: &'a Path, namespace: Vec) -> CodeGenerator<'a, JavaLanguage> { let mut tera = Tera::default(); // Add all templates using `java_templates` module constants // This allows packaging binary without the need of template resources. @@ -331,9 +331,6 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // unwrap here is safe because all the top-level type definition always has a name let isl_type_name = isl_type.name().unwrap().to_string(); self.generate_abstract_data_type(&isl_type_name, isl_type)?; - // Since the fully qualified name of this generator represents the current fully qualified name, - // remove it before generating code for the next ISL type. - L::reset_namespace(&mut self.current_type_fully_qualified_name); } Ok(()) @@ -353,7 +350,6 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { let mut code_gen_context = CodeGenContext::new(); let mut data_model_node = self.convert_isl_type_def_to_data_model_node( type_name, - field_presence, isl_type, &mut code_gen_context, true, @@ -364,8 +360,13 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { .nested_types .push(data_model_node.to_owned()); - // pop out the nested type name from the fully qualified namespace as it has been already added to the type store and to nested types - self.current_type_fully_qualified_name.pop(); + // since nested sequence does not create a separate class, all its nested types should also be added to parent code gen context + if data_model_node.is_sequence() { + parent_code_gen_context + .nested_types + .extend_from_slice(&data_model_node.nested_types); + } + match field_presence { FieldPresence::Optional => Ok(L::target_type_as_optional( data_model_node.fully_qualified_type_ref::().ok_or( @@ -392,7 +393,6 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { let data_model_node = self.convert_isl_type_def_to_data_model_node( isl_type_name, - FieldPresence::Required, // Sets `field_presence` as `Required`, as the top level type definition can not be `Optional`. isl_type, &mut code_gen_context, false, @@ -409,7 +409,15 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { ); context.insert("model", &data_model_node); - self.render_generated_code(isl_type_name, &mut context, &data_model_node) + self.render_generated_code( + isl_type_name, + &mut context, + &data_model_node, + data_model_node + .fully_qualified_type_name() + .unwrap() + .as_slice(), + ) } /// _Note: `field_presence` is only used for variably occurring type references and currently that is only supported with `fields` constraint. @@ -417,7 +425,6 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { fn convert_isl_type_def_to_data_model_node( &mut self, isl_type_name: &String, - field_presence: FieldPresence, isl_type: &IslType, code_gen_context: &mut CodeGenContext, is_nested_type: bool, @@ -482,22 +489,19 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { // TODO: verify the `occurs` value within a field, by default the fields are optional. // add current data model node into the data model store // verify if the field presence was provided as optional and set the type reference name as optional. - let type_name = match field_presence { - FieldPresence::Optional => abstract_data_type.fully_qualified_type_ref::().ok_or( - invalid_abstract_data_type_raw_error( - "Can not determine fully qualified name for the data model", - ), - )?, - FieldPresence::Required => abstract_data_type.fully_qualified_type_ref::().ok_or( - invalid_abstract_data_type_raw_error( - "Can not determine fully qualified name for the data model", - ), - )?, - }; + let type_name = abstract_data_type.fully_qualified_type_ref::(); self.data_model_store .insert(type_name, data_model_node.to_owned()); + // pop out the nested type name from the fully qualified namespace as it has been already added to the type store and to nested types + // For sequence type, it would already have popped out the nested type name. + if !data_model_node.is_sequence() { + // Since the fully qualified name of this generator represents the current fully qualified name, + // remove it before generating code for the next ISL type. + L::reset_namespace(&mut self.current_type_fully_qualified_name); + } + Ok(data_model_node) } @@ -527,12 +531,19 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { type_name: &str, context: &mut Context, data_model_node: &DataModelNode, + fully_qualified_name: &[NamespaceNode], ) -> CodeGenResult<()> { // Add namespace to tera context let mut import_context = Context::new(); - let namespace_ref = self.current_type_fully_qualified_name.as_slice(); - context.insert("namespace", &namespace_ref[0..namespace_ref.len() - 1]); - import_context.insert("namespace", &namespace_ref[0..namespace_ref.len() - 1]); + + context.insert( + "namespace", + &fully_qualified_name[0..fully_qualified_name.len() - 1], + ); + import_context.insert( + "namespace", + &fully_qualified_name[0..fully_qualified_name.len() - 1], + ); // Render or generate file for the template with the given context let template: &Template = &data_model_node.try_into()?; @@ -580,7 +591,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { L::target_type(&schema_type) .as_ref() .map(|type_name| FullyQualifiedTypeReference { - type_name: vec![type_name.to_string()], + type_name: vec![NamespaceNode::Type(type_name.to_string())], parameters: vec![], }) .map(|t| { @@ -838,6 +849,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { for constraint in constraints { match constraint.constraint() { IslConstraintValue::Type(isl_type) => { + // Nested/Anonymous types are not allowed within wrapped scalar models + if matches!(isl_type, IslTypeRef::Anonymous(_, _)) { + return invalid_abstract_data_type_error( + "Nested types are not supported within wrapped scalar types(i.e. within top level ISL type definition's `type` constraint)", + ); + } let type_name = self.handle_duplicate_constraint( found_base_type, "type", @@ -1016,6 +1033,10 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { parent_isl_type: &IslType, ) -> CodeGenResult { let mut sequence_builder = SequenceBuilder::default(); + // For nested sequence type remove the anonymous type name from current fully qualified name + // Nested sequence does not create a separate class, so the anonymous type name shouldn't be used for the fully qualified type name. + L::reset_namespace(&mut self.current_type_fully_qualified_name); + sequence_builder.source(parent_isl_type.to_owned()); for constraint in constraints { match constraint.constraint() { @@ -1081,25 +1102,25 @@ mod isl_to_model_tests { // Initialize code generator for Java let mut java_code_generator = CodeGenerator::::new( Path::new("./"), - vec!["org".to_string(), "example".to_string()], + vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + ], ); let data_model_node = java_code_generator.convert_isl_type_def_to_data_model_node( &"my_struct".to_string(), - FieldPresence::Required, &isl_type, &mut CodeGenContext::new(), false, )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( - abstract_data_type - .fully_qualified_type_ref::() - .unwrap(), + abstract_data_type.fully_qualified_type_ref::(), FullyQualifiedTypeReference { type_name: vec![ - "org".to_string(), - "example".to_string(), - "MyStruct".to_string() + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyStruct".to_string()) ], parameters: vec![] } @@ -1109,9 +1130,9 @@ mod isl_to_model_tests { assert_eq!( structure.name, vec![ - "org".to_string(), - "example".to_string(), - "MyStruct".to_string() + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyStruct".to_string()) ] ); assert!(!structure.is_closed); @@ -1123,7 +1144,7 @@ mod isl_to_model_tests { "foo".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![] }, FieldPresence::Optional @@ -1133,7 +1154,7 @@ mod isl_to_model_tests { "bar".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["Integer".to_string()], + type_name: vec![NamespaceNode::Type("Integer".to_string())], parameters: vec![] }, FieldPresence::Optional @@ -1170,25 +1191,25 @@ mod isl_to_model_tests { // Initialize code generator for Java let mut java_code_generator = CodeGenerator::::new( Path::new("./"), - vec!["org".to_string(), "example".to_string()], + vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + ], ); let data_model_node = java_code_generator.convert_isl_type_def_to_data_model_node( &"my_nested_struct".to_string(), - FieldPresence::Required, &isl_type, &mut CodeGenContext::new(), false, )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( - abstract_data_type - .fully_qualified_type_ref::() - .unwrap(), + abstract_data_type.fully_qualified_type_ref::(), FullyQualifiedTypeReference { type_name: vec![ - "org".to_string(), - "example".to_string(), - "MyNestedStruct".to_string() + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()) ], parameters: vec![] } @@ -1198,9 +1219,9 @@ mod isl_to_model_tests { assert_eq!( structure.name, vec![ - "org".to_string(), - "example".to_string(), - "MyNestedStruct".to_string() + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()) ] ); assert!(!structure.is_closed); @@ -1213,10 +1234,10 @@ mod isl_to_model_tests { FieldReference( FullyQualifiedTypeReference { type_name: vec![ - "org".to_string(), - "example".to_string(), - "MyNestedStruct".to_string(), - "NestedType1".to_string() + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()), + NamespaceNode::Type("NestedType1".to_string()) ], parameters: vec![] }, @@ -1227,7 +1248,7 @@ mod isl_to_model_tests { "bar".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["Integer".to_string()], + type_name: vec![NamespaceNode::Type("Integer".to_string())], parameters: vec![] }, FieldPresence::Optional @@ -1242,15 +1263,15 @@ mod isl_to_model_tests { .as_ref() .unwrap() .fully_qualified_type_ref::(), - Some(FullyQualifiedTypeReference { + FullyQualifiedTypeReference { type_name: vec![ - "org".to_string(), - "example".to_string(), - "MyNestedStruct".to_string(), - "NestedType1".to_string() + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()), + NamespaceNode::Type("NestedType1".to_string()) ], parameters: vec![] - }) + } ); } Ok(()) diff --git a/src/bin/ion/commands/generate/mod.rs b/src/bin/ion/commands/generate/mod.rs index b234e57e..b801685b 100644 --- a/src/bin/ion/commands/generate/mod.rs +++ b/src/bin/ion/commands/generate/mod.rs @@ -7,6 +7,7 @@ mod utils; mod model; use crate::commands::generate::generator::CodeGenerator; +use crate::commands::generate::model::NamespaceNode; use crate::commands::generate::utils::{JavaLanguage, RustLanguage}; use crate::commands::IonCliCommand; use anyhow::{bail, Result}; @@ -124,7 +125,7 @@ impl IonCliCommand for GenerateCommand { match language { "java" => { Self::print_java_code_gen_warnings(); - CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()) + CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect()) .generate_code_for_authorities(&authorities, &mut schema_system)? }, "rust" => { @@ -143,7 +144,7 @@ impl IonCliCommand for GenerateCommand { match language { "java" => { Self::print_java_code_gen_warnings(); - CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()).generate_code_for_schema(&mut schema_system, schema_id)? + CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect()).generate_code_for_schema(&mut schema_system, schema_id)? }, "rust" => { Self::print_rust_code_gen_warnings(); diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index e218240c..a863efe3 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -1,5 +1,6 @@ use derive_builder::Builder; use ion_schema::isl::isl_type::IslType; +use itertools::Itertools; use std::collections::{BTreeSet, HashMap}; use std::fmt::Debug; // This module contains a data model that the code generator can use to render a template based on the type of the model. @@ -76,7 +77,13 @@ impl DataModelNode { pub fn fully_qualified_type_ref(&mut self) -> Option { self.code_gen_type .as_ref() - .and_then(|t| t.fully_qualified_type_ref::()) + .map(|t| t.fully_qualified_type_ref::()) + } + + pub fn fully_qualified_type_name(&self) -> Option { + self.code_gen_type + .as_ref() + .and_then(|t| t.fully_qualified_type_name()) } } @@ -84,7 +91,23 @@ impl DataModelNode { /// e.g. For a `Foo` class in `org.example` namespace /// In Java, `org.example.Foo` /// In Rust, `org::example::Foo` -type FullyQualifiedTypeName = Vec; +type FullyQualifiedTypeName = Vec; + +/// Represents a node in the fully qualified namespace path +#[derive(Debug, Clone, PartialEq, Serialize, Hash, Eq)] +pub enum NamespaceNode { + Package(String), // represents a package or module name + Type(String), // represents a class, struct or enum type name +} + +impl NamespaceNode { + pub fn name(&self) -> &String { + match self { + NamespaceNode::Package(name) => name, + NamespaceNode::Type(name) => name, + } + } +} /// Represents a fully qualified type name for a type reference #[derive(Debug, Clone, PartialEq, Serialize, Hash, Eq)] @@ -122,7 +145,21 @@ impl TryFrom<&Value> for FullyQualifiedTypeReference { .as_array() .unwrap() .iter() - .map(|s| s.as_str().unwrap().to_string()) + .map(|s| { + let namespace_node = s.as_object().unwrap(); + if let Some(package_name) = namespace_node.get("Package") { + NamespaceNode::Package(package_name.as_str().unwrap().to_string()) + } else { + NamespaceNode::Type( + namespace_node + .get("Type") + .unwrap() + .as_str() + .unwrap() + .to_string(), + ) + } + }) .collect(); } else { let parameters_result: Result, tera::Error> = @@ -151,7 +188,11 @@ impl FullyQualifiedTypeReference { /// Provides string representation of this `FullyQualifiedTypeReference` pub fn string_representation(&self) -> String { if self.parameters.is_empty() { - return self.type_name.join(L::namespace_separator()).to_string(); + return self + .type_name + .iter() + .map(|n| n.name()) + .join(L::namespace_separator()); } let parameters = self .parameters @@ -161,7 +202,10 @@ impl FullyQualifiedTypeReference { .join(", "); format!( "{}<{}>", - self.type_name.join(L::namespace_separator()), + self.type_name + .iter() + .map(|n| n.name()) + .join(L::namespace_separator()), parameters ) } @@ -211,20 +255,30 @@ impl AbstractDataType { } } - pub fn fully_qualified_type_ref(&self) -> Option { + pub fn fully_qualified_type_ref(&self) -> FullyQualifiedTypeReference { match self { - AbstractDataType::WrappedScalar(w) => { - Some(w.fully_qualified_type_name().to_owned().into()) - } - AbstractDataType::Scalar(s) => Some(s.base_type.to_owned()), + AbstractDataType::WrappedScalar(w) => w.fully_qualified_type_name().to_owned().into(), + AbstractDataType::Scalar(s) => s.base_type.to_owned(), AbstractDataType::Sequence(seq) => { - Some(L::target_type_as_sequence(seq.element_type.to_owned())) + L::target_type_as_sequence(seq.element_type.to_owned()) } AbstractDataType::WrappedSequence(seq) => { - Some(L::target_type_as_sequence(seq.element_type.to_owned())) + L::target_type_as_sequence(seq.element_type.to_owned()) } - AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()), - AbstractDataType::Enum(enum_type) => Some(enum_type.name.to_owned().into()), + AbstractDataType::Structure(structure) => structure.name.to_owned().into(), + AbstractDataType::Enum(enum_type) => enum_type.name.to_owned().into(), + } + } + + pub fn fully_qualified_type_name(&self) -> Option { + // nested types would return None + match self { + AbstractDataType::WrappedScalar(w) => Some(w.fully_qualified_type_name().to_owned()), + AbstractDataType::Scalar(_) => None, + AbstractDataType::Sequence(_) => None, + AbstractDataType::WrappedSequence(seq) => Some(seq.name.to_owned()), + AbstractDataType::Structure(structure) => Some(structure.name.to_owned()), + AbstractDataType::Enum(enum_type) => Some(enum_type.name.to_owned()), } } } @@ -501,7 +555,7 @@ mod model_tests { fn scalar_builder_test() { let expected_scalar = Scalar { base_type: FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, doc_comment: Some("This is scalar type".to_string()), @@ -512,7 +566,7 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .base_type(vec!["String".to_string()]) + .base_type(vec![NamespaceNode::Type("String".to_string())]) .doc_comment(Some("This is scalar type".to_string())) .source(anonymous_type(vec![type_constraint(named_type_ref( "string", @@ -525,9 +579,9 @@ mod model_tests { #[test] fn wrapped_scalar_builder_test() { let expected_scalar = WrappedScalar { - name: vec!["Foo".to_string()], + name: vec![NamespaceNode::Type("Foo".to_string())], base_type: FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, doc_comment: Some("This is scalar type".to_string()), @@ -538,9 +592,9 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .name(vec!["Foo".to_string()]) + .name(vec![NamespaceNode::Type("Foo".to_string())]) .base_type(FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }) .doc_comment(Some("This is scalar type".to_string())) @@ -557,7 +611,7 @@ mod model_tests { let expected_seq = Sequence { doc_comment: Some("This is sequence type of strings".to_string()), element_type: FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, sequence_type: SequenceType::List, @@ -583,7 +637,7 @@ mod model_tests { // sets the `element_type` for the sequence seq_builder.element_type(FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }); @@ -594,7 +648,11 @@ mod model_tests { #[test] fn struct_builder_test() { let expected_struct = Structure { - name: vec!["org".to_string(), "example".to_string(), "Foo".to_string()], + name: vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("Foo".to_string()), + ], doc_comment: Some("This is a structure".to_string()), is_closed: false, fields: HashMap::from_iter(vec![ @@ -602,7 +660,7 @@ mod model_tests { "foo".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, FieldPresence::Required, @@ -612,7 +670,7 @@ mod model_tests { "bar".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["int".to_string()], + type_name: vec![NamespaceNode::Type("int".to_string())], parameters: vec![], }, FieldPresence::Required, @@ -649,9 +707,9 @@ mod model_tests { // sets all the information about the structure struct_builder .name(vec![ - "org".to_string(), - "example".to_string(), - "Foo".to_string(), + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("Foo".to_string()), ]) .doc_comment(Some("This is a structure".to_string())) .is_closed(false) @@ -660,7 +718,7 @@ mod model_tests { "foo".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, FieldPresence::Required, @@ -670,7 +728,7 @@ mod model_tests { "bar".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["int".to_string()], + type_name: vec![NamespaceNode::Type("int".to_string())], parameters: vec![], }, FieldPresence::Required, diff --git a/src/bin/ion/commands/generate/templates/java/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index f0663f73..28698f7b 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -5,7 +5,7 @@ {% macro class(model, is_nested) %} {% if is_nested == false %} -{% set full_namespace = namespace | join(sep=".") %} +{% set full_namespace = namespace | map(attribute="Package") | join(sep=".") %} package {{ full_namespace }}; import com.amazon.ion.IonReader; @@ -147,8 +147,7 @@ import java.io.IOException; } {% for inline_type in model.nested_types -%} - {% set is_nested = true %} - {{ macros::nested_type(model=inline_type, is_nested=is_nested) }} + {{ macros::nested_type(model=inline_type, is_nested=true) }} {% endfor -%} } {% endmacro model %} diff --git a/src/bin/ion/commands/generate/templates/java/enum.templ b/src/bin/ion/commands/generate/templates/java/enum.templ index c3ea2119..15a21e04 100644 --- a/src/bin/ion/commands/generate/templates/java/enum.templ +++ b/src/bin/ion/commands/generate/templates/java/enum.templ @@ -1,4 +1,4 @@ -{% set full_namespace = namespace | join(sep=".") %} +{% set full_namespace = namespace | map(attribute="Package") | join(sep=".") %} {% if is_nested == false %} package {{ full_namespace }}; import com.amazon.ion.IonReader; diff --git a/src/bin/ion/commands/generate/templates/java/scalar.templ b/src/bin/ion/commands/generate/templates/java/scalar.templ index c71f5f52..98d0a408 100644 --- a/src/bin/ion/commands/generate/templates/java/scalar.templ +++ b/src/bin/ion/commands/generate/templates/java/scalar.templ @@ -1,5 +1,7 @@ +{% import "nested_type.templ" as macros %} + {% macro scalar(model) %} -{% set full_namespace = namespace | join(sep=".") %} +{% set full_namespace = namespace | map(attribute="Package") | join(sep=".") %} package {{ full_namespace }}; import com.amazon.ion.IonReader; diff --git a/src/bin/ion/commands/generate/templates/java/sequence.templ b/src/bin/ion/commands/generate/templates/java/sequence.templ index 15378305..b17bcf99 100644 --- a/src/bin/ion/commands/generate/templates/java/sequence.templ +++ b/src/bin/ion/commands/generate/templates/java/sequence.templ @@ -1,7 +1,9 @@ +{% import "nested_type.templ" as macros %} + {% macro sequence(model) %} {% if is_nested == false %} -{% set full_namespace = namespace | join(sep=".") %} +{% set full_namespace = namespace | map(attribute="Package") | join(sep=".") %} package {{ full_namespace }}; import com.amazon.ion.IonReader; @@ -78,6 +80,10 @@ class {{ model.name }} { } writer.stepOut(); } + + {% for inline_type in model.nested_types -%} + {{ macros::nested_type(model=inline_type, is_nested=true) }} + {% endfor -%} } {% endmacro %} {{ self::sequence(model=model) }} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/templates/java/util_macros.templ b/src/bin/ion/commands/generate/templates/java/util_macros.templ index 731f9671..f2eb7e2b 100644 --- a/src/bin/ion/commands/generate/templates/java/util_macros.templ +++ b/src/bin/ion/commands/generate/templates/java/util_macros.templ @@ -12,7 +12,7 @@ while (reader.hasNext()) { reader.next(); {% if field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name | is_built_in_type == false %} - {{ field_name | camel }}List.add({{ field_value_model.code_gen_type["Sequence"].element_type }}.readFrom(reader)); + {{ field_name | camel }}List.add({{ field_value_model.code_gen_type["Sequence"].element_type | fully_qualified_type_name }}.readFrom(reader)); {% elif field_value_model.code_gen_type["Sequence"].element_type == "bytes[]" %} {{ field_name | camel }}List.add(reader.newBytes()); {% else %} diff --git a/src/bin/ion/commands/generate/templates/rust/scalar.templ b/src/bin/ion/commands/generate/templates/rust/scalar.templ index c6c0b0ef..b25acd1c 100644 --- a/src/bin/ion/commands/generate/templates/rust/scalar.templ +++ b/src/bin/ion/commands/generate/templates/rust/scalar.templ @@ -1,3 +1,5 @@ +{% import "nested_type.templ" as macros %} + {# Verify that the abstract data type is a scalar type and store information for this scalar value #} {% set scalar_info = model.code_gen_type["WrappedScalar"] %} {% set base_type = scalar_info["base_type"] | fully_qualified_type_name %} diff --git a/src/bin/ion/commands/generate/templates/rust/sequence.templ b/src/bin/ion/commands/generate/templates/rust/sequence.templ index 4a4555d4..30382f95 100644 --- a/src/bin/ion/commands/generate/templates/rust/sequence.templ +++ b/src/bin/ion/commands/generate/templates/rust/sequence.templ @@ -1,5 +1,8 @@ +{% import "nested_type.templ" as macros %} + {% set sequence_info = model.code_gen_type["WrappedSequence"] %} + use {{ model.name | snake }}::{{ model.name }}; pub mod {{ model.name | snake }} { @@ -39,7 +42,7 @@ pub mod {{ model.name | snake }} { while reader.next()? != StreamItem::Nothing { {% if sequence_info["element_type"] | fully_qualified_type_name | is_built_in_type == false %} - values.push({{ sequence_info["element_type"] }}::read_from(reader)?); + values.push({{ sequence_info["element_type"] | fully_qualified_type_name }}::read_from(reader)?); {% else %} values.push(reader.read_{% if field.source is defined and field.source == "symbol" %}symbol()?.text().unwrap(){% else %}{{ sequence_info["element_type"] | fully_qualified_type_name | lower | replace(from="string", to ="str") }}()?{% endif %}{% if sequence_info["element_type"] | fully_qualified_type_name | lower== "string" %} .to_string() {% endif %}); {% endif %} @@ -63,4 +66,9 @@ pub mod {{ model.name | snake }} { Ok(()) } } + + + {% for inline_type in model.nested_types -%} + {{ macros::nested_type(model=inline_type, is_nested=true) }} + {% endfor -%} } diff --git a/src/bin/ion/commands/generate/templates/rust/struct.templ b/src/bin/ion/commands/generate/templates/rust/struct.templ index de14e79c..29e25dce 100644 --- a/src/bin/ion/commands/generate/templates/rust/struct.templ +++ b/src/bin/ion/commands/generate/templates/rust/struct.templ @@ -87,8 +87,7 @@ pub mod {{ model.name | snake }} { } {% for inline_type in model.nested_types -%} - {% set is_nested = true %} - {{ macros::nested_type(model=inline_type, is_nested=is_nested) }} + {{ macros::nested_type(model=inline_type, is_nested=true) }} {% endfor -%} } {% endmacro struct %} diff --git a/src/bin/ion/commands/generate/utils.rs b/src/bin/ion/commands/generate/utils.rs index b5038880..6961dad9 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -1,8 +1,9 @@ use crate::commands::generate::model::{ - AbstractDataType, DataModelNode, FullyQualifiedTypeReference, + AbstractDataType, DataModelNode, FullyQualifiedTypeReference, NamespaceNode, }; use crate::commands::generate::result::{invalid_abstract_data_type_error, CodeGenError}; use convert_case::{Case, Casing}; +use itertools::Itertools; use std::fmt::{Display, Formatter}; pub trait Language { @@ -69,10 +70,14 @@ pub trait Language { /// ``` /// To add `NestedType` into the namespace path, `is_nested_type` helps remove any prior types form the path and add this current type. /// i.e. given namespace path as `foo::Foo`, it will first remove `Foo` and then add the current type as `foo::nested_type::NestedType`. - fn add_type_to_namespace(is_nested_type: bool, type_name: &str, namespace: &mut Vec); + fn add_type_to_namespace( + is_nested_type: bool, + type_name: &str, + namespace: &mut Vec, + ); /// Resets the namespace when code generation is complete for a single ISL type - fn reset_namespace(namespace: &mut Vec); + fn reset_namespace(namespace: &mut Vec); /// Returns the `FullyQualifiedReference` that represents the target type as optional in the given programming language /// e.g. In Java, it will return "java.util.Optional" @@ -121,20 +126,20 @@ impl Language for JavaLanguage { ) { Some(wrapper_name) => FullyQualifiedTypeReference { type_name: vec![ - "java".to_string(), - "util".to_string(), - "ArrayList".to_string(), + NamespaceNode::Package("java".to_string()), + NamespaceNode::Package("util".to_string()), + NamespaceNode::Type("ArrayList".to_string()), ], parameters: vec![FullyQualifiedTypeReference { - type_name: vec![wrapper_name], + type_name: vec![NamespaceNode::Type(wrapper_name)], parameters: vec![], }], }, None => FullyQualifiedTypeReference { type_name: vec![ - "java".to_string(), - "util".to_string(), - "ArrayList".to_string(), + NamespaceNode::Package("java".to_string()), + NamespaceNode::Package("util".to_string()), + NamespaceNode::Type("ArrayList".to_string()), ], parameters: vec![target_type], }, @@ -149,7 +154,7 @@ impl Language for JavaLanguage { } fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String { - name.type_name.join(".") + name.type_name.iter().map(|n| n.name()).join(".") } fn template_name(template: &Template) -> String { @@ -165,11 +170,15 @@ impl Language for JavaLanguage { "." } - fn add_type_to_namespace(_is_nested_type: bool, type_name: &str, namespace: &mut Vec) { - namespace.push(type_name.to_case(Case::UpperCamel)) + fn add_type_to_namespace( + _is_nested_type: bool, + type_name: &str, + namespace: &mut Vec, + ) { + namespace.push(NamespaceNode::Type(type_name.to_case(Case::UpperCamel))) } - fn reset_namespace(namespace: &mut Vec) { + fn reset_namespace(namespace: &mut Vec) { // resets the namespace by removing current abstract dta type name namespace.pop(); } @@ -181,7 +190,7 @@ impl Language for JavaLanguage { &target_type.string_representation::(), ) { Some(wrapper_name) => FullyQualifiedTypeReference { - type_name: vec![wrapper_name], + type_name: vec![NamespaceNode::Type(wrapper_name)], parameters: vec![], }, None => target_type, @@ -271,7 +280,7 @@ impl Language for RustLanguage { target_type: FullyQualifiedTypeReference, ) -> FullyQualifiedTypeReference { FullyQualifiedTypeReference { - type_name: vec!["Vec".to_string()], + type_name: vec![NamespaceNode::Type("Vec".to_string())], parameters: vec![target_type], } } @@ -284,7 +293,7 @@ impl Language for RustLanguage { } fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String { - name.type_name.join("::") + name.type_name.iter().map(|n| n.name()).join("::") } fn template_name(template: &Template) -> String { @@ -304,7 +313,11 @@ impl Language for RustLanguage { "::" } - fn add_type_to_namespace(is_nested_type: bool, type_name: &str, namespace: &mut Vec) { + fn add_type_to_namespace( + is_nested_type: bool, + type_name: &str, + namespace: &mut Vec, + ) { // e.g. For example there is a `NestedType` inside `Foo` struct. Rust code generation also generates similar modules for the generated structs. // ```rust // mod foo { @@ -319,19 +332,35 @@ impl Language for RustLanguage { // } // ``` if is_nested_type { - // Assume we have the current namespace as `foo::Foo` - // then the following step will remove `Foo` from the path for nested type. - // So that the final namespace path for `NestedType` will become `foo::nested_type::NestedType` - namespace.pop(); // Remove the parent struct/enum + if let Some(last_value) = namespace.last() { + // Assume we have the current namespace as `foo::Foo` + // then the following step will remove `Foo` from the path for nested type. + // So that the final namespace path for `NestedType` will become `foo::nested_type::NestedType` + if !matches!(last_value, NamespaceNode::Package(_)) { + // if the last value is not module name then pop the type name from namespace + namespace.pop(); // Remove the parent struct/enum + } + } } - namespace.push(type_name.to_case(Case::Snake)); // Add this type's module name to the namespace path - namespace.push(type_name.to_case(Case::UpperCamel)) // Add this type itself to the namespace path + namespace.push(NamespaceNode::Package(type_name.to_case(Case::Snake))); // Add this type's module name to the namespace path + namespace.push(NamespaceNode::Type(type_name.to_case(Case::UpperCamel))) + // Add this type itself to the namespace path } - fn reset_namespace(namespace: &mut Vec) { + fn reset_namespace(namespace: &mut Vec) { // Resets the namespace by removing current abstract data type name and module name - namespace.pop(); - namespace.pop(); + if let Some(last_value) = namespace.last() { + // Check if it is a type then pop the type and module + if matches!(last_value, NamespaceNode::Package(_)) { + // if this is a module then only pop once for the module + namespace.pop(); + } else if matches!(last_value, NamespaceNode::Type(_)) { + namespace.pop(); + if !namespace.is_empty() { + namespace.pop(); + } + } + } } fn target_type_as_optional( From 5515927aeab325df4994fb51c3d7718dba7810d3 Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:13:38 -0700 Subject: [PATCH 11/12] Adds changes for nested type naming (#166) --- .../test/java/org/example/CodeGenTest.java | 17 +++- src/bin/ion/commands/generate/generator.rs | 83 ++++++++++++------- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 5511b3cf..eb10f696 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -48,8 +48,8 @@ class CodeGenTest { e.add(3); // set all the fields of `NestedStruct` - NestedStruct.NestedType1.Builder nb1 = new NestedStruct.NestedType1.Builder(); - NestedStruct.NestedType1 c = nb1.d(false).e(e).build(); + NestedStruct.C.Builder nb1 = new NestedStruct.C.Builder(); + NestedStruct.C c = nb1.d(false).e(e).build(); NestedStruct n = nb.a("hello").b(12).c(c).build(); // getter tests for `NestedStruct` @@ -97,6 +97,19 @@ class CodeGenTest { assertEquals("hi", s.getValue(), "s.getValue() should return \"hi\""); } + @Test void getterAndSetterTestForSequenceWithEnumElement() { + ArrayList a = new ArrayList(); + a.add(SequenceWithEnumElement.Element.FOO); + a.add(SequenceWithEnumElement.Element.BAR); + SequenceWithEnumElement s = new SequenceWithEnumElement(); + + // set all the fields of `Sequence` + s.setValue(a); + + // getter tests for `Sequence` + assertEquals(2, s.getValue().size(), "s.getValue().size() should return ArrayList fo size 3"); + } + @FunctionalInterface interface ReaderFunction { T read(IonReader reader) throws IOException; diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 92b4b2bf..3d9581ab 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -33,8 +33,6 @@ pub(crate) struct CodeGenerator<'a, L: Language> { output: &'a Path, // This field is used by Java code generation to get the namespace for generated code. current_type_fully_qualified_name: Vec, - // Represents a counter for naming nested type definitions - pub(crate) nested_type_counter: usize, pub(crate) data_model_store: HashMap, phantom: PhantomData, } @@ -76,7 +74,6 @@ impl<'a> CodeGenerator<'a, RustLanguage> { output, // Currently Rust code generation doesn't have a `--namespace` option available on the CLI, hence this is default set as an empty vector. current_type_fully_qualified_name: vec![], - nested_type_counter: 0, tera, phantom: PhantomData, data_model_store: HashMap::new(), @@ -101,7 +98,6 @@ impl<'a> CodeGenerator<'a, JavaLanguage> { Self { output, current_type_fully_qualified_name: namespace, - nested_type_counter: 0, tera, phantom: PhantomData, data_model_store: HashMap::new(), @@ -460,7 +456,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { .any(|it| matches!(it.constraint(), IslConstraintValue::Element(_, _))) { if is_nested_type { - self.build_sequence_from_constraints(constraints, code_gen_context, isl_type)? + self.build_sequence_from_constraints( + constraints, + code_gen_context, + isl_type, + Some(isl_type_name), + )? } else { self.build_wrapped_sequence_from_constraints( constraints, @@ -577,6 +578,15 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { /// Provides the `FullyQualifiedTypeReference` to be used for the `AbstractDataType` in the data model. /// Returns `None` when the given ISL type is `struct`, `list` or `sexp` as open-ended types are not supported currently. + /// + /// `type_name_suggestion` represents a name for a nested type based on current model being built. + /// If the nested type is part of, + /// 1. A struct then this represents a field name, + /// 2. A sequence then this represents a predefined name `Element`. + /// 3. If a nested type is nested within both struct and sequence then the precedence + /// will be given to field name to avoid any conflict in naming. + /// 4. For all other cases nested types are not supported and this will be set as `None`. + /// /// _Note: `field_presence` is only used for variably occurring type references and currently that is only supported with `fields` constraint. /// For all other cases `field_presence` will be set as default `FieldPresence::Required`._ fn fully_qualified_type_ref_name( @@ -584,6 +594,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_ref: &IslTypeRef, field_presence: FieldPresence, parent_code_gen_context: &mut CodeGenContext, + type_name_suggestion: Option<&str>, ) -> CodeGenResult> { Ok(match isl_type_ref { IslTypeRef::Named(name, _) => { @@ -606,24 +617,26 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { unimplemented!("Imports in schema are not supported yet!"); } IslTypeRef::Anonymous(type_def, _) => { - let name = self.next_nested_type_name(); - Some(self.generate_nested_type( + let name = type_name_suggestion.map(|t| t.to_string()).ok_or( + invalid_abstract_data_type_raw_error(format!( + "Nested types are not supported while generating code for {} type.", + self.current_type_fully_qualified_name + .last() + .unwrap() + .name() + )), + )?; + let nested_type_name = self.generate_nested_type( &name, type_def, field_presence, parent_code_gen_context, - )?) + )?; + Some(nested_type_name) } }) } - /// Provides the name of the next nested type - fn next_nested_type_name(&mut self) -> String { - self.nested_type_counter += 1; - let name = format!("NestedType{}", self.nested_type_counter); - name - } - /// Returns error if duplicate constraints are present based `found_constraint` flag fn handle_duplicate_constraint( &mut self, @@ -632,6 +645,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type: &IslTypeRef, field_presence: FieldPresence, code_gen_context: &mut CodeGenContext, + type_name_suggestion: Option<&str>, ) -> CodeGenResult { if found_constraint { return invalid_abstract_data_type_error(format!( @@ -639,11 +653,16 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { )); } - self.fully_qualified_type_ref_name(isl_type, field_presence, code_gen_context)? - .ok_or(invalid_abstract_data_type_raw_error(format!( - "Could not determine `FullQualifiedTypeReference` for type {:?}", - isl_type - ))) + self.fully_qualified_type_ref_name( + isl_type, + field_presence, + code_gen_context, + type_name_suggestion, + )? + .ok_or(invalid_abstract_data_type_raw_error(format!( + "Could not determine `FullQualifiedTypeReference` for type {:?}", + isl_type + ))) } /// Builds `AbstractDataType::Structure` from the given constraints. @@ -702,6 +721,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { value.type_reference(), field_presence, code_gen_context, + Some(name), )? .ok_or(invalid_abstract_data_type_raw_error( "Given type doesn't have a name", @@ -799,6 +819,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_ref, FieldPresence::Required, code_gen_context, + None, )?; found_base_type = true; } @@ -849,18 +870,13 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { for constraint in constraints { match constraint.constraint() { IslConstraintValue::Type(isl_type) => { - // Nested/Anonymous types are not allowed within wrapped scalar models - if matches!(isl_type, IslTypeRef::Anonymous(_, _)) { - return invalid_abstract_data_type_error( - "Nested types are not supported within wrapped scalar types(i.e. within top level ISL type definition's `type` constraint)", - ); - } let type_name = self.handle_duplicate_constraint( found_base_type, "type", isl_type, FieldPresence::Required, code_gen_context, + None, )?; wrapped_scalar_builder.base_type(type_name); found_base_type = true; @@ -918,6 +934,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type, FieldPresence::Required, code_gen_context, + None, )?; scalar_builder.base_type(type_name); found_base_type = true; @@ -974,6 +991,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_ref, FieldPresence::Required, code_gen_context, + Some("Element"), )?; wrapped_sequence_builder.element_type(type_name); @@ -1026,11 +1044,19 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { /// } /// ) /// ``` + /// `type_name_suggestion` represents a name for a nested type based on current model being built. + /// If the nested type is part of, + /// 1. A struct then this represents a field name, + /// 2. A sequence then this represents a predefined name `Element`. + /// 3. If a nested type is nested within both struct and sequence then the precedence + /// will be given to field name to avoid any conflict in naming. + /// 4. For all other cases nested types are not supported and this will be set as `None`. fn build_sequence_from_constraints( &mut self, constraints: &[IslConstraint], code_gen_context: &mut CodeGenContext, parent_isl_type: &IslType, + type_name_suggestion: Option<&str>, ) -> CodeGenResult { let mut sequence_builder = SequenceBuilder::default(); // For nested sequence type remove the anonymous type name from current fully qualified name @@ -1046,6 +1072,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_ref, FieldPresence::Required, code_gen_context, + type_name_suggestion, )? .ok_or(invalid_abstract_data_type_raw_error(format!( "Could not determine `FullQualifiedTypeReference` for type {:?}", @@ -1237,7 +1264,7 @@ mod isl_to_model_tests { NamespaceNode::Package("org".to_string()), NamespaceNode::Package("example".to_string()), NamespaceNode::Type("MyNestedStruct".to_string()), - NamespaceNode::Type("NestedType1".to_string()) + NamespaceNode::Type("Foo".to_string()) ], parameters: vec![] }, @@ -1268,7 +1295,7 @@ mod isl_to_model_tests { NamespaceNode::Package("org".to_string()), NamespaceNode::Package("example".to_string()), NamespaceNode::Type("MyNestedStruct".to_string()), - NamespaceNode::Type("NestedType1".to_string()) + NamespaceNode::Type("Foo".to_string()) ], parameters: vec![] } From 9e4ba448388a01896d1179a7eefc595da3e94a9a Mon Sep 17 00:00:00 2001 From: Khushboo <68757952+desaikd@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:48:47 -0700 Subject: [PATCH 12/12] Adds support for imports in code generation (#169) * Modifies `generate_code_for_authorities` to traverse through all subdirectories of a schema authority to generate code * Modifies `generate` to generate code for header and inline imports * Adds a change for scalar template * Adds tests for inline and header imports * Remove `--schema` option --- .../mismatched_sequence_element_type.ion | 1 + .../mismatched_sequence_type.ion | 1 + .../mismatched_import_type.ion | 5 + .../sequence_with_import/empty_sequence.ion | 1 + .../sequence_with_import/valid_elements.ion | 1 + .../valid_fields.ion | 5 + .../valid_optional_fields.ion | 5 + .../valid_unordered_fields.ion | 5 + .../test/java/org/example/CodeGenTest.java | 21 +++- .../schema/sequence_with_import.isl | 13 +++ .../schema/struct_with_inline_import.isl | 8 ++ code-gen-projects/schema/utils/fruits.isl | 4 + src/bin/ion/commands/generate/generator.rs | 99 ++++++++++++------- src/bin/ion/commands/generate/mod.rs | 61 +++--------- .../generate/templates/java/scalar.templ | 2 +- tests/cli.rs | 2 - 16 files changed, 149 insertions(+), 85 deletions(-) create mode 100644 code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_element_type.ion create mode 100644 code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_type.ion create mode 100644 code-gen-projects/input/bad/struct_with_inline_import/mismatched_import_type.ion create mode 100644 code-gen-projects/input/good/sequence_with_import/empty_sequence.ion create mode 100644 code-gen-projects/input/good/sequence_with_import/valid_elements.ion create mode 100644 code-gen-projects/input/good/struct_with_inline_import/valid_fields.ion create mode 100644 code-gen-projects/input/good/struct_with_inline_import/valid_optional_fields.ion create mode 100644 code-gen-projects/input/good/struct_with_inline_import/valid_unordered_fields.ion create mode 100644 code-gen-projects/schema/sequence_with_import.isl create mode 100644 code-gen-projects/schema/struct_with_inline_import.isl create mode 100644 code-gen-projects/schema/utils/fruits.isl diff --git a/code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_element_type.ion b/code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_element_type.ion new file mode 100644 index 00000000..3c2f0c38 --- /dev/null +++ b/code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_element_type.ion @@ -0,0 +1 @@ +[ mango ] // expected apple, banana or strawberry, found mango \ No newline at end of file diff --git a/code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_type.ion b/code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_type.ion new file mode 100644 index 00000000..be765281 --- /dev/null +++ b/code-gen-projects/input/bad/sequence_with_import/mismatched_sequence_type.ion @@ -0,0 +1 @@ +(apple banana) // expected list, found sexp \ No newline at end of file diff --git a/code-gen-projects/input/bad/struct_with_inline_import/mismatched_import_type.ion b/code-gen-projects/input/bad/struct_with_inline_import/mismatched_import_type.ion new file mode 100644 index 00000000..d2d179a5 --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_inline_import/mismatched_import_type.ion @@ -0,0 +1,5 @@ +// simple struct with type mismatched import field +{ + A: "hello", + B: false, // expected field type symbol +} diff --git a/code-gen-projects/input/good/sequence_with_import/empty_sequence.ion b/code-gen-projects/input/good/sequence_with_import/empty_sequence.ion new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/code-gen-projects/input/good/sequence_with_import/empty_sequence.ion @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/code-gen-projects/input/good/sequence_with_import/valid_elements.ion b/code-gen-projects/input/good/sequence_with_import/valid_elements.ion new file mode 100644 index 00000000..7fee1978 --- /dev/null +++ b/code-gen-projects/input/good/sequence_with_import/valid_elements.ion @@ -0,0 +1 @@ +[ apple, strawberry ] \ No newline at end of file diff --git a/code-gen-projects/input/good/struct_with_inline_import/valid_fields.ion b/code-gen-projects/input/good/struct_with_inline_import/valid_fields.ion new file mode 100644 index 00000000..740fbed3 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_inline_import/valid_fields.ion @@ -0,0 +1,5 @@ +// simple struct with all valid fields +{ + A: "hello", + B: apple, +} diff --git a/code-gen-projects/input/good/struct_with_inline_import/valid_optional_fields.ion b/code-gen-projects/input/good/struct_with_inline_import/valid_optional_fields.ion new file mode 100644 index 00000000..a70e671f --- /dev/null +++ b/code-gen-projects/input/good/struct_with_inline_import/valid_optional_fields.ion @@ -0,0 +1,5 @@ +// simple struct with all valid fields +{ + A: "hello", + // B: apple, // since `B` is an optional field, this is a valid struct +} diff --git a/code-gen-projects/input/good/struct_with_inline_import/valid_unordered_fields.ion b/code-gen-projects/input/good/struct_with_inline_import/valid_unordered_fields.ion new file mode 100644 index 00000000..0b8f4ed2 --- /dev/null +++ b/code-gen-projects/input/good/struct_with_inline_import/valid_unordered_fields.ion @@ -0,0 +1,5 @@ +// struct with unordered fields +{ + B: banana, + A: "hello", +} diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index eb10f696..ba8a4b99 100644 --- a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java +++ b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java @@ -150,12 +150,21 @@ void roundtripBadTestForEnumType() throws IOException { runRoundtripBadTest("/bad/enum_type", EnumType::readFrom); } - @Test void roundtripBadTestForSequenceWithEnumElement() throws IOException { runRoundtripBadTest("/bad/sequence_with_enum_element", SequenceWithEnumElement::readFrom); } + @Test + void roundtripBadTestForSequenceWithImport() throws IOException { + runRoundtripBadTest("/bad/sequence_with_import", SequenceWithImport::readFrom); + } + + @Test + void roundtripBadTestForStructWithInlineImport() throws IOException { + runRoundtripBadTest("/bad/struct_with_inline_import", StructWithInlineImport::readFrom); + } + private void runRoundtripBadTest(String path, ReaderFunction readerFunction) throws IOException { File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); @@ -206,6 +215,16 @@ void roundtripGoodTestForSequenceWithEnumElement() throws IOException { runRoundtripGoodTest("/good/sequence_with_enum_element", SequenceWithEnumElement::readFrom, (item, writer) -> item.writeTo(writer)); } + @Test + void roundtripGoodTestForSequenceWithImport() throws IOException { + runRoundtripGoodTest("/good/sequence_with_import", SequenceWithImport::readFrom, (item, writer) -> item.writeTo(writer)); + } + + @Test + void roundtripGoodTestForStructWithInlineImport() throws IOException { + runRoundtripGoodTest("/good/struct_with_inline_import", StructWithInlineImport::readFrom, (item, writer) -> item.writeTo(writer)); + } + private void runRoundtripGoodTest(String path, ReaderFunction readerFunction, WriterFunction writerFunction) throws IOException { File dir = new File(System.getenv("ION_INPUT") + path); String[] fileNames = dir.list(); diff --git a/code-gen-projects/schema/sequence_with_import.isl b/code-gen-projects/schema/sequence_with_import.isl new file mode 100644 index 00000000..9b7051ea --- /dev/null +++ b/code-gen-projects/schema/sequence_with_import.isl @@ -0,0 +1,13 @@ +schema_header::{ + imports: [ + { id: "utils/fruits.isl", type: fruits } + ] +} + +type::{ + name: sequence_with_import, + type: list, + element: fruits +} + +schema_footer::{} \ No newline at end of file diff --git a/code-gen-projects/schema/struct_with_inline_import.isl b/code-gen-projects/schema/struct_with_inline_import.isl new file mode 100644 index 00000000..75224118 --- /dev/null +++ b/code-gen-projects/schema/struct_with_inline_import.isl @@ -0,0 +1,8 @@ +type::{ + name: struct_with_inline_import, + type: struct, + fields: { + A: string, + B: { id: "utils/fruits.isl", type: fruits } + } +} diff --git a/code-gen-projects/schema/utils/fruits.isl b/code-gen-projects/schema/utils/fruits.isl new file mode 100644 index 00000000..7652448c --- /dev/null +++ b/code-gen-projects/schema/utils/fruits.isl @@ -0,0 +1,4 @@ +type::{ + name: fruits, + valid_values: [apple, banana, strawberry] +} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 3d9581ab..73d9ee1f 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -277,30 +277,51 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { schema_system: &mut SchemaSystem, ) -> CodeGenResult<()> { for authority in authorities { - // Sort the directory paths to ensure nested type names are always ordered based - // on directory path. (nested type name uses a counter in its name to represent that type) - let mut paths = fs::read_dir(authority)?.collect::, _>>()?; - paths.sort_by_key(|dir| dir.path()); - for schema_file in paths { - let schema_file_path = schema_file.path(); - let schema_id = schema_file_path.file_name().unwrap().to_str().unwrap(); - - let schema = schema_system.load_isl_schema(schema_id).unwrap(); - - self.generate(schema)?; - } + self.generate_code_for_directory(authority, None, schema_system)?; } Ok(()) } - /// Generates code for given Ion Schema - pub fn generate_code_for_schema( + /// Helper method to generate code for all schema files in a directory + /// `relative_path` is used to provide a relative path to the authority for a nested directory + pub fn generate_code_for_directory>( &mut self, + directory: P, + relative_path: Option<&str>, schema_system: &mut SchemaSystem, - schema_id: &str, ) -> CodeGenResult<()> { - let schema = schema_system.load_isl_schema(schema_id).unwrap(); - self.generate(schema) + let paths = fs::read_dir(&directory)?.collect::, _>>()?; + for schema_file in paths { + let schema_file_path = schema_file.path(); + + // if this is a nested directory then load schema files from it + if schema_file_path.is_dir() { + self.generate_code_for_directory( + &schema_file_path, + Some( + schema_file_path + .strip_prefix(&directory) + .unwrap() + .to_str() + .unwrap(), + ), + schema_system, + )?; + } else { + let schema = if let Some(path) = relative_path { + let relative_path_with_schema_id = Path::new(path) + .join(schema_file_path.file_name().unwrap().to_str().unwrap()); + schema_system + .load_isl_schema(relative_path_with_schema_id.as_path().to_str().unwrap()) + } else { + schema_system + .load_isl_schema(schema_file_path.file_name().unwrap().to_str().unwrap()) + }?; + self.generate(schema)?; + } + } + + Ok(()) } fn generate(&mut self, schema: IslSchema) -> CodeGenResult<()> { @@ -328,7 +349,6 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { let isl_type_name = isl_type.name().unwrap().to_string(); self.generate_abstract_data_type(&isl_type_name, isl_type)?; } - Ok(()) } @@ -597,24 +617,10 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { type_name_suggestion: Option<&str>, ) -> CodeGenResult> { Ok(match isl_type_ref { - IslTypeRef::Named(name, _) => { - let schema_type: IonSchemaType = name.into(); - L::target_type(&schema_type) - .as_ref() - .map(|type_name| FullyQualifiedTypeReference { - type_name: vec![NamespaceNode::Type(type_name.to_string())], - parameters: vec![], - }) - .map(|t| { - if field_presence == FieldPresence::Optional { - L::target_type_as_optional(t) - } else { - t - } - }) - } - IslTypeRef::TypeImport(_, _) => { - unimplemented!("Imports in schema are not supported yet!"); + IslTypeRef::Named(name, _) => Self::target_type_for(field_presence, name), + IslTypeRef::TypeImport(isl_import_type, _) => { + let name = isl_import_type.type_name(); + Self::target_type_for(field_presence, name) } IslTypeRef::Anonymous(type_def, _) => { let name = type_name_suggestion.map(|t| t.to_string()).ok_or( @@ -637,6 +643,27 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { }) } + /// Returns the target type based on given ISL type name and field presence + fn target_type_for( + field_presence: FieldPresence, + name: &String, + ) -> Option { + let schema_type: IonSchemaType = name.into(); + L::target_type(&schema_type) + .as_ref() + .map(|type_name| FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type(type_name.to_string())], + parameters: vec![], + }) + .map(|t| { + if field_presence == FieldPresence::Optional { + L::target_type_as_optional(t) + } else { + t + } + }) + } + /// Returns error if duplicate constraints are present based `found_constraint` flag fn handle_duplicate_constraint( &mut self, diff --git a/src/bin/ion/commands/generate/mod.rs b/src/bin/ion/commands/generate/mod.rs index b801685b..48fec8ec 100644 --- a/src/bin/ion/commands/generate/mod.rs +++ b/src/bin/ion/commands/generate/mod.rs @@ -45,12 +45,6 @@ impl IonCliCommand for GenerateCommand { .short('o') .help("Output directory [default: current directory]"), ) - .arg( - Arg::new("schema") - .long("schema") - .short('s') - .help("Schema file name or schema id"), - ) // `--namespace` is required when Java language is specified for code generation .arg( Arg::new("namespace") @@ -118,49 +112,26 @@ impl IonCliCommand for GenerateCommand { println!("Started generating code..."); - // Extract schema file provided by user - match args.get_one::("schema") { - None => { - // generate code based on schema and programming language - match language { - "java" => { - Self::print_java_code_gen_warnings(); - CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect()) - .generate_code_for_authorities(&authorities, &mut schema_system)? - }, - "rust" => { - Self::print_rust_code_gen_warnings(); - CodeGenerator::::new(output) - .generate_code_for_authorities(&authorities, &mut schema_system)? - } - _ => bail!( - "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", - language - ) - } - } - Some(schema_id) => { - // generate code based on schema and programming language - match language { - "java" => { - Self::print_java_code_gen_warnings(); - CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect()).generate_code_for_schema(&mut schema_system, schema_id)? - }, - "rust" => { - Self::print_rust_code_gen_warnings(); - CodeGenerator::::new(output) - .generate_code_for_authorities(&authorities, &mut schema_system)? - } - _ => bail!( - "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", - language - ) - } + // generate code based on schema and programming language + match language { + "java" => { + Self::print_java_code_gen_warnings(); + CodeGenerator::::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect()) + .generate_code_for_authorities(&authorities, &mut schema_system)? + }, + "rust" => { + Self::print_rust_code_gen_warnings(); + CodeGenerator::::new(output) + .generate_code_for_authorities(&authorities, &mut schema_system)? } + _ => bail!( + "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", + language + ) } println!("Code generation complete successfully!"); - println!("Path to generated code: {}", output.display()); + println!("All the schema files in authority(s) are generated into a flattened namespace, path to generated code: {}", output.display()); Ok(()) } } diff --git a/src/bin/ion/commands/generate/templates/java/scalar.templ b/src/bin/ion/commands/generate/templates/java/scalar.templ index 98d0a408..a5085ad1 100644 --- a/src/bin/ion/commands/generate/templates/java/scalar.templ +++ b/src/bin/ion/commands/generate/templates/java/scalar.templ @@ -69,7 +69,7 @@ class {{ model.name }} { public void writeTo(IonWriter writer) throws IOException { {# Writes `Value` class with a single field `value` as an Ion value #} {% if base_type | is_built_in_type == false %} - this.value.writeTo(writer)?; + this.value.writeTo(writer); {% else %} writer.write{{ base_type | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.value); {% endif %} diff --git a/tests/cli.rs b/tests/cli.rs index 881640c3..0c1a3f80 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -266,8 +266,6 @@ mod code_gen_tests { cmd.args([ "-X", "generate", - "--schema", - "test_schema.isl", "--output", temp_dir.path().to_str().unwrap(), "--language",