Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes to generate anonymous types inside parent type's namespace #109

Merged
merged 4 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ class CodeGenTest {
a.add("foo");
a.add("bar");
a.add("baz");
StructWithFields s = new StructWithFields("hello", 12, new AnonymousType3(a), 10e2);
StructWithFields s = new StructWithFields();

// set all the fields of `StructWithFields`
s.setA("hello");
s.setB(12);
s.setC(a);
s.setD(10e2);
desaikd marked this conversation as resolved.
Show resolved Hide resolved

// getter tests for `StructWithFields`
assertEquals("hello", s.getA(), "s.getA() should return \"hello\"");
Expand All @@ -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 AnonymousType3(new ArrayList<String>()));
s.setC(new ArrayList<String>());
assertEquals(true, s.getC().getValue().isEmpty(), "s.getC().isEmpty() should return `true`");
Comment on lines -42 to 49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make a similar change to the getter, or else it will just lead to user confusion. If setC(List<String> c) accepts a list, then getC() should return a list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change the getters because for an inline type that is for example a struct it would require to return the struct(e.g. check this test case of nested struct hat has a field c which is also a struct ). This will be simplified once we have $codegen_name or similar which will return class C instead of AnonymousTypeX.
Other option is to instead let all the getX() methods of the nested struct be part of the parent class.

s.setD(11e3);
assertEquals(11e3 ,s.getD(), "s.getD() should return `11e3`");
Expand All @@ -51,7 +57,14 @@ class CodeGenTest {
a.add(1);
a.add(2);
a.add(3);
NestedStruct n = new NestedStruct("hello", 12, new AnonymousType1(false, new AnonymousType2(a)));
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`");
Expand All @@ -64,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<Integer>());
assertEquals(0, n.getC().getE().getValue().size(), "s.getC().getE().getValue().size() should return ArrayList fo size 0");
}

@Test void roundtripGoodTestForStructWithFields() throws IOException {
Expand Down
64 changes: 58 additions & 6 deletions src/bin/ion/commands/beta/generate/generator.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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::{
AnonymousType, Field, JavaLanguage, Language, RustLanguage,
};
use crate::commands::beta::generate::utils::{IonSchemaType, Template};
use convert_case::{Case, Casing};
use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue};
Expand Down Expand Up @@ -210,6 +212,47 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
Ok(())
}

/// generates an anonymous type that can be part of another type definition.
/// This will be used by the parent type to add this anonymous type in its namespace or module.
fn generate_anonymous_type(
&mut self,
isl_type_name: &String,
desaikd marked this conversation as resolved.
Show resolved Hide resolved
isl_type: &IslType,
anonymous_types: &mut Vec<AnonymousType>,
) -> CodeGenResult<()> {
// Add an object called `anonymous_types` in tera context
// This will have a list of `anonymous_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 anonymous type into parent type's tera context
anonymous_types.push(AnonymousType {
target_kind_name: isl_type_name.to_case(Case::UpperCamel),
fields: tera_fields,
abstract_data_type: abstract_data_type.to_owned(),
anonymous_types: nested_anonymous_types,
});
} else {
return invalid_abstract_data_type_error(
"Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.",
desaikd marked this conversation as resolved.
Show resolved Hide resolved
);
}

Ok(())
}

fn generate_abstract_data_type(
&mut self,
isl_type_name: &String,
Expand All @@ -218,13 +261,15 @@ 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 anonymous_types = vec![];

// Set the ISL type name for the generated abstract data type
context.insert("target_kind_name", &isl_type_name.to_case(Case::UpperCamel));

let constraints = isl_type.constraints();
for constraint in constraints {
self.map_constraint_to_abstract_data_type(
&mut anonymous_types,
&mut tera_fields,
constraint,
&mut code_gen_context,
Expand All @@ -246,6 +291,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
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("anonymous_types", &anonymous_types);
} else {
return invalid_abstract_data_type_error(
"Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.",
Expand Down Expand Up @@ -291,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<Option<String>> {
fn type_reference_name(
&mut self,
isl_type_ref: &IslTypeRef,
anonymous_types: &mut Vec<AnonymousType>,
) -> CodeGenResult<Option<String>> {
Ok(match isl_type_ref {
IslTypeRef::Named(name, _) => {
let schema_type: IonSchemaType = name.into();
Expand All @@ -302,7 +352,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
}
IslTypeRef::Anonymous(type_def, _) => {
let name = self.next_anonymous_type_name();
self.generate_abstract_data_type(&name, type_def)?;
self.generate_anonymous_type(&name, type_def, anonymous_types)?;

Some(name)
}
Expand All @@ -319,13 +369,14 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
/// Maps the given constraint value to an abstract data type
fn map_constraint_to_abstract_data_type(
&mut self,
anonymous_types: &mut Vec<AnonymousType>,
tera_fields: &mut Vec<Field>,
constraint: &IslConstraint,
code_gen_context: &mut CodeGenContext,
) -> CodeGenResult<()> {
match constraint.constraint() {
IslConstraintValue::Element(isl_type, _) => {
let type_name = self.type_reference_name(isl_type)?;
let type_name = self.type_reference_name(isl_type, anonymous_types)?;

self.verify_and_update_abstract_data_type(
AbstractDataType::Sequence {
Expand Down Expand Up @@ -360,7 +411,8 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
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(), anonymous_types)?;

self.generate_struct_field(
tera_fields,
Expand All @@ -371,7 +423,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
}
}
IslConstraintValue::Type(isl_type) => {
let type_name = self.type_reference_name(isl_type)?;
let type_name = self.type_reference_name(isl_type, anonymous_types)?;

self.verify_and_update_abstract_data_type(
if isl_type.name() == "list" {
Expand Down
209 changes: 209 additions & 0 deletions src/bin/ion/commands/beta/generate/templates/java/anonymous_type.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
{# following macro defines an anonymous type as children class for its parent type definition #}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(🗺️ PR tour) This file adds 3 new macros for generating anonymous types.

  • anonymous_type: Generates a new child class for given anonymous type based on its properties (e.g. target_kind_name, abstract_data_type).
  • initialize_anonymous_type: This is helper macro to initialize anonymous types inside setter methods.
  • define_params_for_anonymous_type: This is helper macro to define arguments to setter methods. (This macrohelps get the base type as argument instead of anonymous type itself, check this code snippet to see how this is used)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just get rid of the anonymous types when it's a List<Something>? (Is that going to be done in a different PR?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to get rid of creating a nested class which contains List<Something>? I have created a separate class for anonymous type with ISL like {type: list, element: int} because there could be more constraints added to this anonymous type which might require adding more variables/APIs to this anonymous type class with List<Something>.
But I can add a check to see if there are no more constraints then the type and element then I can skip generating anonymous type for it.

{% macro anonymous_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("AnonymousType") -%}
public void set{% filter upper_camel -%}{{ field.name }}{% endfilter -%}(
{{ self::define_params_for_anonymous_type(anonymous_types=nested_anonymous_types, field=field, abstract_data_type=abstract_data_type, initial_field_name=field.name) }}
) {
{{ self::initialize_anonymous_type(anonymous_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.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like that this generates javadoc 👍

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::anonymous_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.anonymous_types) }}
{% endfor -%}
}
{% endmacro anonymous_type -%}

{# following macro defines statements to initialize anonymous types for setter methods #}
{% macro initialize_anonymous_type(anonymous_types, field, abstract_data_type) %}
{% set map = anonymous_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(anonymous_types, field, abstract_data_type, initial_field_name) %}
{% set map = anonymous_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("AnonymousType") %}
{{ self::define_params_for_anonymous_type(anonymous_types=inline_type.anonymous_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 %}
Loading