Skip to content

Commit

Permalink
Adds new model changes for Rust code generation (#147)
Browse files Browse the repository at this point in the history
* Adds changes for Rust templates
* Add `string_representation` on `FullyQualifiedTypeReference` which
will have namespace represented based on programming language
* Add tests for Rust code generation
  • Loading branch information
desaikd authored Oct 2, 2024
1 parent 2fe17c1 commit d710bd8
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 248 deletions.
13 changes: 9 additions & 4 deletions src/bin/ion/commands/generate/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
_map: &HashMap<String, tera::Value>,
) -> Result<tera::Value, tera::Error> {
let fully_qualified_type_ref: &FullyQualifiedTypeReference = &value.try_into()?;
Ok(tera::Value::String(fully_qualified_type_ref.to_string()))
Ok(tera::Value::String(
fully_qualified_type_ref.string_representation::<L>(),
))
}

/// Generates code for all the schemas in given authorities
Expand Down Expand Up @@ -325,7 +327,7 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
&self
.data_model_store
.iter()
.map(|(k, v)| (format!("{}", k), v))
.map(|(k, v)| (k.string_representation::<L>(), v))
.collect::<HashMap<String, &DataModelNode>>(),
);
context.insert("model", &data_model_node);
Expand All @@ -340,8 +342,11 @@ impl<'a, L: Language + 'static> CodeGenerator<'a, L> {
code_gen_context: &mut CodeGenContext,
is_nested_type: bool,
) -> CodeGenResult<DataModelNode> {
self.current_type_fully_qualified_name
.push(isl_type_name.to_case(Case::UpperCamel));
L::add_type_to_namespace(
is_nested_type,
isl_type_name,
&mut self.current_type_fully_qualified_name,
);

let constraints = isl_type.constraints();

Expand Down
10 changes: 5 additions & 5 deletions src/bin/ion/commands/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod utils;
mod model;

use crate::commands::generate::generator::CodeGenerator;
use crate::commands::generate::utils::JavaLanguage;
use crate::commands::generate::utils::{JavaLanguage, RustLanguage};
use crate::commands::IonCliCommand;
use anyhow::{bail, Result};
use clap::{Arg, ArgAction, ArgMatches, Command};
Expand Down Expand Up @@ -124,8 +124,8 @@ impl IonCliCommand for GenerateCommand {
CodeGenerator::<JavaLanguage>::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect())
.generate_code_for_authorities(&authorities, &mut schema_system)?,
"rust" => {
// TODO: Initialize and run code generator for `rust`, once the rust templates are modified based on new code generation model
todo!("Rust support is disabled until this is resolved: https://github.com/amazon-ion/ion-cli/issues/136")
CodeGenerator::<RustLanguage>::new(output)
.generate_code_for_authorities(&authorities, &mut schema_system)?
}
_ => bail!(
"Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'",
Expand All @@ -138,8 +138,8 @@ impl IonCliCommand for GenerateCommand {
match language {
"java" => CodeGenerator::<JavaLanguage>::new(output, namespace.unwrap().split('.').map(|s| s.to_string()).collect()).generate_code_for_schema(&mut schema_system, schema_id)?,
"rust" => {
// TODO: Initialize and run code generator for `rust`, once the rust templates are modified based on new code generation model
todo!()
CodeGenerator::<RustLanguage>::new(output)
.generate_code_for_authorities(&authorities, &mut schema_system)?
}
_ => bail!(
"Programming language '{}' is not yet supported. Currently supported targets: 'java', 'rust'",
Expand Down
82 changes: 52 additions & 30 deletions src/bin/ion/commands/generate/model.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use derive_builder::Builder;
use ion_schema::isl::isl_type::IslType;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::fmt::Debug;
// This module contains a data model that the code generator can use to render a template based on the type of the model.
// Currently, this same data model is represented by `AbstractDataType` but it doesn't hold all the information for the template.
// e.g. currently there are different fields in the template that hold this information like fields, target_kind_name, abstract_data_type.
// Also, the current approach doesn't allow having nested sequences in the generated code. Because the `element_type` in `AbstractDataType::Sequence`
// doesn't have information on its nested types' `element_type`. This can be resolved with below defined new data model.
// _Note: This model will eventually use a map (FullQualifiedTypeReference, DataModel) to resolve some the references in container types(sequence or structure)._
// Any changes to the model will require subsequent changes to the templates which use this model.
// TODO: This is not yet used in the implementation, modify current implementation to use this data model.
use crate::commands::generate::context::SequenceType;
use crate::commands::generate::utils::Language;
use serde::Serialize;
use serde::ser::Error;
use serde::{Serialize, Serializer};
use serde_json::Value;

/// Represent a node in the data model tree of the generated code.
Expand Down Expand Up @@ -104,24 +106,6 @@ impl From<FullyQualifiedTypeName> for FullyQualifiedTypeReference {
}
}

impl Display for FullyQualifiedTypeReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.parameters.is_empty() {
return write!(f, "{}", self.type_name.join("."));
}
write!(f, "{}<", self.type_name.join("."))?;

for (i, parameter) in self.parameters.iter().enumerate() {
if i == self.parameters.len() - 1 {
write!(f, "{}", parameter)?;
} else {
write!(f, "{},", parameter)?;
}
}
write!(f, ">")
}
}

// This is useful for code generator to convert input `serde_json::Value` coming from tera(template engine) into `FullyQualifiedTypeReference`
impl TryFrom<&Value> for FullyQualifiedTypeReference {
type Error = tera::Error;
Expand Down Expand Up @@ -163,6 +147,24 @@ impl FullyQualifiedTypeReference {
pub fn with_parameters(&mut self, parameters: Vec<FullyQualifiedTypeReference>) {
self.parameters = parameters;
}

/// Provides string representation of this `FullyQualifiedTypeReference`
pub fn string_representation<L: Language>(&self) -> String {
if self.parameters.is_empty() {
return format!("{}", self.type_name.join(&L::namespace_separator()));
}
let parameters = self
.parameters
.iter()
.map(|p| p.string_representation::<L>())
.collect::<Vec<_>>()
.join(", ");
format!(
"{}<{}>",
self.type_name.join(&L::namespace_separator()),
parameters
)
}
}

/// A target-language-agnostic data type that determines which template(s) to use for code generation.
Expand Down Expand Up @@ -221,6 +223,26 @@ impl AbstractDataType {
}
}

/// Helper function for serializing abstract data type's `source` field that represents an ISL type.
/// This method returns the name for the given ISL type.
// TODO: `IslType` does not implement `Serialize`, once that is available this method can be removed.
fn serialize_type_name<S>(isl_type: &IslType, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
isl_type
.name()
.as_ref()
.ok_or(S::Error::custom("Isl type doesn't have a name"))?
.serialize(serializer)
}

/// Helper function for checking to skip or serialize `source` field in abstract data type that represents an ISL type.
/// This method returns true if the ISl type doesn't have a name, otherwise returns false.
fn is_anonymous(isl_type: &IslType) -> bool {
isl_type.name().is_none()
}

/// Represents a scalar type (e.g. a string or integer or user defined type)
#[allow(dead_code)]
#[derive(Debug, Clone, Builder, PartialEq, Serialize)]
Expand All @@ -245,8 +267,8 @@ pub struct Scalar {
// Represents the source ISL type which can be used to get other constraints useful for this type.
// For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type.
// This will also be useful for `text` type to verify if this is a `string` or `symbol`.
// TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized.
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "is_anonymous")]
#[serde(serialize_with = "serialize_type_name")]
source: IslType,
}

Expand Down Expand Up @@ -286,8 +308,8 @@ pub struct WrappedScalar {
// Represents the source ISL type which can be used to get other constraints useful for this type.
// For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type.
// This will also be useful for `text` type to verify if this is a `string` or `symbol`.
// TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized.
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "is_anonymous")]
#[serde(serialize_with = "serialize_type_name")]
source: IslType,
}

Expand Down Expand Up @@ -330,8 +352,8 @@ pub struct WrappedSequence {
// Represents the source ISL type which can be used to get other constraints useful for this type.
// For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type.
// This will also be useful for `text` type to verify if this is a `string` or `symbol`.
// TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized.
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "is_anonymous")]
#[serde(serialize_with = "serialize_type_name")]
source: IslType,
}

Expand Down Expand Up @@ -365,8 +387,8 @@ pub struct Sequence {
// Represents the source ISL type which can be used to get other constraints useful for this type.
// For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type.
// This will also be useful for `text` type to verify if this is a `string` or `symbol`.
// TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized.
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "is_anonymous")]
#[serde(serialize_with = "serialize_type_name")]
pub(crate) source: IslType,
}

Expand Down Expand Up @@ -406,8 +428,8 @@ pub struct Structure {
// Represents the source ISL type which can be used to get other constraints useful for this type.
// For example, getting the length of this sequence from `container_length` constraint or getting a `regex` value for string type.
// This will also be useful for `text` type to verify if this is a `string` or `symbol`.
// TODO: `IslType` does not implement `Serialize`, define a custom implementation or define methods on this field that returns values which could be serialized.
#[serde(skip_serializing)]
#[serde(skip_serializing_if = "is_anonymous")]
#[serde(serialize_with = "serialize_type_name")]
pub(crate) source: IslType,
}

Expand Down
135 changes: 5 additions & 130 deletions src/bin/ion/commands/generate/templates/rust/nested_type.templ
Original file line number Diff line number Diff line change
@@ -1,133 +1,8 @@
{% import "util_macros.templ" as util_macros %}

{# following macro defines an anonymous type as children class for its parent type definition #}
{% macro nested_type(target_kind_name, fields, abstract_data_type, nested_anonymous_types) -%}
#[derive(Debug, Clone, Default)]
pub struct {{ target_kind_name }} {
{% for field in fields -%}
{{ field.name | snake | indent(first = true) }}: {{ field.value_type }},
{% endfor %}
}

impl {{ target_kind_name }} {
pub fn new({% for field in fields | sort(attribute="name") -%}{{ field.name | snake }}: {{ field.value_type }},{% endfor %}) -> Self {
Self {
{% for field in fields -%}
{{ field.name | snake }},
{% endfor %}
}
}


{% for field in fields -%}pub fn {{ field.name | snake }}(&self) -> &{{ field.value_type }} {
&self.{{ field.name | snake }}
}
{% endfor %}


pub fn read_from(reader: &mut Reader) -> SerdeResult<Self> {
let mut abstract_data_type = {{ target_kind_name }}::default();
{% if abstract_data_type == "Value"%}
abstract_data_type.value = {% if fields[0].value_type | is_built_in_type == false %}
{{ fields[0].value_type }}::read_from(reader)?;
{% else %}
reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ fields[0].value_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if fields[0].value_type | lower == "string" %} .to_string() {% endif %};
{% endif %}
{% elif abstract_data_type is object and abstract_data_type is containing("Structure") %}
reader.step_in()?;
while reader.next()? != StreamItem::Nothing {
if let Some(field_name) = reader.field_name()?.text() {
match field_name {
{% for field in fields -%}
{% if field.value_type | is_built_in_type == false %}
{% if field.value_type is containing("Vec") %}
"{{ field.name }}" => { {{ util_macros::read_as_sequence(field=field) }} }
{% else %}
"{{ field.name }}" => { abstract_data_type.{{ field.name | snake }} = {{ field.value_type }}::read_from(reader)?; }
{% endif %}
{% else %}
"{{ field.name }}" => { abstract_data_type.{{ field.name | snake}} = reader.read_{% if field.isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ field.value_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if field.value_type | lower== "string" %} .to_string() {% endif %}; }
{% endif %}
{% endfor %}
_ => {
{% if abstract_data_type["Structure"] %}
return validation_error(
"Can not read field name:{{ field.name }} for {{ target_kind_name }} as it doesn't exist in the given schema type definition."
);
{% endif %}
}
}
}
}
reader.step_out()?;
{% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %}
if reader.ion_type() != Some(IonType::{{ abstract_data_type["Sequence"].sequence_type }}) {
return validation_error(format!(
"Expected {{ abstract_data_type["Sequence"].sequence_type }}, found {} while reading {{ target_kind_name }}.", reader.ion_type().unwrap()
));
}
reader.step_in()?;

abstract_data_type.value = {
let mut values = vec![];

while reader.next()? != StreamItem::Nothing {
{% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %}
values.push({{ abstract_data_type["Sequence"].element_type }}::read_from(reader)?);
{% else %}
values.push(reader.read_{% if fields[0].isl_type_name == "symbol" %}symbol()?.text().unwrap(){% else %}{{ abstract_data_type["Sequence"].element_type | lower | replace(from="string", to ="str") }}()?{% endif %}{% if abstract_data_type["Sequence"].element_type | lower== "string" %} .to_string() {% endif %});
{% endif %}
}
values
};
reader.step_out()?;
{% else %}
return validation_error("Can not resolve read API template for {{ target_kind_name }}");
{% endif %}
Ok(abstract_data_type)
}

pub fn write_to<W: IonWriter>(&self, writer: &mut W) -> SerdeResult<()> {
{% if abstract_data_type == "Value" %}
{% for field in fields %}
{% if field.value_type | is_built_in_type == false %}
self.{{ field.name | snake }}.write_to(writer)?;
{% else %}
writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.value_type | lower }}{% endif %}(self.value.to_owned())?;
{% endif %}
{% endfor %}
{% elif abstract_data_type is object and abstract_data_type is containing("Structure") %}
writer.step_in(IonType::Struct)?;
{% for field in fields %}
writer.set_field_name("{{ field.name }}");
{% if field.value_type | is_built_in_type == false %}
{% if field.value_type is containing("Vec") %}
{{ util_macros::write_as_sequence(field=field) }}
{% else %}
self.{{ field.name | snake }}.write_to(writer)?;
{% endif %}
{% else %}
{# TODO: Change the following `to_owned` to only be used when writing i64,f32,f64,bool which require owned value as input #}
writer.write_{% if field.isl_type_name == "symbol" %}symbol{% else %}{{ field.value_type | lower }}{% endif %}(self.{{ field.name | snake }}.to_owned())?;
{% endif %}
{% endfor %}
writer.step_out()?;
{% elif abstract_data_type is object and abstract_data_type is containing("Sequence") %}
writer.step_in(IonType::{{ abstract_data_type["Sequence"].sequence_type }})?;
for value in &self.value {
{% if abstract_data_type["Sequence"].element_type | is_built_in_type == false %}
value.write_to(writer)?;
{% else %}
writer.write_{% if fields[0].isl_type_name == "symbol" %}symbol{% else %}{{ abstract_data_type["Sequence"].element_type | lower }}{% endif %}(value.to_owned())?;
{% endif %}
}
writer.step_out()?;
{% endif %}
Ok(())
}
}

{% for inline_type in nested_anonymous_types -%}
{{ self::nested_type(target_kind_name=inline_type.target_kind_name, fields=inline_type.fields, abstract_data_type=inline_type.abstract_data_type, nested_anonymous_types=inline_type.nested_types) }}
{% endfor -%}
{% endmacro %}
{% macro nested_type(model, is_nested) -%}
{% if model.code_gen_type is containing("Structure")%}
{% include "struct.templ" %}
{% endif %}
{% endmacro nested_type -%}
Loading

0 comments on commit d710bd8

Please sign in to comment.