diff --git a/code-gen-projects/schema/scalar.isl b/code-gen-projects/schema/scalar.isl new file mode 100644 index 0000000..cc3957a --- /dev/null +++ b/code-gen-projects/schema/scalar.isl @@ -0,0 +1,4 @@ +type::{ + name: scalar, + type: string +} \ No newline at end of file diff --git a/src/bin/ion/commands/generate/generator.rs b/src/bin/ion/commands/generate/generator.rs index 6b99faf..ad14a97 100644 --- a/src/bin/ion/commands/generate/generator.rs +++ b/src/bin/ion/commands/generate/generator.rs @@ -1,7 +1,7 @@ use crate::commands::generate::context::CodeGenContext; use crate::commands::generate::model::{ AbstractDataType, DataModelNode, FieldPresence, FieldReference, FullyQualifiedTypeReference, - StructureBuilder, + ScalarBuilder, StructureBuilder, WrappedScalarBuilder, }; use crate::commands::generate::result::{ invalid_abstract_data_type_error, invalid_abstract_data_type_raw_error, CodeGenResult, @@ -287,6 +287,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { type_name, isl_type, &mut code_gen_context, + true, )?; // add this nested type to parent code gene context's current list of nested types @@ -315,6 +316,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_name, isl_type, &mut code_gen_context, + false, )?; // add the entire type store and the data model node into tera's context to be used to render template @@ -336,6 +338,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { isl_type_name: &String, isl_type: &IslType, code_gen_context: &mut CodeGenContext, + is_nested_type: bool, ) -> CodeGenResult { self.current_type_fully_qualified_name .push(isl_type_name.to_case(Case::UpperCamel)); @@ -348,6 +351,12 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { .any(|it| matches!(it.constraint(), IslConstraintValue::Fields(_, _))) { self.build_structure_from_constraints(constraints, code_gen_context, isl_type)? + } else if Self::contains_scalar_constraints(constraints) { + if is_nested_type { + self.build_scalar_from_constraints(constraints, code_gen_context, isl_type)? + } else { + self.build_wrapped_scalar_from_constraints(constraints, code_gen_context, isl_type)? + } } else { todo!("Support for sequences, maps, scalars, and tuples not implemented yet.") }; @@ -371,6 +380,13 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(data_model_node) } + /// Verifies if the given constraints contain a `type` constraint without any container type references. (e.g. `sexp`, `list`, `struct`) + fn contains_scalar_constraints(constraints: &[IslConstraint]) -> bool { + constraints.iter().any(|it| matches!(it.constraint(), IslConstraintValue::Type(isl_type_ref) if isl_type_ref.name().as_str() != "list" + && isl_type_ref.name().as_str() != "sexp" + && isl_type_ref.name().as_str() != "struct")) + } + fn render_generated_code( &mut self, type_name: &str, @@ -447,7 +463,33 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { name } - /// Build structure from constraints + /// Builds `AbstractDataType::Structure` from the given constraints. + /// e.g. for a given type definition as below: + /// ``` + /// type::{ + /// name: Foo, + /// type: struct, + /// fields: { + /// a: string, + /// b: int, + /// } + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::Structure( + /// Structure { + /// name: vec!["org", "example", "Foo"], // assuming the namespace is `org.example` + /// fields: { + /// a: FieldReference { FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] }, FieldPresence::Optional }, + /// b: FieldReference { FullyQualifiedTypeReference { type_name: vec!["int"], parameters: vec![] }, FieldPresence::Optional }, + /// }, // HashMap with fields defined through `fields` constraint above + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType {name: "foo", .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// is_closed: false, // If the fields constraint was annotated with `closed` then this would be true. + /// } + /// ) + /// ``` fn build_structure_from_constraints( &mut self, constraints: &[IslConstraint], @@ -455,6 +497,9 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { parent_isl_type: &IslType, ) -> CodeGenResult { let mut structure_builder = StructureBuilder::default(); + structure_builder + .name(self.current_type_fully_qualified_name.to_owned()) + .source(parent_isl_type.to_owned()); for constraint in constraints { match constraint.constraint() { IslConstraintValue::Fields(struct_fields, is_closed) => { @@ -479,17 +524,11 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { } // unwrap here is safe as the `current_abstract_data_type_builder` will either be initialized with default implementation // or already initialized with a previous structure related constraint at this point. - structure_builder - .fields(fields) - .source(parent_isl_type.to_owned()) - .is_closed(*is_closed) - .name(self.current_type_fully_qualified_name.to_owned()); + structure_builder.fields(fields).is_closed(*is_closed); } IslConstraintValue::Type(_) => { // by default fields aren't closed - structure_builder - .is_closed(false) - .source(parent_isl_type.to_owned()); + structure_builder.is_closed(false); } _ => { return invalid_abstract_data_type_error( @@ -501,6 +540,122 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> { Ok(AbstractDataType::Structure(structure_builder.build()?)) } + + /// Builds `AbstractDataType::WrappedScalar` from the given constraints. + /// ``` + /// type::{ + /// name: Foo, + /// type: string, + /// } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::WrappedScalar( + /// WrappedScalar { + /// name: vec!["org", "example", "Foo"], // assuming the namespace is `org.example` + /// base_type: FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] } + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType {name: "foo", .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + /// + /// _Note: Currently code generator would return an error when there are multiple `type` constraints in the type definition. + /// This avoids providing conflicting type constraints in the type definition._ + fn build_wrapped_scalar_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut wrapped_scalar_builder = WrappedScalarBuilder::default(); + wrapped_scalar_builder + .name(self.current_type_fully_qualified_name.to_owned()) + .source(parent_isl_type.to_owned()); + + let mut found_base_type = false; + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Type(isl_type) => { + if found_base_type { + return invalid_abstract_data_type_error("Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types."); + } + let type_name = self + .fully_qualified_type_ref_name(isl_type, code_gen_context)? + .ok_or(invalid_abstract_data_type_raw_error(format!( + "Could not determine `FullQualifiedTypeReference` for type {:?}", + isl_type + )))?; + + // by default fields aren't closed + wrapped_scalar_builder.base_type(type_name); + found_base_type = true; + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); + } + } + } + + Ok(AbstractDataType::WrappedScalar( + wrapped_scalar_builder.build()?, + )) + } + + /// Builds `AbstractDataType::Scalar` from the given constraints. + /// ``` + /// { type: string } + /// ``` + /// This method builds `AbstractDataType`as following: + /// ``` + /// AbstractDataType::Scalar( + /// Scalar { + /// base_type: FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] } + /// doc_comment: None // There is no doc comment defined in above ISL type def + /// source: IslType { .. } // Represents the `IslType` that is getting converted to `AbstractDataType` + /// } + /// ) + /// ``` + /// + /// _Note: Currently code generator would return an error when there are multiple `type` constraints in the type definition. + /// This avoids providing conflicting type constraints in the type definition._ + fn build_scalar_from_constraints( + &mut self, + constraints: &[IslConstraint], + code_gen_context: &mut CodeGenContext, + parent_isl_type: &IslType, + ) -> CodeGenResult { + let mut scalar_builder = ScalarBuilder::default(); + scalar_builder.source(parent_isl_type.to_owned()); + + let mut found_base_type = false; + for constraint in constraints { + match constraint.constraint() { + IslConstraintValue::Type(isl_type) => { + if found_base_type { + return invalid_abstract_data_type_error("Multiple `type` constraints in the type definitions are not supported in code generation as it can lead to conflicting types."); + } + let type_name = self + .fully_qualified_type_ref_name(isl_type, code_gen_context)? + .ok_or(invalid_abstract_data_type_raw_error( + "Could not determine `FullQualifiedTypeReference` for `struct`, `list` or `sexp` as open ended container types aren't supported." + ))?; + + scalar_builder.base_type(type_name); + found_base_type = true; + } + _ => { + return invalid_abstract_data_type_error( + "Could not determine the abstract data type due to conflicting constraints", + ); + } + } + } + + Ok(AbstractDataType::Scalar(scalar_builder.build()?)) + } } #[cfg(test)] @@ -535,6 +690,7 @@ mod isl_to_model_tests { &"my_struct".to_string(), &isl_type, &mut CodeGenContext::new(), + false, )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( @@ -620,6 +776,7 @@ mod isl_to_model_tests { &"my_nested_struct".to_string(), &isl_type, &mut CodeGenContext::new(), + false, )?; let abstract_data_type = data_model_node.code_gen_type.unwrap(); assert_eq!( diff --git a/src/bin/ion/commands/generate/model.rs b/src/bin/ion/commands/generate/model.rs index 93cc4bb..e001ccc 100644 --- a/src/bin/ion/commands/generate/model.rs +++ b/src/bin/ion/commands/generate/model.rs @@ -204,7 +204,7 @@ impl AbstractDataType { AbstractDataType::WrappedScalar(w) => { Some(w.fully_qualified_type_name().to_owned().into()) } - AbstractDataType::Scalar(s) => Some(s.name.to_owned().into()), + AbstractDataType::Scalar(s) => Some(s.base_type.to_owned()), AbstractDataType::Sequence(seq) => Some(seq.element_type.to_owned()), AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()), } @@ -226,10 +226,11 @@ pub struct Scalar { // element: string // this is a nested scalar type // } // ``` - // Corresponding `FullyQualifiedName` would be `vec!["String"]`. - name: FullyQualifiedTypeName, + // Corresponding `FullyQualifiedReference` would be `FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] }`. + base_type: FullyQualifiedTypeReference, // Represents doc comment for the generated code // If the doc comment is provided for this scalar type then this is `Some(doc_comment)`, other it is None. + #[builder(default)] doc_comment: Option, // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. @@ -265,16 +266,12 @@ pub struct WrappedScalar { // type: string // } // ``` - // Corresponding `FullyQualifiedTypeReference` would be as following: - // ``` - // FullyQualifiedTypeReference { - // type_name: vec!["Foo"], // name of the wrapped scalar type - // parameters: vec![FullyQualifiedTypeReference {type_name: vec!["String"] }] // base type name for the scalar value - // } - // ``` - name: FullyQualifiedTypeReference, + // Corresponding `name` would be `vec!["Foo"]` and `base_type` would be `FullyQualifiedTypeReference { type_name: vec!["String"], parameters: vec![] }`. + name: FullyQualifiedTypeName, + base_type: FullyQualifiedTypeReference, // Represents doc comment for the generated code // If the doc comment is provided for this scalar type then this is `Some(doc_comment)`, other it is None. + #[builder(default)] doc_comment: Option, // Represents the source ISL type which can be used to get other constraints useful for this type. // For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type. @@ -286,7 +283,7 @@ pub struct WrappedScalar { impl WrappedScalar { pub fn fully_qualified_type_name(&self) -> &FullyQualifiedTypeName { - &self.name.type_name + &self.name } } @@ -396,7 +393,10 @@ mod model_tests { #[test] fn scalar_builder_test() { let expected_scalar = Scalar { - name: vec![], + base_type: FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![], + }, doc_comment: Some("This is scalar type".to_string()), source: anonymous_type(vec![type_constraint(named_type_ref("string"))]), }; @@ -405,7 +405,7 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .name(vec![]) + .base_type(vec!["String".to_string()]) .doc_comment(Some("This is scalar type".to_string())) .source(anonymous_type(vec![type_constraint(named_type_ref( "string", @@ -418,12 +418,10 @@ mod model_tests { #[test] fn wrapped_scalar_builder_test() { let expected_scalar = WrappedScalar { - name: FullyQualifiedTypeReference { - type_name: vec!["Foo".to_string()], - parameters: vec![FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], - parameters: vec![], - }], + name: vec!["Foo".to_string()], + base_type: FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![], }, doc_comment: Some("This is scalar type".to_string()), source: anonymous_type(vec![type_constraint(named_type_ref("string"))]), @@ -433,12 +431,10 @@ mod model_tests { // sets all the information about the scalar type scalar_builder - .name(FullyQualifiedTypeReference { - type_name: vec!["Foo".to_string()], - parameters: vec![FullyQualifiedTypeReference { - type_name: vec!["String".to_string()], - parameters: vec![], - }], + .name(vec!["Foo".to_string()]) + .base_type(FullyQualifiedTypeReference { + type_name: vec!["String".to_string()], + parameters: vec![], }) .doc_comment(Some("This is scalar type".to_string())) .source(anonymous_type(vec![type_constraint(named_type_ref( diff --git a/src/bin/ion/commands/generate/templates/java/scalar.templ b/src/bin/ion/commands/generate/templates/java/scalar.templ index f5c7954..c71f5f5 100644 --- a/src/bin/ion/commands/generate/templates/java/scalar.templ +++ b/src/bin/ion/commands/generate/templates/java/scalar.templ @@ -1,69 +1,77 @@ -package {{ namespace }}; -import java.util.ArrayList; +{% macro scalar(model) %} +{% set full_namespace = namespace | join(sep=".") %} + +package {{ full_namespace }}; import com.amazon.ion.IonReader; import com.amazon.ion.IonException; import com.amazon.ion.IonWriter; import com.amazon.ion.IonType; import java.io.IOException; -public class {{ target_kind_name }} { - private {{ fields[0].value_type }} value; +{# Verify that the abstract data type is a scalar type and store information for this scalar value #} +{% set scalar_info = model.code_gen_type["WrappedScalar"] %} +{% set base_type = scalar_info["base_type"] | fully_qualified_type_name %} + +class {{ model.name }} { + private {{ base_type }} value; - public {{ target_kind_name }}() {} + public {{ model.name }}() {} - public {{ fields[0].value_type }} getValue() { + public {{ base_type }} getValue() { return this.value; } - public void setValue({{ fields[0].value_type }} value) { + public void setValue({{ base_type }} value) { this.value = value; return; } /** - * Reads a {{ target_kind_name }} from an {@link IonReader}. + * Reads a {{ model.name }} from an {@link IonReader}. * * This method does not advance the reader at the current level. * The caller is responsible for positioning the reader on the value to read. */ - public static {{ target_kind_name }} readFrom(IonReader reader) { + public static {{ model.name }} readFrom(IonReader reader) { {# Initializes all the fields of this class #} - {{ fields[0].value_type }} value = - {% if fields[0].value_type == "boolean" %} + {{ base_type }} value = + {% if base_type == "boolean" %} false - {% elif fields[0].value_type == "int" or fields[0].value_type == "double" %} + {% elif base_type == "int" or base_type == "double" %} 0 {% else %} null {% endif %}; {# Reads `Value` class with a single field `value` #} - value = {% if fields[0].value_type | is_built_in_type %} - {% if fields[0].value_type == "bytes[]" %} + value = {% if base_type | is_built_in_type %} + {% if base_type == "bytes[]" %} reader.newBytes(); {% else %} - reader.{{ fields[0].value_type | camel }}Value(); + reader.{{ base_type | camel }}Value(); {% endif %} {% else %} - {{ fields[0].value_type }}.readFrom(reader); + {{ base_type }}.readFrom(reader); {% endif %} - {{ target_kind_name }} {{ target_kind_name | camel }} = new {{ target_kind_name }}(); - {{ target_kind_name | camel }}.value = value; + {{ model.name }} {{ model.name | camel }} = new {{ model.name }}(); + {{ model.name | camel }}.value = value; - return {{ target_kind_name | camel }}; + return {{ model.name | camel }}; } /** - * Writes a {{ target_kind_name }} as Ion from an {@link IonWriter}. + * Writes a {{ model.name }} as Ion from an {@link IonWriter}. * * This method does not close the writer after writing is complete. * The caller is responsible for closing the stream associated with the writer. */ public void writeTo(IonWriter writer) throws IOException { {# Writes `Value` class with a single field `value` as an Ion value #} - {% if fields[0].value_type | is_built_in_type == false %} + {% if base_type | is_built_in_type == false %} this.value.writeTo(writer)?; {% else %} - writer.write{{ fields[0].isl_type_name | upper_camel }}(this.value); + writer.write{{ base_type | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.value); {% endif %} } } +{% endmacro %} +{{ self::scalar(model=model) }} \ No newline at end of file diff --git a/tests/cli.rs b/tests/cli.rs index d9d4de0..b55aa03 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -239,6 +239,17 @@ mod code_gen_tests { & ["private int id;", "private String name;"], & ["public String getName() {", "public int getId() {"] )] + #[case( + "Scalar", + r#" + type::{ + name: scalar, + type: string + } + "#, + & ["private String value;"], + & ["public String getValue() {"] + )] /// Calls ion-cli generate with different schema file. Pass the test if the return value contains the expected properties and accessors. fn test_code_generation_in_java( #[case] test_name: &str,