From 1fb7d037a8704fc0c3e866b80de6d52d643a4a03 Mon Sep 17 00:00:00 2001 From: Tomasz Kulik Date: Fri, 8 Nov 2024 08:11:23 +0100 Subject: [PATCH] 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 | 76 +++++++++++++++ .../cw-schema-codegen/playground/src/main.rs | 35 +++++++ 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, 354 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..0da2c38fa --- /dev/null +++ b/packages/cw-schema-codegen/playground/playground.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass, field +from dataclasses_json import dataclass_json, config +from typing import Optional, Iterable +import sys + + +# 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) + + +### +### TESTS: +### + +for input in sys.stdin: + input = input.rstrip() + try: + deserialized = SomeEnum.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..aa2cd5d47 --- /dev/null +++ b/packages/cw-schema-codegen/playground/src/main.rs @@ -0,0 +1,35 @@ + +use serde::{Deserialize, Serialize}; + + +#[derive(Serialize, Deserialize)] +pub enum SomeEnum { + Field1, + Field2(u32, u32), + Field3 { + a: String, + b: u32 + }, + Field4(Box), + Field5 { a: Box }, +} + + +#[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()); +} + +#[cfg(feature = "deserialize")] +fn main() { + use std::io::BufRead; + for line in std::io::BufReader::new(std::io::stdin()).lines() { + let line = line.unwrap(); + println!("{line}"); + let _: SomeEnum = 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()