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 support for imports in code generation #169

Merged
merged 9 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain the purpose of this particular change? Is it just a refactoring, or is there a technical reason why it was necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It allows traversing through sub directories of the authority. Just created separate method that I cna reuse for each directory getting traversed.

}
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().clone().unwrap();
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![],
})
Comment on lines +654 to +657
Copy link
Contributor

Choose a reason for hiding this comment

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

This is only going to work if the target type is defined in the same namespace as the location from which it is referenced. If you have e.g. org.example.util.Fruit and you are trying to use it in org.example.SequenceWithImport, then compilation of the generated java code will fail with an unresolved reference error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a warning saying generated code uses a flattened namespace.

.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
Loading