From 6c6e2ad68da1ff12536e8650ab1fcc91cf2ab174 Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Wed, 6 Nov 2024 08:17:03 +0100 Subject: [PATCH 1/5] feat: Python codegen init --- packages/cw-schema-codegen/src/lib.rs | 1 + packages/cw-schema-codegen/src/main.rs | 3 +- packages/cw-schema-codegen/src/python/mod.rs | 137 ++++++++++++++++++ .../cw-schema-codegen/src/python/template.rs | 41 ++++++ .../templates/python/enum.tpl.py | 40 +++++ .../templates/python/struct.tpl.py | 30 ++++ .../snapshots/rust_tpl__complex_enum.snap | 1 + .../tests/snapshots/rust_tpl__empty_enum.snap | 1 + .../snapshots/rust_tpl__empty_struct.snap | 1 + .../snapshots/rust_tpl__named_struct.snap | 1 + .../snapshots/rust_tpl__simple_enum.snap | 1 + .../snapshots/rust_tpl__tuple_struct.snap | 1 + 12 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 packages/cw-schema-codegen/src/python/mod.rs create mode 100644 packages/cw-schema-codegen/src/python/template.rs create mode 100644 packages/cw-schema-codegen/templates/python/enum.tpl.py create mode 100644 packages/cw-schema-codegen/templates/python/struct.tpl.py 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..74f3d9e00 --- /dev/null +++ b/packages/cw-schema-codegen/src/python/mod.rs @@ -0,0 +1,137 @@ +use self::template::{ + EnumTemplate, EnumVariantTemplate, FieldTemplate, StructTemplate, TypeTemplate, +}; +use heck::ToPascalCase; +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 => "number".into(), + cw_schema::NodeType::Double => "number".into(), + cw_schema::NodeType::Boolean => "boolean".into(), + cw_schema::NodeType::String => "string".into(), + cw_schema::NodeType::Integer { .. } => "string".into(), + cw_schema::NodeType::Binary => "Uint8Array".into(), + cw_schema::NodeType::Optional { inner } => { + let inner = &schema.definitions[inner]; + format!("{} | null", 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 { .. } => "string".into(), + cw_schema::NodeType::Address => "string".into(), + cw_schema::NodeType::Checksum => todo!(), + cw_schema::NodeType::HexBinary => todo!(), + cw_schema::NodeType::Timestamp => todo!(), + cw_schema::NodeType::Unit => Cow::Borrowed("void"), + } +} + +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(), + ), + }, + }; + + 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(), + } + } + }, + }) + .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..21ee91261 --- /dev/null +++ b/packages/cw-schema-codegen/templates/python/enum.tpl.py @@ -0,0 +1,40 @@ +# This code is @generated by cw-schema-codegen. Do not modify this manually. + +/** +{% for doc in docs %} + * {{ doc }} +{% endfor %} + */ + +type {{ name }} = +{% for variant in variants %} + | + + /** + {% for doc in variant.docs %} + * {{ doc }} + {% endfor %} + */ + + {% match variant.ty %} + {% when TypeTemplate::Unit %} + { "{{ variant.name }}": {} } + {% when TypeTemplate::Tuple with (types) %} + { "{{ variant.name }}": [{{ types|join(", ") }}] } + {% when TypeTemplate::Named with { fields } %} + { "{{ variant.name }}": { + {% for field in fields %} + /** + {% for doc in field.docs %} + * {{ doc }} + {% endfor %} + */ + + {{ field.name }}: {{ field.ty }}; + {% endfor %} + } } + {% endmatch %} +{% endfor %} +; + +export { {{ name }} }; 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..08b30d5d4 --- /dev/null +++ b/packages/cw-schema-codegen/templates/python/struct.tpl.py @@ -0,0 +1,30 @@ +# This code is @generated by cw-schema-codegen. Do not modify this manually. + +/** +{% for doc in docs %} + * {{ doc }} +{% endfor %} + */ + +type {{ name }} = +{% match ty %} + {% when TypeTemplate::Unit %} + void + {% when TypeTemplate::Tuple with (types) %} + [{{ types|join(", ") }}] + {% when TypeTemplate::Named with { fields } %} + { + {% for field in fields %} + /** + {% for doc in field.docs %} + * {{ doc }} + {% endfor %} + */ + + {{ field.name }}: {{ field.ty }}; + {% endfor %} + } +{% endmatch %} +; + +export { {{ name }} }; 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. From fc490791b57150c99b63cfab3b4b7eb2bcafc30f Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Fri, 8 Nov 2024 08:11:23 +0100 Subject: [PATCH 2/5] feat: Add Python codegen --- Cargo.toml | 2 +- .../cw-schema-codegen/playground/.gitignore | 1 + .../cw-schema-codegen/playground/Cargo.lock | 96 ++++++++++++++ .../cw-schema-codegen/playground/Cargo.toml | 11 ++ .../playground/playground.py | 119 ++++++++++++++++++ .../cw-schema-codegen/playground/src/main.rs | 59 +++++++++ packages/cw-schema-codegen/playground/test.sh | 1 + packages/cw-schema-codegen/src/python/mod.rs | 3 + .../templates/python/enum.tpl.py | 60 ++++----- .../templates/python/struct.tpl.py | 29 ++++- .../cw-schema-codegen/tests/python_tpl.rs | 29 +++++ .../snapshots/python_tpl__simple_enum.snap | 42 +++++++ 12 files changed, 421 insertions(+), 31 deletions(-) create mode 100644 packages/cw-schema-codegen/playground/.gitignore create mode 100644 packages/cw-schema-codegen/playground/Cargo.lock create mode 100644 packages/cw-schema-codegen/playground/Cargo.toml create mode 100644 packages/cw-schema-codegen/playground/playground.py create mode 100644 packages/cw-schema-codegen/playground/src/main.rs create mode 100755 packages/cw-schema-codegen/playground/test.sh create mode 100644 packages/cw-schema-codegen/tests/python_tpl.rs create mode 100644 packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap diff --git a/Cargo.toml b/Cargo.toml index c94fa1046..c06f9b080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = ["packages/*"] -exclude = ["contracts"] +exclude = ["contracts", "packages/cw-schema-codegen/playground"] # Resolver has to be set explicitly in workspaces # due to https://github.com/rust-lang/cargo/issues/9956 diff --git a/packages/cw-schema-codegen/playground/.gitignore b/packages/cw-schema-codegen/playground/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/packages/cw-schema-codegen/playground/.gitignore @@ -0,0 +1 @@ +/target diff --git a/packages/cw-schema-codegen/playground/Cargo.lock b/packages/cw-schema-codegen/playground/Cargo.lock new file mode 100644 index 000000000..867ff0bae --- /dev/null +++ b/packages/cw-schema-codegen/playground/Cargo.lock @@ -0,0 +1,96 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serialization" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" diff --git a/packages/cw-schema-codegen/playground/Cargo.toml b/packages/cw-schema-codegen/playground/Cargo.toml new file mode 100644 index 000000000..a5aa7ba8f --- /dev/null +++ b/packages/cw-schema-codegen/playground/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "serialization" +version = "0.1.0" +edition = "2021" + +[features] +deserialize = [] + +[dependencies] +serde = { version = "1.0.215", features = ["derive", "serde_derive"] } +serde_json = "1.0.133" diff --git a/packages/cw-schema-codegen/playground/playground.py b/packages/cw-schema-codegen/playground/playground.py new file mode 100644 index 000000000..ac850f87d --- /dev/null +++ b/packages/cw-schema-codegen/playground/playground.py @@ -0,0 +1,119 @@ +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from typing import Optional, Iterable +import sys +import json + + +# TODO tkulik: try to get rid of the `dataclasses_json` dependency + + +enum_field = lambda: field(default=None, metadata=config(exclude=lambda x: x is None)) + +@dataclass_json +@dataclass +class SomeEnum: + class VariantIndicator: + pass + + class Field3Type: + a: str + b: int + + class Field5Type: + a: Iterable['SomeEnum'] + + Field1: Optional[VariantIndicator] = enum_field() + Field2: Optional[tuple[int, int]] = enum_field() + Field3: Optional[Field3Type] = enum_field() + Field4: Optional[Iterable['SomeEnum']] = enum_field() + Field5: Optional[Field5Type] = enum_field() + + def deserialize(json): + if not ":" in json: + if json == '"Field1"': + return SomeEnum(Field1=SomeEnum.VariantIndicator()) + else: + raise Exception(f"Deserialization error, undefined variant: {json}") + else: + return SomeEnum.from_json(json) + + def serialize(self): + if self.Field1 is not None: + return '"Field1"' + else: + return SomeEnum.to_json(self) + +@dataclass_json +@dataclass +class UnitStructure: + def deserialize(json): + if json == "null": + return UnitStructure() + else: + Exception(f"Deserialization error, undefined value: {json}") + + def serialize(self): + return 'null' + +@dataclass_json +@dataclass +class TupleStructure: + Tuple: tuple[int, str, int] + + def deserialize(json): + return TupleStructure.from_json(f'{{ "Tuple": {json} }}') + + def serialize(self): + return json.dumps(self.Tuple) + +@dataclass_json +@dataclass +class NamedStructure: + a: str + b: int + c: Iterable['SomeEnum'] + + def deserialize(json): + return NamedStructure.from_json(json) + + def serialize(self): + return self.to_json() + +### +### TESTS: +### + +for (index, input) in enumerate(sys.stdin): + input = input.rstrip() + try: + if index < 5: + deserialized = SomeEnum.deserialize(input) + elif index == 5: + deserialized = UnitStructure.deserialize(input) + elif index == 6: + deserialized = TupleStructure.deserialize(input) + else: + deserialized = NamedStructure.deserialize(input) + except: + raise(Exception(f"This json can't be deserialized: {input}")) + serialized = deserialized.serialize() + print(serialized) + + +# def handle_msg(json): +# a = SomeEnum.deserialize(json) +# if a.Field1 is not None: +# print("SomeEnum::Field1") +# elif a.Field2 is not None: +# print(a.Field2[0]) +# print(a.Field2[1]) +# elif a.Field3 is not None: +# print(a.Field3) +# elif a.Field4 is not None: +# print(a.Field4) +# elif a.Field5 is not None: +# print(a.Field5) + +# handle_msg('"Field1"') +# handle_msg('{"Field2": [10, 12]}') \ No newline at end of file diff --git a/packages/cw-schema-codegen/playground/src/main.rs b/packages/cw-schema-codegen/playground/src/main.rs new file mode 100644 index 000000000..747e7ab89 --- /dev/null +++ b/packages/cw-schema-codegen/playground/src/main.rs @@ -0,0 +1,59 @@ + +use serde::{Deserialize, Serialize}; + + +#[derive(Serialize, Deserialize)] +pub enum SomeEnum { + Field1, + Field2(u32, u32), + Field3 { + a: String, + b: u32 + }, + Field4(Box), + Field5 { a: Box }, +} + +#[derive(Serialize, Deserialize)] +pub struct UnitStructure; + +#[derive(Serialize, Deserialize)] +pub struct TupleStructure(u32, String, u128); + +#[derive(Serialize, Deserialize)] +pub struct NamedStructure { + a: String, + b: u8, + c: SomeEnum +} + + +#[cfg(not(feature = "deserialize"))] +fn main() { + println!("{}", serde_json::to_string(&SomeEnum::Field1).unwrap()); + println!("{}", serde_json::to_string(&SomeEnum::Field2(10, 23)).unwrap()); + println!("{}", serde_json::to_string(&SomeEnum::Field3 {a: "sdf".to_string(), b: 12}).unwrap()); + println!("{}", serde_json::to_string(&SomeEnum::Field4(Box::new(SomeEnum::Field1))).unwrap()); + println!("{}", serde_json::to_string(&SomeEnum::Field5 { a: Box::new(SomeEnum::Field1) }).unwrap()); + println!("{}", serde_json::to_string(&UnitStructure {}).unwrap()); + println!("{}", serde_json::to_string(&TupleStructure(10, "aasdf".to_string(), 2)).unwrap()); + println!("{}", serde_json::to_string(&NamedStructure {a: "awer".to_string(), b: 4, c: SomeEnum::Field1}).unwrap()); +} + +#[cfg(feature = "deserialize")] +fn main() { + use std::io::BufRead; + for (index, line) in std::io::BufReader::new(std::io::stdin()).lines().enumerate() { + let line = line.unwrap(); + println!("{line}"); + if index < 5 { + let _: SomeEnum = serde_json::from_str(&line).unwrap(); + } else if index == 5 { + let _: UnitStructure = serde_json::from_str(&line).unwrap(); + } else if index == 6 { + let _: TupleStructure = serde_json::from_str(&line).unwrap(); + } else { + let _: NamedStructure = serde_json::from_str(&line).unwrap(); + } + } +} diff --git a/packages/cw-schema-codegen/playground/test.sh b/packages/cw-schema-codegen/playground/test.sh new file mode 100755 index 000000000..921d0e29b --- /dev/null +++ b/packages/cw-schema-codegen/playground/test.sh @@ -0,0 +1 @@ +cargo run | python playground.py | cargo run --features "deserialize" \ No newline at end of file diff --git a/packages/cw-schema-codegen/src/python/mod.rs b/packages/cw-schema-codegen/src/python/mod.rs index 74f3d9e00..da918a8b0 100644 --- a/packages/cw-schema-codegen/src/python/mod.rs +++ b/packages/cw-schema-codegen/src/python/mod.rs @@ -43,6 +43,7 @@ fn expand_node_name<'a>( cw_schema::NodeType::HexBinary => todo!(), cw_schema::NodeType::Timestamp => todo!(), cw_schema::NodeType::Unit => Cow::Borrowed("void"), + _ => todo!() } } @@ -82,6 +83,7 @@ where .map(|item| expand_node_name(schema, &schema.definitions[*item])) .collect(), ), + _ => todo!() }, }; @@ -123,6 +125,7 @@ where .collect(), } } + _ => todo!() }, }) .collect(), diff --git a/packages/cw-schema-codegen/templates/python/enum.tpl.py b/packages/cw-schema-codegen/templates/python/enum.tpl.py index 21ee91261..d4ecbce50 100644 --- a/packages/cw-schema-codegen/templates/python/enum.tpl.py +++ b/packages/cw-schema-codegen/templates/python/enum.tpl.py @@ -1,40 +1,44 @@ # This code is @generated by cw-schema-codegen. Do not modify this manually. +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from typing import Optional, Iterable -/** -{% for doc in docs %} - * {{ doc }} -{% endfor %} - */ +enum_field = lambda: field(default=None, metadata=config(exclude=lambda x: x is None)) -type {{ name }} = -{% for variant in variants %} - | +@dataclass_json +@dataclass +class {{ name }}: + '''{% for doc in docs %} + {{ doc }} + {% endfor %}''' + + class VariantIndicator: + ''' + This structure is an indicator of the simple enum variant that is currently contained + in this enum structure. It's used only for the enum variants that does not contain + any inner structure. It's constructed automatically and it's not intend to be manually + used by the user. + ''' + pass - /** - {% for doc in variant.docs %} - * {{ doc }} - {% endfor %} - */ +{% for variant in variants %} + '''{% for doc in variant.docs %} + {{ doc }} + {% endfor %}''' {% match variant.ty %} {% when TypeTemplate::Unit %} - { "{{ variant.name }}": {} } + {{ variant.name }}: Optional[VariantIndicator] = enum_field() {% when TypeTemplate::Tuple with (types) %} - { "{{ variant.name }}": [{{ types|join(", ") }}] } + {{ variant.name }}: Optional[tuple[{{ types|join(", ") }}]] = enum_field() {% when TypeTemplate::Named with { fields } %} - { "{{ variant.name }}": { + class {{ variant.name }}Type: {% for field in fields %} - /** - {% for doc in field.docs %} - * {{ doc }} - {% endfor %} - */ - - {{ field.name }}: {{ field.ty }}; + '''{% for doc in field.docs %} + # {{ doc }} + {% endfor %}''' + {{ field.name }}: {{ field.ty }} {% endfor %} - } } + {{ variant.name }}: Optional[{{ variant.name }}Type] = enum_field() {% endmatch %} -{% endfor %} -; - -export { {{ 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 index 08b30d5d4..ef7ef8698 100644 --- a/packages/cw-schema-codegen/templates/python/struct.tpl.py +++ b/packages/cw-schema-codegen/templates/python/struct.tpl.py @@ -25,6 +25,31 @@ {% endfor %} } {% endmatch %} -; -export { {{ name }} }; + + +# This code is @generated by cw-schema-codegen. Do not modify this manually. +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from typing import Optional, Iterable + +@dataclass_json +@dataclass +class {{ name }}: + '''{% for doc in docs %} + {{ doc }} + {% endfor %}''' + + {% match ty %} + {% when TypeTemplate::Unit %} + pass + {% when TypeTemplate::Tuple with (types) %} + {{ variant.name }}: tuple[{{ types|join(", ") }}] + {% when TypeTemplate::Named with { fields } %} + {% for field in fields %} + '''{% for doc in field.docs %} + # {{ doc }} + {% endfor %}''' + {{ field.name }}: {{ field.ty }} + {% 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..410995922 --- /dev/null +++ b/packages/cw-schema-codegen/tests/python_tpl.rs @@ -0,0 +1,29 @@ +use std::borrow::Cow; + +use askama::Template; +use cw_schema_codegen::python::template::{ + EnumTemplate, EnumVariantTemplate, FieldTemplate, StructTemplate, TypeTemplate, +}; + +#[test] +fn simple_enum() { + let tpl = EnumTemplate { + name: Cow::Borrowed("Simple"), + docs: Cow::Borrowed(&[Cow::Borrowed("Simple enum")]), + variants: Cow::Borrowed(&[ + EnumVariantTemplate { + name: Cow::Borrowed("One"), + docs: Cow::Borrowed(&[Cow::Borrowed("One variant")]), + ty: TypeTemplate::Unit, + }, + EnumVariantTemplate { + name: Cow::Borrowed("Two"), + docs: Cow::Borrowed(&[Cow::Borrowed("Two variant")]), + ty: TypeTemplate::Unit, + }, + ]), + }; + + let rendered = tpl.render().unwrap(); + insta::assert_snapshot!(rendered); +} 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..3ec74d43f --- /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: rendered +snapshot_kind: text +--- +# This code is @generated by cw-schema-codegen. Do not modify this manually. +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from typing import Optional, Iterable + +enum_field = lambda: field(default=None, metadata=config(exclude=lambda x: x is None)) + +@dataclass_json +@dataclass +class Simple: + ''' + Simple enum + ''' + + class VariantIndicator: + ''' + This structure is an indicator of the simple enum variant that is currently contained + in this enum structure. It's used only for the enum variants that does not contain + any inner structure. It's constructed automatically and it's not intend to be manually + used by the user. + ''' + pass + + + + ''' + One variant + ''' + + One: Optional[VariantIndicator] = enum_field() + + + ''' + Two variant + ''' + + Two: Optional[VariantIndicator] = enum_field() From 2a737c85a640af87c7cea521cd2e93937ecf27a9 Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Fri, 22 Nov 2024 15:42:51 +0100 Subject: [PATCH 3/5] chore: Replace dataclasses and dataclasses_json with pydantic --- .../playground/playground.py | 142 +++++++----------- .../templates/python/enum.tpl.py | 68 ++++----- .../templates/python/struct.tpl.py | 72 +++------ .../snapshots/python_tpl__simple_enum.snap | 54 +++---- 4 files changed, 130 insertions(+), 206 deletions(-) diff --git a/packages/cw-schema-codegen/playground/playground.py b/packages/cw-schema-codegen/playground/playground.py index ac850f87d..6a3582e88 100644 --- a/packages/cw-schema-codegen/playground/playground.py +++ b/packages/cw-schema-codegen/playground/playground.py @@ -1,84 +1,46 @@ -from dataclasses import dataclass, field -from dataclasses_json import dataclass_json, config -from typing import Optional, Iterable import sys -import json +from typing import Literal, Union, Tuple +from pydantic import BaseModel, RootModel -# TODO tkulik: try to get rid of the `dataclasses_json` dependency +class SomeEnum(RootModel): + class Field1(RootModel[Literal['Field1']]): + pass + class Field2(BaseModel): + Field2: Tuple[int, int] -enum_field = lambda: field(default=None, metadata=config(exclude=lambda x: x is None)) + class Field3(BaseModel): + class __InnerStruct(BaseModel): + a: str + b: int + Field3: __InnerStruct -@dataclass_json -@dataclass -class SomeEnum: - class VariantIndicator: - pass + class Field4(BaseModel): + Field4: 'SomeEnum' - class Field3Type: - a: str - b: int - - class Field5Type: - a: Iterable['SomeEnum'] - - Field1: Optional[VariantIndicator] = enum_field() - Field2: Optional[tuple[int, int]] = enum_field() - Field3: Optional[Field3Type] = enum_field() - Field4: Optional[Iterable['SomeEnum']] = enum_field() - Field5: Optional[Field5Type] = enum_field() - - def deserialize(json): - if not ":" in json: - if json == '"Field1"': - return SomeEnum(Field1=SomeEnum.VariantIndicator()) - else: - raise Exception(f"Deserialization error, undefined variant: {json}") - else: - return SomeEnum.from_json(json) - - def serialize(self): - if self.Field1 is not None: - return '"Field1"' - else: - return SomeEnum.to_json(self) - -@dataclass_json -@dataclass -class UnitStructure: - def deserialize(json): - if json == "null": - return UnitStructure() - else: - Exception(f"Deserialization error, undefined value: {json}") - - def serialize(self): - return 'null' - -@dataclass_json -@dataclass -class TupleStructure: - Tuple: tuple[int, str, int] - - def deserialize(json): - return TupleStructure.from_json(f'{{ "Tuple": {json} }}') - - def serialize(self): - return json.dumps(self.Tuple) - -@dataclass_json -@dataclass -class NamedStructure: + class Field5(BaseModel): + class __InnerStruct(BaseModel): + a: 'SomeEnum' + Field5: __InnerStruct + + root: Union[Field1, Field2, Field3, Field4, Field5] + + +class UnitStructure(RootModel): + root: None + + +class TupleStructure(RootModel): + root: Tuple[int, str, int] + + +class NamedStructure(BaseModel): a: str b: int - c: Iterable['SomeEnum'] + c: SomeEnum + - def deserialize(json): - return NamedStructure.from_json(json) - - def serialize(self): - return self.to_json() ### ### TESTS: @@ -88,32 +50,36 @@ def serialize(self): input = input.rstrip() try: if index < 5: - deserialized = SomeEnum.deserialize(input) + deserialized = SomeEnum.model_validate_json(input) elif index == 5: - deserialized = UnitStructure.deserialize(input) + deserialized = UnitStructure.model_validate_json(input) elif index == 6: - deserialized = TupleStructure.deserialize(input) + deserialized = TupleStructure.model_validate_json(input) else: - deserialized = NamedStructure.deserialize(input) + deserialized = NamedStructure.model_validate_json(input) except: raise(Exception(f"This json can't be deserialized: {input}")) - serialized = deserialized.serialize() + serialized = deserialized.model_dump_json() print(serialized) # def handle_msg(json): -# a = SomeEnum.deserialize(json) -# if a.Field1 is not None: +# a = SomeEnum.model_validate_json(json) +# if isinstance(a.root, SomeEnum.Field1): # print("SomeEnum::Field1") -# elif a.Field2 is not None: -# print(a.Field2[0]) -# print(a.Field2[1]) -# elif a.Field3 is not None: -# print(a.Field3) -# elif a.Field4 is not None: -# print(a.Field4) -# elif a.Field5 is not None: -# print(a.Field5) +# elif isinstance(a.root, SomeEnum.Field2): +# print(a.root.Field2[0]) +# print(a.root.Field2[1]) +# elif isinstance(a.root, SomeEnum.Field3): +# print(a.root.Field3) +# elif isinstance(a.root, SomeEnum.Field4): +# print(a.root.Field4) +# elif isinstance(a.root, SomeEnum.Field5): +# print(a.root.Field5) # handle_msg('"Field1"') -# handle_msg('{"Field2": [10, 12]}') \ No newline at end of file +# handle_msg('{"Field2": [10, 12]}') +# handle_msg('{"Field3": { "a": "10", "b": 12 } }') +# handle_msg('{"Field4": { "Field4": "Field1" } }') +# handle_msg('{"Field5": { "a": "Field1" } }') +# handle_msg('{"Field5": { "a": { "Field5": { "a": "Field1" } } } }') \ No newline at end of file diff --git a/packages/cw-schema-codegen/templates/python/enum.tpl.py b/packages/cw-schema-codegen/templates/python/enum.tpl.py index d4ecbce50..d4bb2b5c7 100644 --- a/packages/cw-schema-codegen/templates/python/enum.tpl.py +++ b/packages/cw-schema-codegen/templates/python/enum.tpl.py @@ -1,44 +1,38 @@ # This code is @generated by cw-schema-codegen. Do not modify this manually. -from dataclasses import dataclass, field -from dataclasses_json import dataclass_json, config -from typing import Optional, Iterable -enum_field = lambda: field(default=None, metadata=config(exclude=lambda x: x is None)) +import typing +from pydantic import BaseModel, RootModel -@dataclass_json -@dataclass -class {{ name }}: - '''{% for doc in docs %} +class {{ name }}(RootModel): + """{% for doc in docs %} {{ doc }} - {% endfor %}''' - - class VariantIndicator: - ''' - This structure is an indicator of the simple enum variant that is currently contained - in this enum structure. It's used only for the enum variants that does not contain - any inner structure. It's constructed automatically and it's not intend to be manually - used by the user. - ''' - pass - + {% endfor %}""" {% for variant in variants %} - '''{% for doc in variant.docs %} - {{ doc }} - {% endfor %}''' - {% match variant.ty %} - {% when TypeTemplate::Unit %} - {{ variant.name }}: Optional[VariantIndicator] = enum_field() - {% when TypeTemplate::Tuple with (types) %} - {{ variant.name }}: Optional[tuple[{{ types|join(", ") }}]] = enum_field() - {% when TypeTemplate::Named with { fields } %} - class {{ variant.name }}Type: - {% for field in fields %} - '''{% for doc in field.docs %} - # {{ doc }} - {% endfor %}''' - {{ field.name }}: {{ field.ty }} - {% endfor %} - {{ variant.name }}: Optional[{{ variant.name }}Type] = enum_field() - {% endmatch %} +{% match variant.ty %} +{% when TypeTemplate::Unit %} + class {{ variant.name }}(RootModel): + """{% for doc in variant.docs %} + {{ doc }} + {% endfor %}""" + root: None +{% 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 __Inner: + """{% 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 %} \ 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 index ef7ef8698..6fa24d395 100644 --- a/packages/cw-schema-codegen/templates/python/struct.tpl.py +++ b/packages/cw-schema-codegen/templates/python/struct.tpl.py @@ -1,55 +1,31 @@ # This code is @generated by cw-schema-codegen. Do not modify this manually. -/** -{% for doc in docs %} - * {{ doc }} -{% endfor %} - */ - -type {{ name }} = -{% match ty %} - {% when TypeTemplate::Unit %} - void - {% when TypeTemplate::Tuple with (types) %} - [{{ types|join(", ") }}] - {% when TypeTemplate::Named with { fields } %} - { - {% for field in fields %} - /** - {% for doc in field.docs %} - * {{ doc }} - {% endfor %} - */ - - {{ field.name }}: {{ field.ty }}; - {% endfor %} - } -{% endmatch %} - +import typing +from pydantic import BaseModel, RootModel -# This code is @generated by cw-schema-codegen. Do not modify this manually. -from dataclasses import dataclass, field -from dataclasses_json import dataclass_json, config -from typing import Optional, Iterable - -@dataclass_json -@dataclass -class {{ name }}: +{% match ty %} +{% when TypeTemplate::Unit %} +class {{ name }}(RootModel): '''{% for doc in docs %} {{ doc }} {% endfor %}''' - - {% match ty %} - {% when TypeTemplate::Unit %} - pass - {% when TypeTemplate::Tuple with (types) %} - {{ variant.name }}: tuple[{{ types|join(", ") }}] - {% when TypeTemplate::Named with { fields } %} - {% for field in fields %} - '''{% for doc in field.docs %} - # {{ doc }} - {% endfor %}''' - {{ field.name }}: {{ field.ty }} - {% endfor %} - {% endmatch %} + 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/snapshots/python_tpl__simple_enum.snap b/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap index 3ec74d43f..e937c0fb7 100644 --- a/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap +++ b/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap @@ -4,39 +4,27 @@ expression: rendered snapshot_kind: text --- # This code is @generated by cw-schema-codegen. Do not modify this manually. -from dataclasses import dataclass, field -from dataclasses_json import dataclass_json, config -from typing import Optional, Iterable -enum_field = lambda: field(default=None, metadata=config(exclude=lambda x: x is None)) +import typing +from pydantic import BaseModel, RootModel -@dataclass_json -@dataclass -class Simple: - ''' +class Simple(RootModel): + """ Simple enum - ''' - - class VariantIndicator: - ''' - This structure is an indicator of the simple enum variant that is currently contained - in this enum structure. It's used only for the enum variants that does not contain - any inner structure. It's constructed automatically and it's not intend to be manually - used by the user. - ''' - pass - - - - ''' - One variant - ''' - - One: Optional[VariantIndicator] = enum_field() - - - ''' - Two variant - ''' - - Two: Optional[VariantIndicator] = enum_field() + """ + + + + class One(RootModel): + """ + One variant + """ + root: None + + + + class Two(RootModel): + """ + Two variant + """ + root: None From 777aca330c16079a43c8525ae1f723c6761cb8be Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Tue, 26 Nov 2024 14:22:46 +0100 Subject: [PATCH 4/5] chore: Add tests and validate schemas --- .../playground/playground.py | 25 +-- packages/cw-schema-codegen/src/python/mod.rs | 27 ++- .../templates/python/enum.tpl.py | 27 +-- .../templates/python/struct.tpl.py | 3 +- .../cw-schema-codegen/tests/python_tpl.rs | 175 +++++++++++++++--- .../snapshots/python_tpl__simple_enum.snap | 42 +++-- 6 files changed, 217 insertions(+), 82 deletions(-) diff --git a/packages/cw-schema-codegen/playground/playground.py b/packages/cw-schema-codegen/playground/playground.py index 6a3582e88..f3adbfad1 100644 --- a/packages/cw-schema-codegen/playground/playground.py +++ b/packages/cw-schema-codegen/playground/playground.py @@ -24,7 +24,7 @@ class __InnerStruct(BaseModel): a: 'SomeEnum' Field5: __InnerStruct - root: Union[Field1, Field2, Field3, Field4, Field5] + root: Union[Field1, Field2, Field3, Field4, Field5,] class UnitStructure(RootModel): @@ -46,21 +46,14 @@ class NamedStructure(BaseModel): ### TESTS: ### -for (index, input) in enumerate(sys.stdin): - input = input.rstrip() - try: - if index < 5: - deserialized = SomeEnum.model_validate_json(input) - elif index == 5: - deserialized = UnitStructure.model_validate_json(input) - elif index == 6: - deserialized = TupleStructure.model_validate_json(input) - else: - deserialized = NamedStructure.model_validate_json(input) - except: - raise(Exception(f"This json can't be deserialized: {input}")) - serialized = deserialized.model_dump_json() - print(serialized) +print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(UnitStructure.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(TupleStructure.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) +print(NamedStructure.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) # def handle_msg(json): diff --git a/packages/cw-schema-codegen/src/python/mod.rs b/packages/cw-schema-codegen/src/python/mod.rs index da918a8b0..b60d0e0c0 100644 --- a/packages/cw-schema-codegen/src/python/mod.rs +++ b/packages/cw-schema-codegen/src/python/mod.rs @@ -1,7 +1,6 @@ use self::template::{ EnumTemplate, EnumVariantTemplate, FieldTemplate, StructTemplate, TypeTemplate, }; -use heck::ToPascalCase; use std::{borrow::Cow, io}; pub mod template; @@ -15,15 +14,15 @@ fn expand_node_name<'a>( let items = &schema.definitions[items]; format!("{}[]", expand_node_name(schema, items)).into() } - cw_schema::NodeType::Float => "number".into(), - cw_schema::NodeType::Double => "number".into(), - cw_schema::NodeType::Boolean => "boolean".into(), - cw_schema::NodeType::String => "string".into(), - cw_schema::NodeType::Integer { .. } => "string".into(), - cw_schema::NodeType::Binary => "Uint8Array".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!("{} | null", expand_node_name(schema, inner)).into() + format!("typing.Optional[{}]", expand_node_name(schema, inner)).into() } cw_schema::NodeType::Struct(..) => node.name.as_ref().into(), cw_schema::NodeType::Tuple { ref items } => { @@ -37,13 +36,13 @@ fn expand_node_name<'a>( } cw_schema::NodeType::Enum { .. } => node.name.as_ref().into(), - cw_schema::NodeType::Decimal { .. } => "string".into(), - cw_schema::NodeType::Address => "string".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 => Cow::Borrowed("void"), - _ => todo!() + cw_schema::NodeType::Unit => "None".into(), + _ => todo!(), } } @@ -83,7 +82,7 @@ where .map(|item| expand_node_name(schema, &schema.definitions[*item])) .collect(), ), - _ => todo!() + _ => todo!(), }, }; @@ -125,7 +124,7 @@ where .collect(), } } - _ => todo!() + _ => todo!(), }, }) .collect(), diff --git a/packages/cw-schema-codegen/templates/python/enum.tpl.py b/packages/cw-schema-codegen/templates/python/enum.tpl.py index d4bb2b5c7..5f647687e 100644 --- a/packages/cw-schema-codegen/templates/python/enum.tpl.py +++ b/packages/cw-schema-codegen/templates/python/enum.tpl.py @@ -1,6 +1,7 @@ # 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): @@ -15,7 +16,7 @@ class {{ variant.name }}(RootModel): """{% for doc in variant.docs %} {{ doc }} {% endfor %}""" - root: None + root: typing.Literal['{{ variant.name }}'] {% when TypeTemplate::Tuple with (types) %} class {{ variant.name }}(BaseModel): """{% for doc in variant.docs %} @@ -23,16 +24,18 @@ class {{ variant.name }}(BaseModel): {% endfor %}""" {{ variant.name }}: typing.Tuple[{{ types|join(", ") }}] {% when TypeTemplate::Named with { fields } %} - class __Inner: - """{% for doc in variant.docs %} - {{ doc }} - {% endfor %}""" - {% for field in fields %} - {{ field.name }}: {{ field.ty }} - """{% for doc in field.docs %} - # {{ doc }} - {% endfor %}""" - {% endfor %} + 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 %} \ No newline at end of file +{% 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 index 6fa24d395..c565beda4 100644 --- a/packages/cw-schema-codegen/templates/python/struct.tpl.py +++ b/packages/cw-schema-codegen/templates/python/struct.tpl.py @@ -1,6 +1,7 @@ # This code is @generated by cw-schema-codegen. Do not modify this manually. import typing +import decimal from pydantic import BaseModel, RootModel @@ -10,7 +11,7 @@ class {{ name }}(RootModel): '''{% for doc in docs %} {{ doc }} {% endfor %}''' - root: None + root: None {% when TypeTemplate::Tuple with (types) %} class {{ name }}(RootModel): '''{% for doc in docs %} diff --git a/packages/cw-schema-codegen/tests/python_tpl.rs b/packages/cw-schema-codegen/tests/python_tpl.rs index 410995922..347d977b0 100644 --- a/packages/cw-schema-codegen/tests/python_tpl.rs +++ b/packages/cw-schema-codegen/tests/python_tpl.rs @@ -1,29 +1,156 @@ -use std::borrow::Cow; +use cw_schema::Schemaifier; +use serde::{Deserialize, Serialize}; +use std::io::Write; -use askama::Template; -use cw_schema_codegen::python::template::{ - EnumTemplate, EnumVariantTemplate, FieldTemplate, StructTemplate, TypeTemplate, -}; +#[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() { - let tpl = EnumTemplate { - name: Cow::Borrowed("Simple"), - docs: Cow::Borrowed(&[Cow::Borrowed("Simple enum")]), - variants: Cow::Borrowed(&[ - EnumVariantTemplate { - name: Cow::Borrowed("One"), - docs: Cow::Borrowed(&[Cow::Borrowed("One variant")]), - ty: TypeTemplate::Unit, - }, - EnumVariantTemplate { - name: Cow::Borrowed("Two"), - docs: Cow::Borrowed(&[Cow::Borrowed("Two variant")]), - ty: TypeTemplate::Unit, - }, - ]), - }; - - let rendered = tpl.render().unwrap(); - insta::assert_snapshot!(rendered); + // 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 index e937c0fb7..a8a9224d1 100644 --- a/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap +++ b/packages/cw-schema-codegen/tests/snapshots/python_tpl__simple_enum.snap @@ -1,30 +1,42 @@ --- source: packages/cw-schema-codegen/tests/python_tpl.rs -expression: rendered +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 Simple(RootModel): - """ - Simple enum - """ +class SomeEnum(RootModel): + """""" - class One(RootModel): - """ - One variant - """ - root: None + class Field1(RootModel): + """""" + root: typing.Literal['Field1'] - class Two(RootModel): - """ - Two variant - """ - root: None + 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, ] From 285abd6dc296e14200466425b7e2814fd14cc6dd Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Wed, 27 Nov 2024 11:30:18 +0100 Subject: [PATCH 5/5] chore: Remove python playground --- Cargo.toml | 2 +- .../cw-schema-codegen/playground/.gitignore | 1 - .../cw-schema-codegen/playground/Cargo.lock | 96 ------------------- .../cw-schema-codegen/playground/Cargo.toml | 11 --- .../playground/playground.py | 78 --------------- .../cw-schema-codegen/playground/src/main.rs | 59 ------------ packages/cw-schema-codegen/playground/test.sh | 1 - 7 files changed, 1 insertion(+), 247 deletions(-) delete mode 100644 packages/cw-schema-codegen/playground/.gitignore delete mode 100644 packages/cw-schema-codegen/playground/Cargo.lock delete mode 100644 packages/cw-schema-codegen/playground/Cargo.toml delete mode 100644 packages/cw-schema-codegen/playground/playground.py delete mode 100644 packages/cw-schema-codegen/playground/src/main.rs delete mode 100755 packages/cw-schema-codegen/playground/test.sh diff --git a/Cargo.toml b/Cargo.toml index c06f9b080..c94fa1046 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = ["packages/*"] -exclude = ["contracts", "packages/cw-schema-codegen/playground"] +exclude = ["contracts"] # Resolver has to be set explicitly in workspaces # due to https://github.com/rust-lang/cargo/issues/9956 diff --git a/packages/cw-schema-codegen/playground/.gitignore b/packages/cw-schema-codegen/playground/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/packages/cw-schema-codegen/playground/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/packages/cw-schema-codegen/playground/Cargo.lock b/packages/cw-schema-codegen/playground/Cargo.lock deleted file mode 100644 index 867ff0bae..000000000 --- a/packages/cw-schema-codegen/playground/Cargo.lock +++ /dev/null @@ -1,96 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "proc-macro2" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "serde" -version = "1.0.215" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.215" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.133" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serialization" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "2.0.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" diff --git a/packages/cw-schema-codegen/playground/Cargo.toml b/packages/cw-schema-codegen/playground/Cargo.toml deleted file mode 100644 index a5aa7ba8f..000000000 --- a/packages/cw-schema-codegen/playground/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "serialization" -version = "0.1.0" -edition = "2021" - -[features] -deserialize = [] - -[dependencies] -serde = { version = "1.0.215", features = ["derive", "serde_derive"] } -serde_json = "1.0.133" diff --git a/packages/cw-schema-codegen/playground/playground.py b/packages/cw-schema-codegen/playground/playground.py deleted file mode 100644 index f3adbfad1..000000000 --- a/packages/cw-schema-codegen/playground/playground.py +++ /dev/null @@ -1,78 +0,0 @@ -import sys -from typing import Literal, Union, Tuple -from pydantic import BaseModel, RootModel - - -class SomeEnum(RootModel): - class Field1(RootModel[Literal['Field1']]): - pass - - class Field2(BaseModel): - Field2: Tuple[int, int] - - class Field3(BaseModel): - class __InnerStruct(BaseModel): - a: str - b: int - Field3: __InnerStruct - - class Field4(BaseModel): - Field4: 'SomeEnum' - - class Field5(BaseModel): - class __InnerStruct(BaseModel): - a: 'SomeEnum' - Field5: __InnerStruct - - root: Union[Field1, Field2, Field3, Field4, Field5,] - - -class UnitStructure(RootModel): - root: None - - -class TupleStructure(RootModel): - root: Tuple[int, str, int] - - -class NamedStructure(BaseModel): - a: str - b: int - c: SomeEnum - - - -### -### TESTS: -### - -print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(SomeEnum.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(UnitStructure.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(TupleStructure.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) -print(NamedStructure.model_validate_json(sys.stdin.readline().rstrip()).model_dump_json()) - - -# def handle_msg(json): -# a = SomeEnum.model_validate_json(json) -# if isinstance(a.root, SomeEnum.Field1): -# print("SomeEnum::Field1") -# elif isinstance(a.root, SomeEnum.Field2): -# print(a.root.Field2[0]) -# print(a.root.Field2[1]) -# elif isinstance(a.root, SomeEnum.Field3): -# print(a.root.Field3) -# elif isinstance(a.root, SomeEnum.Field4): -# print(a.root.Field4) -# elif isinstance(a.root, SomeEnum.Field5): -# print(a.root.Field5) - -# handle_msg('"Field1"') -# handle_msg('{"Field2": [10, 12]}') -# handle_msg('{"Field3": { "a": "10", "b": 12 } }') -# handle_msg('{"Field4": { "Field4": "Field1" } }') -# handle_msg('{"Field5": { "a": "Field1" } }') -# handle_msg('{"Field5": { "a": { "Field5": { "a": "Field1" } } } }') \ No newline at end of file diff --git a/packages/cw-schema-codegen/playground/src/main.rs b/packages/cw-schema-codegen/playground/src/main.rs deleted file mode 100644 index 747e7ab89..000000000 --- a/packages/cw-schema-codegen/playground/src/main.rs +++ /dev/null @@ -1,59 +0,0 @@ - -use serde::{Deserialize, Serialize}; - - -#[derive(Serialize, Deserialize)] -pub enum SomeEnum { - Field1, - Field2(u32, u32), - Field3 { - a: String, - b: u32 - }, - Field4(Box), - Field5 { a: Box }, -} - -#[derive(Serialize, Deserialize)] -pub struct UnitStructure; - -#[derive(Serialize, Deserialize)] -pub struct TupleStructure(u32, String, u128); - -#[derive(Serialize, Deserialize)] -pub struct NamedStructure { - a: String, - b: u8, - c: SomeEnum -} - - -#[cfg(not(feature = "deserialize"))] -fn main() { - println!("{}", serde_json::to_string(&SomeEnum::Field1).unwrap()); - println!("{}", serde_json::to_string(&SomeEnum::Field2(10, 23)).unwrap()); - println!("{}", serde_json::to_string(&SomeEnum::Field3 {a: "sdf".to_string(), b: 12}).unwrap()); - println!("{}", serde_json::to_string(&SomeEnum::Field4(Box::new(SomeEnum::Field1))).unwrap()); - println!("{}", serde_json::to_string(&SomeEnum::Field5 { a: Box::new(SomeEnum::Field1) }).unwrap()); - println!("{}", serde_json::to_string(&UnitStructure {}).unwrap()); - println!("{}", serde_json::to_string(&TupleStructure(10, "aasdf".to_string(), 2)).unwrap()); - println!("{}", serde_json::to_string(&NamedStructure {a: "awer".to_string(), b: 4, c: SomeEnum::Field1}).unwrap()); -} - -#[cfg(feature = "deserialize")] -fn main() { - use std::io::BufRead; - for (index, line) in std::io::BufReader::new(std::io::stdin()).lines().enumerate() { - let line = line.unwrap(); - println!("{line}"); - if index < 5 { - let _: SomeEnum = serde_json::from_str(&line).unwrap(); - } else if index == 5 { - let _: UnitStructure = serde_json::from_str(&line).unwrap(); - } else if index == 6 { - let _: TupleStructure = serde_json::from_str(&line).unwrap(); - } else { - let _: NamedStructure = serde_json::from_str(&line).unwrap(); - } - } -} diff --git a/packages/cw-schema-codegen/playground/test.sh b/packages/cw-schema-codegen/playground/test.sh deleted file mode 100755 index 921d0e29b..000000000 --- a/packages/cw-schema-codegen/playground/test.sh +++ /dev/null @@ -1 +0,0 @@ -cargo run | python playground.py | cargo run --features "deserialize" \ No newline at end of file