diff --git a/code-gen-projects/README.md b/code-gen-projects/README.md new file mode 100644 index 0000000..a8827ed --- /dev/null +++ b/code-gen-projects/README.md @@ -0,0 +1,134 @@ +# Code generation projects + +This directory contains 2 projects that are used in tests for code generation and serve as an example of +how to use `ion-cli` code generator under the `generate` subcommand with an existing project. + +## Table of contents + +* [/input](#input) +* [/schema](#schema) +* [/java](#java) + * [Gradle build process](#gradle-build-process) +* [/rust](#rust) + * [Cargo build process](#cargo-build-process) + +## /input + +This directory contains some good and bad test Ion files based on corresponding schema in `/schema`. + +## /schema + +This directory contains all the schema files used in testing code generation with `ion-cli` `generate` subcommand. + +## /java + +This directory contains a Java project called `code-gen-demo` which is a gradle project which has tests that uses the +generated code based +on schema file provided in `/schema` and test Ion file provided in `/input`. + +### Gradle build process + +To generate code as part of the build process of this project, a gradle build task is defined inside `build.gradle.kts`. +This task performs following steps: + +- Gets the executable path for `ion-cli` through an environment variable `ION_CLI`. If the environment variable is not + set then it uses the local executable named `ion`. +- Sets the schema directory as `/schema` which will be used by `generate` subcommand to generate code for the schema + files inside it. +- Sets the path to output directory where the code will be generated and sets it as source directory. +- It runs the `ion-cli` `generate` subcommand with the set schema directory and a namespace where the code will be + generated. + +Following is a sample build task you can add in an existing gradle project to generate code for your schemas, + +``` +val ionSchemaSourceCodeDir = "YOUR_SOURCE_SCHEMA_DIRECTORY" +val generatedIonSchemaModelDir = "${layout.buildDirectory.get()}/generated/java" +sourceSets { + main { + java.srcDir(generatedIonSchemaModelDir) + } +} + + +tasks { + val ionCodegen = create("ionCodegen") { + inputs.files(ionSchemaSourceCodeDir) + outputs.file(generatedIonSchemaModelDir) + + val ionCli = System.getenv("ION_CLI") ?: "ion" + + commandLine(ionCli) + .args( + "beta", "generate", + "-l", "java", + "-n", "NAMESPACE_FOR_GENERATED_CODE", + "-d", ionSchemaSourceCodeDir, + "-o", generatedIonSchemaModelDir, + ) + .workingDir(rootProject.projectDir) + } + + withType { + options.encoding = "UTF-8" + if (JavaVersion.current() != JavaVersion.VERSION_1_8) { + options.release.set(8) + } + dependsOn(ionCodegen) + } +} +``` + +## /rust + +This directory contains a Rust project called `code-gen-demo` which is a cargo project which has tests that uses the +generated code based +on schema file provided in `/schema` and test Ion file provided in `/input`. + +### Cargo build process + +To generate code as part of the build process of this cargo project, a cargo build script is defined in `build.rs`. +This task performs following steps: + +- Gets the executable path for `ion-cli` through an environment variable `ION_CLI`. If the environment variable is not + set then it uses the local executable named `ion`. +- Sets the schema directory as `/schema` which will be used by `generate` subcommand to generate code for the schema + files inside it. +- Sets the path to output directory where the code will be generated (e.g. `OUT_DIR`). +- It runs the `ion-cli` `generate` subcommand with the set schema directory and a namespace where the code will be + generated. + +Following is sample build script you can add in your existing cargo project to generate code using `ion-cli`. + +```rust +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let out_dir = env::var("OUT_DIR").unwrap(); + + // Invokes the ion-cli executable using environment variable ION_CLI if present, otherwise uses local executable named `ion` + let ion_cli = env::var("ION_CLI").unwrap_or("ion".to_string()); + println!("cargo:warn=Running command: {}", ion_cli); + let mut cmd = std::process::Command::new(ion_cli); + cmd.arg("beta") + .arg("generate") + .arg("-l") + .arg("rust") + .arg("-d") + .arg("YOUR_SOURCE_SCHEMA_DIRECTORY") + .arg("-o") + .arg(&out_dir); + + println!("cargo:warn=Running: {:?}", cmd); + + let output = cmd.output().expect("failed to execute process"); + + println!("status: {}", output.status); + io::stdout().write_all(&output.stdout).unwrap(); + io::stderr().write_all(&output.stderr).unwrap(); + + assert!(output.status.success()); + + println!("cargo:rerun-if-changed=input/"); + println!("cargo:rerun-if-changed=schema/"); +} +``` \ 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 new file mode 100644 index 0000000..aa1c307 --- /dev/null +++ b/code-gen-projects/input/bad/nested_struct/mismatched_sequence_type.ion @@ -0,0 +1,9 @@ +// nested struct with mismatched sequence type +{ + A: "hello", + B: 12, + C: { + D: false, + E: (1 2 3) // expected list + } +} \ No newline at end of file diff --git a/code-gen-projects/input/bad/nested_struct/mismatched_type.ion b/code-gen-projects/input/bad/nested_struct/mismatched_type.ion index e8c48db..30ae03b 100644 --- a/code-gen-projects/input/bad/nested_struct/mismatched_type.ion +++ b/code-gen-projects/input/bad/nested_struct/mismatched_type.ion @@ -4,5 +4,6 @@ B: 12, C: { D: 1e0, // expected type: bool + E: [1, 2, 3] } } diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion index 9d60dd2..bcb578d 100644 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_element.ion @@ -2,7 +2,7 @@ { A: "hello", B: 12, - C: [1, 2, 3], + C: (1 2 3), D: 10e2 } diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion new file mode 100644 index 0000000..300a99f --- /dev/null +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_sequence_type.ion @@ -0,0 +1,7 @@ +// simple struct with type mismatched sequence type +{ + A: "hello", + B: 12, + C: ["foo", "bar", "baz"], // expected sexp + D: 10e2 +} diff --git a/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion index 675df97..3cad533 100644 --- a/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion +++ b/code-gen-projects/input/bad/struct_with_fields/mismatched_type.ion @@ -2,7 +2,7 @@ { A: "hello", B: false, // expected field type: int - C: ["foo", "bar", "baz"], + C: ("foo" "bar" "baz"), D: 10e2 } diff --git a/code-gen-projects/input/good/nested_struct/empty_values.ion b/code-gen-projects/input/good/nested_struct/empty_values.ion index 545e6b2..798cc96 100644 --- a/code-gen-projects/input/good/nested_struct/empty_values.ion +++ b/code-gen-projects/input/good/nested_struct/empty_values.ion @@ -1,7 +1,8 @@ -// nested struct with empty string and zeros +// nested struct with empty string, list and zeros { C: { D: false, + E: [], }, A: "", B: 0, diff --git a/code-gen-projects/input/good/nested_struct/valid_fields.ion b/code-gen-projects/input/good/nested_struct/valid_fields.ion index 391c8cd..f63ab8a 100644 --- a/code-gen-projects/input/good/nested_struct/valid_fields.ion +++ b/code-gen-projects/input/good/nested_struct/valid_fields.ion @@ -4,5 +4,6 @@ B: 12, C: { D: false, + E: [1, 2, 3] } } diff --git a/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion index da21667..cca4717 100644 --- a/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion +++ b/code-gen-projects/input/good/nested_struct/valid_unordered_fields.ion @@ -4,6 +4,7 @@ A: "hello", C: { D: false, + E: [1, 2, 3] } } diff --git a/code-gen-projects/input/good/struct_with_fields/empty_values.ion b/code-gen-projects/input/good/struct_with_fields/empty_values.ion index 67986dc..a5c1386 100644 --- a/code-gen-projects/input/good/struct_with_fields/empty_values.ion +++ b/code-gen-projects/input/good/struct_with_fields/empty_values.ion @@ -1,6 +1,6 @@ // struct with empty list, empty string and zeros { - C: [], + C: (), A: "", B: 0, D: 0e0, diff --git a/code-gen-projects/input/good/struct_with_fields/valid_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_fields.ion index f306089..1ccd777 100644 --- a/code-gen-projects/input/good/struct_with_fields/valid_fields.ion +++ b/code-gen-projects/input/good/struct_with_fields/valid_fields.ion @@ -2,6 +2,6 @@ { A: "hello", B: 12, - C: ["foo", "bar", "baz"], + C: ("foo" "bar" "baz"), D: 10e2 } diff --git a/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion index 80b9a4e..62d974f 100644 --- a/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion +++ b/code-gen-projects/input/good/struct_with_fields/valid_unordered_fields.ion @@ -1,7 +1,7 @@ // struct with unordered fields { - C: ["foo", "bar", "baz"], + C: ("foo" "bar" "baz"), A: "hello", B: 12, D: 10e2, diff --git a/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java b/code-gen-projects/java/code-gen-demo/src/test/java/org/example/CodeGenTest.java index 3e093bc..387b997 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 @@ -26,7 +26,13 @@ class CodeGenTest { a.add("foo"); a.add("bar"); a.add("baz"); - StructWithFields s = new StructWithFields("hello", 12, new AnonymousType2(a), 10e2); + StructWithFields s = new StructWithFields(); + + // set all the fields of `StructWithFields` + s.setA("hello"); + s.setB(12); + s.setC(a); + s.setD(10e2); // getter tests for `StructWithFields` assertEquals("hello", s.getA(), "s.getA() should return \"hello\""); @@ -39,7 +45,7 @@ class CodeGenTest { assertEquals("hi", s.getA(), "s.getA() should return \"hi\""); s.setB(6); assertEquals(6, s.getB(), "s.getB() should return `6`"); - s.setC(new AnonymousType2(new ArrayList())); + s.setC(new ArrayList()); assertEquals(true, s.getC().getValue().isEmpty(), "s.getC().isEmpty() should return `true`"); s.setD(11e3); assertEquals(11e3 ,s.getD(), "s.getD() should return `11e3`"); @@ -47,10 +53,22 @@ class CodeGenTest { @Test void getterAndSetterTestForNestedStruct() { // getter tests for `NestedStruct` - NestedStruct n = new NestedStruct("hello", 12, new AnonymousType1(false)); + ArrayList a = new ArrayList(); + a.add(1); + a.add(2); + a.add(3); + NestedStruct n = new NestedStruct(); + + // set all the fields of `NestedStruct` + n.setA("hello"); + n.setB(12); + n.setC(false, a); + + // getter tests for `NestedStruct` assertEquals("hello", n.getA(), "n.getA() should return \"hello\""); assertEquals(12, n.getB(), "n.getB() should return `12`"); assertEquals(false, n.getC().getD(), "n.getC().getD() should return `false`"); + assertEquals(3, n.getC().getE().getValue().size(), "n.getC().getE().getValue().size() should return ArrayList fo size 3"); // setter tests for `NestedStruct` n.setA("hi"); @@ -59,6 +77,8 @@ class CodeGenTest { assertEquals(6, n.getB(), "s.getB() should return `6`"); n.getC().setD(true); assertEquals(true, n.getC().getD(), "s.getC().getD() should return `true`"); + n.getC().setE(new ArrayList()); + assertEquals(0, n.getC().getE().getValue().size(), "s.getC().getE().getValue().size() should return ArrayList fo size 0"); } @Test void roundtripGoodTestForStructWithFields() throws IOException { 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 8741095..dc9597f 100644 --- a/code-gen-projects/rust/code-gen-demo/src/lib.rs +++ b/code-gen-projects/rust/code-gen-demo/src/lib.rs @@ -21,8 +21,8 @@ mod tests { } #[test_resources("../../input/good/struct_with_fields/**/*.ion")] - fn roundtrip_good_test_generated_code_structs_with_fields(file_name: &str) -> IonResult<()> { - let ion_string = fs::read_to_string(file_name)?; + fn roundtrip_good_test_generated_code_structs_with_fields(file_name: &str) -> SerdeResult<()> { + let ion_string = fs::read_to_string(file_name).unwrap(); let mut reader = ReaderBuilder::new().build(ion_string.clone())?; let mut buffer = Vec::new(); let mut text_writer = TextWriterBuilder::default().build(&mut buffer)?; @@ -42,8 +42,8 @@ mod tests { } #[test_resources("../../input/bad/struct_with_fields/**/*.ion")] - fn roundtrip_bad_test_generated_code_structs_with_fields(file_name: &str) -> IonResult<()> { - let ion_string = fs::read_to_string(file_name)?; + fn roundtrip_bad_test_generated_code_structs_with_fields(file_name: &str) -> SerdeResult<()> { + 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 reader.next()?; @@ -54,8 +54,8 @@ mod tests { } #[test_resources("../../input/good/nested_struct/**/*.ion")] - fn roundtrip_good_test_generated_code_nested_structs(file_name: &str) -> IonResult<()> { - let ion_string = fs::read_to_string(file_name)?; + fn roundtrip_good_test_generated_code_nested_structs(file_name: &str) -> SerdeResult<()> { + let ion_string = fs::read_to_string(file_name).unwrap(); let mut reader = ReaderBuilder::new().build(ion_string.clone())?; let mut buffer = Vec::new(); let mut text_writer = TextWriterBuilder::default().build(&mut buffer)?; @@ -75,8 +75,8 @@ mod tests { } #[test_resources("../../input/bad/nested_struct/**/*.ion")] - fn roundtrip_bad_test_generated_code_nested_structs(file_name: &str) -> IonResult<()> { - let ion_string = fs::read_to_string(file_name)?; + fn roundtrip_bad_test_generated_code_nested_structs(file_name: &str) -> SerdeResult<()> { + 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 reader.next()?; diff --git a/code-gen-projects/schema/nested_struct.isl b/code-gen-projects/schema/nested_struct.isl index bdea322..2e37fbc 100644 --- a/code-gen-projects/schema/nested_struct.isl +++ b/code-gen-projects/schema/nested_struct.isl @@ -5,7 +5,8 @@ type::{ B: int, C: { fields: { - D: bool + D: bool, + E: { type: list, element: int } } } } diff --git a/code-gen-projects/schema/struct_with_fields.isl b/code-gen-projects/schema/struct_with_fields.isl index f03fbb0..f1eec2c 100644 --- a/code-gen-projects/schema/struct_with_fields.isl +++ b/code-gen-projects/schema/struct_with_fields.isl @@ -3,7 +3,7 @@ type::{ fields: { A: string, B: int, - C: { element: string }, + C: { element: string, type: sexp }, D: float, } } diff --git a/src/bin/ion/commands/beta/generate/context.rs b/src/bin/ion/commands/beta/generate/context.rs index 7acf77d..2fd7bb1 100644 --- a/src/bin/ion/commands/beta/generate/context.rs +++ b/src/bin/ion/commands/beta/generate/context.rs @@ -38,7 +38,10 @@ pub enum AbstractDataType { // } // ``` Value, - // A series of zero or more values whose type is described by the nested `String` (e.g. a list) + // 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::{ @@ -52,7 +55,10 @@ pub enum AbstractDataType { // value: Vec // } // ``` - Sequence(String), + 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, @@ -75,6 +81,22 @@ pub enum AbstractDataType { 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, + } + } +} + impl Display for AbstractDataType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( @@ -82,9 +104,17 @@ impl Display for AbstractDataType { "{}", match self { AbstractDataType::Value => "single value struct", - AbstractDataType::Sequence(_) => "sequence 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)] +pub enum SequenceType { + List, + SExp, +} diff --git a/src/bin/ion/commands/beta/generate/generator.rs b/src/bin/ion/commands/beta/generate/generator.rs index 6695be4..20e8fab 100644 --- a/src/bin/ion/commands/beta/generate/generator.rs +++ b/src/bin/ion/commands/beta/generate/generator.rs @@ -1,6 +1,8 @@ -use crate::commands::beta::generate::context::{AbstractDataType, CodeGenContext}; +use crate::commands::beta::generate::context::{AbstractDataType, CodeGenContext, SequenceType}; use crate::commands::beta::generate::result::{invalid_abstract_data_type_error, CodeGenResult}; -use crate::commands::beta::generate::utils::{Field, JavaLanguage, Language, RustLanguage}; +use crate::commands::beta::generate::utils::{ + Field, JavaLanguage, Language, NestedType, RustLanguage, +}; use crate::commands::beta::generate::utils::{IonSchemaType, Template}; use convert_case::{Case, Casing}; use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue}; @@ -24,8 +26,8 @@ pub(crate) struct CodeGenerator<'a, L: Language> { // 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 anonymous type definitions - pub(crate) anonymous_type_counter: usize, + // Represents a counter for naming nested type definitions + pub(crate) nested_type_counter: usize, phantom: PhantomData, } @@ -38,7 +40,10 @@ impl<'a> CodeGenerator<'a, RustLanguage> { .unwrap(); // Render the imports into output file - let rendered = tera.render("import.templ", &Context::new()).unwrap(); + let rendered_import = tera.render("import.templ", &Context::new()).unwrap(); + // Render the SerdeResult that is used in generated read-write APIs + let rendered_result = tera.render("result.templ", &Context::new()).unwrap(); + let mut file = OpenOptions::new() // In order for the file to be created, OpenOptions::write or OpenOptions::append access must be used // reference: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create @@ -47,12 +52,13 @@ impl<'a> CodeGenerator<'a, RustLanguage> { .create(true) .open(output.join("ion_generated_code.rs")) .unwrap(); - file.write_all(rendered.as_bytes()).unwrap(); + file.write_all(rendered_import.as_bytes()).unwrap(); + file.write_all(rendered_result.as_bytes()).unwrap(); Self { output, namespace: None, - anonymous_type_counter: 0, + nested_type_counter: 0, tera, phantom: PhantomData, } @@ -70,7 +76,7 @@ impl<'a> CodeGenerator<'a, JavaLanguage> { Self { output, namespace: Some(namespace), - anonymous_type_counter: 0, + nested_type_counter: 0, tera, phantom: PhantomData, } @@ -160,8 +166,8 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { schema_system: &mut SchemaSystem, ) -> CodeGenResult<()> { for authority in authorities { - // Sort the directory paths to ensure anonymous type names are always ordered based - // on directory path. (anonymous type name uses a counter in its name to represent that type) + // 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 { @@ -206,6 +212,47 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { 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. + 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![]; + let mut code_gen_context = CodeGenContext::new(); + let mut nested_anonymous_types = vec![]; + let constraints = isl_type.constraints(); + for constraint in constraints { + self.map_constraint_to_abstract_data_type( + &mut nested_anonymous_types, + &mut tera_fields, + constraint, + &mut code_gen_context, + )?; + } + + // TODO: verify the `occurs` value within a field, by default the fields are optional. + if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { + // Add the nested type into parent type's tera context + nested_types.push(NestedType { + target_kind_name: type_name.to_case(Case::UpperCamel), + fields: tera_fields, + abstract_data_type: abstract_data_type.to_owned(), + nested_types: nested_anonymous_types, + }); + } else { + return invalid_abstract_data_type_error( + "Can not determine abstract data type, specified constraints do not map to an abstract data type.", + ); + } + + Ok(()) + } + fn generate_abstract_data_type( &mut self, isl_type_name: &String, @@ -214,6 +261,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { 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)); @@ -221,20 +269,32 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { 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 + .iter() + .any(|Field { value_type, .. }| value_type.is_none()) + { + return invalid_abstract_data_type_error("Currently code generation does not support open ended types. \ + Error can be due to a missing `type` constraint or `element` constraint in the type definition."); + } + // add fields for template // TODO: verify the `occurs` value within a field, by default the fields are optional. if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { context.insert("fields", &tera_fields); context.insert("abstract_data_type", abstract_data_type); + context.insert("nested_types", &nested_types); } else { return invalid_abstract_data_type_error( - "Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.", + "Can not determine abstract data type, specified constraints do not map to an abstract data type.", ); } @@ -277,7 +337,11 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { } /// Provides name of the type reference that will be used for generated abstract data type - fn type_reference_name(&mut self, isl_type_ref: &IslTypeRef) -> CodeGenResult { + fn type_reference_name( + &mut self, + isl_type_ref: &IslTypeRef, + nested_types: &mut Vec, + ) -> CodeGenResult> { Ok(match isl_type_ref { IslTypeRef::Named(name, _) => { let schema_type: IonSchemaType = name.into(); @@ -287,50 +351,68 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { unimplemented!("Imports in schema are not supported yet!"); } IslTypeRef::Anonymous(type_def, _) => { - let name = self.next_anonymous_type_name(); - self.generate_abstract_data_type(&name, type_def)?; + let name = self.next_nested_type_name(); + self.generate_nested_type(&name, type_def, nested_types)?; - name + Some(name) } }) } - /// Provides the name of the next anonymous type - fn next_anonymous_type_name(&mut self) -> String { - self.anonymous_type_counter += 1; - let name = format!("AnonymousType{}", self.anonymous_type_counter); + /// 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 } /// Maps the given constraint value to an abstract data type fn map_constraint_to_abstract_data_type( &mut self, + nested_types: &mut Vec, tera_fields: &mut Vec, constraint: &IslConstraint, code_gen_context: &mut CodeGenContext, ) -> CodeGenResult<()> { match constraint.constraint() { IslConstraintValue::Element(isl_type, _) => { - let type_name = self.type_reference_name(isl_type)?; - self.verify_abstract_data_type_consistency( - AbstractDataType::Sequence(type_name.to_owned()), - code_gen_context, - )?; - self.generate_struct_field( + 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, - L::target_type_as_sequence(&type_name), - isl_type.name(), - "value", + code_gen_context, )?; + + // 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", + )?; + } else { + self.generate_struct_field(tera_fields, None, isl_type.name(), "value")?; + } } 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_abstract_data_type_consistency( + self.verify_and_update_abstract_data_type( AbstractDataType::Structure(*content_closed), + tera_fields, code_gen_context, )?; for (name, value) in fields.iter() { - let type_name = self.type_reference_name(value.type_reference())?; + let type_name = + self.type_reference_name(value.type_reference(), nested_types)?; self.generate_struct_field( tera_fields, @@ -341,13 +423,39 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { } } IslConstraintValue::Type(isl_type) => { - let type_name = self.type_reference_name(isl_type)?; - - self.verify_abstract_data_type_consistency( - AbstractDataType::Value, + 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 { + AbstractDataType::Value + }, + tera_fields, code_gen_context, )?; - self.generate_struct_field(tera_fields, type_name, isl_type.name(), "value")?; + + // 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", + )?; + } else { + self.generate_struct_field(tera_fields, type_name, isl_type.name(), "value")?; + } } _ => {} } @@ -358,7 +466,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { fn generate_struct_field( &mut self, tera_fields: &mut Vec, - abstract_data_type_name: String, + abstract_data_type_name: Option, isl_type_name: String, field_name: &str, ) -> CodeGenResult<()> { @@ -373,6 +481,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { /// Verify that the current abstract data type is same as previously determined abstract data type /// This is referring to abstract data type determined with each constraint that is verifies /// that all the constraints map to a single abstract data type and not different abstract data types. + /// Also, updates the underlying `element_type` for List and SExp. /// e.g. /// ``` /// type::{ @@ -386,14 +495,86 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { /// ``` /// 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_abstract_data_type_consistency( + fn verify_and_update_abstract_data_type( &mut self, current_abstract_data_type: AbstractDataType, + tera_fields: &mut Vec, code_gen_context: &mut CodeGenContext, ) -> CodeGenResult<()> { if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { - 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)); + match abstract_data_type { + // In the case when a `type` constraint occurs before `element` constraint. The element type for the sequence + // needs to be updated based on `element` constraint whereas sequence type will be used as per `type` constraint. + // e.g. For a schema as below: + // ``` + // type::{ + // name: sequence_type, + // type: sexp, + // element: string, + // } + // ``` + // Here, first `type` constraint would set the `AbstractDataType::Sequence{ element_type: T, sequence_type: "sexp"}` + // which uses generic type T and sequence type is sexp. Next `element` constraint would + // set the `AbstractDataType::Sequence{ element_type: String, sequence_type: "list"}`. + // Now this method performs verification that if the above described case occurs + // then it updates the `element_type` as per `element` constraint + // and `sequence_type` as per `type` constraint. + AbstractDataType::Sequence { + element_type, + sequence_type, + } if abstract_data_type != ¤t_abstract_data_type + && (element_type.is_none()) + && matches!( + ¤t_abstract_data_type, + &AbstractDataType::Sequence { .. } + ) => + { + // if current abstract data type is sequence and element_type is generic T or Object, + // then this was set by a `type` constraint in sequence field, + // so remove all previous fields that allows `Object` and update with current abstract_data_type. + tera_fields.pop(); + code_gen_context.with_abstract_data_type(AbstractDataType::Sequence { + element_type: current_abstract_data_type.element_type(), + sequence_type: sequence_type.to_owned(), + }); + } + // In the case when a `type` constraint occurs before `element` constraint. The element type for the sequence + // needs to be updated based on `element` constraint whereas sequence type will be used as per `type` constraint. + // e.g. For a schema as below: + // ``` + // type::{ + // name: sequence_type, + // element: string, + // type: sexp, + // } + // ``` + // Here, first `element` constraint would set the `AbstractDataType::Sequence{ element_type: String, sequence_type: "list"}` , + // Next `type` constraint would set the `AbstractDataType::Sequence{ element_type: T, sequence_type: "sexp"}` + // which uses generic type `T` and sequence type is sexp. Now this method performs verification that + // if the above described case occurs then it updates the `element_type` as per `element` constraint + // and `sequence_type` as per `type` constraint. + AbstractDataType::Sequence { element_type, .. } + if abstract_data_type != ¤t_abstract_data_type + && (current_abstract_data_type.element_type().is_none()) + && matches!( + ¤t_abstract_data_type, + &AbstractDataType::Sequence { .. } + ) => + { + // if `element` constraint has already set the abstract data_type to `Sequence` + // then remove previous fields as new fields will be added again after updating `element_type`. + // `type` constraint does update the ISL type name to either `list` or `sexp`, + // which needs to be updated within `abstract_data_type` as well. + tera_fields.pop(); + code_gen_context.with_abstract_data_type(AbstractDataType::Sequence { + element_type: element_type.to_owned(), + sequence_type: current_abstract_data_type.sequence_type(), + }) + } + _ 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)); + } + _ => {} } } else { code_gen_context.with_abstract_data_type(current_abstract_data_type); diff --git a/src/bin/ion/commands/beta/generate/templates/java/class.templ b/src/bin/ion/commands/beta/generate/templates/java/class.templ index 4a457c0..74c87f3 100644 --- a/src/bin/ion/commands/beta/generate/templates/java/class.templ +++ b/src/bin/ion/commands/beta/generate/templates/java/class.templ @@ -1,3 +1,6 @@ +{# Includes the macros for anonymous types that will be added as child classes #} +{% import "nested_type.templ" as macros %} + package {{ namespace }}; import java.util.ArrayList; import com.amazon.ion.IonReader; @@ -6,24 +9,31 @@ import com.amazon.ion.IonWriter; import com.amazon.ion.IonType; import java.io.IOException; -public final class {{ target_kind_name }} { -{% for field in fields -%} - private {{ field.value_type }} {{ field.name | camel }}; -{% endfor %} +public class {{ target_kind_name }} { + {% for field in fields -%} + private {{ field.value_type }} {{ field.name | camel }}; + {% endfor %} - public {{ target_kind_name }}({% for field in fields | sort(attribute="name") -%}{{ field.value_type }} {{ field.name | camel }}{% if not loop.last %},{% endif %}{% endfor %}) { - {% for field in fields -%} - this.{{ field.name | camel }} = {{ 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 -%}public {{ field.value_type }} set{% filter upper_camel %}{{ field.name }}{% endfilter %}({{ field.value_type }} {{ field.name | camel }}) { - return this.{{ field.name | camel }} = {{ field.name | camel }}; + {% 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 }}; + return; + {% endif %} } {% endfor %} @@ -35,7 +45,7 @@ public final class {{ target_kind_name }} { * The caller is responsible for positioning the reader on the value to read. */ public static {{ target_kind_name }} readFrom(IonReader reader) { - {# Intializes all the fields of this class #} + {# Initializes all the fields of this class #} {% for field in fields -%} {{ field.value_type }} {{ field.name | camel }} = {% if field.value_type == "boolean" %} @@ -84,26 +94,39 @@ public final class {{ target_kind_name }} { 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 abstract_data_type["Sequence"].sequence_type == "List" %} + if(reader.getType() != IonType.LIST) { + throw new IonException("Expected list, found " + reader.getType() + " while reading {{ target_kind_name }}."); + } + {% elif abstract_data_type["Sequence"].sequence_type == "SExp" %} + if(reader.getType() != IonType.SEXP) { + throw new IonException("Expected sexpression, found " + reader.getType() + " while reading {{ target_kind_name }}."); + } + {% endif %} reader.stepIn(); value = new {{ fields[0].value_type }}(); {# Iterate through the `ArraList` 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"] | is_built_in_type == false %} - value.add({{ abstract_data_type["Sequence"] }}.readFrom(reader)); + {% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %} + value.add({{ abstract_data_type["Sequence"].element_type }}.readFrom(reader)); {% else %} - {% if abstract_data_type["Sequence"] == "bytes[]" %} + {% if abstract_data_type["Sequence"].element_type == "bytes[]" %} value.add(reader.newBytes()); {% else %} - value.add(reader.{{ abstract_data_type["Sequence"] | camel }}Value()); + value.add(reader.{{ abstract_data_type["Sequence"].element_type | camel }}Value()); {% endif %} {% endif %} } reader.stepOut(); {% endif %} - return new {{ target_kind_name }}({% for field in fields | sort(attribute="name") -%}{{ field.name | camel }}{% if not loop.last %},{% endif %}{% endfor %}); - } + {{ 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}. @@ -135,15 +158,23 @@ public final class {{ target_kind_name }} { 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.LIST); - for ({{ abstract_data_type["Sequence"] }} value: this.value) { - {% if abstract_data_type["Sequence"] | is_built_in_type == false %} + {% if abstract_data_type["Sequence"].sequence_type == "List" %} + writer.stepIn(IonType.LIST); + {% else %} + writer.stepIn(IonType.SEXP); + {% endif %} + 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"] | upper_camel }}(value); + writer.write{{ abstract_data_type["Sequence"].element_type | upper_camel }}(value); {% endif %} } writer.stepOut(); {% endif %} } + + {% 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) }} + {% endfor -%} } diff --git a/src/bin/ion/commands/beta/generate/templates/java/nested_type.templ b/src/bin/ion/commands/beta/generate/templates/java/nested_type.templ new file mode 100644 index 0000000..b9cb20d --- /dev/null +++ b/src/bin/ion/commands/beta/generate/templates/java/nested_type.templ @@ -0,0 +1,209 @@ +{# 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 %} + {% 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 abstract_data_type["Sequence"].sequence_type == "List" %} + if(reader.getType() != IonType.LIST) { + throw new IonException("Expected list, found " + reader.getType() + " while reading {{ target_kind_name }}."); + } + {% elif abstract_data_type["Sequence"].sequence_type == "SExp" %} + if(reader.getType() != IonType.SEXP) { + throw new IonException("Expected sexpression, found " + reader.getType() + " while reading {{ target_kind_name }}."); + } + {% endif %} + reader.stepIn(); + value = new {{ fields[0].value_type }}(); + {# Iterate through the `ArraList` 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)); + {% else %} + {% if abstract_data_type["Sequence"].element_type == "bytes[]" %} + value.add(reader.newBytes()); + {% else %} + value.add(reader.{{ abstract_data_type["Sequence"].element_type | camel }}Value()); + {% endif %} + {% 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 %} + this.{{ field.name | camel }}.writeTo(writer); + {% 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 #} + {% if abstract_data_type["Sequence"].sequence_type == "List" %} + writer.stepIn(IonType.LIST); + {% else %} + writer.stepIn(IonType.SEXP); + {% endif %} + 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 diff --git a/src/bin/ion/commands/beta/generate/templates/rust/import.templ b/src/bin/ion/commands/beta/generate/templates/rust/import.templ index 059b9ca..c9d5993 100644 --- a/src/bin/ion/commands/beta/generate/templates/rust/import.templ +++ b/src/bin/ion/commands/beta/generate/templates/rust/import.templ @@ -1 +1 @@ -use ion_rs::{IonResult, IonReader, Reader, IonWriter, StreamItem}; +use ion_rs::{IonResult, IonError, IonReader, Reader, IonWriter, StreamItem}; diff --git a/src/bin/ion/commands/beta/generate/templates/rust/nested_type.templ b/src/bin/ion/commands/beta/generate/templates/rust/nested_type.templ new file mode 100644 index 0000000..86dafd5 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/templates/rust/nested_type.templ @@ -0,0 +1,135 @@ +{# 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 | 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 %} + "{{ field.name }}" => { abstract_data_type.{{ field.name | snake }} = {{ field.value_type }}::read_from(reader)?; } + {% 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 abstract_data_type["Sequence"].sequence_type == "List" %} + if reader.ion_type() != Some(IonType::List) { + return validation_error(format!( + "Expected list, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() + )); + } + {% elif abstract_data_type["Sequence"].sequence_type == "SExp" %} + if reader.ion_type() != Some(IonType::SExp) { + return validation_error(format!( + "Expected sexpression, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() + )); + } + {% endif %} + 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)?; + {% 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 %} + self.{{ field.name | snake }}.write_to(writer)?; + {% 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") %} + {% if abstract_data_type["Sequence"].sequence_type == "List" %} + writer.step_in(IonType::List)?; + {% else %} + writer.step_in(IonType::SExp)?; + {% endif %} + 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 diff --git a/src/bin/ion/commands/beta/generate/templates/rust/result.templ b/src/bin/ion/commands/beta/generate/templates/rust/result.templ new file mode 100644 index 0000000..9514c70 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/templates/rust/result.templ @@ -0,0 +1,25 @@ +/// Represents serde result +pub type SerdeResult = Result; + +/// Represents an error found during code generation +#[derive(Debug)] +pub enum SerdeError { + // Represents error found while reading or writing Ion data using Ion reader or writer. + IonError { source: IonError }, + // Represents error found while validating Ion data in `read_from` API for given data model. + ValidationError { description: String }, +} + +/// A convenience method for creating an SerdeError::ValidationError +/// with the provided description text. +pub fn validation_error>(description: S) -> SerdeResult { + Err(SerdeError::ValidationError { + description: description.as_ref().to_string(), + }) +} + +impl From for SerdeError { + fn from(value: IonError) -> Self { + SerdeError::IonError { source: value } + } +} diff --git a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ index 6e1e265..0dcdc0d 100644 --- a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ +++ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ @@ -1,110 +1,139 @@ -#[derive(Debug, Clone, Default)] -pub struct {{ target_kind_name }} { -{% for field in fields -%} - {{ field.name | snake | indent(first = true) }}: {{ field.value_type }}, -{% endfor %} -} +{# Includes the macros for anonymous types that will be added as child classes #} +{% import "nested_type.templ" as macros %} +use {{ target_kind_name | snake }}::{{ target_kind_name }}; +pub mod {{ target_kind_name | snake }} { + use super::*; -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 %} - } + #[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 %} + + {% 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) -> IonResult { - 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 | 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 %} - "{{ field.name }}" => { abstract_data_type.{{ field.name | snake }} = {{ field.value_type }}::read_from(reader)?; } - {% 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 %}; } + 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 | 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 %} + "{{ field.name }}" => { abstract_data_type.{{ field.name | snake }} = {{ field.value_type }}::read_from(reader)?; } + {% 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 %} - {% endfor %} - _ => { - {% if abstract_data_type["Structure"] %} - return IonResult::decoding_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") %} - reader.step_in()?; - abstract_data_type.value = { - let mut values = vec![]; + reader.step_out()?; + {% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %} + {% if abstract_data_type["Sequence"].sequence_type == "List" %} + if reader.ion_type() != Some(IonType::List) { + return validation_error(format!( + "Expected list, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() + )); + } + {% elif abstract_data_type["Sequence"].sequence_type == "SExp" %} + if reader.ion_type() != Some(IonType::SExp) { + return validation_error(format!( + "Expected sexpression, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap() + )); + } + {% endif %} + 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) + } - while reader.next()? != StreamItem::Nothing { - {% if abstract_data_type["Sequence"] | is_built_in_type == false %} - values.push({{ abstract_data_type["Sequence"] }}::read_from(reader)?); + 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 %} - values.push(reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ abstract_data_type["Sequence"] | lower | replace(from="string", to ="str") }}()?{% endif %}{% if abstract_data_type["Sequence"] | lower== "string" %} .to_string() {% endif %}); + writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.value_type | lower }}{% endif %}(self.value)?; {% endif %} - } - values - }; - reader.step_out()?; - {% else %} - return IonResult::decoding_error("Can not resolve read API template for {{ target_kind_name }}"); - {% endif %} - Ok(abstract_data_type) - } - - pub fn write_to(&self, writer: &mut W) -> IonResult<()> { - {% 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)?; - {% 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 %} - self.{{ field.name | snake }}.write_to(writer)?; - {% 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::List)?; - for value in &self.value { - {% if abstract_data_type["Sequence"] | is_built_in_type == false %} - value.write_to(writer)?; + {% 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 %} + self.{{ field.name | snake }}.write_to(writer)?; + {% 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") %} + {% if abstract_data_type["Sequence"].sequence_type == "List" %} + writer.step_in(IonType::List)?; {% else %} - writer.write_{% if fields[0].isl_type_name == "symbol" %}symbol{% else %}{{ abstract_data_type["Sequence"] | lower }}{% endif %}(value.to_owned())?; + writer.step_in(IonType::SExp)?; {% endif %} - } - writer.step_out()?; - {% endif %} - Ok(()) + 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_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) }} + {% endfor -%} } diff --git a/src/bin/ion/commands/beta/generate/utils.rs b/src/bin/ion/commands/beta/generate/utils.rs index 34e9385..b3852d9 100644 --- a/src/bin/ion/commands/beta/generate/utils.rs +++ b/src/bin/ion/commands/beta/generate/utils.rs @@ -9,10 +9,37 @@ use std::fmt::{Display, Formatter}; #[derive(Serialize)] pub struct Field { pub(crate) name: String, - pub(crate) value_type: 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" + // } + // 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, } +/// 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; @@ -27,13 +54,13 @@ pub trait Language { fn file_name_for_type(name: &str) -> String; /// Maps the given ISL type to a target type name - fn target_type(ion_schema_type: &IonSchemaType) -> String; + 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 "Vec" - fn target_type_as_sequence(target_type: &str) -> String; + fn target_type_as_sequence(target_type: &Option) -> Option; /// Returns the [Case] based on programming languages /// e.g. @@ -66,21 +93,29 @@ impl Language for JavaLanguage { name.to_case(Case::UpperCamel) } - fn target_type(ion_schema_type: &IonSchemaType) -> String { + fn target_type(ion_schema_type: &IonSchemaType) -> Option { use IonSchemaType::*; - match ion_schema_type { - Int => "int", - String | Symbol => "String", - Float => "double", - Bool => "boolean", - Blob | Clob => "byte[]", - SchemaDefined(name) => name, - } - .to_string() + Some( + match ion_schema_type { + Int => "int", + String | Symbol => "String", + Float => "double", + Bool => "boolean", + Blob | Clob => "byte[]", + List | SExp => return None, + SchemaDefined(name) => name, + } + .to_string(), + ) } - fn target_type_as_sequence(target_type: &str) -> String { - format!("ArrayList<{}>", target_type) + 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 field_name_case() -> Case { @@ -98,6 +133,21 @@ impl Language for JavaLanguage { } } +impl JavaLanguage { + fn wrapper_class(primitive_data_type: &str) -> Option { + match primitive_data_type { + "int" => Some("Integer".to_string()), + "bool" => Some("Boolean".to_string()), + "double" => Some("Double".to_string()), + "long" => Some("Long".to_string()), + _ => { + // for any other non-primitive types return None + None + } + } + } +} + impl Display for JavaLanguage { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "java") @@ -119,21 +169,26 @@ impl Language for RustLanguage { "ion_generated_code".to_string() } - fn target_type(ion_schema_type: &IonSchemaType) -> String { + fn target_type(ion_schema_type: &IonSchemaType) -> Option { use IonSchemaType::*; - match ion_schema_type { - Int => "i64", - String | Symbol => "String", - Float => "f64", - Bool => "bool", - Blob | Clob => "Vec", - SchemaDefined(name) => name, - } - .to_string() + Some( + match ion_schema_type { + Int => "i64", + String | Symbol => "String", + Float => "f64", + Bool => "bool", + Blob | Clob => "Vec", + List | SExp => return None, + SchemaDefined(name) => name, + } + .to_string(), + ) } - fn target_type_as_sequence(target_type: &str) -> String { - format!("Vec<{}>", target_type) + fn target_type_as_sequence(target_type: &Option) -> Option { + target_type + .as_ref() + .map(|target_type_name| format!("Vec<{}>", target_type_name)) } fn field_name_case() -> Case { @@ -192,6 +247,8 @@ pub enum IonSchemaType { Bool, Blob, Clob, + SExp, + List, SchemaDefined(String), // A user defined schema type } @@ -215,9 +272,11 @@ impl From<&str> for IonSchemaType { "decimal" | "timestamp" => { unimplemented!("Decimal, Number and Timestamp aren't support yet!") } - "list" | "struct" | "sexp" => { + "struct" => { unimplemented!("Generic containers aren't supported yet!") } + "list" => List, + "sexp" => SExp, _ => SchemaDefined(value.to_case(Case::UpperCamel)), } } diff --git a/tests/code-gen-tests.rs b/tests/code-gen-tests.rs index ade8b5c..9b5b6cc 100644 --- a/tests/code-gen-tests.rs +++ b/tests/code-gen-tests.rs @@ -1,7 +1,12 @@ #![cfg(feature = "experimental-code-gen")] use anyhow::Result; +use assert_cmd::Command; +use rstest::rstest; +use std::fs; +use std::fs::File; use std::io::Write; +use tempfile::TempDir; #[test] fn roundtrip_tests_for_generated_code_gradle() -> Result<()> { @@ -84,3 +89,49 @@ fn roundtrip_tests_for_generated_code_cargo() -> Result<()> { assert!(cargo_test_output.status.success()); Ok(()) } + +#[cfg(feature = "experimental-code-gen")] +#[rstest] +#[case::any_element_list( + r#" + type::{ + name: any_element_list, + type: list, // this doesn't specify the type for elements in the list with `element` constraint + } + "#, +)] +#[case::any_sequence_type( + r#" + type::{ + name: any_sequence_type, + element: int, // this doesn't specify the type of sequence with `type` constraint + } + "# +)] +/// Calls ion-cli beta generate with different unsupported schema types. Verify that `generate` subcommand returns an error for these schema types. +fn test_unsupported_schema_types_failures(#[case] test_schema: &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([ + "beta", + "generate", + "--schema", + "test_schema.isl", + "--output", + temp_dir.path().to_str().unwrap(), + "--language", + "java", + "--namespace", + "org.example", + "--directory", + temp_dir.path().to_str().unwrap(), + ]); + let command_assert = cmd.assert(); + // Code generation process should return an error for unsupported schema types + command_assert.failure(); + Ok(()) +}