diff --git a/packages/cw-schema-codegen/src/lib.rs b/packages/cw-schema-codegen/src/lib.rs index b56983812..224f90fb8 100644 --- a/packages/cw-schema-codegen/src/lib.rs +++ b/packages/cw-schema-codegen/src/lib.rs @@ -1,3 +1,4 @@ pub mod go; +pub mod python; pub mod rust; pub mod typescript; diff --git a/packages/cw-schema-codegen/src/main.rs b/packages/cw-schema-codegen/src/main.rs index 86cd7a2b4..b8038d315 100644 --- a/packages/cw-schema-codegen/src/main.rs +++ b/packages/cw-schema-codegen/src/main.rs @@ -70,7 +70,8 @@ where Language::Typescript => { cw_schema_codegen::typescript::process_node(output, schema, node) } - Language::Go | Language::Python => todo!(), + Language::Python => cw_schema_codegen::python::process_node(output, schema, node), + Language::Go => todo!(), } })?; diff --git a/packages/cw-schema-codegen/src/python/mod.rs b/packages/cw-schema-codegen/src/python/mod.rs new file mode 100644 index 000000000..b60d0e0c0 --- /dev/null +++ b/packages/cw-schema-codegen/src/python/mod.rs @@ -0,0 +1,139 @@ +use self::template::{ + EnumTemplate, EnumVariantTemplate, FieldTemplate, StructTemplate, TypeTemplate, +}; +use std::{borrow::Cow, io}; + +pub mod template; + +fn expand_node_name<'a>( + schema: &'a cw_schema::SchemaV1, + node: &'a cw_schema::Node, +) -> Cow<'a, str> { + match node.value { + cw_schema::NodeType::Array { items } => { + let items = &schema.definitions[items]; + format!("{}[]", expand_node_name(schema, items)).into() + } + cw_schema::NodeType::Float => "float".into(), + cw_schema::NodeType::Double => "float".into(), + cw_schema::NodeType::Boolean => "bool".into(), + cw_schema::NodeType::String => "str".into(), + cw_schema::NodeType::Integer { .. } => "int".into(), + cw_schema::NodeType::Binary => "bytes".into(), + cw_schema::NodeType::Optional { inner } => { + let inner = &schema.definitions[inner]; + format!("typing.Optional[{}]", expand_node_name(schema, inner)).into() + } + cw_schema::NodeType::Struct(..) => node.name.as_ref().into(), + cw_schema::NodeType::Tuple { ref items } => { + let items = items + .iter() + .map(|item| expand_node_name(schema, &schema.definitions[*item])) + .collect::>() + .join(", "); + + format!("[{}]", items).into() + } + cw_schema::NodeType::Enum { .. } => node.name.as_ref().into(), + + cw_schema::NodeType::Decimal { .. } => "decimal.Decimal".into(), + cw_schema::NodeType::Address => "str".into(), + cw_schema::NodeType::Checksum => todo!(), + cw_schema::NodeType::HexBinary => todo!(), + cw_schema::NodeType::Timestamp => todo!(), + cw_schema::NodeType::Unit => "None".into(), + _ => todo!(), + } +} + +fn prepare_docs(desc: Option<&str>) -> Cow<'_, [Cow<'_, str>]> { + desc.map(|desc| desc.lines().map(Into::into).collect()) + .unwrap_or(Cow::Borrowed(&[])) +} + +pub fn process_node( + output: &mut O, + schema: &cw_schema::SchemaV1, + node: &cw_schema::Node, +) -> io::Result<()> +where + O: io::Write, +{ + match node.value { + cw_schema::NodeType::Struct(ref sty) => { + let structt = StructTemplate { + name: node.name.clone(), + docs: prepare_docs(node.description.as_deref()), + ty: match sty { + cw_schema::StructType::Unit => TypeTemplate::Unit, + cw_schema::StructType::Named { ref properties } => TypeTemplate::Named { + fields: properties + .iter() + .map(|(name, prop)| FieldTemplate { + name: Cow::Borrowed(name), + docs: prepare_docs(prop.description.as_deref()), + ty: expand_node_name(schema, &schema.definitions[prop.value]), + }) + .collect(), + }, + cw_schema::StructType::Tuple { ref items } => TypeTemplate::Tuple( + items + .iter() + .map(|item| expand_node_name(schema, &schema.definitions[*item])) + .collect(), + ), + _ => todo!(), + }, + }; + + writeln!(output, "{structt}")?; + } + cw_schema::NodeType::Enum { ref cases, .. } => { + let enumm = EnumTemplate { + name: node.name.clone(), + docs: prepare_docs(node.description.as_deref()), + variants: cases + .iter() + .map(|(name, case)| EnumVariantTemplate { + name: name.clone(), + docs: prepare_docs(case.description.as_deref()), + ty: match case.value { + cw_schema::EnumValue::Unit => TypeTemplate::Unit, + cw_schema::EnumValue::Tuple { ref items } => { + let items = items + .iter() + .map(|item| { + expand_node_name(schema, &schema.definitions[*item]) + }) + .collect(); + + TypeTemplate::Tuple(items) + } + cw_schema::EnumValue::Named { ref properties, .. } => { + TypeTemplate::Named { + fields: properties + .iter() + .map(|(name, prop)| FieldTemplate { + name: Cow::Borrowed(name), + docs: prepare_docs(prop.description.as_deref()), + ty: expand_node_name( + schema, + &schema.definitions[prop.value], + ), + }) + .collect(), + } + } + _ => todo!(), + }, + }) + .collect(), + }; + + writeln!(output, "{enumm}")?; + } + _ => (), + } + + Ok(()) +} diff --git a/packages/cw-schema-codegen/src/python/template.rs b/packages/cw-schema-codegen/src/python/template.rs new file mode 100644 index 000000000..11b860ed9 --- /dev/null +++ b/packages/cw-schema-codegen/src/python/template.rs @@ -0,0 +1,41 @@ +use askama::Template; +use std::borrow::Cow; + +#[derive(Clone)] +pub struct EnumVariantTemplate<'a> { + pub name: Cow<'a, str>, + pub docs: Cow<'a, [Cow<'a, str>]>, + pub ty: TypeTemplate<'a>, +} + +#[derive(Template)] +#[template(escape = "none", path = "python/enum.tpl.py")] +pub struct EnumTemplate<'a> { + pub name: Cow<'a, str>, + pub docs: Cow<'a, [Cow<'a, str>]>, + pub variants: Cow<'a, [EnumVariantTemplate<'a>]>, +} + +#[derive(Clone)] +pub struct FieldTemplate<'a> { + pub name: Cow<'a, str>, + pub docs: Cow<'a, [Cow<'a, str>]>, + pub ty: Cow<'a, str>, +} + +#[derive(Clone)] +pub enum TypeTemplate<'a> { + Unit, + Tuple(Cow<'a, [Cow<'a, str>]>), + Named { + fields: Cow<'a, [FieldTemplate<'a>]>, + }, +} + +#[derive(Template)] +#[template(escape = "none", path = "python/struct.tpl.py")] +pub struct StructTemplate<'a> { + pub name: Cow<'a, str>, + pub docs: Cow<'a, [Cow<'a, str>]>, + pub ty: TypeTemplate<'a>, +} diff --git a/packages/cw-schema-codegen/templates/python/enum.tpl.py b/packages/cw-schema-codegen/templates/python/enum.tpl.py new file mode 100644 index 000000000..5f647687e --- /dev/null +++ b/packages/cw-schema-codegen/templates/python/enum.tpl.py @@ -0,0 +1,41 @@ +# This code is @generated by cw-schema-codegen. Do not modify this manually. + +import typing +import decimal +from pydantic import BaseModel, RootModel + +class {{ name }}(RootModel): + """{% for doc in docs %} + {{ doc }} + {% endfor %}""" + +{% for variant in variants %} +{% match variant.ty %} +{% when TypeTemplate::Unit %} + class {{ variant.name }}(RootModel): + """{% for doc in variant.docs %} + {{ doc }} + {% endfor %}""" + root: typing.Literal['{{ variant.name }}'] +{% when TypeTemplate::Tuple with (types) %} + class {{ variant.name }}(BaseModel): + """{% for doc in variant.docs %} + {{ doc }} + {% endfor %}""" + {{ variant.name }}: typing.Tuple[{{ types|join(", ") }}] +{% when TypeTemplate::Named with { fields } %} + class {{ variant.name }}(BaseModel): + class __Inner(BaseModel): + """{% for doc in variant.docs %} + {{ doc }} + {% endfor %}""" + {% for field in fields %} + {{ field.name }}: {{ field.ty }} + """{% for doc in field.docs %} + {{ doc }} + {% endfor %}""" + {% endfor %} + {{ variant.name }}: __Inner +{% endmatch %} +{% endfor %} + root: typing.Union[ {% for variant in variants %} {{ variant.name }}, {% endfor %} ] \ No newline at end of file diff --git a/packages/cw-schema-codegen/templates/python/struct.tpl.py b/packages/cw-schema-codegen/templates/python/struct.tpl.py new file mode 100644 index 000000000..c565beda4 --- /dev/null +++ b/packages/cw-schema-codegen/templates/python/struct.tpl.py @@ -0,0 +1,32 @@ +# This code is @generated by cw-schema-codegen. Do not modify this manually. + +import typing +import decimal +from pydantic import BaseModel, RootModel + + +{% match ty %} +{% when TypeTemplate::Unit %} +class {{ name }}(RootModel): + '''{% for doc in docs %} + {{ doc }} + {% endfor %}''' + root: None +{% when TypeTemplate::Tuple with (types) %} +class {{ name }}(RootModel): + '''{% for doc in docs %} + {{ doc }} + {% endfor %}''' + root: typing.Tuple[{{ types|join(", ") }}] +{% when TypeTemplate::Named with { fields } %} +class {{ name }}(BaseModel): + '''{% for doc in docs %} + {{ doc }} + {% endfor %}''' + {% for field in fields %} + {{ field.name }}: {{ field.ty }} + '''{% for doc in field.docs %} + # {{ doc }} + {% endfor %}''' + {% endfor %} +{% endmatch %} diff --git a/packages/cw-schema-codegen/tests/python_tpl.rs b/packages/cw-schema-codegen/tests/python_tpl.rs new file mode 100644 index 000000000..347d977b0 --- /dev/null +++ b/packages/cw-schema-codegen/tests/python_tpl.rs @@ -0,0 +1,156 @@ +use cw_schema::Schemaifier; +use serde::{Deserialize, Serialize}; +use std::io::Write; + +#[derive(Schemaifier, Serialize, Deserialize)] +pub enum SomeEnum { + Field1, + Field2(u32, u32), + Field3 { a: String, b: u32 }, + // Field4(Box), // TODO tkulik: Do we want to support Box ? + // Field5 { a: Box }, +} + +#[derive(Schemaifier, Serialize, Deserialize)] +pub struct UnitStructure; + +#[derive(Schemaifier, Serialize, Deserialize)] +pub struct TupleStructure(u32, String, u128); + +#[derive(Schemaifier, Serialize, Deserialize)] +pub struct NamedStructure { + a: String, + b: u8, + c: SomeEnum, +} + +#[test] +fn simple_enum() { + // generate the schemas for each of the above types + let schemas = [ + cw_schema::schema_of::(), + cw_schema::schema_of::(), + cw_schema::schema_of::(), + cw_schema::schema_of::(), + ]; + + // run the codegen to typescript + for schema in schemas { + let cw_schema::Schema::V1(schema) = schema else { + panic!(); + }; + + let output = schema + .definitions + .iter() + .map(|node| { + let mut buf = Vec::new(); + cw_schema_codegen::python::process_node(&mut buf, &schema, node).unwrap(); + String::from_utf8(buf).unwrap() + }) + .collect::(); + + insta::assert_snapshot!(output); + } +} + +macro_rules! validator { + ($typ:ty) => {{ + let a: Box ()> = Box::new(|output| { + serde_json::from_str::<$typ>(output).unwrap(); + }); + a + }}; +} + +#[test] +fn assert_validity() { + let schemas = [ + ( + "SomeEnum", + cw_schema::schema_of::(), + serde_json::to_string(&SomeEnum::Field1).unwrap(), + validator!(SomeEnum), + ), + ( + "SomeEnum", + cw_schema::schema_of::(), + serde_json::to_string(&SomeEnum::Field2(10, 23)).unwrap(), + validator!(SomeEnum), + ), + ( + "SomeEnum", + cw_schema::schema_of::(), + serde_json::to_string(&SomeEnum::Field3 { + a: "sdf".to_string(), + b: 12, + }) + .unwrap(), + validator!(SomeEnum), + ), + ( + "UnitStructure", + cw_schema::schema_of::(), + serde_json::to_string(&UnitStructure {}).unwrap(), + validator!(UnitStructure), + ), + ( + "TupleStructure", + cw_schema::schema_of::(), + serde_json::to_string(&TupleStructure(10, "aasdf".to_string(), 2)).unwrap(), + validator!(TupleStructure), + ), + ( + "NamedStructure", + cw_schema::schema_of::(), + serde_json::to_string(&NamedStructure { + a: "awer".to_string(), + b: 4, + c: SomeEnum::Field1, + }) + .unwrap(), + validator!(NamedStructure), + ), + ]; + + for (type_name, schema, example, validator) in schemas { + let cw_schema::Schema::V1(schema) = schema else { + unreachable!(); + }; + + let schema_output = schema + .definitions + .iter() + .map(|node| { + let mut buf = Vec::new(); + cw_schema_codegen::python::process_node(&mut buf, &schema, node).unwrap(); + String::from_utf8(buf).unwrap() + }) + .collect::(); + + let mut file = tempfile::NamedTempFile::with_suffix(".py").unwrap(); + file.write_all(schema_output.as_bytes()).unwrap(); + file.write( + format!( + "import sys; print({type_name}.model_validate_json('{example}').model_dump_json())" + ) + .as_bytes(), + ) + .unwrap(); + file.flush().unwrap(); + + let output = std::process::Command::new("python") + .arg(file.path()) + .output() + .unwrap(); + + assert!( + output.status.success(), + "stdout: {stdout}, stderr: {stderr}\n\n schema:\n {schema_output}", + stdout = String::from_utf8_lossy(&output.stdout), + stderr = String::from_utf8_lossy(&output.stderr), + ); + + validator(&String::from_utf8_lossy(&output.stdout)) + } +} diff --git a/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap b/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap new file mode 100644 index 000000000..a8a9224d1 --- /dev/null +++ b/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap @@ -0,0 +1,42 @@ +--- +source: packages/cw-schema-codegen/tests/python_tpl.rs +expression: output +snapshot_kind: text +--- +# This code is @generated by cw-schema-codegen. Do not modify this manually. + +import typing +import decimal +from pydantic import BaseModel, RootModel + +class SomeEnum(RootModel): + """""" + + + + class Field1(RootModel): + """""" + root: typing.Literal['Field1'] + + + + class Field2(BaseModel): + """""" + Field2: typing.Tuple[int, int] + + + + class Field3(BaseModel): + class __Inner(BaseModel): + """""" + + a: str + """""" + + b: int + """""" + + Field3: __Inner + + + root: typing.Union[ Field1, Field2, Field3, ] diff --git a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__complex_enum.snap b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__complex_enum.snap index 00c3725ff..e1c4a9758 100644 --- a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__complex_enum.snap +++ b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__complex_enum.snap @@ -1,6 +1,7 @@ --- source: packages/cw-schema-codegen/tests/rust_tpl.rs expression: rendered +snapshot_kind: text --- // This code is @generated by cw-schema-codegen. Do not modify this manually. diff --git a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_enum.snap b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_enum.snap index 8e97ca0d4..cb79d1753 100644 --- a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_enum.snap +++ b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_enum.snap @@ -1,6 +1,7 @@ --- source: packages/cw-schema-codegen/tests/rust_tpl.rs expression: rendered +snapshot_kind: text --- // This code is @generated by cw-schema-codegen. Do not modify this manually. diff --git a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_struct.snap b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_struct.snap index be4cd9a49..3103d42b2 100644 --- a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_struct.snap +++ b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__empty_struct.snap @@ -1,6 +1,7 @@ --- source: packages/cw-schema-codegen/tests/rust_tpl.rs expression: rendered +snapshot_kind: text --- // This code is @generated by cw-schema-codegen. Do not modify this manually. diff --git a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__named_struct.snap b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__named_struct.snap index b6d3ba954..dd3525ae0 100644 --- a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__named_struct.snap +++ b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__named_struct.snap @@ -1,6 +1,7 @@ --- source: packages/cw-schema-codegen/tests/rust_tpl.rs expression: rendered +snapshot_kind: text --- // This code is @generated by cw-schema-codegen. Do not modify this manually. diff --git a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__simple_enum.snap b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__simple_enum.snap index 7d5b52b2e..e7ed6b8cf 100644 --- a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__simple_enum.snap +++ b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__simple_enum.snap @@ -1,6 +1,7 @@ --- source: packages/cw-schema-codegen/tests/rust_tpl.rs expression: rendered +snapshot_kind: text --- // This code is @generated by cw-schema-codegen. Do not modify this manually. diff --git a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__tuple_struct.snap b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__tuple_struct.snap index 90eff5f1c..358693556 100644 --- a/packages/cw-schema-codegen/tests/snapshots/rust_tpl__tuple_struct.snap +++ b/packages/cw-schema-codegen/tests/snapshots/rust_tpl__tuple_struct.snap @@ -1,6 +1,7 @@ --- source: packages/cw-schema-codegen/tests/rust_tpl.rs expression: rendered +snapshot_kind: text --- // This code is @generated by cw-schema-codegen. Do not modify this manually.