Skip to content

Commit

Permalink
Adds support for imports in code generation (#169)
Browse files Browse the repository at this point in the history
* Modifies `generate_code_for_authorities` to traverse through all
subdirectories of a schema authority to generate code
* Modifies `generate` to generate code for header and inline imports
* Adds a change for scalar template
* Adds tests for inline and header imports
* Remove `--schema` option
  • Loading branch information
desaikd committed Nov 1, 2024
1 parent 5515927 commit 9e4ba44
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ mango ] // expected apple, banana or strawberry, found mango
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(apple banana) // expected list, found sexp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// simple struct with type mismatched import field
{
A: "hello",
B: false, // expected field type symbol
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ apple, strawberry ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// simple struct with all valid fields
{
A: "hello",
B: apple,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// simple struct with all valid fields
{
A: "hello",
// B: apple, // since `B` is an optional field, this is a valid struct
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// struct with unordered fields
{
B: banana,
A: "hello",
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,21 @@ void roundtripBadTestForEnumType() throws IOException {
runRoundtripBadTest("/bad/enum_type", EnumType::readFrom);
}


@Test
void roundtripBadTestForSequenceWithEnumElement() throws IOException {
runRoundtripBadTest("/bad/sequence_with_enum_element", SequenceWithEnumElement::readFrom);
}

@Test
void roundtripBadTestForSequenceWithImport() throws IOException {
runRoundtripBadTest("/bad/sequence_with_import", SequenceWithImport::readFrom);
}

@Test
void roundtripBadTestForStructWithInlineImport() throws IOException {
runRoundtripBadTest("/bad/struct_with_inline_import", StructWithInlineImport::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 @@ -206,6 +215,16 @@ void roundtripGoodTestForSequenceWithEnumElement() throws IOException {
runRoundtripGoodTest("/good/sequence_with_enum_element", SequenceWithEnumElement::readFrom, (item, writer) -> item.writeTo(writer));
}

@Test
void roundtripGoodTestForSequenceWithImport() throws IOException {
runRoundtripGoodTest("/good/sequence_with_import", SequenceWithImport::readFrom, (item, writer) -> item.writeTo(writer));
}

@Test
void roundtripGoodTestForStructWithInlineImport() throws IOException {
runRoundtripGoodTest("/good/struct_with_inline_import", StructWithInlineImport::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
13 changes: 13 additions & 0 deletions code-gen-projects/schema/sequence_with_import.isl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
schema_header::{
imports: [
{ id: "utils/fruits.isl", type: fruits }
]
}

type::{
name: sequence_with_import,
type: list,
element: fruits
}

schema_footer::{}
8 changes: 8 additions & 0 deletions code-gen-projects/schema/struct_with_inline_import.isl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type::{
name: struct_with_inline_import,
type: struct,
fields: {
A: string,
B: { id: "utils/fruits.isl", type: fruits }
}
}
4 changes: 4 additions & 0 deletions code-gen-projects/schema/utils/fruits.isl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type::{
name: fruits,
valid_values: [apple, banana, strawberry]
}
99 changes: 63 additions & 36 deletions src/bin/ion/commands/generate/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,30 +277,51 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
schema_system: &mut SchemaSystem,
) -> CodeGenResult<()> {
for authority in authorities {
// 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::<Result<Vec<_>, _>>()?;
paths.sort_by_key(|dir| dir.path());
for schema_file in paths {
let schema_file_path = schema_file.path();
let schema_id = schema_file_path.file_name().unwrap().to_str().unwrap();

let schema = schema_system.load_isl_schema(schema_id).unwrap();

self.generate(schema)?;
}
self.generate_code_for_directory(authority, None, schema_system)?;
}
Ok(())
}

/// Generates code for given Ion Schema
pub fn generate_code_for_schema(
/// Helper method to generate code for all schema files in a directory
/// `relative_path` is used to provide a relative path to the authority for a nested directory
pub fn generate_code_for_directory<P: AsRef<Path>>(
&mut self,
directory: P,
relative_path: Option<&str>,
schema_system: &mut SchemaSystem,
schema_id: &str,
) -> CodeGenResult<()> {
let schema = schema_system.load_isl_schema(schema_id).unwrap();
self.generate(schema)
let paths = fs::read_dir(&directory)?.collect::<Result<Vec<_>, _>>()?;
for schema_file in paths {
let schema_file_path = schema_file.path();

// if this is a nested directory then load schema files from it
if schema_file_path.is_dir() {
self.generate_code_for_directory(
&schema_file_path,
Some(
schema_file_path
.strip_prefix(&directory)
.unwrap()
.to_str()
.unwrap(),
),
schema_system,
)?;
} else {
let schema = if let Some(path) = relative_path {
let relative_path_with_schema_id = Path::new(path)
.join(schema_file_path.file_name().unwrap().to_str().unwrap());
schema_system
.load_isl_schema(relative_path_with_schema_id.as_path().to_str().unwrap())
} else {
schema_system
.load_isl_schema(schema_file_path.file_name().unwrap().to_str().unwrap())
}?;
self.generate(schema)?;
}
}

Ok(())
}

fn generate(&mut self, schema: IslSchema) -> CodeGenResult<()> {
Expand Down Expand Up @@ -328,7 +349,6 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
let isl_type_name = isl_type.name().unwrap().to_string();
self.generate_abstract_data_type(&isl_type_name, isl_type)?;
}

Ok(())
}

Expand Down Expand Up @@ -597,24 +617,10 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
type_name_suggestion: Option<&str>,
) -> CodeGenResult<Option<FullyQualifiedTypeReference>> {
Ok(match isl_type_ref {
IslTypeRef::Named(name, _) => {
let schema_type: IonSchemaType = name.into();
L::target_type(&schema_type)
.as_ref()
.map(|type_name| FullyQualifiedTypeReference {
type_name: vec![NamespaceNode::Type(type_name.to_string())],
parameters: vec![],
})
.map(|t| {
if field_presence == FieldPresence::Optional {
L::target_type_as_optional(t)
} else {
t
}
})
}
IslTypeRef::TypeImport(_, _) => {
unimplemented!("Imports in schema are not supported yet!");
IslTypeRef::Named(name, _) => Self::target_type_for(field_presence, name),
IslTypeRef::TypeImport(isl_import_type, _) => {
let name = isl_import_type.type_name();
Self::target_type_for(field_presence, name)
}
IslTypeRef::Anonymous(type_def, _) => {
let name = type_name_suggestion.map(|t| t.to_string()).ok_or(
Expand All @@ -637,6 +643,27 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
})
}

/// Returns the target type based on given ISL type name and field presence
fn target_type_for(
field_presence: FieldPresence,
name: &String,
) -> Option<FullyQualifiedTypeReference> {
let schema_type: IonSchemaType = name.into();
L::target_type(&schema_type)
.as_ref()
.map(|type_name| FullyQualifiedTypeReference {
type_name: vec![NamespaceNode::Type(type_name.to_string())],
parameters: vec![],
})
.map(|t| {
if field_presence == FieldPresence::Optional {
L::target_type_as_optional(t)
} else {
t
}
})
}

/// Returns error if duplicate constraints are present based `found_constraint` flag
fn handle_duplicate_constraint(
&mut self,
Expand Down
61 changes: 16 additions & 45 deletions src/bin/ion/commands/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ impl IonCliCommand for GenerateCommand {
.short('o')
.help("Output directory [default: current directory]"),
)
.arg(
Arg::new("schema")
.long("schema")
.short('s')
.help("Schema file name or schema id"),
)
// `--namespace` is required when Java language is specified for code generation
.arg(
Arg::new("namespace")
Expand Down Expand Up @@ -118,49 +112,26 @@ impl IonCliCommand for GenerateCommand {

println!("Started generating code...");

// Extract schema file provided by user
match args.get_one::<String>("schema") {
None => {
// generate code based on schema and programming language
match language {
"java" => {
Self::print_java_code_gen_warnings();
CodeGenerator::<JavaLanguage>::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect())
.generate_code_for_authorities(&authorities, &mut schema_system)?
},
"rust" => {
Self::print_rust_code_gen_warnings();
CodeGenerator::<RustLanguage>::new(output)
.generate_code_for_authorities(&authorities, &mut schema_system)?
}
_ => bail!(
"Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'",
language
)
}
}
Some(schema_id) => {
// generate code based on schema and programming language
match language {
"java" => {
Self::print_java_code_gen_warnings();
CodeGenerator::<JavaLanguage>::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect()).generate_code_for_schema(&mut schema_system, schema_id)?
},
"rust" => {
Self::print_rust_code_gen_warnings();
CodeGenerator::<RustLanguage>::new(output)
.generate_code_for_authorities(&authorities, &mut schema_system)?
}
_ => bail!(
"Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'",
language
)
}
// generate code based on schema and programming language
match language {
"java" => {
Self::print_java_code_gen_warnings();
CodeGenerator::<JavaLanguage>::new(output, namespace.unwrap().split('.').map(|s| NamespaceNode::Package(s.to_string())).collect())
.generate_code_for_authorities(&authorities, &mut schema_system)?
},
"rust" => {
Self::print_rust_code_gen_warnings();
CodeGenerator::<RustLanguage>::new(output)
.generate_code_for_authorities(&authorities, &mut schema_system)?
}
_ => bail!(
"Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'",
language
)
}

println!("Code generation complete successfully!");
println!("Path to generated code: {}", output.display());
println!("All the schema files in authority(s) are generated into a flattened namespace, path to generated code: {}", output.display());
Ok(())
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/bin/ion/commands/generate/templates/java/scalar.templ
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class {{ model.name }} {
public void writeTo(IonWriter writer) throws IOException {
{# Writes `Value` class with a single field `value` as an Ion value #}
{% if base_type | is_built_in_type == false %}
this.value.writeTo(writer)?;
this.value.writeTo(writer);
{% else %}
writer.write{{ base_type | replace(from="double", to="float") | replace(from="boolean", to="bool") | upper_camel }}(this.value);
{% endif %}
Expand Down
2 changes: 0 additions & 2 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,6 @@ mod code_gen_tests {
cmd.args([
"-X",
"generate",
"--schema",
"test_schema.isl",
"--output",
temp_dir.path().to_str().unwrap(),
"--language",
Expand Down

0 comments on commit 9e4ba44

Please sign in to comment.