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/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/nested_struct/mismatched_sequence_type.ion b/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion index aa1c3078..6b244a95 100644 --- a/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion +++ b/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion @@ -6,4 +6,4 @@ D: false, E: (1 2 3) // expected list } -} \ No newline at end of file +} diff --git a/code-gen-projects/input/bad/scalar/mismatched_type.ion b/code-gen-projects/input/bad/scalar/mismatched_type.ion index 39c18a90..854c85ab 100644 --- a/code-gen-projects/input/bad/scalar/mismatched_type.ion +++ b/code-gen-projects/input/bad/scalar/mismatched_type.ion @@ -1 +1 @@ -12 // expected string \ No newline at end of file +12 // expected string 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_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 index bd2fc7e6..a3e42013 100644 --- a/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion +++ b/code-gen-projects/input/bad/sequence/mismatched_sequence_type.ion @@ -1 +1 @@ -("foo" "bar" "baz") // expected list \ No newline at end of file +("foo" "bar" "baz") // expected list 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/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_fields/mismatched_sequence_element.ion b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion similarity index 61% rename from code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion rename to code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion index bcb578df..723ed823 100644 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion +++ b/code-gen-projects/input/bad/struct_with_enum_fields/mismatched_sequence_element_type.ion @@ -2,7 +2,6 @@ { A: "hello", B: 12, - C: (1 2 3), + 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/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_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion index 3cad5335..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 @@ -5,4 +5,3 @@ C: ("foo" "bar" "baz"), D: 10e2 } - 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/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/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/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/nested_struct/valid_unordered_fields.ion b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion index cca47176..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 @@ -7,4 +7,3 @@ 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 index b4fa0125..d5f18f8e 100644 --- a/code-gen-projects/input/good/scalar/empty_value.ion +++ b/code-gen-projects/input/good/scalar/empty_value.ion @@ -1,2 +1,2 @@ // 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 index 554b705d..64e295b1 100644 --- a/code-gen-projects/input/good/scalar/valid_value.ion +++ b/code-gen-projects/input/good/scalar/valid_value.ion @@ -1,2 +1,2 @@ // a scalar value of string type -"Hello World!" \ No newline at end of file +"Hello World!" diff --git a/code-gen-projects/input/good/sequence/empty_sequence.ion b/code-gen-projects/input/good/sequence/empty_sequence.ion index 0637a088..fe51488c 100644 --- a/code-gen-projects/input/good/sequence/empty_sequence.ion +++ b/code-gen-projects/input/good/sequence/empty_sequence.ion @@ -1 +1 @@ -[] \ 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 index 43892360..1935ae9c 100644 --- a/code-gen-projects/input/good/sequence/valid_elements.ion +++ b/code-gen-projects/input/good/sequence/valid_elements.ion @@ -1 +1 @@ -["foo", "bar", "baz"] \ No newline at end of file +["foo", "bar", "baz"] 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/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_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/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/input/good/struct_with_fields/valid_unordered_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion index 62d974f9..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,4 +1,3 @@ - // struct with unordered fields { C: ("foo" "bar" "baz"), 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 8519ac99..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 @@ -15,54 +15,42 @@ import java.io.IOException; import java.io.InputStream; import java.io.FileInputStream; +import java.io.BufferedInputStream; import java.io.File; class CodeGenTest { private static final IonSystem ionSystem = IonSystemBuilder.standard().build(); 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(); + @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.setC(a); - 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.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() { + @Test void builderTestForNestedStruct() { // getter tests for `NestedStruct` - ArrayList a = new ArrayList(); - a.add(1); - a.add(2); - a.add(3); - 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); - n.setC(false, a); + 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` assertEquals("hello", n.getA(), "n.getA() should return \"hello\""); @@ -70,18 +58,18 @@ class CodeGenTest { 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`"); - n.getC().setE(new ArrayList()); - assertEquals(0, n.getC().getE().size(), "s.getC().getE().size() should return ArrayList fo size 0"); + // 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 getterAndSetterTestForSequence() { + @Test void getterAndSetterTestForSequence() { ArrayList a = new ArrayList(); a.add("foo"); a.add("bar"); @@ -93,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() { @@ -113,147 +97,152 @@ class CodeGenTest { assertEquals("hi", s.getValue(), "s.getValue() should return \"hi\""); } - @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 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"); } - @Test void roundtripBadTestForStructWithFields() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/bad/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(); - assertThrows(Throwable.class, () -> { StructWithFields s = StructWithFields.readFrom(reader); }); - } - } + @FunctionalInterface + interface ReaderFunction { + T read(IonReader reader) throws IOException; } - @Test void roundtripGoodTestForNestedStruct() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/good/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(); - NestedStruct n = NestedStruct.readFrom(reader); - IonWriter writer = b.build(out); - n.writeTo(writer); - writer.close(); - assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray())); - } - } + @FunctionalInterface + interface WriterFunction { + void write(T item, IonWriter writer) throws IOException; } - @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); }); - } - } + @Test + void roundtripBadTestForScalar() throws IOException { + runRoundtripBadTest("/bad/scalar", Scalar::readFrom); } - @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 { + runRoundtripBadTest("/bad/sequence", Sequence::readFrom); + } + + @Test + void roundtripBadTestForStructWithFields() throws IOException { + runRoundtripBadTest("/bad/struct_with_fields", StructWithFields::readFrom); + } + + @Test + 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); + } + + @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 roundtripBadTestForSequence() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/bad/sequence"); + @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(); 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, () -> { Sequence s = Sequence.readFrom(reader); }); + assertThrows(Throwable.class, () -> readerFunction.read(reader)); } } } - @Test void roundtripGoodTestForScalar() throws IOException { - File dir = new File(System.getenv("ION_INPUT") + "/good/scalar"); + @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)); + } + + @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)); + } + + @Test + 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(); 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(); - Scalar s = Scalar.readFrom(reader); IonWriter writer = b.build(out); - s.writeTo(writer); + T item = readerFunction.read(reader); + writerFunction.write(item, 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/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/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/sequence.isl b/code-gen-projects/schema/sequence.isl index 23affb38..8331affa 100644 --- a/code-gen-projects/schema/sequence.isl +++ b/code-gen-projects/schema/sequence.isl @@ -2,4 +2,4 @@ type::{ name: sequence, type: list, element: string -} \ No newline at end of file +} 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/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_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/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/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/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..73d9ee1f 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,15 +1,24 @@ -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, SequenceType}; +use crate::commands::generate::model::{ + AbstractDataType, DataModelNode, EnumBuilder, FieldPresence, FieldReference, + FullyQualifiedTypeReference, NamespaceNode, ScalarBuilder, SequenceBuilder, StructureBuilder, + WrappedScalarBuilder, WrappedSequenceBuilder, +}; +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_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; @@ -23,14 +32,13 @@ 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>, - // Represents a counter for naming nested type definitions - pub(crate) nested_type_counter: usize, + current_type_fully_qualified_name: Vec, + 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 @@ -39,6 +47,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), @@ -63,16 +72,17 @@ impl<'a> CodeGenerator<'a, RustLanguage> { Self { output, - namespace: None, - nested_type_counter: 0, + // 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![], 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. @@ -80,22 +90,23 @@ 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), ]) .unwrap(); Self { output, - namespace: Some(namespace), - nested_type_counter: 0, + current_type_fully_qualified_name: namespace, 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 +146,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 +165,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 +175,101 @@ 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( + "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 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( - "`is_built_in_type` called with non-String Value", + "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: + /// + /// [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.string_representation::(), + )) + } + /// Generates code for all the schemas in given authorities pub fn generate_code_for_authorities( &mut self, @@ -177,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<()> { @@ -212,118 +333,246 @@ 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); + + 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 - 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)?; } - Ok(()) } /// 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, - 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![]; + field_presence: FieldPresence, + 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, - )?; - } + let mut data_model_node = self.convert_isl_type_def_to_data_model_node( + type_name, + isl_type, + &mut code_gen_context, + true, + )?; - // 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.", - ); + // 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()); + + // 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); } - Ok(()) + 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( &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, + false, + )?; + + // 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)| (k.string_representation::(), v)) + .collect::>(), + ); + context.insert("model", &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. + /// 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, + isl_type: &IslType, + code_gen_context: &mut CodeGenContext, + is_nested_type: bool, + ) -> CodeGenResult { + L::add_type_to_namespace( + is_nested_type, + isl_type_name, + &mut self.current_type_fully_qualified_name, + ); 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 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. + // * 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() - .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."); - } + 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, + Some(isl_type_name), + )? + } else { + self.build_wrapped_sequence_from_constraints( + constraints, + code_gen_context, + 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)? + } else { + self.build_wrapped_scalar_from_constraints(constraints, code_gen_context, isl_type)? + } + } else { + todo!("Support for maps and tuples not implemented yet.") + }; + + 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(), + }; - // 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); - } else { - return invalid_abstract_data_type_error( - "Can not determine abstract data type, specified constraints do not map to an abstract data type.", - ); + // 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 = 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); } - self.render_generated_code(isl_type_name, &mut context, &mut code_gen_context) + 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")) + } + + /// 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, context: &mut Context, - code_gen_context: &mut CodeGenContext, + data_model_node: &DataModelNode, + fully_qualified_name: &[NamespaceNode], ) -> CodeGenResult<()> { // Add namespace to tera context - if let Some(namespace) = self.namespace { - context.insert("namespace", namespace); - } + let mut import_context = Context::new(); + + 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 = &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,409 +596,737 @@ 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. + /// + /// `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( &mut self, isl_type_ref: &IslTypeRef, - nested_types: &mut Vec, - ) -> CodeGenResult> { + field_presence: FieldPresence, + parent_code_gen_context: &mut CodeGenContext, + 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) - } - 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 = self.next_nested_type_name(); - self.generate_nested_type(&name, type_def, nested_types)?; - - Some(name) + 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 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 + } + }) } - /// Maps the given constraint value to an abstract data type - fn map_constraint_to_abstract_data_type( + /// Returns error if duplicate constraints are present based `found_constraint` flag + fn handle_duplicate_constraint( &mut self, - nested_types: &mut Vec, - tera_fields: &mut Vec, - constraint: &IslConstraint, + found_constraint: bool, + constraint_name: &str, + isl_type: &IslTypeRef, + field_presence: FieldPresence, 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, - )?; + type_name_suggestion: Option<&str>, + ) -> 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 + )); + } - // 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(), - }); - } + 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 + ))) + } - // 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(()); - } - } + /// 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], + code_gen_context: &mut CodeGenContext, + 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) => { + // 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, + Some(name), + )? + .ok_or(invalid_abstract_data_type_raw_error( + "Given type doesn't have a name", + ))?; + fields.insert( + name.to_string(), + 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 + // or already initialized with a previous structure related constraint at this point. + structure_builder.fields(fields).is_closed(*is_closed); } - - // 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); + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ) } } - 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(); + } + + 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!" + ); } - self.generate_struct_field( - tera_fields, - type_name, - isl_type_name, - name, - abstract_data_type, + + let _type_name = self.handle_duplicate_constraint( + found_base_type, + "type", + isl_type_ref, + FieldPresence::Required, + code_gen_context, + None, )?; + found_base_type = true; + } + _ => { + 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(), - }); - } + Ok(AbstractDataType::Enum(enum_builder.build()?)) + } - // 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(()); - } - } - } - } + /// 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()); - // 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", + let mut found_base_type = false; + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Type(isl_type) => { + 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; + } + 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(()) + + Ok(AbstractDataType::WrappedScalar( + wrapped_scalar_builder.build()?, + )) } - /// Generates a struct field based on field name and value(data type) - fn generate_struct_field( + /// 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, - 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(()) + 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) => { + let type_name = self.handle_duplicate_constraint( + found_base_type, + "type", + isl_type, + FieldPresence::Required, + code_gen_context, + None, + )?; + 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()?)) } - /// 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. + /// Builds `AbstractDataType::WrappedSequence` from the given constraints. /// ``` /// type::{ /// name: foo, - /// type: string, - /// fields:{ - /// source: String, - /// destination: String - /// } + /// type: list, + /// element: 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( + /// 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, - current_abstract_data_type: AbstractDataType, - tera_fields: &mut Vec, + constraints: &[IslConstraint], 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(), - }); + 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, + FieldPresence::Required, + code_gen_context, + Some("Element"), + )?; + + wrapped_sequence_builder.element_type(type_name); + found_element_constraint = true; } - // 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(), - }) + 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; } - // 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(), - )) + IslConstraintValue::ContainerLength(_) => { + // TODO: add support for container length + // this is currently not supported and is a no-op } - _ 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)); + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); } - _ => {} } - } else { - code_gen_context.with_abstract_data_type(current_abstract_data_type); + } + 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` + /// } + /// ) + /// ``` + /// `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 + // 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() { + IslConstraintValue::Element(isl_type_ref, _) => { + let type_name = self + .fully_qualified_type_ref_name( + 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 {:?}", + 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)] +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![ + 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(), + &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::(), + FullyQualifiedTypeReference { + type_name: vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("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![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("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![NamespaceNode::Type("String".to_string())], + parameters: vec![] + }, + FieldPresence::Optional + ) + ), + ( + "bar".to_string(), + FieldReference( + FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type("Integer".to_string())], + parameters: vec![] + }, + FieldPresence::Optional + ) + ) + ]) + ) + } + Ok(()) + } + + #[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![ + 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(), + &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::(), + FullyQualifiedTypeReference { + type_name: vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()) + ], + parameters: vec![] + } + ); + assert!(matches!(abstract_data_type, AbstractDataType::Structure(_))); + if let AbstractDataType::Structure(structure) = abstract_data_type { + assert_eq!( + structure.name, + vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("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![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()), + NamespaceNode::Type("Foo".to_string()) + ], + parameters: vec![] + }, + FieldPresence::Optional + ) + ), + ( + "bar".to_string(), + FieldReference( + FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type("Integer".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::(), + FullyQualifiedTypeReference { + type_name: vec![ + NamespaceNode::Package("org".to_string()), + NamespaceNode::Package("example".to_string()), + NamespaceNode::Type("MyNestedStruct".to_string()), + NamespaceNode::Type("Foo".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..48fec8ec 100644 --- a/src/bin/ion/commands/generate/mod.rs +++ b/src/bin/ion/commands/generate/mod.rs @@ -7,14 +7,17 @@ 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}; 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 { @@ -42,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") @@ -115,38 +112,46 @@ 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" => - CodeGenerator::::new(output, namespace.unwrap().as_str()) - .generate_code_for_authorities(&authorities, &mut schema_system)?, - "rust" => - 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" => 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)?, - _ => 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(()) } } + +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 01c13d8d..a863efe3 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -1,34 +1,56 @@ use derive_builder::Builder; use ion_schema::isl::isl_type::IslType; -use std::collections::HashMap; +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. // 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 serde::Serialize; +use crate::commands::generate::utils::Language; +use serde::ser::Error; +use serde::{Serialize, Serializer}; +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 +58,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,34 +66,152 @@ 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() + .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()) + } } /// Represents a fully qualified type name for a type definition /// 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 -#[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![], + } + } +} + +// 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| { + 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> = + 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; + } + + /// Provides string representation of this `FullyQualifiedTypeReference` + pub fn string_representation(&self) -> String { + if self.parameters.is_empty() { + return self + .type_name + .iter() + .map(|n| n.name()) + .join(L::namespace_separator()); + } + let parameters = self + .parameters + .iter() + .map(|p| p.string_representation::()) + .collect::>() + .join(", "); + format!( + "{}<{}>", + self.type_name + .iter() + .map(|n| n.name()) + .join(L::namespace_separator()), + 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)] @@ -81,12 +222,16 @@ pub enum AbstractDataType { Scalar(Scalar), // A series of zero or more values whose type is described by the nested `element_type` 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), + // Represents an enum type + Enum(Enum), } impl AbstractDataType { - #![allow(dead_code)] + #[allow(dead_code)] pub fn doc_comment(&self) -> Option<&str> { match self { AbstractDataType::WrappedScalar(WrappedScalar { doc_comment, .. }) => { @@ -95,19 +240,67 @@ 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, .. }) => { + 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()) + } + AbstractDataType::Enum(Enum { 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) -> FullyQualifiedTypeReference { 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) => w.fully_qualified_type_name().to_owned().into(), + AbstractDataType::Scalar(s) => s.base_type.to_owned(), + AbstractDataType::Sequence(seq) => { + L::target_type_as_sequence(seq.element_type.to_owned()) + } + AbstractDataType::WrappedSequence(seq) => { + L::target_type_as_sequence(seq.element_type.to_owned()) + } + 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()), + } + } +} + +/// 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) @@ -125,16 +318,17 @@ 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. // 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, } @@ -164,40 +358,35 @@ 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. // 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, } impl WrappedScalar { pub fn fully_qualified_type_name(&self) -> &FullyQualifiedTypeName { - &self.name.type_name + &self.name } } /// 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::{ /// name: sequence_type, -/// element: int +/// element: int, +/// type: list /// } /// ``` /// Corresponding generated code in Rust would look like following: @@ -209,11 +398,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, @@ -222,11 +412,46 @@ 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")] 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`. + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] + pub(crate) source: IslType, +} + /// Represents a collection of field name/value pairs (e.g. a map) /// e.g. Given below ISL, /// ``` @@ -250,35 +475,73 @@ 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, + #[serde(skip_serializing_if = "is_anonymous")] + #[serde(serialize_with = "serialize_type_name")] + pub(crate) source: IslType, } /// Represents whether the field is required or not -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Serialize)] -enum FieldPresence { +#[derive(Debug, Clone, PartialEq, Serialize, Copy)] +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, +); + +/// 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 { @@ -291,7 +554,10 @@ mod model_tests { #[test] fn scalar_builder_test() { let expected_scalar = Scalar { - name: vec![], + base_type: FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type("String".to_string())], + parameters: vec![], + }, doc_comment: Some("This is scalar type".to_string()), source: anonymous_type(vec![type_constraint(named_type_ref("string"))]), }; @@ -300,7 +566,7 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .name(vec![]) + .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", @@ -313,12 +579,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![NamespaceNode::Type("Foo".to_string())], + base_type: FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type("String".to_string())], + parameters: vec![], }, doc_comment: Some("This is scalar type".to_string()), source: anonymous_type(vec![type_constraint(named_type_ref("string"))]), @@ -328,12 +592,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![NamespaceNode::Type("Foo".to_string())]) + .base_type(FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type("String".to_string())], + parameters: vec![], }) .doc_comment(Some("This is scalar type".to_string())) .source(anonymous_type(vec![type_constraint(named_type_ref( @@ -347,10 +609,9 @@ 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()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, sequence_type: SequenceType::List, @@ -364,8 +625,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")), @@ -377,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![], }); @@ -388,15 +648,19 @@ mod model_tests { #[test] 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(), + 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![ ( "foo".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, FieldPresence::Required, @@ -406,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, @@ -443,18 +707,18 @@ 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("This is a structure") + .doc_comment(Some("This is a structure".to_string())) .is_closed(false) .fields(HashMap::from_iter(vec![ ( "foo".to_string(), FieldReference( FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], + type_name: vec![NamespaceNode::Type("String".to_string())], parameters: vec![], }, FieldPresence::Required, @@ -464,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/result.rs b/src/bin/ion/commands/generate/result.rs index 16b9ea73..d3ba0826 100644 --- a/src/bin/ion/commands/generate/result.rs +++ b/src/bin/ion/commands/generate/result.rs @@ -1,3 +1,7 @@ +use crate::commands::generate::model::{ + EnumBuilderError, ScalarBuilderError, SequenceBuilderError, StructureBuilderError, + WrappedScalarBuilderError, WrappedSequenceBuilderError, +}; use ion_schema::result::IonSchemaError; use thiserror::Error; @@ -24,6 +28,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 +39,59 @@ 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: WrappedSequenceBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} + +impl From for CodeGenError { + fn from(value: StructureBuilderError) -> Self { + CodeGenError::DataModelBuilderError { + description: value.to_string(), + } + } +} + +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/class.templ b/src/bin/ion/commands/generate/templates/java/class.templ index 048f96cf..28698f7b 100644 --- a/src/bin/ion/commands/generate/templates/java/class.templ +++ b/src/bin/ion/commands/generate/templates/java/class.templ @@ -2,62 +2,81 @@ {% 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 | map(attribute="Package") | 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 }}() {} + private {{ 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) }} - ) { - {{ macros::initialize_anonymous_type(nested_types=nested_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 }}; + {% for field_name, field_val in struct_info["fields"] %} + {% 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 %} + 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 {{ 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) { - {# 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 %} + public static {{ model.name }} readFrom(IonReader reader) { + {# Initializes the builder for this class #} + Builder builder = new Builder(); {# Reads `Structure` class with multiple fields based on `field.name` #} reader.stepIn(); @@ -65,60 +84,71 @@ 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[]" %} - reader.newBytes(); + {% 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 }}": + builder.{{ 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, field_occurrence=field_occurrence) }} {% else %} - {{ field.value_type }}.readFrom(reader); - {% endif %} + {{ 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 }}; - {% endfor %} - - return {{ target_kind_name | camel }}; + return builder.build(); } /** - * 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. + * 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 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 %} + {% 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") %} + {{ util_macros::write_as_sequence(field_value=field_value,field_name=field_name,type_store=type_store) }} + {% else %} + this.{{ field_name | camel }}.writeTo(writer); + {% endif %} {% else %} - writer.write{{ field.isl_type_name | 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(); } - {% 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 -%} + {{ macros::nested_type(model=inline_type, is_nested=true) }} {% 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/enum.templ b/src/bin/ion/commands/generate/templates/java/enum.templ new file mode 100644 index 00000000..15a21e04 --- /dev/null +++ b/src/bin/ion/commands/generate/templates/java/enum.templ @@ -0,0 +1,57 @@ +{% set full_namespace = namespace | map(attribute="Package") | 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 99efe133..644f0d28 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,10 @@ {% 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 -%} - } -{% 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] %} - {% 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 %} -{% endmacro %} \ No newline at end of file +{% 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/java/scalar.templ b/src/bin/ion/commands/generate/templates/java/scalar.templ index f5c7954c..a5085ad1 100644 --- a/src/bin/ion/commands/generate/templates/java/scalar.templ +++ b/src/bin/ion/commands/generate/templates/java/scalar.templ @@ -1,69 +1,79 @@ -package {{ namespace }}; -import java.util.ArrayList; +{% import "nested_type.templ" as macros %} + +{% macro scalar(model) %} +{% set full_namespace = namespace | map(attribute="Package") | 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 %} - this.value.writeTo(writer)?; + {% 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/src/bin/ion/commands/generate/templates/java/sequence.templ b/src/bin/ion/commands/generate/templates/java/sequence.templ index 0f4c8a7f..b17bcf99 100644 --- a/src/bin/ion/commands/generate/templates/java/sequence.templ +++ b/src/bin/ion/commands/generate/templates/java/sequence.templ @@ -1,81 +1,89 @@ -package {{ namespace }}; -import java.util.ArrayList; +{% import "nested_type.templ" as macros %} + +{% macro sequence(model) %} + +{% if is_nested == false %} +{% set full_namespace = namespace | map(attribute="Package") | 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 %} -public class {{ target_kind_name }} { - private {{ fields[0].value_type }} value; +{# 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 {{ target_kind_name }}() {} +class {{ model.name }} { + private java.util.ArrayList<{{ sequence_info["element_type"] | fully_qualified_type_name }}> value; - public {{ fields[0].value_type }} getValue() { + public {{ model.name }}() {} + + 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(); } + + {% 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 c3c81d64..f2eb7e2b 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,39 @@ {# following macro defines statements to read a class field as sequence #} -{% macro read_as_sequence(field) %} - new {{ field.value_type }}(); - {# 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 }}."); - } - 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()); - {% else %} - {{ field.name | camel }}.add(reader.{{ field.abstract_data_type["Sequence"].element_type | 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 | 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 %} + {{ 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) %} +{% 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/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 6caa5ee0..ddaeb497 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,10 @@ {% 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" %} + {% 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/rust/scalar.templ b/src/bin/ion/commands/generate/templates/rust/scalar.templ index cd9d9d7d..b25acd1c 100644 --- a/src/bin/ion/commands/generate/templates/rust/scalar.templ +++ b/src/bin/ion/commands/generate/templates/rust/scalar.templ @@ -1,40 +1,46 @@ -use {{ target_kind_name | snake }}::{{ target_kind_name }}; -pub mod {{ target_kind_name | snake }} { +{% 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 %} + +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..30382f95 100644 --- a/src/bin/ion/commands/generate/templates/rust/sequence.templ +++ b/src/bin/ion/commands/generate/templates/rust/sequence.templ @@ -1,31 +1,37 @@ -use {{ target_kind_name | snake }}::{{ target_kind_name }}; -pub mod {{ target_kind_name | snake }} { +{% 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 }} { 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 +41,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"] | fully_qualified_type_name }}::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,16 +54,21 @@ 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()?; 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 5c00c9e8..29e25dce 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,9 @@ 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 -%} + {{ macros::nested_type(model=inline_type, is_nested=true) }} {% 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 a182c140..6961dad9 100644 --- a/src/bin/ion/commands/generate/utils.rs +++ b/src/bin/ion/commands/generate/utils.rs @@ -1,49 +1,11 @@ -use crate::commands::generate::context::AbstractDataType; +use crate::commands::generate::model::{ + AbstractDataType, DataModelNode, FullyQualifiedTypeReference, NamespaceNode, +}; use crate::commands::generate::result::{invalid_abstract_data_type_error, CodeGenError}; use convert_case::{Case, Casing}; -use serde::Serialize; +use itertools::Itertools; 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,22 +20,71 @@ 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. /// 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: &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" + fn target_type_as_optional( + target_type: FullyQualifiedTypeReference, + ) -> FullyQualifiedTypeReference; } pub struct JavaLanguage; @@ -107,17 +118,43 @@ 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_or_none( + &target_type.string_representation::(), + ) { + Some(wrapper_name) => FullyQualifiedTypeReference { + type_name: vec![ + NamespaceNode::Package("java".to_string()), + NamespaceNode::Package("util".to_string()), + NamespaceNode::Type("ArrayList".to_string()), + ], + parameters: vec![FullyQualifiedTypeReference { + type_name: vec![NamespaceNode::Type(wrapper_name)], + parameters: vec![], + }], + }, + None => FullyQualifiedTypeReference { + type_name: vec![ + NamespaceNode::Package("java".to_string()), + NamespaceNode::Package("util".to_string()), + NamespaceNode::Type("ArrayList".to_string()), + ], + parameters: vec![target_type], + }, + } + } + + fn is_built_in_type(type_name: String) -> bool { + matches!( + type_name.as_str(), + "int" | "String" | "boolean" | "byte[]" | "double" + ) } - fn is_built_in_type(name: &str) -> bool { - matches!(name, "int" | "String" | "boolean" | "byte[]" | "double") + fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String { + name.type_name.iter().map(|n| n.name()).join(".") } fn template_name(template: &Template) -> String { @@ -125,15 +162,48 @@ impl Language for JavaLanguage { Template::Struct => "class".to_string(), Template::Scalar => "scalar".to_string(), Template::Sequence => "sequence".to_string(), + Template::Enum => "enum".to_string(), + } + } + + fn namespace_separator() -> &'static str { + "." + } + + 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) { + // resets the namespace by removing current abstract dta type name + namespace.pop(); + } + + 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![NamespaceNode::Type(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()), _ => { @@ -142,6 +212,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 { @@ -181,14 +276,24 @@ 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![NamespaceNode::Type("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" + ) + } + + fn fully_qualified_type_ref(name: &FullyQualifiedTypeReference) -> String { + name.type_name.iter().map(|n| n.name()).join("::") } fn template_name(template: &Template) -> String { @@ -196,8 +301,78 @@ 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() + } } } + + fn namespace_separator() -> &'static str { + "::" + } + + 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 { + // struct Foo { + // ... + // } + // mod nested_type { + // struct NestedType { + // ... + // } + // } + // } + // ``` + if is_nested_type { + 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(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) { + // Resets the namespace by removing current abstract data type name and module name + 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( + 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 { @@ -216,21 +391,28 @@ 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> 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(_) | AbstractDataType::WrappedSequence(_) => { + Ok(Template::Sequence) + } AbstractDataType::Structure(_) => Ok(Template::Struct), - }, - None => invalid_abstract_data_type_error( + AbstractDataType::Enum(_) => Ok(Template::Enum), + } + } 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..0c1a3f80 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( @@ -336,7 +232,7 @@ mod code_gen_tests { name: simple_struct, fields: { name: string, - id: int, + id: { type: int, occurs: required }, } } "#, @@ -344,58 +240,15 @@ mod code_gen_tests { & ["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", + "Scalar", r#" type::{ - name: struct_with_nested_type, - fields: { - nested_type: { type: int } - } + name: scalar, + type: string } "#, - & ["private int nestedType;"], - & ["public int getNestedType() {"] + & ["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( @@ -408,13 +261,11 @@ 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", "generate", - "--schema", - "test_schema.isl", "--output", temp_dir.path().to_str().unwrap(), "--language", diff --git a/tests/code-gen-tests.rs b/tests/code-gen-tests.rs index 90c63c62..a7b8d559 100644 --- a/tests/code-gen-tests.rs +++ b/tests/code-gen-tests.rs @@ -96,6 +96,8 @@ fn roundtrip_tests_for_generated_code_cargo() -> Result<()> { Ok(()) } +//TODO: Add cargo roundtrip tests once the rust templates are modified based on new code generation model + #[rstest] #[case::any_element_list( r#" @@ -128,7 +130,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",