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

Adds read-write API generation support for Java code generation #100

Merged
merged 8 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
1 change: 1 addition & 0 deletions code-gen-projects/input/struct_with_fields.ion
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
A: "hello",
B: 12,
C: ["foo", "bar", "baz"],
D: 10e2
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) Added this field to test for double value in Java.

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,71 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import com.amazon.ion.system.IonReaderBuilder;
import com.amazon.ion.IonReader;
import com.amazon.ion.system.IonTextWriterBuilder;
import com.amazon.ion.IonWriter;
import com.amazon.ion.IonSystem;
import com.amazon.ion.system.IonSystemBuilder;
import com.amazon.ion.IonLoader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.File;

class CodeGenTest {
// TODO: Add roundtrip tests after read-write APIs are supported for Java code generation
private static final IonSystem ionSystem = IonSystemBuilder.standard().build();
private static final IonLoader ionLoader = ionSystem.getLoader();

@Test void getterAndSetterTestForStructWithFields() {
desaikd marked this conversation as resolved.
Show resolved Hide resolved
ArrayList<String> a = new ArrayList<String>();
a.add("foo");
a.add("bar");
a.add("baz");
StructWithFields s = new StructWithFields("hello", 12, new AnonymousType2(a));
StructWithFields s = new StructWithFields("hello", 12, new AnonymousType2(a), 10e2);
assertEquals("hello", s.getA(), "s.getA() should return \"hello\"");
assertEquals(12, s.getB(), "s.getB() should return `12`");
assertEquals(3, s.getC().getValue().size(), "s.getC().getValue() should return ArrayList fo size 3");
assertEquals(10e2, s.getD(), "s.getD() should return `10e2`");
}

@Test void getterAndSetterTestForNestedStruct() {
NestedStruct n = new NestedStruct("hello", 12, new AnonymousType1(false));
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`");
NestedStruct n = new NestedStruct("hello", 12, new AnonymousType1(false));
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`");
}

@Test void roundtripTestForStructWithFields() throws IOException {
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) Added roundtrip tests for generated read-write APIs

popematt marked this conversation as resolved.
Show resolved Hide resolved
File f = new File("./../../input/struct_with_fields.ion");
InputStream inputStream = new FileInputStream(f);
IonTextWriterBuilder b = IonTextWriterBuilder.standard();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IonReaderBuilder readerBuilder = IonReaderBuilder.standard();
try (IonReader reader = readerBuilder.build(inputStream)) {
reader.next();
StructWithFields s = StructWithFields.readFrom(reader);
IonWriter writer = b.build(out);
s.writeTo(writer);
writer.close();
}
assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray()));
}

@Test void roundtripTestForNestedStruct() throws IOException {
File f = new File("./../../input/nested_struct.ion");
InputStream inputStream = new FileInputStream(f);
IonTextWriterBuilder b = IonTextWriterBuilder.standard();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IonReaderBuilder readerBuilder = IonReaderBuilder.standard();
try (IonReader reader = readerBuilder.build(inputStream)) {
reader.next();
NestedStruct n = NestedStruct.readFrom(reader);
IonWriter writer = b.build(out);
n.writeTo(writer);
writer.close();
}
assertEquals(ionLoader.load(f), ionLoader.load(out.toByteArray()));
}
}
3 changes: 2 additions & 1 deletion code-gen-projects/schema/struct_with_fields.isl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ type::{
fields: {
A: string,
B: int,
C: { element: string }
C: { element: string },
D: float,
popematt marked this conversation as resolved.
Show resolved Hide resolved
}
}

111 changes: 111 additions & 0 deletions src/bin/ion/commands/beta/generate/templates/java/class.templ
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package {{ namespace }};
import java.util.ArrayList;
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 final class {{ target_kind_name }} {
{% for field in fields -%}
Expand All @@ -16,4 +21,110 @@ public final class {{ target_kind_name }} {
return this.{{ field.name | camel }};
}
{% endfor %}

desaikd marked this conversation as resolved.
Show resolved Hide resolved
public static {{ target_kind_name }} readFrom(IonReader reader) {
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) Added readFrom API in the class template for reading an Ion value into the given data model.

{# Intializes all the fields of this class #}
{% for field in fields -%}
{{ field.value }} {{ field.name | camel }} =
desaikd marked this conversation as resolved.
Show resolved Hide resolved
{% if field.value == "boolean" %}
false
Copy link
Contributor

Choose a reason for hiding this comment

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

If you use Boolean instead of boolean, then this could be initialized to null. Similarly for int/Integer and double/Double.

However, if it's not an optional field, then we're better off using the primitive types to avoid the overhead of the boxed types.

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 am going to keep this as boolean/int/double for now. I will change it in separate PR to consider nulls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reference: #101

{% elif field.value == "int" or field.value == "double" %}
0
{% else %}
null
{% endif %};
{% endfor %}
{% if abstract_data_type == "Value"%}
{# Reads `Value` class with a signle field `value` #}
desaikd marked this conversation as resolved.
Show resolved Hide resolved
value = {% if fields[0].value | is_built_in_type %}
{% if fields[0].value == "bytes[]" %}
reader.newBytes();
desaikd marked this conversation as resolved.
Show resolved Hide resolved
{% else %}
reader.{{ fields[0].value | camel }}Value();
{% endif %}
{% else %}
{{ fields[0].value }}.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 | is_built_in_type %}
{% if field.value == "bytes[]" %}
reader.newBytes();
{% else %}
reader.{{ field.value | camel }}Value();
{% endif %}
{% else %}
{{ field.value }}.readFrom(reader);
{% endif %}
break;
{% endfor %}
default:
throw new IonException("Can not read field name:" + fieldName + " for {{ target_kind_name }} as it doesn't exist in the given schema type definition.");
}
}
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 `ArraList` #}
reader.stepIn();
value = new {{ fields[0].value }}();
{# Iterate through the `ArraList` and read each element in it based on the data type provided in `abstract_data_type[Sequence]` #}
desaikd marked this conversation as resolved.
Show resolved Hide resolved
while (reader.hasNext()) {
reader.next();
{% if abstract_data_type["Sequence"] | is_built_in_type == false %}
value.add({{ abstract_data_type["Sequence"] }}.readFrom(reader));
{% else %}
{% if abstract_data_type["Sequence"] == "bytes[]" %}
value.add(reader.newBytes());
{% else %}
value.add(reader.{{ abstract_data_type["Sequence"] | 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 %});
Copy link
Contributor

Choose a reason for hiding this comment

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

We really need to have a builder class so that we can avoid things like sorting the field names and having to check to see if we're on the last loop iteration to see if we need a comma.

}

public void writeTo(IonWriter writer) throws IOException {
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) Added writeTo API in the class template for writing the given data model as Ion.

{% if abstract_data_type == "Value" %}
{# Writes `Value` class with a signle field `value` as an Ion value #}
desaikd marked this conversation as resolved.
Show resolved Hide resolved
{% for field in fields %}
{% if field.value | is_built_in_type == false %}
this.{{ field.name | camel }}.writeTo(writer)?;
{% else %}
writer.write{% if field.isl_type_name == "symbol" %}Symbol{% else %}{{ field.isl_type_name | upper_camel }}{% endif %}(this.value);
desaikd marked this conversation as resolved.
Show resolved Hide resolved
{% 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);
desaikd marked this conversation as resolved.
Show resolved Hide resolved
{% for field in fields %}
writer.setFieldName("{{ field.name }}");
{% if field.value | is_built_in_type == false %}
this.{{ field.name | camel }}.writeTo(writer);
{% else %}
writer.write{% if field.isl_type_name == "symbol" %}Symbol{% else %}{{ field.isl_type_name | upper_camel }}{% endif %}(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 `ArraList` 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 %}
value.writeTo(writer);
{% else %}
writer.write{% if fields[0].isl_type_name == "symbol" %}Symbol{% else %}{{ abstract_data_type["Sequence"] | upper_camel }}{% endif %}(value);
{% endif %}
}
writer.stepOut();
{% endif %}
}
}
2 changes: 1 addition & 1 deletion src/bin/ion/commands/beta/generate/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl Language for JavaLanguage {
}

fn is_built_in_type(name: &str) -> bool {
matches!(name, "int" | "String" | "boolean" | "byte[]" | "float")
matches!(name, "int" | "String" | "boolean" | "byte[]" | "double")
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) ion-java uses doubleValue API for IonReader, so changing this similar to ion-java.

}

fn template_name(template: &Template) -> String {
Expand Down
Loading