Skip to content

Commit

Permalink
Adds support for Java enum generation (#158)
Browse files Browse the repository at this point in the history
* adds changes for enum support
* adds tests for enum generation
* Adds changes for placeholder Rust enum template
* Fixes bug for Rust namespace in generated code
  • Loading branch information
desaikd authored Oct 24, 2024
1 parent 61b34e3 commit 7de9bb1
Show file tree
Hide file tree
Showing 27 changed files with 372 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FoobarBaz // expected FooBarBaz, found FoobarBaz
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello // expected (foo, bar, baz or FooBarBaz) found hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"foo" // expected a symbol value foo for enum, found string "foo"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// struct with mismatched sequence element
{
A: "hello",
B: 12,
C: (1 2 3), // expected sexpression of strings
D: 10e2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// simple struct with type mismatched sequence type
{
A: "hello",
B: 12,
C: ["foo", "bar", "baz"], // expected sexp
D: 10e2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// simple struct with type mismatched fields
{
A: "hello",
B: false, // expected field type: int
C: ("foo" "bar" "baz"),
D: 10e2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// simple struct with all valid fields
{
A: "hello",
B: 12,
// C: ("foo" "bar" "baz"), // since `C` is a required field, this is an invalid struct
D: 10e2
}
1 change: 1 addition & 0 deletions code-gen-projects/input/good/enum_type/valid_value_1.ion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo
1 change: 1 addition & 0 deletions code-gen-projects/input/good/enum_type/valid_value_2.ion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bar
1 change: 1 addition & 0 deletions code-gen-projects/input/good/enum_type/valid_value_3.ion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
baz
1 change: 1 addition & 0 deletions code-gen-projects/input/good/enum_type/valid_value_4.ion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FooBarBaz
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// struct with empty list, empty string and zeros
{
C: (),
A: "",
B: 0,
D: 0e0,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// simple struct with all valid fields
{
A: "hello",
B: 12,
C: ("foo" "bar" "baz"),
D: 10e2,
E: foo
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// simple struct with all valid fields
{
A: "hello",
B: 12,
C: ("foo" "bar" "baz"),
// D: 10e2, // since `D` is optional field, this is a valid struct
E: foo,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// struct with unordered fields
{
C: ("foo" "bar" "baz"),
A: "hello",
B: 12,
E: foo,
D: 10e2,
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ void roundtripBadTestForNestedStruct() throws IOException {
runRoundtripBadTest("/bad/nested_struct", NestedStruct::readFrom);
}

@Test
void roundtripBadTestForStructWithEnumFields() throws IOException {
runRoundtripBadTest("/bad/struct_with_enum_fields", StructWithEnumFields::readFrom);
}

@Test
void roundtripBadTestForEnumType() throws IOException {
runRoundtripBadTest("/bad/enum_type", EnumType::readFrom);
}

private <T> void runRoundtripBadTest(String path, ReaderFunction<T> readerFunction) throws IOException {
File dir = new File(System.getenv("ION_INPUT") + path);
String[] fileNames = dir.list();
Expand Down Expand Up @@ -161,6 +171,17 @@ void roundtripGoodTestForNestedStruct() throws IOException {
runRoundtripGoodTest("/good/nested_struct", NestedStruct::readFrom, (item, writer) -> item.writeTo(writer));
}

@Test
void roundtripGoodTestForStructWithEnumFields() throws IOException {
runRoundtripGoodTest("/good/struct_with_enum_fields", StructWithEnumFields::readFrom, (item, writer) -> item.writeTo(writer));
}


@Test
void roundtripGoodTestForEnumType() throws IOException {
runRoundtripGoodTest("/good/enum_type", EnumType::readFrom, (item, writer) -> item.writeTo(writer));
}

private <T> void runRoundtripGoodTest(String path, ReaderFunction<T> readerFunction, WriterFunction<T> writerFunction) throws IOException {
File dir = new File(System.getenv("ION_INPUT") + path);
String[] fileNames = dir.list();
Expand Down
4 changes: 4 additions & 0 deletions code-gen-projects/schema/enum_type.isl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type::{
name: enum_type,
valid_values: [foo, bar, baz, FooBarBaz]
}
12 changes: 12 additions & 0 deletions code-gen-projects/schema/struct_with_enum_fields.isl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type::{
name: struct_with_enum_fields,
type: struct,
fields: {
A: string,
B: int,
C: { element: string, type: sexp, occurs: required },
D: float,
E: { valid_values: [foo, bar, baz] }
}
}

113 changes: 109 additions & 4 deletions src/bin/ion/commands/generate/generator.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::commands::generate::context::{CodeGenContext, SequenceType};
use crate::commands::generate::model::{
AbstractDataType, DataModelNode, FieldPresence, FieldReference, FullyQualifiedTypeReference,
ScalarBuilder, SequenceBuilder, StructureBuilder, WrappedScalarBuilder, WrappedSequenceBuilder,
AbstractDataType, DataModelNode, EnumBuilder, FieldPresence, FieldReference,
FullyQualifiedTypeReference, ScalarBuilder, SequenceBuilder, StructureBuilder,
WrappedScalarBuilder, WrappedSequenceBuilder,
};
use crate::commands::generate::result::{
invalid_abstract_data_type_error, invalid_abstract_data_type_raw_error, CodeGenResult,
Expand All @@ -10,12 +11,14 @@ use crate::commands::generate::templates;
use crate::commands::generate::utils::{IonSchemaType, Template};
use crate::commands::generate::utils::{JavaLanguage, Language, RustLanguage};
use convert_case::{Case, Casing};
use ion_schema::external::ion_rs::element::Value;
use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue};
use ion_schema::isl::isl_type::IslType;
use ion_schema::isl::isl_type_reference::IslTypeRef;
use ion_schema::isl::util::ValidValue;
use ion_schema::isl::IslSchema;
use ion_schema::system::SchemaSystem;
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
Expand Down Expand Up @@ -46,6 +49,7 @@ impl<'a> CodeGenerator<'a, RustLanguage> {
("struct.templ", templates::rust::STRUCT),
("scalar.templ", templates::rust::SCALAR),
("sequence.templ", templates::rust::SEQUENCE),
("enum.templ", templates::rust::ENUM),
("util_macros.templ", templates::rust::UTIL_MACROS),
("import.templ", templates::rust::IMPORT),
("nested_type.templ", templates::rust::NESTED_TYPE),
Expand Down Expand Up @@ -89,6 +93,7 @@ impl<'a> CodeGenerator<'a, JavaLanguage> {
("class.templ", templates::java::CLASS),
("scalar.templ", templates::java::SCALAR),
("sequence.templ", templates::java::SEQUENCE),
("enum.templ", templates::java::ENUM),
("util_macros.templ", templates::java::UTIL_MACROS),
("nested_type.templ", templates::java::NESTED_TYPE),
])
Expand Down Expand Up @@ -328,7 +333,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
self.generate_abstract_data_type(&isl_type_name, isl_type)?;
// Since the fully qualified name of this generator represents the current fully qualified name,
// remove it before generating code for the next ISL type.
self.current_type_fully_qualified_name.pop();
L::reset_namespace(&mut self.current_type_fully_qualified_name);
}

Ok(())
Expand Down Expand Up @@ -436,6 +441,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
// * The sequence type for `Sequence` will be stored based on `type` constraint with either `list` or `sexp`.
// * If given list of constraints has any `type` constraint except `type: list`, `type: struct` and `type: sexp`, then `AbstractDataType::Scalar` needs to be constructed.
// * The `base_type` for `Scalar` will be stored based on `type` constraint.
// * If given list of constraints has any `valid_values` constraint which contains exclusively symbol values, then `AbstractDataType::Enum` needs to be constructed.
// * All the other constraints except the above ones are not yet supported by code generator.
let abstract_data_type = if constraints
.iter()
Expand All @@ -455,6 +461,8 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
isl_type,
)?
}
} else if Self::contains_enum_constraints(constraints) {
self.build_enum_from_constraints(constraints, code_gen_context, isl_type)?
} else if Self::contains_scalar_constraints(constraints) {
if is_nested_type {
self.build_scalar_from_constraints(constraints, code_gen_context, isl_type)?
Expand Down Expand Up @@ -500,6 +508,20 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
&& isl_type_ref.name().as_str() != "struct"))
}

/// Verifies if the given constraints contain a `valid_values` constraint with only symbol values.
fn contains_enum_constraints(constraints: &[IslConstraint]) -> bool {
constraints.iter().any(|it| {
if let IslConstraintValue::ValidValues(valid_values) = it.constraint() {
valid_values
.values()
.iter()
.all(|val| matches!(val, ValidValue::Element(Value::Symbol(_))))
} else {
false
}
})
}

fn render_generated_code(
&mut self,
type_name: &str,
Expand Down Expand Up @@ -697,6 +719,89 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
Ok(AbstractDataType::Structure(structure_builder.build()?))
}

/// Builds `AbstractDataType::Enum` from the given constraints.
/// e.g. for a given type definition as below:
/// ```
/// type::{
/// name: Foo,
/// type: symbol,
/// valid_values: [foo, bar, baz]
/// }
/// ```
/// This method builds `AbstractDataType`as following:
/// ```
/// AbstractDataType::Enum(
/// Enum {
/// name: vec!["org", "example", "Foo"], // assuming the namespace is `org.example`
/// variants: HashSet::from_iter(
/// vec![
/// "foo",
/// "bar",
/// "baz"
/// ].iter()) // Represents enum variants
/// doc_comment: None // There is no doc comment defined in above ISL type def
/// source: IslType {name: "foo", .. } // Represents the `IslType` that is getting converted to `AbstractDataType`
/// }
/// )
/// ```
fn build_enum_from_constraints(
&mut self,
constraints: &[IslConstraint],
code_gen_context: &mut CodeGenContext,
parent_isl_type: &IslType,
) -> CodeGenResult<AbstractDataType> {
let mut enum_builder = EnumBuilder::default();
enum_builder
.name(self.current_type_fully_qualified_name.to_owned())
.source(parent_isl_type.to_owned());
let mut found_base_type = false;

for constraint in constraints {
match constraint.constraint() {
IslConstraintValue::ValidValues(valid_values_constraint) => {
let valid_values = valid_values_constraint
.values()
.iter()
.map(|v| match v {
ValidValue::Element(Value::Symbol(symbol_val) ) => {
symbol_val.text().map(|s| s.to_string()).ok_or(invalid_abstract_data_type_raw_error(
"Could not determine enum variant name",
))
}
_ => invalid_abstract_data_type_error(
"Only `valid_values` constraint with values of type `symbol` are supported yet!"
),
})
.collect::<CodeGenResult<Vec<String>>>()?;
enum_builder.variants(BTreeSet::from_iter(valid_values));
}
IslConstraintValue::Type(isl_type_ref) => {
if isl_type_ref.name() != "symbol" {
return invalid_abstract_data_type_error(
"Only `valid_values` constraint with values of type `symbol` are supported yet!"
);
}

let _type_name = self.handle_duplicate_constraint(
found_base_type,
"type",
isl_type_ref,
FieldPresence::Required,
code_gen_context,
)?;
found_base_type = true;
}
_ => {
return invalid_abstract_data_type_error(
"Could not determine the abstract data type due to conflicting constraints",
)
}
}
}

Ok(AbstractDataType::Enum(enum_builder.build()?))
}

/// Builds `AbstractDataType::WrappedScalar` from the given constraints.
/// ```
/// type::{
Expand Down
43 changes: 42 additions & 1 deletion src/bin/ion/commands/generate/model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use derive_builder::Builder;
use ion_schema::isl::isl_type::IslType;
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use std::fmt::Debug;
// This module contains a data model that the code generator can use to render a template based on the type of the model.
// Currently, this same data model is represented by `AbstractDataType` but it doesn't hold all the information for the template.
Expand Down Expand Up @@ -182,6 +182,8 @@ pub enum AbstractDataType {
WrappedSequence(WrappedSequence),
// A collection of field name/value pairs (e.g. a map)
Structure(Structure),
// Represents an enum type
Enum(Enum),
}

impl AbstractDataType {
Expand All @@ -203,6 +205,9 @@ impl AbstractDataType {
AbstractDataType::Structure(Structure { doc_comment, .. }) => {
doc_comment.as_ref().map(|s| s.as_str())
}
AbstractDataType::Enum(Enum { doc_comment, .. }) => {
doc_comment.as_ref().map(|s| s.as_str())
}
}
}

Expand All @@ -219,6 +224,7 @@ impl AbstractDataType {
Some(L::target_type_as_sequence(seq.element_type.to_owned()))
}
AbstractDataType::Structure(structure) => Some(structure.name.to_owned().into()),
AbstractDataType::Enum(enum_type) => Some(enum_type.name.to_owned().into()),
}
}
}
Expand Down Expand Up @@ -448,6 +454,41 @@ pub struct FieldReference(
pub(crate) FieldPresence,
);

/// Represents an enum type
/// e.g. Given below ISL,
/// ```
/// type::{
/// name: enum_type,
/// valid_values: [foo, bar, baz]
/// }
/// ```
/// Corresponding generated code in Rust would look like following:
/// ```
/// enum EnumType {
/// Foo,
/// Bar,
/// Baz
/// }
/// ```
#[allow(dead_code)]
#[derive(Debug, Clone, Builder, PartialEq, Serialize)]
#[builder(setter(into))]
pub struct Enum {
// Represents the fully qualified name for this data model
pub(crate) name: FullyQualifiedTypeName,
// The variants of this enum
variants: BTreeSet<String>,
// Represents doc comment for the generated code
#[builder(default)]
doc_comment: Option<String>,
// Represents the source ISL type which can be used to get other constraints useful for this type.
// For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type.
// This will also be useful for `text` type to verify if this is a `string` or `symbol`.
#[serde(skip_serializing_if = "is_anonymous")]
#[serde(serialize_with = "serialize_type_name")]
source: IslType,
}

#[cfg(test)]
mod model_tests {
use super::*;
Expand Down
Loading

0 comments on commit 7de9bb1

Please sign in to comment.