From cc6a21c55684d2534e81ecc70f06a1fbc145c635 Mon Sep 17 00:00:00 2001 From: Khushboo Desai Date: Fri, 2 Feb 2024 15:08:25 -0800 Subject: [PATCH 1/4] Modifies definition of `DataModel` * Renames `DataModel` to `AbstractDataType` * Adds parameter to `Sequence` enum variant to store data type information for the sequence --- Cargo.lock | 2 +- src/bin/ion/commands/beta/generate/context.rs | 35 +-- .../ion/commands/beta/generate/generator.rs | 209 +++++++++--------- src/bin/ion/commands/beta/generate/result.rs | 2 +- .../beta/generate/templates/rust/struct.templ | 14 +- src/bin/ion/commands/beta/generate/utils.rs | 16 +- 6 files changed, 142 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 975c8db0..fb1a2a79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,7 +717,7 @@ dependencies = [ [[package]] name = "ion-cli" -version = "0.4.2" +version = "0.5.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/src/bin/ion/commands/beta/generate/context.rs b/src/bin/ion/commands/beta/generate/context.rs index 97fd7e18..f0aec6fa 100644 --- a/src/bin/ion/commands/beta/generate/context.rs +++ b/src/bin/ion/commands/beta/generate/context.rs @@ -3,40 +3,43 @@ use std::fmt::{Display, Formatter}; /// Represents a context that will be used for code generation pub struct CodeGenContext { - // Initially the data_model field is set to None. - // Once an ISL type definition is mapped to a data model this will have Some value. - pub(crate) data_model: Option, + // Initially the abstract_data_type field is set to None. + // Once an ISL type definition is mapped to an abstract data type this will have Some value. + pub(crate) abstract_data_type: Option, } impl CodeGenContext { pub fn new() -> Self { - Self { data_model: None } + Self { + abstract_data_type: None, + } } - pub fn with_data_model(&mut self, data_model: DataModel) { - self.data_model = Some(data_model); + pub fn with_abstract_data_type(&mut self, abstract_data_type: AbstractDataType) { + self.abstract_data_type = Some(abstract_data_type); } } -/// Represents a data model type that can be used to determine which templates can be used for code generation. +/// Represents an abstract data type type that can be used to determine which templates can be used for code generation. #[derive(Debug, Clone, PartialEq, Serialize)] -pub enum DataModel { - Value, // a struct with a scalar value (used for `type` constraint) - // TODO: Make Sequence parameterized over data type. - // add a data type for sequence here that can be used to read elements for that data type. - Sequence, // a struct with a sequence/collection value (used for `element` constraint) +pub enum AbstractDataType { + // a struct with a scalar value (used for `type` constraint) + Value, + // a struct with a sequence/collection value (used for `element` constraint) + // the parameter string represents the data type of the sequence + Sequence(String), Struct, } -impl Display for DataModel { +impl Display for AbstractDataType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { - DataModel::Value => "single value struct", - DataModel::Sequence => "sequence value struct", - DataModel::Struct => "struct", + AbstractDataType::Value => "single value struct", + AbstractDataType::Sequence(_) => "sequence value struct", + AbstractDataType::Struct => "struct", } ) } diff --git a/src/bin/ion/commands/beta/generate/generator.rs b/src/bin/ion/commands/beta/generate/generator.rs index 183c9f2a..6ad9d9f1 100644 --- a/src/bin/ion/commands/beta/generate/generator.rs +++ b/src/bin/ion/commands/beta/generate/generator.rs @@ -1,5 +1,5 @@ -use crate::commands::beta::generate::context::{CodeGenContext, DataModel}; -use crate::commands::beta::generate::result::{invalid_data_model_error, CodeGenResult}; +use crate::commands::beta::generate::context::{AbstractDataType, CodeGenContext}; +use crate::commands::beta::generate::result::{invalid_abstract_data_type_error, CodeGenResult}; use crate::commands::beta::generate::utils::{Field, Import, Language}; use crate::commands::beta::generate::utils::{IonSchemaType, Template}; use convert_case::{Case, Casing}; @@ -75,7 +75,7 @@ impl<'a> CodeGenerator<'a> { self.tera.register_filter("upper_camel", Self::upper_camel); for isl_type in schema.types() { - self.generate_data_model(&mut modules, isl_type)?; + self.generate_abstract_data_type(&mut modules, isl_type)?; } if self.language == Language::Rust { @@ -88,13 +88,13 @@ impl<'a> CodeGenerator<'a> { Ok(()) } - /// Generates data model based on given ISL type definition - fn generate_data_model( + /// Generates abstract data type based on given ISL type definition + fn generate_abstract_data_type( &mut self, modules: &mut Vec, isl_type: &IslType, ) -> CodeGenResult<()> { - let data_model_name = match isl_type.name().clone() { + let abstract_data_type_name = match isl_type.name().clone() { None => { format!("AnonymousType{}", self.anonymous_type_counter) } @@ -106,15 +106,15 @@ impl<'a> CodeGenerator<'a> { let mut imports: Vec = vec![]; let mut code_gen_context = CodeGenContext::new(); - // Set the target kind name of the data model (i.e. enum/class) + // Set the target kind name of the abstract data type (i.e. enum/class) context.insert( "target_kind_name", - &data_model_name.to_case(Case::UpperCamel), + &abstract_data_type_name.to_case(Case::UpperCamel), ); let constraints = isl_type.constraints(); for constraint in constraints { - self.map_constraint_to_data_model( + self.map_constraint_to_abstract_data_type( modules, &mut tera_fields, &mut imports, @@ -126,13 +126,13 @@ impl<'a> CodeGenerator<'a> { // add imports for the template context.insert("imports", &imports); - // generate read and write APIs for the data model + // generate read and write APIs for the abstract data type self.generate_read_api(&mut context, &mut tera_fields, &mut code_gen_context)?; self.generate_write_api(&mut context, &mut tera_fields, &mut code_gen_context); - modules.push(self.language.file_name(&data_model_name)); + modules.push(self.language.file_name(&abstract_data_type_name)); // Render or generate file for the template with the given context - let template: &Template = &code_gen_context.data_model.as_ref().try_into()?; + let template: &Template = &code_gen_context.abstract_data_type.as_ref().try_into()?; let rendered = self .tera .render( @@ -142,15 +142,15 @@ impl<'a> CodeGenerator<'a> { .unwrap(); let mut file = File::create(self.output.join(format!( "{}.{}", - self.language.file_name(&data_model_name), + self.language.file_name(&abstract_data_type_name), self.language.file_extension() )))?; file.write_all(rendered.as_bytes())?; Ok(()) } - /// Maps the given constraint value to a data model - fn map_constraint_to_data_model( + /// Maps the given constraint value to an abstract data type + fn map_constraint_to_abstract_data_type( &mut self, modules: &mut Vec, tera_fields: &mut Vec, @@ -160,48 +160,42 @@ impl<'a> CodeGenerator<'a> { ) -> CodeGenResult<()> { match constraint.constraint() { IslConstraintValue::Element(isl_type, _) => { - self.verify_data_model_consistency(DataModel::Sequence, code_gen_context)?; - self.generate_struct_field( - tera_fields, - isl_type, - modules, - "value", - imports, + let type_name = self.type_reference_name(isl_type, modules, imports)?; + self.verify_abstract_data_type_consistency( + AbstractDataType::Sequence(type_name.to_owned()), code_gen_context, )?; + self.generate_struct_field(tera_fields, type_name, "value", code_gen_context)?; } IslConstraintValue::Fields(fields, _content_closed) => { - self.verify_data_model_consistency(DataModel::Struct, code_gen_context)?; + self.verify_abstract_data_type_consistency( + AbstractDataType::Struct, + code_gen_context, + )?; for (name, value) in fields.iter() { - self.generate_struct_field( - tera_fields, - value.type_reference(), - modules, - name, - imports, - code_gen_context, - )?; + let type_name = + self.type_reference_name(value.type_reference(), modules, imports)?; + + self.generate_struct_field(tera_fields, type_name, name, code_gen_context)?; } } IslConstraintValue::Type(isl_type) => { - self.verify_data_model_consistency(DataModel::Value, code_gen_context)?; - self.generate_struct_field( - tera_fields, - isl_type, - modules, - "value", - imports, + let type_name = self.type_reference_name(isl_type, modules, imports)?; + + self.verify_abstract_data_type_consistency( + AbstractDataType::Value, code_gen_context, )?; + self.generate_struct_field(tera_fields, type_name, "value", code_gen_context)?; } _ => {} } Ok(()) } - /// Verify that the current data model is same as previously determined data model - /// This is referring to data model determined with each constraint that is verifies - /// that all the constraints map to a single data model and not different data models. + /// Verify that the current abstract data type is same as previously determined abstract data type + /// This is referring to abstract data type determined with each constraint that is verifies + /// that all the constraints map to a single abstract data type and not different abstract data types. /// e.g. /// ``` /// type::{ @@ -213,54 +207,29 @@ impl<'a> CodeGenerator<'a> { /// } /// } /// ``` - /// For the above schema, both `fields` and `type` constraints map to different data models + /// For the above schema, both `fields` and `type` constraints map to different abstract data types /// respectively Struct(with given fields `source` and `destination`) and Value(with a single field that has String data type). - fn verify_data_model_consistency( + fn verify_abstract_data_type_consistency( &mut self, - current_data_model: DataModel, + current_abstract_data_type: AbstractDataType, code_gen_context: &mut CodeGenContext, ) -> CodeGenResult<()> { - if let Some(data_model) = &code_gen_context.data_model { - if data_model != ¤t_data_model { - return invalid_data_model_error(format!("Can not determine abstract data type as current constraint {} conflicts with prior constraints for {}.", current_data_model, data_model)); + if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { + if abstract_data_type != ¤t_abstract_data_type { + return invalid_abstract_data_type_error(format!("Can not determine abstract data type as current constraint {} conflicts with prior constraints for {}.", current_abstract_data_type, abstract_data_type)); } } else { - code_gen_context.with_data_model(current_data_model); + code_gen_context.with_abstract_data_type(current_abstract_data_type); } Ok(()) } - /// Generates a struct field based on field name and value(data type) - fn generate_struct_field( + /// Provides name of the type reference that will be used for generated abstract data type + fn type_reference_name( &mut self, - tera_fields: &mut Vec, isl_type_ref: &IslTypeRef, modules: &mut Vec, - field_name: &str, imports: &mut Vec, - code_gen_context: &mut CodeGenContext, - ) -> CodeGenResult<()> { - let value = self.generate_field_value(isl_type_ref, modules, imports, code_gen_context)?; - - tera_fields.push(Field { - name: { - match self.language { - Language::Rust => field_name.to_case(Case::Snake), - Language::Java => field_name.to_case(Case::Camel), - } - }, - value, - }); - Ok(()) - } - - /// Generates field value in a struct which represents a data type in codegen's programming language - fn generate_field_value( - &mut self, - isl_type_ref: &IslTypeRef, - modules: &mut Vec, - imports: &mut Vec, - code_gen_context: &mut CodeGenContext, ) -> CodeGenResult { Ok(match isl_type_ref { IslTypeRef::Named(name, _) => { @@ -271,34 +240,63 @@ impl<'a> CodeGenerator<'a> { }); } let schema_type: IonSchemaType = name.into(); - self.generate_sequence_field_value( - schema_type.target_type(&self.language).to_string(), - code_gen_context, - ) + schema_type.target_type(&self.language).to_string() } IslTypeRef::TypeImport(_, _) => { unimplemented!("Imports in schema are not supported yet!"); } IslTypeRef::Anonymous(type_def, _) => { self.anonymous_type_counter += 1; - self.generate_data_model(modules, type_def)?; + self.generate_abstract_data_type(modules, type_def)?; let name = format!("AnonymousType{}", self.anonymous_type_counter); imports.push(Import { module_name: name.to_case(Case::Snake), type_name: name.to_case(Case::UpperCamel), }); - self.generate_sequence_field_value(name, code_gen_context) + name } }) } + /// Generates a struct field based on field name and value(data type) + fn generate_struct_field( + &mut self, + tera_fields: &mut Vec, + abstraxt_data_type_name: String, + field_name: &str, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult<()> { + let value = self.generate_field_value(abstraxt_data_type_name, code_gen_context)?; + + tera_fields.push(Field { + name: { + match self.language { + Language::Rust => field_name.to_case(Case::Snake), + Language::Java => field_name.to_case(Case::Camel), + } + }, + value, + }); + Ok(()) + } + + /// Generates field value in a struct which represents a data type in codegen's programming language + fn generate_field_value( + &mut self, + abstract_data_type_name: String, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult { + Ok(self.generate_sequence_field_value(abstract_data_type_name, code_gen_context)) + } + /// Generates an appropriately typed sequence in the target programming language to use as a field value pub fn generate_sequence_field_value( &mut self, name: String, code_gen_context: &mut CodeGenContext, ) -> String { - if code_gen_context.data_model == Some(DataModel::Sequence) { + if code_gen_context.abstract_data_type == Some(AbstractDataType::Sequence(name.to_owned())) + { return match self.language { Language::Rust => { format!("Vec<{}>", name) @@ -322,24 +320,29 @@ impl<'a> CodeGenerator<'a> { ) -> CodeGenResult<()> { let mut read_statements = vec![]; - if code_gen_context.data_model == Some(DataModel::Struct) - || code_gen_context.data_model == Some(DataModel::Value) - || code_gen_context.data_model == Some(DataModel::Sequence) + if code_gen_context.abstract_data_type == Some(AbstractDataType::Struct) + || code_gen_context.abstract_data_type == Some(AbstractDataType::Value) + || matches!( + code_gen_context.abstract_data_type, + Some(AbstractDataType::Sequence(_)) + ) { context.insert("fields", &tera_fields); - if let Some(data_model) = &code_gen_context.data_model { - context.insert("data_model", data_model); + if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { + context.insert("abstract_data_type", abstract_data_type); } else { - return invalid_data_model_error( - "Can not determine data model, constraints are mapping not mapping to a data model.", + return invalid_abstract_data_type_error( + "Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.", ); } for tera_field in tera_fields { if !self.is_built_in_type(&tera_field.value) { - if code_gen_context.data_model == Some(DataModel::Sequence) { + if let Some(AbstractDataType::Sequence(sequence_type)) = + &code_gen_context.abstract_data_type + { read_statements.push(format!( - "\"{}\" => {{ data_model.{} =", + "\"{}\" => {{ abstract_data_type.{} =", &tera_field.name, &tera_field.name, )); read_statements.push( @@ -349,7 +352,6 @@ impl<'a> CodeGenerator<'a> { while reader.next()? != StreamItem::Nothing {"# .to_string(), ); - let sequence_type = &tera_field.value.replace("Vec<", "").replace('>', ""); if !self.is_built_in_type(sequence_type) { read_statements.push(format!( "values.push({}::read_from(reader)?)", @@ -367,26 +369,26 @@ impl<'a> CodeGenerator<'a> { values }}"# .to_string(), ); - } else if code_gen_context.data_model == Some(DataModel::Value) { + } else if code_gen_context.abstract_data_type == Some(AbstractDataType::Value) { context.insert( "read_statement", &format!("{}::read_from(reader)?", &tera_field.value,), ); } else { read_statements.push(format!( - "\"{}\" => {{ data_model.{} = {}::read_from(reader)?;}}", + "\"{}\" => {{ abstract_data_type.{} = {}::read_from(reader)?;}}", &tera_field.name, &tera_field.name, &tera_field.value, )); } } else { - if code_gen_context.data_model == Some(DataModel::Value) { + if code_gen_context.abstract_data_type == Some(AbstractDataType::Value) { context.insert( "read_statement", &format!("reader.read_{}()?", &tera_field.value.to_lowercase(),), ); } read_statements.push(format!( - "\"{}\" => {{ data_model.{} = reader.read_{}()?;}}", + "\"{}\" => {{ abstract_data_type.{} = reader.read_{}()?;}}", &tera_field.name, &tera_field.name, &tera_field.value.to_lowercase() @@ -398,8 +400,8 @@ impl<'a> CodeGenerator<'a> { Ok(()) } - /// Generates write API for a data model - /// This adds statements for writing data model as Ion value that will be used by data model templates + /// Generates write API for an abstract data type + /// This adds statements for writing abstract data type as Ion value that will be used by abstract data type templates // TODO: add support for Java fn generate_write_api( &mut self, @@ -408,7 +410,7 @@ impl<'a> CodeGenerator<'a> { code_gen_context: &mut CodeGenContext, ) { let mut write_statements = Vec::new(); - if code_gen_context.data_model == Some(DataModel::Value) { + if code_gen_context.abstract_data_type == Some(AbstractDataType::Value) { for tera_field in tera_fields { if !self.is_built_in_type(&tera_field.value) { write_statements.push(format!("self.{}.write_to(writer)?;", &tera_field.name,)); @@ -419,7 +421,7 @@ impl<'a> CodeGenerator<'a> { )); } } - } else if code_gen_context.data_model == Some(DataModel::Struct) { + } else if code_gen_context.abstract_data_type == Some(AbstractDataType::Struct) { write_statements.push("writer.step_in(IonType::Struct)?;".to_string()); for tera_field in tera_fields { write_statements.push(format!("writer.set_field_name(\"{}\");", &tera_field.name)); @@ -435,10 +437,11 @@ impl<'a> CodeGenerator<'a> { } } write_statements.push("writer.step_out()?;".to_string()); - } else if code_gen_context.data_model == Some(DataModel::Sequence) { + } else if let Some(AbstractDataType::Sequence(sequence_type)) = + &code_gen_context.abstract_data_type + { write_statements.push("writer.step_in(IonType::List)?;".to_string()); - for tera_field in tera_fields { - let sequence_type = &tera_field.value.replace("Vec<", "").replace('>', ""); + for _tera_field in tera_fields { write_statements.push("for value in self.value {".to_string()); if !self.is_built_in_type(sequence_type) { write_statements.push("value.write_to(writer)?;".to_string()); diff --git a/src/bin/ion/commands/beta/generate/result.rs b/src/bin/ion/commands/beta/generate/result.rs index 5c338103..16b9ea73 100644 --- a/src/bin/ion/commands/beta/generate/result.rs +++ b/src/bin/ion/commands/beta/generate/result.rs @@ -28,7 +28,7 @@ pub enum CodeGenError { /// A convenience method for creating an CodeGen containing an CodeGenError::InvalidDataModel /// with the provided description text. -pub fn invalid_data_model_error>(description: S) -> CodeGenResult { +pub fn invalid_abstract_data_type_error>(description: S) -> CodeGenResult { Err(CodeGenError::InvalidDataModel { description: description.as_ref().to_string(), }) diff --git a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ index e49560b6..217ec7c9 100644 --- a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ +++ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ @@ -28,12 +28,12 @@ impl {{ target_kind_name }} { {% if statements %} pub fn read_from(reader: &mut Reader) -> IonResult { reader.next()?; - {% if data_model == "UnitStruct"%} - let mut data_model = {{ target_kind_name }}::default(); - data_model.value = {{ read_statement }}; + {% if abstract_data_type == "UnitStruct"%} + let mut abstract_data_type = {{ target_kind_name }}::default(); + abstract_data_type.value = {{ read_statement }}; {% else %} - {% if data_model == "Struct"%}reader.step_in()?;{% endif %} - let mut data_model = {{ target_kind_name }}::default(); + {% if abstract_data_type == "Struct"%}reader.step_in()?;{% endif %} + let mut abstract_data_type = {{ target_kind_name }}::default(); while reader.next()? != StreamItem::Nothing { if let Some(field_name) = reader.field_name()?.text() { match field_name { @@ -45,9 +45,9 @@ impl {{ target_kind_name }} { } } } - {% if data_model == "Struct"%}reader.step_out()?;{% endif %} + {% if abstract_data_type == "Struct"%}reader.step_out()?;{% endif %} {% endif %} - Ok(data_model) + Ok(abstract_data_type) } {% endif %} diff --git a/src/bin/ion/commands/beta/generate/utils.rs b/src/bin/ion/commands/beta/generate/utils.rs index 8e53157c..32e3c967 100644 --- a/src/bin/ion/commands/beta/generate/utils.rs +++ b/src/bin/ion/commands/beta/generate/utils.rs @@ -1,5 +1,5 @@ -use crate::commands::beta::generate::context::DataModel; -use crate::commands::beta::generate::result::{invalid_data_model_error, CodeGenError}; +use crate::commands::beta::generate::context::AbstractDataType; +use crate::commands::beta::generate::result::{invalid_abstract_data_type_error, CodeGenError}; use convert_case::{Case, Casing}; use serde::Serialize; use std::fmt::{Display, Formatter}; @@ -86,15 +86,15 @@ impl Template { } } -impl TryFrom> for Template { +impl TryFrom> for Template { type Error = CodeGenError; - fn try_from(value: Option<&DataModel>) -> Result { + fn try_from(value: Option<&AbstractDataType>) -> Result { match value { - Some(DataModel::Value) | Some(DataModel::Sequence) | Some(DataModel::Struct) => { - Ok(Template::Struct) - } - None => invalid_data_model_error( + Some(AbstractDataType::Value) + | Some(AbstractDataType::Sequence(_)) + | Some(AbstractDataType::Struct) => Ok(Template::Struct), + None => invalid_abstract_data_type_error( "Can not get a template without determining data model first.", ), } From 6bd7644e534118ebbda2cc860fc3e8e918056185 Mon Sep 17 00:00:00 2001 From: Khushboo Desai Date: Wed, 7 Feb 2024 13:14:02 -0800 Subject: [PATCH 2/4] Adds `Language` trait for generator * Adds `Language` as trait insatead of enum * Adds `RustLanguage` and `JavaLanguage` as implementations of `Language` * Uses `Language` trait as generic for `CodeGenerator` * Moves read and write API generation code to templates * Reorganizes `CodeGenerator` to keep common behavior between languages in `CodeGenerator` * Moves Rust related generation cod e in `CodeGenerator` * Moves Java related generation cod e in `CodeGenerator` --- .../ion/commands/beta/generate/generator.rs | 469 +++++++----------- src/bin/ion/commands/beta/generate/mod.rs | 16 +- .../beta/generate/templates/rust/struct.templ | 70 ++- src/bin/ion/commands/beta/generate/utils.rs | 183 ++++--- tests/cli.rs | 4 +- 5 files changed, 376 insertions(+), 366 deletions(-) diff --git a/src/bin/ion/commands/beta/generate/generator.rs b/src/bin/ion/commands/beta/generate/generator.rs index 6ad9d9f1..8a99a093 100644 --- a/src/bin/ion/commands/beta/generate/generator.rs +++ b/src/bin/ion/commands/beta/generate/generator.rs @@ -1,6 +1,6 @@ use crate::commands::beta::generate::context::{AbstractDataType, CodeGenContext}; use crate::commands::beta::generate::result::{invalid_abstract_data_type_error, CodeGenResult}; -use crate::commands::beta::generate::utils::{Field, Import, Language}; +use crate::commands::beta::generate::utils::{Field, Import, JavaLanguage, Language, RustLanguage}; use crate::commands::beta::generate::utils::{IonSchemaType, Template}; use convert_case::{Case, Casing}; use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue}; @@ -10,42 +10,91 @@ use ion_schema::isl::IslSchema; use std::collections::HashMap; use std::fs::File; use std::io::Write; +use std::marker::PhantomData; use std::path::Path; use tera::{Context, Tera}; -// TODO: generator can store language and output path as it doesn't change during code generation process -pub(crate) struct CodeGenerator<'a> { +pub(crate) struct CodeGenerator<'a, L: Language> { // Represents the templating engine - tera // more information: https://docs.rs/tera/latest/tera/ pub(crate) tera: Tera, - language: Language, output: &'a Path, // Represents a counter for naming anonymous type definitions pub(crate) anonymous_type_counter: usize, + phantom: PhantomData, } -impl<'a> CodeGenerator<'a> { - pub fn new(language: Language, output: &'a Path) -> Self { +impl<'a> CodeGenerator<'a, RustLanguage> { + pub fn new(output: &'a Path) -> CodeGenerator { Self { - language, output, anonymous_type_counter: 0, tera: Tera::new("src/bin/ion/commands/beta/generate/templates/**/*.templ").unwrap(), + phantom: PhantomData, } } - /// Returns true if its a built in type otherwise returns false - pub fn is_built_in_type(&self, name: &str) -> bool { - match self.language { - Language::Rust => { - matches!(name, "i64" | "String" | "bool" | "Vec" | "f64") - } - Language::Java => { - matches!(name, "int" | "String" | "boolean" | "byte[]" | "float") - } + /// Generates code in Rust for given Ion Schema + pub fn generate(&mut self, schema: IslSchema) -> CodeGenResult<()> { + // this will be used for Rust to create mod.rs which lists all the generated modules + let mut modules = vec![]; + let mut module_context = tera::Context::new(); + + // Register a tera filter that can be used to convert a string to upper camel case + self.tera.register_filter("upper_camel", Self::upper_camel); + // Register a tera filter that can be used to see if a type is built in data type or not + self.tera + .register_filter("is_built_in_type", Self::is_built_in_type); + + for isl_type in schema.types() { + self.generate_abstract_data_type(&mut modules, isl_type)?; } + + self.generate_modules(&mut modules, &mut module_context)?; + + Ok(()) } + pub fn generate_modules( + &mut self, + modules: &mut Vec, + module_context: &mut Context, + ) -> CodeGenResult<()> { + module_context.insert("modules", &modules); + let rendered = self.tera.render("rust/mod.templ", module_context)?; + let mut file = File::create(self.output.join("mod.rs"))?; + file.write_all(rendered.as_bytes())?; + Ok(()) + } +} + +impl<'a> CodeGenerator<'a, JavaLanguage> { + pub fn new(output: &'a Path) -> CodeGenerator { + Self { + output, + anonymous_type_counter: 0, + tera: Tera::new("src/bin/ion/commands/beta/generate/templates/**/*.templ").unwrap(), + phantom: PhantomData, + } + } + + /// Generates code in Java for given Ion Schema + pub fn generate(&mut self, schema: IslSchema) -> CodeGenResult<()> { + // this will be used for Rust to create mod.rs which lists all the generated modules + let mut modules = vec![]; + + // Register a tera filter that can be used to convert a string to upper camel case + self.tera.register_filter("upper_camel", Self::upper_camel); + + for isl_type in schema.types() { + self.generate_abstract_data_type(&mut modules, isl_type)?; + } + + Ok(()) + } +} + +impl<'a, L: Language> CodeGenerator<'a, L> { /// Represents a [tera] filter that converts given tera string value to [upper camel case]. /// Returns error if the given value is not a string. /// @@ -65,41 +114,31 @@ impl<'a> CodeGenerator<'a> { )) } - /// Generates code for given Ion Schema - pub fn generate(&mut self, schema: IslSchema) -> CodeGenResult<()> { - // this will be used for Rust to create mod.rs which lists all the generated modules - let mut modules = vec![]; - let mut module_context = tera::Context::new(); - - // Register a tera filter that can be used to convert a string to upper camel case - self.tera.register_filter("upper_camel", Self::upper_camel); - - for isl_type in schema.types() { - self.generate_abstract_data_type(&mut modules, isl_type)?; - } - - if self.language == Language::Rust { - module_context.insert("modules", &modules); - let rendered = self.tera.render("rust/mod.templ", &module_context)?; - let mut file = File::create(self.output.join("mod.rs"))?; - file.write_all(rendered.as_bytes())?; - } - - Ok(()) + /// Represents a [tera] filter that return true if the value is a built in type, otherwise returns false. + /// + /// For more information: + /// + /// [tera]: + pub fn is_built_in_type( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + Ok(tera::Value::Bool(L::is_built_in_type( + value + .as_str() + .ok_or(tera::Error::msg("Required string for this filter"))?, + ))) } - /// Generates abstract data type based on given ISL type definition fn generate_abstract_data_type( &mut self, modules: &mut Vec, isl_type: &IslType, ) -> CodeGenResult<()> { - let abstract_data_type_name = match isl_type.name().clone() { - None => { - format!("AnonymousType{}", self.anonymous_type_counter) - } - Some(name) => name, - }; + let abstract_data_type_name = isl_type + .name() + .clone() + .unwrap_or_else(|| format!("AnonymousType{}", self.anonymous_type_counter)); let mut context = Context::new(); let mut tera_fields = vec![]; @@ -126,29 +165,97 @@ impl<'a> CodeGenerator<'a> { // add imports for the template context.insert("imports", &imports); - // generate read and write APIs for the abstract data type - self.generate_read_api(&mut context, &mut tera_fields, &mut code_gen_context)?; - self.generate_write_api(&mut context, &mut tera_fields, &mut code_gen_context); - modules.push(self.language.file_name(&abstract_data_type_name)); + // add fields for template + if code_gen_context.abstract_data_type == Some(AbstractDataType::Struct) + || code_gen_context.abstract_data_type == Some(AbstractDataType::Value) + || matches!( + code_gen_context.abstract_data_type, + Some(AbstractDataType::Sequence(_)) + ) + { + context.insert("fields", &tera_fields); + if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { + context.insert("abstract_data_type", abstract_data_type); + } else { + return invalid_abstract_data_type_error( + "Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.", + ); + } + } + + self.render_generated_code( + modules, + &abstract_data_type_name, + &mut context, + &mut code_gen_context, + ) + } + + fn render_generated_code( + &mut self, + modules: &mut Vec, + abstract_data_type_name: &str, + context: &mut Context, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult<()> { + modules.push(L::file_name(abstract_data_type_name)); // Render or generate file for the template with the given context let template: &Template = &code_gen_context.abstract_data_type.as_ref().try_into()?; let rendered = self .tera .render( - &format!("{}/{}.templ", &self.language, template.name(&self.language)), - &context, + &format!( + "{}/{}.templ", + L::string_value(), + L::template_as_string(template) + ), + context, ) .unwrap(); let mut file = File::create(self.output.join(format!( "{}.{}", - self.language.file_name(&abstract_data_type_name), - self.language.file_extension() + L::file_name(abstract_data_type_name), + L::file_extension() )))?; file.write_all(rendered.as_bytes())?; Ok(()) } + /// 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, + modules: &mut Vec, + imports: &mut Vec, + ) -> CodeGenResult { + Ok(match isl_type_ref { + IslTypeRef::Named(name, _) => { + if !L::is_built_in_type(name) { + imports.push(Import { + module_name: name.to_case(Case::Snake), + type_name: name.to_case(Case::UpperCamel), + }); + } + let schema_type: IonSchemaType = name.into(); + L::target_type(&schema_type) + } + IslTypeRef::TypeImport(_, _) => { + unimplemented!("Imports in schema are not supported yet!"); + } + IslTypeRef::Anonymous(type_def, _) => { + self.anonymous_type_counter += 1; + self.generate_abstract_data_type(modules, type_def)?; + let name = format!("AnonymousType{}", self.anonymous_type_counter); + imports.push(Import { + module_name: name.to_case(Case::Snake), + type_name: name.to_case(Case::UpperCamel), + }); + name + } + }) + } + /// Maps the given constraint value to an abstract data type fn map_constraint_to_abstract_data_type( &mut self, @@ -165,7 +272,11 @@ impl<'a> CodeGenerator<'a> { AbstractDataType::Sequence(type_name.to_owned()), code_gen_context, )?; - self.generate_struct_field(tera_fields, type_name, "value", code_gen_context)?; + self.generate_struct_field( + tera_fields, + L::target_type_as_sequence(&type_name), + "value", + )?; } IslConstraintValue::Fields(fields, _content_closed) => { self.verify_abstract_data_type_consistency( @@ -176,7 +287,7 @@ impl<'a> CodeGenerator<'a> { let type_name = self.type_reference_name(value.type_reference(), modules, imports)?; - self.generate_struct_field(tera_fields, type_name, name, code_gen_context)?; + self.generate_struct_field(tera_fields, type_name, name)?; } } IslConstraintValue::Type(isl_type) => { @@ -186,13 +297,27 @@ impl<'a> CodeGenerator<'a> { AbstractDataType::Value, code_gen_context, )?; - self.generate_struct_field(tera_fields, type_name, "value", code_gen_context)?; + self.generate_struct_field(tera_fields, type_name, "value")?; } _ => {} } Ok(()) } + /// Generates a struct field based on field name and value(data type) + fn generate_struct_field( + &mut self, + tera_fields: &mut Vec, + abstract_data_type_name: String, + field_name: &str, + ) -> CodeGenResult<()> { + tera_fields.push(Field { + name: field_name.to_case(L::field_name_case()), + value: abstract_data_type_name, + }); + Ok(()) + } + /// Verify that the current abstract data type is same as previously determined abstract data type /// This is referring to abstract data type determined with each constraint that is verifies /// that all the constraints map to a single abstract data type and not different abstract data types. @@ -223,238 +348,4 @@ impl<'a> CodeGenerator<'a> { } Ok(()) } - - /// 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, - modules: &mut Vec, - imports: &mut Vec, - ) -> CodeGenResult { - Ok(match isl_type_ref { - IslTypeRef::Named(name, _) => { - if !self.is_built_in_type(name) { - imports.push(Import { - module_name: name.to_case(Case::Snake), - type_name: name.to_case(Case::UpperCamel), - }); - } - let schema_type: IonSchemaType = name.into(); - schema_type.target_type(&self.language).to_string() - } - IslTypeRef::TypeImport(_, _) => { - unimplemented!("Imports in schema are not supported yet!"); - } - IslTypeRef::Anonymous(type_def, _) => { - self.anonymous_type_counter += 1; - self.generate_abstract_data_type(modules, type_def)?; - let name = format!("AnonymousType{}", self.anonymous_type_counter); - imports.push(Import { - module_name: name.to_case(Case::Snake), - type_name: name.to_case(Case::UpperCamel), - }); - name - } - }) - } - - /// Generates a struct field based on field name and value(data type) - fn generate_struct_field( - &mut self, - tera_fields: &mut Vec, - abstraxt_data_type_name: String, - field_name: &str, - code_gen_context: &mut CodeGenContext, - ) -> CodeGenResult<()> { - let value = self.generate_field_value(abstraxt_data_type_name, code_gen_context)?; - - tera_fields.push(Field { - name: { - match self.language { - Language::Rust => field_name.to_case(Case::Snake), - Language::Java => field_name.to_case(Case::Camel), - } - }, - value, - }); - Ok(()) - } - - /// Generates field value in a struct which represents a data type in codegen's programming language - fn generate_field_value( - &mut self, - abstract_data_type_name: String, - code_gen_context: &mut CodeGenContext, - ) -> CodeGenResult { - Ok(self.generate_sequence_field_value(abstract_data_type_name, code_gen_context)) - } - - /// Generates an appropriately typed sequence in the target programming language to use as a field value - pub fn generate_sequence_field_value( - &mut self, - name: String, - code_gen_context: &mut CodeGenContext, - ) -> String { - if code_gen_context.abstract_data_type == Some(AbstractDataType::Sequence(name.to_owned())) - { - return match self.language { - Language::Rust => { - format!("Vec<{}>", name) - } - Language::Java => { - format!("ArrayList<{}>", name) - } - }; - } - name - } - - /// Generates Generates a read API for an abstract data type. - /// This adds statements for reading each the Ion value(s) that collectively represent the given abstract data type. - // TODO: add support for Java - fn generate_read_api( - &mut self, - context: &mut Context, - tera_fields: &mut Vec, - code_gen_context: &mut CodeGenContext, - ) -> CodeGenResult<()> { - let mut read_statements = vec![]; - - if code_gen_context.abstract_data_type == Some(AbstractDataType::Struct) - || code_gen_context.abstract_data_type == Some(AbstractDataType::Value) - || matches!( - code_gen_context.abstract_data_type, - Some(AbstractDataType::Sequence(_)) - ) - { - context.insert("fields", &tera_fields); - if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { - context.insert("abstract_data_type", abstract_data_type); - } else { - return invalid_abstract_data_type_error( - "Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.", - ); - } - - for tera_field in tera_fields { - if !self.is_built_in_type(&tera_field.value) { - if let Some(AbstractDataType::Sequence(sequence_type)) = - &code_gen_context.abstract_data_type - { - read_statements.push(format!( - "\"{}\" => {{ abstract_data_type.{} =", - &tera_field.name, &tera_field.name, - )); - read_statements.push( - r#"{ - let mut values = vec![]; - reader.step_in()?; - while reader.next()? != StreamItem::Nothing {"# - .to_string(), - ); - if !self.is_built_in_type(sequence_type) { - read_statements.push(format!( - "values.push({}::read_from(reader)?)", - sequence_type - )); - } else { - read_statements.push(format!( - "values.push(reader.read_{}()?)", - sequence_type.to_lowercase() - )); - } - - read_statements.push( - r#"} - values }}"# - .to_string(), - ); - } else if code_gen_context.abstract_data_type == Some(AbstractDataType::Value) { - context.insert( - "read_statement", - &format!("{}::read_from(reader)?", &tera_field.value,), - ); - } else { - read_statements.push(format!( - "\"{}\" => {{ abstract_data_type.{} = {}::read_from(reader)?;}}", - &tera_field.name, &tera_field.name, &tera_field.value, - )); - } - } else { - if code_gen_context.abstract_data_type == Some(AbstractDataType::Value) { - context.insert( - "read_statement", - &format!("reader.read_{}()?", &tera_field.value.to_lowercase(),), - ); - } - read_statements.push(format!( - "\"{}\" => {{ abstract_data_type.{} = reader.read_{}()?;}}", - &tera_field.name, - &tera_field.name, - &tera_field.value.to_lowercase() - )); - } - } - } - context.insert("statements", &read_statements); - Ok(()) - } - - /// Generates write API for an abstract data type - /// This adds statements for writing abstract data type as Ion value that will be used by abstract data type templates - // TODO: add support for Java - fn generate_write_api( - &mut self, - context: &mut Context, - tera_fields: &mut Vec, - code_gen_context: &mut CodeGenContext, - ) { - let mut write_statements = Vec::new(); - if code_gen_context.abstract_data_type == Some(AbstractDataType::Value) { - for tera_field in tera_fields { - if !self.is_built_in_type(&tera_field.value) { - write_statements.push(format!("self.{}.write_to(writer)?;", &tera_field.name,)); - } else { - write_statements.push(format!( - "writer.write_{}(self.value)?;", - &tera_field.value.to_lowercase(), - )); - } - } - } else if code_gen_context.abstract_data_type == Some(AbstractDataType::Struct) { - write_statements.push("writer.step_in(IonType::Struct)?;".to_string()); - for tera_field in tera_fields { - write_statements.push(format!("writer.set_field_name(\"{}\");", &tera_field.name)); - - if !self.is_built_in_type(&tera_field.value) { - write_statements.push(format!("self.{}.write_to(writer)?;", &tera_field.name,)); - } else { - write_statements.push(format!( - "writer.write_{}(self.{})?;", - &tera_field.value.to_lowercase(), - &tera_field.name - )); - } - } - write_statements.push("writer.step_out()?;".to_string()); - } else if let Some(AbstractDataType::Sequence(sequence_type)) = - &code_gen_context.abstract_data_type - { - write_statements.push("writer.step_in(IonType::List)?;".to_string()); - for _tera_field in tera_fields { - write_statements.push("for value in self.value {".to_string()); - if !self.is_built_in_type(sequence_type) { - write_statements.push("value.write_to(writer)?;".to_string()); - } else { - write_statements.push(format!( - "writer.write_{}(value)?;", - &sequence_type.to_lowercase(), - )); - } - write_statements.push("}".to_string()); - } - write_statements.push("writer.step_out()?;".to_string()); - } - context.insert("write_statements", &write_statements); - } } diff --git a/src/bin/ion/commands/beta/generate/mod.rs b/src/bin/ion/commands/beta/generate/mod.rs index 0abb0e78..23123334 100644 --- a/src/bin/ion/commands/beta/generate/mod.rs +++ b/src/bin/ion/commands/beta/generate/mod.rs @@ -4,15 +4,14 @@ mod result; mod utils; use crate::commands::beta::generate::generator::CodeGenerator; -use crate::commands::beta::generate::utils::Language; +use crate::commands::beta::generate::utils::{JavaLanguage, RustLanguage}; use crate::commands::IonCliCommand; -use anyhow::Result; +use anyhow::{bail, Result}; use clap::{Arg, ArgAction, ArgMatches, Command}; use ion_schema::authority::{DocumentAuthority, FileSystemDocumentAuthority}; use ion_schema::system::SchemaSystem; use std::fs; use std::path::{Path, PathBuf}; - pub struct GenerateCommand; impl IonCliCommand for GenerateCommand { @@ -62,7 +61,7 @@ impl IonCliCommand for GenerateCommand { fn run(&self, _command_path: &mut Vec, args: &ArgMatches) -> Result<()> { // Extract programming language for code generation - let language: Language = args.get_one::("language").unwrap().as_str().into(); + let language: &str = args.get_one::("language").unwrap().as_str(); // Extract output path information where the generated code will be saved // Create a module `ion_data_model` for storing all the generated code in the output directory @@ -102,7 +101,14 @@ impl IonCliCommand for GenerateCommand { println!("Started generating code..."); // generate code based on schema and programming language - CodeGenerator::new(language, output).generate(schema)?; + match language { + "java" => CodeGenerator::::new(output).generate(schema)?, + "rust" => CodeGenerator::::new(output).generate(schema)?, + _ => bail!( + "Unsupported programming language: {}, this tool only supports Java and Rust code generation.", + language + ) + } println!("Code generation complete successfully!"); println!("Path to generated code: {}", output.display()); diff --git a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ index 217ec7c9..cb319179 100644 --- a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ +++ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ @@ -25,22 +25,44 @@ impl {{ target_kind_name }} { } {% endfor %} - {% if statements %} + pub fn read_from(reader: &mut Reader) -> IonResult { reader.next()?; - {% if abstract_data_type == "UnitStruct"%} + {% if abstract_data_type == "Value"%} let mut abstract_data_type = {{ target_kind_name }}::default(); - abstract_data_type.value = {{ read_statement }}; + abstract_data_type.value = {% if target_kind_name | is_built_in_type == false %} + {{ target_kind_name }}::read_from(reader)?; + {% else %} + reader.read_{{ target_kind_name }}()?; + {% endif %} {% else %} {% if abstract_data_type == "Struct"%}reader.step_in()?;{% endif %} let mut abstract_data_type = {{ target_kind_name }}::default(); while reader.next()? != StreamItem::Nothing { if let Some(field_name) = reader.field_name()?.text() { match field_name { - - {% for statement in statements -%} - {{ statement }} - {% endfor %} + {% for field in fields -%} + {% if field.value | is_built_in_type == false %} + {% if abstract_data_type is object and abstract_data_type["Sequence"] | length != 0 %} + "{{ field.name }}" => { abstract_data_type.{{ field.name }} = { + let mut values = vec![]; + reader.step_in()?; + while reader.next()? != StreamItem::Nothing { + {% if abstract_data_type["Sequence"] | is_built_in_type == false %} + values.push({{ abstract_data_type["Sequence"] }}::read_from(reader)?); + {% else %} + values.push(reader.read_{{ abstract_data_type["Sequence"] }}()?); + {% endif %} + } + values + };} + {% elif abstract_data_type == "Struct" %} + "{{ field.name }}" => { abstract_data_type.{{ field.name }} = {{ field.value }}::read_from(reader)?; } + {% endif %} + {% else %} + "{{ field.name }}" => { abstract_data_type.{{ field.name }} = reader.read_{{ field.value }}()?; } + {% endif %} + {% endfor %} _ => {} } } @@ -49,12 +71,38 @@ impl {{ target_kind_name }} { {% endif %} Ok(abstract_data_type) } - {% endif %} pub fn write_to(&self, writer: &mut W) -> IonResult<()> { - {% for statement in write_statements -%} - {{ statement }} - {% endfor %} + {% if abstract_data_type == "Value" %} + {% for field in fields %} + {% if field.value | is_built_in_type ==false %} + self.{{ field.name }}.write_to(writer)?; + {% else %} + writer.write_{{ field.value | lower }}(self.value)?; + {% endif %} + {% endfor %} + {% elif abstract_data_type == "Struct"%} + writer.step_in(IonType::Struct)?; + {% for field in fields %} + writer.set_field_name("{{ field.name }}"); + {% if field.value | is_built_in_type == false %} + self.{{ field.name }}.write_to(writer)?; + {% else %} + writer.write_{{ field.value | lower }}(self.{{ field.name }})?; + {% endif %} + {% endfor %} + writer.step_out()?; + {% elif abstract_data_type is object and abstract_data_type["Sequence"] | length != 0 %} + writer.step_in(IonType::List)?; + for value in self.value { + {% if abstract_data_type["Sequence"] | is_built_in_type == false %} + value.write_to(writer)?; + {% else %} + writer.write_{{ abstract_data_type["Sequence"] | lower }}(value)?; + {% endif %} + } + writer.step_out()?; + {% endif %} Ok(()) } } diff --git a/src/bin/ion/commands/beta/generate/utils.rs b/src/bin/ion/commands/beta/generate/utils.rs index 32e3c967..b75f5681 100644 --- a/src/bin/ion/commands/beta/generate/utils.rs +++ b/src/bin/ion/commands/beta/generate/utils.rs @@ -20,49 +20,144 @@ pub struct Import { pub(crate) type_name: String, } -/// Represent the programming language for code generation. -#[derive(Debug, Clone, PartialEq)] -pub enum Language { - Rust, - Java, +pub trait Language { + /// Provides a file extension based on programming language + fn file_extension() -> String; + + /// Returns string representation of programming language + fn string_value() -> String; + + /// Provides file name based on programming language standards + fn file_name(name: &str) -> String; + + /// Maps the given ISL type name to a target type + fn target_type(ion_schema_type: &IonSchemaType) -> String; + + /// Provides given target type as sequence + /// e.g. + /// target_type = "Foo" returns "ArrayList" + /// target_type = "Foo" returns "Vec" + fn target_type_as_sequence(target_type: &str) -> String; + + /// Returns the [Case] based on programming languages + /// e.g. + /// Rust field name case -> [Case::Snake] + /// Java field name case -> [Case::Camel] + fn field_name_case() -> Case; + + /// Returns true if its a built in type otherwise returns false + fn is_built_in_type(name: &str) -> bool; + + /// Returns the template as string based on programming language + /// e.g. + /// Template::Struct -> "struct" + /// Template::Class -> "class" + fn template_as_string(template: &Template) -> String; } -impl Language { - pub fn file_extension(&self) -> &str { - match self { - Language::Rust => "rs", - Language::Java => "java", +pub struct JavaLanguage; + +impl Language for JavaLanguage { + fn file_extension() -> String { + "java".to_string() + } + + fn string_value() -> String { + "java".to_string() + } + + fn file_name(name: &str) -> String { + name.to_case(Case::UpperCamel) + } + + fn target_type(ion_schema_type: &IonSchemaType) -> String { + use IonSchemaType::*; + match ion_schema_type { + Int => "int", + String | Symbol => "String", + Float => "double", + Bool => "boolean", + Blob | Clob => "byte[]", + SchemaDefined(name) => name, } + .to_string() + } + + fn target_type_as_sequence(target_type: &str) -> String { + format!("ArrayList<{}>", target_type) } - pub fn file_name(&self, name: &str) -> String { - match self { - Language::Rust => name.to_case(Case::Snake), - Language::Java => name.to_case(Case::UpperCamel), + fn field_name_case() -> Case { + Case::Camel + } + + fn is_built_in_type(name: &str) -> bool { + matches!(name, "int" | "String" | "boolean" | "byte[]" | "float") + } + + fn template_as_string(template: &Template) -> String { + match template { + Template::Struct => "class".to_string(), } } } -impl From<&str> for Language { - fn from(value: &str) -> Self { - match value { - "java" => Language::Java, - "rust" => Language::Rust, - _ => unreachable!("Unsupported programming language: {}, this tool only supports Java and Rust code generation.", value) +impl Display for JavaLanguage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "java") + } +} + +pub struct RustLanguage; + +impl Language for RustLanguage { + fn file_extension() -> String { + "rs".to_string() + } + + fn string_value() -> String { + "rust".to_string() + } + + fn file_name(name: &str) -> String { + name.to_case(Case::Snake) + } + + fn target_type(ion_schema_type: &IonSchemaType) -> String { + use IonSchemaType::*; + match ion_schema_type { + Int => "i64", + String | Symbol => "String", + Float => "f64", + Bool => "bool", + Blob | Clob => "Vec", + SchemaDefined(name) => name, + } + .to_string() + } + + fn target_type_as_sequence(target_type: &str) -> String { + format!("Vec<{}>", target_type) + } + + fn field_name_case() -> Case { + Case::Snake + } + + fn is_built_in_type(name: &str) -> bool { + matches!(name, "i64" | "String" | "bool" | "Vec" | "f64") + } + + fn template_as_string(template: &Template) -> String { + match template { + Template::Struct => "struct".to_string(), } } } -impl Display for Language { +impl Display for RustLanguage { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Language::Rust => { - write!(f, "rust") - } - Language::Java => { - write!(f, "java") - } - } + write!(f, "rust") } } @@ -76,16 +171,6 @@ pub enum Template { Struct, // Represents a template for a Rust struct or Java class } -impl Template { - /// Returns a string that represent the template file name based on given programming language. - pub fn name(&self, language: &Language) -> &str { - match language { - Language::Rust => "struct", - Language::Java => "class", - } - } -} - impl TryFrom> for Template { type Error = CodeGenError; @@ -116,26 +201,6 @@ pub enum IonSchemaType { SchemaDefined(String), // A user defined schema type } -impl IonSchemaType { - /// Maps the given ISL type name to a target type - pub fn target_type(&self, language: &Language) -> &str { - use IonSchemaType::*; - use Language::*; - match (self, language) { - (Int, Rust) => "i64", - (Int, Java) => "int", - (String | Symbol, _) => "String", - (Float, Rust) => "f64", - (Float, Java) => "double", - (Bool, Rust) => "bool", - (Bool, Java) => "boolean", - (Blob | Clob, Rust) => "Vec", - (Blob | Clob, Java) => "byte[]", - (SchemaDefined(name), _) => name, - } - } -} - impl From<&str> for IonSchemaType { fn from(value: &str) -> Self { use IonSchemaType::*; diff --git a/tests/cli.rs b/tests/cli.rs index 02a0314e..fe7adb62 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -297,7 +297,7 @@ fn test_code_generation_in_rust( let mut cmd = Command::cargo_bin("ion")?; let temp_dir = TempDir::new()?; let input_schema_path = temp_dir.path().join("test_schema.isl"); - let mut input_schema_file = File::create(&input_schema_path)?; + let mut input_schema_file = File::create(input_schema_path)?; input_schema_file.write(test_schema.as_bytes())?; input_schema_file.flush()?; cmd.args([ @@ -412,7 +412,7 @@ fn test_code_generation_in_java( let mut cmd = Command::cargo_bin("ion")?; let temp_dir = TempDir::new()?; let input_schema_path = temp_dir.path().join("test_schema.isl"); - let mut input_schema_file = File::create(&input_schema_path)?; + let mut input_schema_file = File::create(input_schema_path)?; input_schema_file.write(test_schema.as_bytes())?; input_schema_file.flush()?; cmd.args([ From 948c8dd09acac29a38e2583ca205ddbb5b7fbcdd Mon Sep 17 00:00:00 2001 From: Khushboo Desai Date: Wed, 14 Feb 2024 11:19:20 -0800 Subject: [PATCH 3/4] Adds suggested changes * Adds new filter `snake` for templates * Modifies `Import` to only contain name field (Template will use appopriate casing filter to add an import statement based on `Import`) * Adds doc comments changes * Renames `Language#string_value()` to `name()` * Modifies `Display` for `AbstractDataType` --- src/bin/ion/commands/beta/generate/context.rs | 12 +-- .../ion/commands/beta/generate/generator.rs | 80 +++++++++---------- src/bin/ion/commands/beta/generate/mod.rs | 2 +- .../beta/generate/templates/java/class.templ | 2 +- .../beta/generate/templates/rust/struct.templ | 2 +- src/bin/ion/commands/beta/generate/utils.rs | 30 +++---- 6 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/bin/ion/commands/beta/generate/context.rs b/src/bin/ion/commands/beta/generate/context.rs index f0aec6fa..88c0156b 100644 --- a/src/bin/ion/commands/beta/generate/context.rs +++ b/src/bin/ion/commands/beta/generate/context.rs @@ -20,14 +20,14 @@ impl CodeGenContext { } } -/// Represents an abstract data type type that can be used to determine which templates can be used for code generation. +/// A target-language-agnostic data type that determines which template(s) to use for code generation. #[derive(Debug, Clone, PartialEq, Serialize)] pub enum AbstractDataType { - // a struct with a scalar value (used for `type` constraint) + // A scalar value (e.g. a string or integer or user defined type) Value, - // a struct with a sequence/collection value (used for `element` constraint) - // the parameter string represents the data type of the sequence + // A series of zero or more values whose type is described by the nested `String` (e.g. a list) Sequence(String), + // A collection of field name/value pairs (e.g. a map) Struct, } @@ -37,8 +37,8 @@ impl Display for AbstractDataType { f, "{}", match self { - AbstractDataType::Value => "single value struct", - AbstractDataType::Sequence(_) => "sequence value struct", + AbstractDataType::Value => "scalar value struct", + AbstractDataType::Sequence(_) => "sequence", AbstractDataType::Struct => "struct", } ) diff --git a/src/bin/ion/commands/beta/generate/generator.rs b/src/bin/ion/commands/beta/generate/generator.rs index 8a99a093..6af268f4 100644 --- a/src/bin/ion/commands/beta/generate/generator.rs +++ b/src/bin/ion/commands/beta/generate/generator.rs @@ -29,7 +29,7 @@ impl<'a> CodeGenerator<'a, RustLanguage> { Self { output, anonymous_type_counter: 0, - tera: Tera::new("src/bin/ion/commands/beta/generate/templates/**/*.templ").unwrap(), + tera: Tera::new("src/bin/ion/commands/beta/generate/templates/rust/*.templ").unwrap(), phantom: PhantomData, } } @@ -40,8 +40,9 @@ impl<'a> CodeGenerator<'a, RustLanguage> { let mut modules = vec![]; let mut module_context = tera::Context::new(); - // Register a tera filter that can be used to convert a string to upper camel case + // Register a tera filter that can be used to convert a string based on case self.tera.register_filter("upper_camel", Self::upper_camel); + self.tera.register_filter("snake", Self::snake); // Register a tera filter that can be used to see if a type is built in data type or not self.tera .register_filter("is_built_in_type", Self::is_built_in_type); @@ -61,7 +62,7 @@ impl<'a> CodeGenerator<'a, RustLanguage> { module_context: &mut Context, ) -> CodeGenResult<()> { module_context.insert("modules", &modules); - let rendered = self.tera.render("rust/mod.templ", module_context)?; + let rendered = self.tera.render("mod.templ", module_context)?; let mut file = File::create(self.output.join("mod.rs"))?; file.write_all(rendered.as_bytes())?; Ok(()) @@ -73,7 +74,7 @@ impl<'a> CodeGenerator<'a, JavaLanguage> { Self { output, anonymous_type_counter: 0, - tera: Tera::new("src/bin/ion/commands/beta/generate/templates/**/*.templ").unwrap(), + tera: Tera::new("src/bin/ion/commands/beta/generate/templates/java/*.templ").unwrap(), phantom: PhantomData, } } @@ -83,7 +84,7 @@ impl<'a> CodeGenerator<'a, JavaLanguage> { // this will be used for Rust to create mod.rs which lists all the generated modules let mut modules = vec![]; - // Register a tera filter that can be used to convert a string to upper camel case + // Register a tera filter that can be used to convert a string based on case self.tera.register_filter("upper_camel", Self::upper_camel); for isl_type in schema.types() { @@ -114,6 +115,25 @@ impl<'a, L: Language> CodeGenerator<'a, L> { )) } + /// Represents a [tera] filter that converts given tera string value to [snake case]. + /// Returns error if the given value is not a string. + /// + /// For more information: + /// + /// [tera]: + /// [upper camel case]: + pub fn snake( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + Ok(tera::Value::String( + value + .as_str() + .ok_or(tera::Error::msg("Required string for this filter"))? + .to_case(Case::Snake), + )) + } + /// Represents a [tera] filter that return true if the value is a built in type, otherwise returns false. /// /// For more information: @@ -124,9 +144,9 @@ impl<'a, L: Language> CodeGenerator<'a, L> { _map: &HashMap, ) -> Result { Ok(tera::Value::Bool(L::is_built_in_type( - value - .as_str() - .ok_or(tera::Error::msg("Required string for this filter"))?, + value.as_str().ok_or(tera::Error::msg( + "`is_built_in_type` called with non-String Value", + ))?, ))) } @@ -135,7 +155,7 @@ impl<'a, L: Language> CodeGenerator<'a, L> { modules: &mut Vec, isl_type: &IslType, ) -> CodeGenResult<()> { - let abstract_data_type_name = isl_type + let isl_type_name = isl_type .name() .clone() .unwrap_or_else(|| format!("AnonymousType{}", self.anonymous_type_counter)); @@ -145,11 +165,8 @@ impl<'a, L: Language> CodeGenerator<'a, L> { let mut imports: Vec = vec![]; let mut code_gen_context = CodeGenContext::new(); - // Set the target kind name of the abstract data type (i.e. enum/class) - context.insert( - "target_kind_name", - &abstract_data_type_name.to_case(Case::UpperCamel), - ); + // 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 { @@ -166,29 +183,16 @@ impl<'a, L: Language> CodeGenerator<'a, L> { context.insert("imports", &imports); // add fields for template - if code_gen_context.abstract_data_type == Some(AbstractDataType::Struct) - || code_gen_context.abstract_data_type == Some(AbstractDataType::Value) - || matches!( - code_gen_context.abstract_data_type, - Some(AbstractDataType::Sequence(_)) - ) - { + if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { context.insert("fields", &tera_fields); - if let Some(abstract_data_type) = &code_gen_context.abstract_data_type { - context.insert("abstract_data_type", abstract_data_type); - } else { - return invalid_abstract_data_type_error( + context.insert("abstract_data_type", abstract_data_type); + } else { + return invalid_abstract_data_type_error( "Can not determine abstract data type, constraints are mapping not mapping to an abstract data type.", ); - } } - self.render_generated_code( - modules, - &abstract_data_type_name, - &mut context, - &mut code_gen_context, - ) + self.render_generated_code(modules, &isl_type_name, &mut context, &mut code_gen_context) } fn render_generated_code( @@ -205,11 +209,7 @@ impl<'a, L: Language> CodeGenerator<'a, L> { let rendered = self .tera .render( - &format!( - "{}/{}.templ", - L::string_value(), - L::template_as_string(template) - ), + &format!("{}.templ", L::template_as_string(template)), context, ) .unwrap(); @@ -233,8 +233,7 @@ impl<'a, L: Language> CodeGenerator<'a, L> { IslTypeRef::Named(name, _) => { if !L::is_built_in_type(name) { imports.push(Import { - module_name: name.to_case(Case::Snake), - type_name: name.to_case(Case::UpperCamel), + name: name.to_string(), }); } let schema_type: IonSchemaType = name.into(); @@ -248,8 +247,7 @@ impl<'a, L: Language> CodeGenerator<'a, L> { self.generate_abstract_data_type(modules, type_def)?; let name = format!("AnonymousType{}", self.anonymous_type_counter); imports.push(Import { - module_name: name.to_case(Case::Snake), - type_name: name.to_case(Case::UpperCamel), + name: name.to_string(), }); name } diff --git a/src/bin/ion/commands/beta/generate/mod.rs b/src/bin/ion/commands/beta/generate/mod.rs index 23123334..7adea3cf 100644 --- a/src/bin/ion/commands/beta/generate/mod.rs +++ b/src/bin/ion/commands/beta/generate/mod.rs @@ -105,7 +105,7 @@ impl IonCliCommand for GenerateCommand { "java" => CodeGenerator::::new(output).generate(schema)?, "rust" => CodeGenerator::::new(output).generate(schema)?, _ => bail!( - "Unsupported programming language: {}, this tool only supports Java and Rust code generation.", + "Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'", language ) } diff --git a/src/bin/ion/commands/beta/generate/templates/java/class.templ b/src/bin/ion/commands/beta/generate/templates/java/class.templ index 588bf80b..2f94bcc7 100644 --- a/src/bin/ion/commands/beta/generate/templates/java/class.templ +++ b/src/bin/ion/commands/beta/generate/templates/java/class.templ @@ -1,5 +1,5 @@ {% if import %} -import ion_data_model.{{ import_type }}; +import ion_data_model.{{ import.name | upper_camel }}; {% endif %} public final class {{ target_kind_name }} { {% for field in fields -%} diff --git a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ index cb319179..3cd73723 100644 --- a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ +++ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ @@ -1,6 +1,6 @@ use ion_rs::{IonResult, IonReader, Reader, StreamItem}; {% for import in imports %} -use crate::ion_data_model::{{ import.module_name }}::{{ import.type_name }}; +use crate::ion_data_model::{{ import.name | snake }}::{{ import.name | upper_camel }}; {% endfor %} #[derive(Debug, Clone, Default)] diff --git a/src/bin/ion/commands/beta/generate/utils.rs b/src/bin/ion/commands/beta/generate/utils.rs index b75f5681..fd6a4c28 100644 --- a/src/bin/ion/commands/beta/generate/utils.rs +++ b/src/bin/ion/commands/beta/generate/utils.rs @@ -12,25 +12,27 @@ pub struct Field { pub(crate) value: String, } -/// Represents an import statement in a module file. +/// Represents an import in a generated code file. /// This will be used by template engine to fill import statements of a type definition. #[derive(Serialize)] pub struct Import { - pub(crate) module_name: String, - pub(crate) type_name: String, + pub(crate) name: String, } pub trait Language { /// Provides a file extension based on programming language fn file_extension() -> String; - /// Returns string representation of programming language - fn string_value() -> String; + /// Returns string representation of programming language name + fn name() -> String; - /// Provides file name based on programming language standards + /// Provides generated code's file name for given `name` based on programming language standards + /// e.g. + /// In Rust, this will return a string casing `name` to [Case::Snake]. + /// In Java, this will return a string casing `name` to [Case::UpperCamel] fn file_name(name: &str) -> String; - /// Maps the given ISL type name to a target type + /// Maps the given ISL type to a target type name fn target_type(ion_schema_type: &IonSchemaType) -> String; /// Provides given target type as sequence @@ -45,13 +47,13 @@ pub trait Language { /// Java field name case -> [Case::Camel] fn field_name_case() -> Case; - /// Returns true if its a built in type otherwise returns false + /// Returns true if the type name specified is provided by the target language implementation fn is_built_in_type(name: &str) -> bool; /// Returns the template as string based on programming language /// e.g. - /// Template::Struct -> "struct" - /// Template::Class -> "class" + /// In Rust, Template::Struct -> "struct" + /// In Java, Template::Struct -> "class" fn template_as_string(template: &Template) -> String; } @@ -62,7 +64,7 @@ impl Language for JavaLanguage { "java".to_string() } - fn string_value() -> String { + fn name() -> String { "java".to_string() } @@ -115,7 +117,7 @@ impl Language for RustLanguage { "rs".to_string() } - fn string_value() -> String { + fn name() -> String { "rust".to_string() } @@ -176,9 +178,7 @@ impl TryFrom> for Template { fn try_from(value: Option<&AbstractDataType>) -> Result { match value { - Some(AbstractDataType::Value) - | Some(AbstractDataType::Sequence(_)) - | Some(AbstractDataType::Struct) => Ok(Template::Struct), + Some(_) => Ok(Template::Struct), None => invalid_abstract_data_type_error( "Can not get a template without determining data model first.", ), From ba59f352f61a704f6ab71d6e0625a7ac359bd75f Mon Sep 17 00:00:00 2001 From: Khushboo Desai Date: Mon, 19 Feb 2024 12:39:02 -0800 Subject: [PATCH 4/4] adds suggested changes --- src/bin/ion/commands/beta/generate/context.rs | 43 +++++++++++++++++++ .../ion/commands/beta/generate/generator.rs | 15 +++---- src/bin/ion/commands/beta/generate/utils.rs | 14 +++--- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/bin/ion/commands/beta/generate/context.rs b/src/bin/ion/commands/beta/generate/context.rs index 88c0156b..dda97057 100644 --- a/src/bin/ion/commands/beta/generate/context.rs +++ b/src/bin/ion/commands/beta/generate/context.rs @@ -24,10 +24,53 @@ impl CodeGenContext { #[derive(Debug, Clone, PartialEq, Serialize)] pub enum AbstractDataType { // A scalar value (e.g. a string or integer or user defined type) + // e.g. Given below ISL, + // ``` + // type::{ + // name: value_type, + // type: int + // } + // ``` + // Corresponding abstract type in Rust would look like following: + // ``` + // struct ValueType { + // value: i64 + // } + // ``` Value, // A series of zero or more values whose type is described by the nested `String` (e.g. a list) + // e.g. Given below ISL, + // ``` + // type::{ + // name: sequence_type, + // element: int + // } + // ``` + // Corresponding abstract type in Rust would look like following: + // ``` + // struct SequenceType { + // value: Vec + // } + // ``` Sequence(String), // A collection of field name/value pairs (e.g. a map) + // e.g. Given below ISL, + // ``` + // type::{ + // name: struct_type, + // fields: { + // a: int, + // b: string, + // } + // } + // ``` + // Corresponding abstract type in Rust would look like following: + // ``` + // struct StructType { + // a: i64, + // b: String, + // } + // ``` Struct, } diff --git a/src/bin/ion/commands/beta/generate/generator.rs b/src/bin/ion/commands/beta/generate/generator.rs index 6af268f4..e9ea1a38 100644 --- a/src/bin/ion/commands/beta/generate/generator.rs +++ b/src/bin/ion/commands/beta/generate/generator.rs @@ -110,7 +110,9 @@ impl<'a, L: Language> CodeGenerator<'a, L> { Ok(tera::Value::String( value .as_str() - .ok_or(tera::Error::msg("Required string for this filter"))? + .ok_or(tera::Error::msg( + "the `upper_camel` filter only accepts strings", + ))? .to_case(Case::UpperCamel), )) } @@ -129,7 +131,7 @@ impl<'a, L: Language> CodeGenerator<'a, L> { Ok(tera::Value::String( value .as_str() - .ok_or(tera::Error::msg("Required string for this filter"))? + .ok_or(tera::Error::msg("the `snake` filter only accepts strings"))? .to_case(Case::Snake), )) } @@ -202,20 +204,17 @@ impl<'a, L: Language> CodeGenerator<'a, L> { context: &mut Context, code_gen_context: &mut CodeGenContext, ) -> CodeGenResult<()> { - modules.push(L::file_name(abstract_data_type_name)); + modules.push(L::file_name_for_type(abstract_data_type_name)); // Render or generate file for the template with the given context let template: &Template = &code_gen_context.abstract_data_type.as_ref().try_into()?; let rendered = self .tera - .render( - &format!("{}.templ", L::template_as_string(template)), - context, - ) + .render(&format!("{}.templ", L::template_name(template)), context) .unwrap(); let mut file = File::create(self.output.join(format!( "{}.{}", - L::file_name(abstract_data_type_name), + L::file_name_for_type(abstract_data_type_name), L::file_extension() )))?; file.write_all(rendered.as_bytes())?; diff --git a/src/bin/ion/commands/beta/generate/utils.rs b/src/bin/ion/commands/beta/generate/utils.rs index fd6a4c28..98483fd9 100644 --- a/src/bin/ion/commands/beta/generate/utils.rs +++ b/src/bin/ion/commands/beta/generate/utils.rs @@ -26,11 +26,11 @@ pub trait Language { /// Returns string representation of programming language name fn name() -> String; - /// Provides generated code's file name for given `name` based on programming language standards + /// Provides generated code's file name for given type `name` based on programming language standards /// e.g. /// In Rust, this will return a string casing `name` to [Case::Snake]. /// In Java, this will return a string casing `name` to [Case::UpperCamel] - fn file_name(name: &str) -> String; + fn file_name_for_type(name: &str) -> String; /// Maps the given ISL type to a target type name fn target_type(ion_schema_type: &IonSchemaType) -> String; @@ -54,7 +54,7 @@ pub trait Language { /// e.g. /// In Rust, Template::Struct -> "struct" /// In Java, Template::Struct -> "class" - fn template_as_string(template: &Template) -> String; + fn template_name(template: &Template) -> String; } pub struct JavaLanguage; @@ -68,7 +68,7 @@ impl Language for JavaLanguage { "java".to_string() } - fn file_name(name: &str) -> String { + fn file_name_for_type(name: &str) -> String { name.to_case(Case::UpperCamel) } @@ -97,7 +97,7 @@ impl Language for JavaLanguage { matches!(name, "int" | "String" | "boolean" | "byte[]" | "float") } - fn template_as_string(template: &Template) -> String { + fn template_name(template: &Template) -> String { match template { Template::Struct => "class".to_string(), } @@ -121,7 +121,7 @@ impl Language for RustLanguage { "rust".to_string() } - fn file_name(name: &str) -> String { + fn file_name_for_type(name: &str) -> String { name.to_case(Case::Snake) } @@ -150,7 +150,7 @@ impl Language for RustLanguage { matches!(name, "i64" | "String" | "bool" | "Vec" | "f64") } - fn template_as_string(template: &Template) -> String { + fn template_name(template: &Template) -> String { match template { Template::Struct => "struct".to_string(), }