From 880da8f23d6b2a1add004dfe6cd728142a8c0eb1 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Tue, 18 Nov 2025 15:00:35 +0100 Subject: [PATCH 1/3] feat(idl-v2): IDL v2 template gen from AST (askama) --- Cargo.lock | 2 + rs/idl-meta/Cargo.toml | 11 +- rs/idl-meta/src/ast.rs | 35 ++ rs/idl-meta/templates/field.askama | 7 + rs/idl-meta/templates/idl.askama | 13 + rs/idl-meta/templates/program.askama | 52 ++ rs/idl-meta/templates/service.askama | 54 +++ rs/idl-meta/templates/struct_def.askama | 17 + rs/idl-meta/templates/type.askama | 25 + rs/idl-meta/templates/variant.askama | 7 + .../snapshots/templates__idl_globals.snap | 7 + .../snapshots/templates__idl_program.snap | 33 ++ .../snapshots/templates__idl_service.snap | 56 +++ .../tests/snapshots/templates__type_enum.snap | 29 ++ rs/idl-meta/tests/templates.rs | 456 ++++++++++++++++++ 15 files changed, 803 insertions(+), 1 deletion(-) create mode 100644 rs/idl-meta/templates/field.askama create mode 100644 rs/idl-meta/templates/idl.askama create mode 100644 rs/idl-meta/templates/program.askama create mode 100644 rs/idl-meta/templates/service.askama create mode 100644 rs/idl-meta/templates/struct_def.askama create mode 100644 rs/idl-meta/templates/type.askama create mode 100644 rs/idl-meta/templates/variant.askama create mode 100644 rs/idl-meta/tests/snapshots/templates__idl_globals.snap create mode 100644 rs/idl-meta/tests/snapshots/templates__idl_program.snap create mode 100644 rs/idl-meta/tests/snapshots/templates__idl_service.snap create mode 100644 rs/idl-meta/tests/snapshots/templates__type_enum.snap create mode 100644 rs/idl-meta/tests/templates.rs diff --git a/Cargo.lock b/Cargo.lock index c6cc02387..db0bf7311 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7247,6 +7247,8 @@ dependencies = [ name = "sails-idl-meta" version = "0.9.2" dependencies = [ + "askama", + "insta", "scale-info", ] diff --git a/rs/idl-meta/Cargo.toml b/rs/idl-meta/Cargo.toml index 19b44ab57..10bd7c7d3 100644 --- a/rs/idl-meta/Cargo.toml +++ b/rs/idl-meta/Cargo.toml @@ -10,8 +10,17 @@ repository.workspace = true rust-version.workspace = true [dependencies] +askama = { workspace = true, optional = true } scale-info.workspace = true +[dev-dependencies] +insta.workspace = true + [features] -default = [ "ast" ] +default = ["ast"] ast = [] +templates = ["dep:askama", "ast"] + +[[test]] +name = "templates" +required-features = ["templates"] diff --git a/rs/idl-meta/src/ast.rs b/rs/idl-meta/src/ast.rs index e7dd220fb..b0a26c3bf 100644 --- a/rs/idl-meta/src/ast.rs +++ b/rs/idl-meta/src/ast.rs @@ -16,6 +16,11 @@ use core::fmt::{Display, Write}; /// - `program` holds an optional `program { ... }` block; /// - `services` contains all top-level `service { ... }` definitions. #[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "idl.askama", escape = "none") +)] pub struct IdlDoc { pub globals: Vec<(String, Option)>, pub program: Option, @@ -30,6 +35,11 @@ pub struct IdlDoc { /// - may define shared types in `types { ... }`, /// - may contain documentation comments and annotations. #[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "program.askama", escape = "none") +)] pub struct ProgramUnit { pub name: String, pub ctors: Vec, @@ -76,6 +86,11 @@ pub struct CtorFunc { /// - defines service-local `types { ... }`, /// - may contain documentation comments and annotations. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "service.askama", escape = "none") +)] pub struct ServiceUnit { pub name: String, pub extends: Vec, @@ -360,6 +375,11 @@ impl core::str::FromStr for PrimitiveType { /// `Type` describes either a struct or enum with an optional list of generic /// type parameters, along with documentation and annotations taken from IDL. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "type.askama", escape = "none") +)] pub struct Type { pub name: String, pub type_params: Vec, @@ -408,6 +428,11 @@ pub enum TypeDef { /// - classic form with named fields, /// - tuple-like form with unnamed fields. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "struct_def.askama", escape = "none") +)] pub struct StructDef { pub fields: Vec, } @@ -437,6 +462,11 @@ impl StructDef { /// `name` is `None` for tuple-like structs / variants; otherwise it stores the /// field identifier from IDL. Each field keeps its own documentation and annotations. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "field.askama", escape = "none") +)] pub struct StructField { pub name: Option, pub type_decl: TypeDecl, @@ -459,6 +489,11 @@ pub struct EnumDef { /// - `def` is a `StructDef` describing the payload shape (unit / classic / tuple), /// - `docs` and `annotations` are attached to the variant in IDL. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "templates", + derive(askama::Template), + template(path = "variant.askama", escape = "none") +)] pub struct EnumVariant { pub name: String, pub def: StructDef, diff --git a/rs/idl-meta/templates/field.askama b/rs/idl-meta/templates/field.askama new file mode 100644 index 000000000..db84d169b --- /dev/null +++ b/rs/idl-meta/templates/field.askama @@ -0,0 +1,7 @@ +{%- for d in docs %} +/// {{ d }} +{%- endfor %} +{%- for (k, v) in annotations %} +@{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} +{%- endfor %} +{% if name.is_some() -%}{{ name.as_ref().unwrap() }}: {% endif -%}{{- type_decl -}} diff --git a/rs/idl-meta/templates/idl.askama b/rs/idl-meta/templates/idl.askama new file mode 100644 index 000000000..790997629 --- /dev/null +++ b/rs/idl-meta/templates/idl.askama @@ -0,0 +1,13 @@ +{# -------- GLOBALS -------- #} +{%- for (k, v) in globals %} +!@{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} +{%- endfor %} +{# -------- SERVICES -------- #} +{%- for s in services -%} +{{ s }} +{%- endfor -%} +{# -------- PROGRAM -------- #} +{%- if program.is_some() -%} +{%- let p = program.as_ref().unwrap() -%} +{{ p }} +{%- endif -%} diff --git a/rs/idl-meta/templates/program.askama b/rs/idl-meta/templates/program.askama new file mode 100644 index 000000000..384acb404 --- /dev/null +++ b/rs/idl-meta/templates/program.askama @@ -0,0 +1,52 @@ +{%- for d in docs %} +/// {{ d }} +{%- endfor %} +{%- for (k, v) in annotations %} +@{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} +{%- endfor %} +program {{ name }} { + {%- if ctors.len() > 0 %} + constructors { + {%- for f in ctors %} + + {%- for d in f.docs %} + /// {{ d }} + {%- endfor %} + {%- for (k, v) in f.annotations %} + @{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} + {%- endfor %} + {{ f.name }}( + {%- for p in f.params %} + {%- if loop.first %}{{ p }}{% else %}, {{ p }}{% endif -%} + {%- endfor -%} + ); + + {%- endfor %} + } + {%- endif %} + + {%- if services.len() > 0 %} + services { + {%- for s in services %} + + {%- for d in s.docs %} + /// {{ d }} + {%- endfor %} + {%- for (k, v) in s.annotations %} + @{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} + {%- endfor %} + {% if s.route.is_some() %}{{ s.route.as_ref().unwrap() }}: {% endif %}{{ s.name }}, + + {%- endfor %} + } + {%- endif %} + + {%- if types.len() > 0 %} + types { + {%- for t in types -%} + {{ t | indent(8) }} + {%- endfor %} + } + {%- endif %} +} +{{- "\n" -}} diff --git a/rs/idl-meta/templates/service.askama b/rs/idl-meta/templates/service.askama new file mode 100644 index 000000000..bfe33d9c6 --- /dev/null +++ b/rs/idl-meta/templates/service.askama @@ -0,0 +1,54 @@ +{%- for d in docs -%} +/// {{ d }} +{%- endfor %} +{%- for (k, v) in annotations -%} +@{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} +{%- endfor %} +service {{ name }} { + {%- if extends.len() > 0 %} + extends { + {%- for ext in extends %} + {%- endfor %} + } + {%- endif %} + + {%- if events.len() > 0 %} + events { + {%- for evt in events -%} + {{ evt | indent(8) }}, + {%- endfor %} + } + {%- endif %} + + {%- if funcs.len() > 0 %} + functions { + {%- for f in funcs -%} + + {%- for d in f.docs %} + /// {{ d }} + {%- endfor %} + {%- for (k, v) in f.annotations %} + @{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} + {%- endfor %} + {{ f.name }}( + {%- for p in f.params %} + {%- if loop.first %}{{ p }}{% else %}, {{ p }}{% endif -%} + {%- endfor -%} + ) + {%- if !f.returns_void() %} -> {{ f.output }}{%- endif -%} + {%- if f.throws.is_some() %} throws {{ f.throws.as_ref().unwrap() }}{%- endif -%} + ; + + {%- endfor %} + } + {%- endif %} + + {%- if types.len() > 0 %} + types { + {%- for t in types -%} + {{ t | indent(8) }} + {%- endfor %} + } + {%- endif %} +} +{{- "\n" -}} diff --git a/rs/idl-meta/templates/struct_def.askama b/rs/idl-meta/templates/struct_def.askama new file mode 100644 index 000000000..f13d73600 --- /dev/null +++ b/rs/idl-meta/templates/struct_def.askama @@ -0,0 +1,17 @@ +{%- if is_unit() -%} +{%- else if is_inline() -%} ( + {%- for field in fields -%} + {%- if loop.first %}{{ field.type_decl }}{% else %}, {{ field.type_decl }}{% endif -%} + {%- endfor -%} +) +{%- else if is_tuple() %} ( + {%- for field in fields -%} + {{ field | indent(4) }}, + {%- endfor %} +) +{%- else %} { + {%- for field in fields -%} + {{ field | indent(4) }}, + {%- endfor %} +} +{%- endif -%} diff --git a/rs/idl-meta/templates/type.askama b/rs/idl-meta/templates/type.askama new file mode 100644 index 000000000..295db3f2d --- /dev/null +++ b/rs/idl-meta/templates/type.askama @@ -0,0 +1,25 @@ +{%- for d in docs %} +/// {{ d }} +{%- endfor %} +{%- for (k, v) in annotations %} +@{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} +{%- endfor %} +{% match def %}{% when TypeDef::Struct(_) %}struct{% when TypeDef::Enum(_) %}enum{% endmatch %} {{ name }} + +{%- if type_params.len() > 0 -%} +< +{%- for type_param in type_params -%} +{%- if loop.first %}{{ type_param }}{% else %}, {{ type_param }}{% endif -%} +{%- endfor -%} +> +{%- endif -%} + +{%- match def %} +{% when TypeDef::Struct(def) -%} +{{ def -}}{%- if def.is_unit() || def.is_tuple() -%};{%- endif -%} +{% when TypeDef::Enum(enum_def) %} { + {%- for variant in enum_def.variants -%} + {{ variant | indent(4) }}, + {%- endfor %} +} +{%- endmatch -%} diff --git a/rs/idl-meta/templates/variant.askama b/rs/idl-meta/templates/variant.askama new file mode 100644 index 000000000..e3b14d004 --- /dev/null +++ b/rs/idl-meta/templates/variant.askama @@ -0,0 +1,7 @@ +{%- for d in docs %} +/// {{ d }} +{%- endfor %} +{%- for (k, v) in annotations %} +@{{ k }} {%- if v.is_some() -%}: {{ v.as_ref().unwrap() }}{% endif %} +{%- endfor %} +{{ name }}{{ def -}} diff --git a/rs/idl-meta/tests/snapshots/templates__idl_globals.snap b/rs/idl-meta/tests/snapshots/templates__idl_globals.snap new file mode 100644 index 000000000..a4c91bb5b --- /dev/null +++ b/rs/idl-meta/tests/snapshots/templates__idl_globals.snap @@ -0,0 +1,7 @@ +--- +source: rs/idl-meta/tests/templates.rs +expression: idl +--- +!@sails: 0.1.0 +!@include: ownable.idl +!@include: git://github.com/some_repo/tippable.idl diff --git a/rs/idl-meta/tests/snapshots/templates__idl_program.snap b/rs/idl-meta/tests/snapshots/templates__idl_program.snap new file mode 100644 index 000000000..e83953aaa --- /dev/null +++ b/rs/idl-meta/tests/snapshots/templates__idl_program.snap @@ -0,0 +1,33 @@ +--- +source: rs/idl-meta/tests/templates.rs +expression: idl +--- +!@sails: 0.1.0 +!@include: ownable.idl +!@include: git://github.com/some_repo/tippable.idl + +/// Demo Program +program Demo { + constructors { + /// Program constructor (called once at the very beginning of the program lifetime) + Create(counter: Option, dog_position: Option<(i32, i32)>); + /// Another program constructor + /// (called once at the very beginning of the program lifetime) + Default(); + } + services { + Ping, + Counter, + /// Another Counter service + Counter2: Counter, + } + types { + struct DoThatParam { + p1: u32, + p2: ActorId, + p3: ManyVariants, + } + struct TupleStruct(u32); + struct UnitStruct; + } +} diff --git a/rs/idl-meta/tests/snapshots/templates__idl_service.snap b/rs/idl-meta/tests/snapshots/templates__idl_service.snap new file mode 100644 index 000000000..fdd608671 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/templates__idl_service.snap @@ -0,0 +1,56 @@ +--- +source: rs/idl-meta/tests/templates.rs +expression: idl +--- +!@sails: 0.1.0 +!@include: ownable.idl +!@include: git://github.com/some_repo/tippable.idl + +service Counter { + events { + /// Emitted when a new value is added to the counter + Added(u32), + /// Emitted when a value is subtracted from the counter + Subtracted(u32), + } + functions { + /// Add a value to the counter + Add(value: u32) -> u32; + /// Substract a value from the counter + Sub(value: u32) -> u32 throws String; + /// Get the current value + @query + Value() -> u32; + } +} + +service ThisThat { + functions { + /// Some func + /// With multiline doc + DoThis(p1: u32, p2: String, p3: (Option, NonZeroU8), p4: TupleStruct) -> (String, u32) throws (String); + } + types { + struct DoThatParam { + /// Parametr p1: u32 + p1: u32, + p2: ActorId, + p3: ManyVariants, + } + enum ManyVariants { + One, + Two(u32), + Three(Option), + Four { + a: u32, + b: Option, + }, + Five ( + String, + @key: value + H256, + ), + Six((u32)), + } + } +} diff --git a/rs/idl-meta/tests/snapshots/templates__type_enum.snap b/rs/idl-meta/tests/snapshots/templates__type_enum.snap new file mode 100644 index 000000000..29a128277 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/templates__type_enum.snap @@ -0,0 +1,29 @@ +--- +source: rs/idl-meta/tests/templates.rs +expression: idl +--- +/// SomeType Enum +@rusttype: sails-idl-meta::SomeType +enum SomeType { + /// Unit-like Variant + Unit, + /// Tuple-like Variant + Tuple(u32), + /// Tuple-like Variant with field docs + TupleWithDocs ( + /// Some docs + Option, + /// Some docs + (u32, u32), + ), + /// Struct-like Variant + Struct { + p1: Option, + p2: (u32, u32), + }, + /// Generic Struct-like Variant + GenericStruct { + p1: Option, + p2: (T2, T2), + }, +} diff --git a/rs/idl-meta/tests/templates.rs b/rs/idl-meta/tests/templates.rs new file mode 100644 index 000000000..abd341aa4 --- /dev/null +++ b/rs/idl-meta/tests/templates.rs @@ -0,0 +1,456 @@ +use askama::Template; +use sails_idl_meta::*; + +fn globals() -> Vec<(String, Option)> { + vec![ + ("sails".to_string(), Some("0.1.0".to_string())), + ("include".to_string(), Some("ownable.idl".to_string())), + ( + "include".to_string(), + Some("git://github.com/some_repo/tippable.idl".to_string()), + ), + ] +} + +fn enum_variants_type() -> Type { + use PrimitiveType::*; + use TypeDecl::*; + + Type { + name: "SomeType".to_string(), + type_params: vec![ + TypeParameter { + name: "T1".to_string(), + ty: None, + }, + TypeParameter { + name: "T2".to_string(), + ty: None, + }, + ], + def: TypeDef::Enum(EnumDef { + variants: vec![ + EnumVariant { + name: "Unit".to_string(), + def: StructDef { fields: vec![] }, + docs: vec!["Unit-like Variant".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "Tuple".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec!["Tuple-like Variant".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "TupleWithDocs".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: None, + type_decl: TypeDecl::option(Primitive(U32)), + docs: vec!["Some docs".to_string()], + annotations: vec![], + }, + StructField { + name: None, + type_decl: Tuple(vec![Primitive(U32), Primitive(U32)]), + docs: vec!["Some docs".to_string()], + annotations: vec![], + }, + ], + }, + docs: vec!["Tuple-like Variant with field docs".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "Struct".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: TypeDecl::option(Primitive(U32)), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: Tuple(vec![Primitive(U32), Primitive(U32)]), + docs: vec![], + annotations: vec![], + }, + ], + }, + docs: vec!["Struct-like Variant".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "GenericStruct".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: TypeDecl::option(Named("T1".to_string(), vec![])), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: Tuple(vec![ + Named("T2".to_string(), vec![]), + Named("T2".to_string(), vec![]), + ]), + docs: vec![], + annotations: vec![], + }, + ], + }, + docs: vec!["Generic Struct-like Variant".to_string()], + annotations: vec![], + }, + ], + }), + docs: vec!["SomeType Enum".to_string()], + annotations: vec![( + "rusttype".to_string(), + Some("sails-idl-meta::SomeType".to_string()), + )], + } +} + +fn counter_service() -> ServiceUnit { + use PrimitiveType::*; + use TypeDecl::*; + + ServiceUnit { + name: "Counter".to_string(), + extends: vec![], + funcs: vec![ + ServiceFunc { + name: "Add".to_string(), + params: vec![FuncParam { + name: "value".to_string(), + type_decl: Primitive(U32), + }], + output: Primitive(U32), + throws: None, + kind: FunctionKind::Command, + docs: vec!["Add a value to the counter".to_string()], + annotations: vec![], + }, + ServiceFunc { + name: "Sub".to_string(), + params: vec![FuncParam { + name: "value".to_string(), + type_decl: Primitive(U32), + }], + output: Primitive(U32), + throws: Some(Primitive(String)), + kind: FunctionKind::Command, + docs: vec!["Substract a value from the counter".to_string()], + annotations: vec![], + }, + ServiceFunc { + name: "Value".to_string(), + params: vec![], + output: Primitive(U32), + throws: None, + kind: FunctionKind::Query, + docs: vec!["Get the current value".to_string()], + annotations: vec![("query".to_string(), None)], + }, + ], + events: vec![ + ServiceEvent { + name: "Added".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec!["Emitted when a new value is added to the counter".to_string()], + annotations: vec![], + }, + ServiceEvent { + name: "Subtracted".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec!["Emitted when a value is subtracted from the counter".to_string()], + annotations: vec![], + }, + ], + types: vec![], + docs: vec![], + annotations: vec![], + } +} + +fn this_that_service() -> ServiceUnit { + use PrimitiveType::*; + use TypeDecl::*; + use TypeDef::*; + + ServiceUnit { + name: "ThisThat ".to_string(), + extends: vec![], + funcs: vec![ServiceFunc { + name: "DoThis".to_string(), + params: vec![ + FuncParam { + name: "p1".to_string(), + type_decl: Primitive(U32), + }, + FuncParam { + name: "p2".to_string(), + type_decl: Primitive(String), + }, + FuncParam { + name: "p3".to_string(), + type_decl: Tuple(vec![ + TypeDecl::option(Primitive(H160)), + Named("NonZeroU8".to_string(), vec![]), + ]), + }, + FuncParam { + name: "p4".to_string(), + type_decl: Named("TupleStruct".to_string(), vec![]), + }, + ], + output: Tuple(vec![Primitive(String), Primitive(U32)]), + throws: Some(Tuple(vec![Primitive(String)])), + kind: FunctionKind::Command, + docs: vec!["Some func".to_string(), "With multiline doc".to_string()], + annotations: vec![], + }], + events: vec![], + types: vec![ + Type { + name: "DoThatParam".to_string(), + type_params: vec![], + def: Struct(StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: Primitive(U32), + docs: vec!["Parametr p1: u32".to_string()], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: Primitive(ActorId), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p3".to_string()), + type_decl: Named("ManyVariants".to_string(), vec![]), + docs: vec![], + annotations: vec![], + }, + ], + }), + docs: vec![], + annotations: vec![], + }, + Type { + name: "ManyVariants".to_string(), + type_params: vec![], + def: Enum(EnumDef { + variants: vec![ + EnumVariant { + name: "One".to_string(), + def: StructDef { fields: vec![] }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Two".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Three".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: TypeDecl::option(Primitive(U32)), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Four".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: Some("a".to_string()), + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("b".to_string()), + type_decl: TypeDecl::option(Primitive(U16)), + docs: vec![], + annotations: vec![], + }, + ], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Five".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: None, + type_decl: Primitive(String), + docs: vec![], + annotations: vec![], + }, + StructField { + name: None, + type_decl: Primitive(H256), + docs: vec![], + annotations: vec![( + "key".to_string(), + Some("value".to_string()), + )], + }, + ], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Six".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Tuple(vec![Primitive(U32)]), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec![], + annotations: vec![], + }, + ], + }), + docs: vec![], + annotations: vec![], + }, + ], + docs: vec![], + annotations: vec![], + } +} + +fn program_unit() -> ProgramUnit { + use PrimitiveType::*; + use TypeDecl::*; + use TypeDef::*; + + ProgramUnit { + name: "Demo".to_string(), + ctors: vec![ + CtorFunc { + name: "Create".to_string(), + params: vec![FuncParam { name: "counter".to_string(), type_decl: TypeDecl::option(Primitive(U32)) }, + FuncParam { name: "dog_position".to_string(), type_decl: TypeDecl::option(Tuple(vec![Primitive(I32), Primitive(I32)])) }], + docs: vec!["Program constructor (called once at the very beginning of the program lifetime)".to_string()], + annotations: vec![], + }, + CtorFunc { + name: "Default".to_string(), + params: vec![], + docs: vec!["Another program constructor".to_string(), "(called once at the very beginning of the program lifetime)".to_string()], + annotations: vec![], + }, + ], + services: vec![ServiceExpo { name: "Ping".to_string(), route: None, docs: vec![], annotations: vec![] }, + ServiceExpo { name: "Counter".to_string(), route: None, docs: vec![], annotations: vec![] }, + ServiceExpo { name: "Counter".to_string(), route: Some("Counter2".to_string()), docs: vec!["Another Counter service".to_owned()], annotations: vec![] }], + types: vec![ + Type { name: "DoThatParam".to_string(), type_params: vec![], def: Struct(StructDef {fields: vec![ + StructField { name: Some("p1".to_string()), type_decl: Primitive(U32), docs: vec![], annotations: vec![] }, + StructField { name: Some("p2".to_string()), type_decl: Primitive(ActorId), docs: vec![], annotations: vec![] }, + StructField { name: Some("p3".to_string()), type_decl: Named("ManyVariants".to_string(), vec![]), docs: vec![], annotations: vec![] }, + ]}) , docs: vec![], annotations: vec![] }, + Type { name: "TupleStruct".to_string(), type_params: vec![], def: Struct(StructDef {fields: vec![ + StructField { name: None, type_decl: Primitive(U32), docs: vec![], annotations: vec![] }, + ]}) , docs: vec![], annotations: vec![] }, + Type { name: "UnitStruct".to_string(), type_params: vec![], def: Struct(StructDef {fields: vec![]}) , docs: vec![], annotations: vec![] }, + ], + docs: vec!["Demo Program".to_string()], + annotations: vec![], + } +} + +#[test] +fn type_enum() { + let ty = enum_variants_type(); + let idl = ty.render().unwrap(); + insta::assert_snapshot!(idl); +} + +#[test] +fn idl_globals() { + let doc = IdlDoc { + globals: globals(), + program: None, + services: vec![], + }; + let idl = doc.render().unwrap(); + insta::assert_snapshot!(idl); +} + +#[test] +fn idl_program() { + let doc = IdlDoc { + globals: globals(), + program: Some(program_unit()), + services: vec![], + }; + let idl = doc.render().unwrap(); + insta::assert_snapshot!(idl); +} + +#[test] +fn idl_service() { + let doc = IdlDoc { + globals: globals(), + program: None, + services: vec![counter_service(), this_that_service()], + }; + let idl = doc.render().unwrap(); + insta::assert_snapshot!(idl); +} From 59bab9e64d4a2cadb2deb02157633b978fdc24db Mon Sep 17 00:00:00 2001 From: vobradovich Date: Wed, 19 Nov 2025 17:44:52 +0100 Subject: [PATCH 2/3] fix: struct-like TypeDecl, fixture --- rs/idl-meta/src/ast.rs | 58 ++- rs/idl-meta/src/lib.rs | 2 +- rs/idl-meta/tests/fixture/mod.rs | 484 ++++++++++++++++++ .../snapshots/templates__idl_service.snap | 4 +- rs/idl-meta/tests/templates.rs | 426 +-------------- 5 files changed, 533 insertions(+), 441 deletions(-) create mode 100644 rs/idl-meta/tests/fixture/mod.rs diff --git a/rs/idl-meta/src/ast.rs b/rs/idl-meta/src/ast.rs index b0a26c3bf..3c02b90ff 100644 --- a/rs/idl-meta/src/ast.rs +++ b/rs/idl-meta/src/ast.rs @@ -141,7 +141,7 @@ impl ServiceFunc { /// /// Stores the parameter name as written in IDL and its fully resolved type /// (`TypeDecl`), preserving declaration order. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FuncParam { pub name: String, pub type_decl: TypeDecl, @@ -173,33 +173,53 @@ pub type ServiceEvent = EnumVariant; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum TypeDecl { /// Slice type `[T]`. - Slice(Box), + Slice { item: Box }, /// Fixed-length array type `[T; N]`. - Array(Box, u32), + Array { item: Box, len: u32 }, /// Tuple type `(T1, T2, ...)`, including `()` for an empty tuple. - Tuple(Vec), - /// Built-in primitive type from `PrimitiveType`. - Primitive(PrimitiveType), + Tuple { types: Vec }, /// Named type, possibly generic (e.g. `Point`). /// /// - known named type, e.g. `Option`, `Result` /// - user-defined named type /// - generic type parameter (e.g. `T`) used in type definitions. - Named(String, Vec), + Named { + name: String, + generics: Vec, + }, + /// Built-in primitive type from `PrimitiveType`. + Primitive(PrimitiveType), } impl TypeDecl { + pub fn named(name: String) -> TypeDecl { + TypeDecl::Named { + name, + generics: vec![], + } + } + + pub fn tuple(types: Vec) -> TypeDecl { + TypeDecl::Tuple { types } + } + pub fn option(item: TypeDecl) -> TypeDecl { - TypeDecl::Named("Option".to_string(), vec![item]) + TypeDecl::Named { + name: "Option".to_string(), + generics: vec![item], + } } pub fn result(ok: TypeDecl, err: TypeDecl) -> TypeDecl { - TypeDecl::Named("Result".to_string(), vec![ok, err]) + TypeDecl::Named { + name: "Result".to_string(), + generics: vec![ok, err], + } } pub fn option_type_decl(ty: &TypeDecl) -> Option { match ty { - TypeDecl::Named(name, generics) if name == "Option" => { + TypeDecl::Named { name, generics } if name == "Option" => { if let [item] = generics.as_slice() { Some(item.clone()) } else { @@ -212,7 +232,7 @@ impl TypeDecl { pub fn result_type_decl(ty: &TypeDecl) -> Option<(TypeDecl, TypeDecl)> { match ty { - TypeDecl::Named(name, generics) if name == "Result" => { + TypeDecl::Named { name, generics } if name == "Result" => { if let [ok, err] = generics.as_slice() { Some((ok.clone(), err.clone())) } else { @@ -228,11 +248,11 @@ impl Display for TypeDecl { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { use TypeDecl::*; match self { - Slice(item) => write!(f, "[{item}]"), - Array(item, len) => write!(f, "[{item}; {len}]"), - Tuple(type_decls) => { + Slice { item } => write!(f, "[{item}]"), + Array { item, len } => write!(f, "[{item}; {len}]"), + Tuple { types } => { f.write_char('(')?; - for (i, ty) in type_decls.iter().enumerate() { + for (i, ty) in types.iter().enumerate() { if i > 0 { f.write_str(", ")?; } @@ -241,8 +261,7 @@ impl Display for TypeDecl { f.write_char(')')?; Ok(()) } - Primitive(primitive_type) => write!(f, "{primitive_type}"), - Named(name, generics) => { + Named { name, generics } => { write!(f, "{name}")?; if !generics.is_empty() { f.write_char('<')?; @@ -256,6 +275,7 @@ impl Display for TypeDecl { } Ok(()) } + Primitive(primitive_type) => write!(f, "{primitive_type}"), } } } @@ -413,8 +433,8 @@ impl Display for TypeParameter { /// Underlying definition of a named type: either a struct or an enum. /// /// This mirrors the two composite categories in the IDL: -/// - `Struct` — record / tuple / unit structs; -/// - `Enum` — tagged unions with variants that may carry payloads. +/// - `Struct` - record / tuple / unit structs; +/// - `Enum` - tagged unions with variants that may carry payloads. #[derive(Debug, Clone, PartialEq)] pub enum TypeDef { Struct(StructDef), diff --git a/rs/idl-meta/src/lib.rs b/rs/idl-meta/src/lib.rs index 122b364a6..ecae4889e 100644 --- a/rs/idl-meta/src/lib.rs +++ b/rs/idl-meta/src/lib.rs @@ -3,7 +3,7 @@ extern crate alloc; #[cfg(feature = "ast")] -pub mod ast; +mod ast; #[cfg(feature = "ast")] pub use ast::*; diff --git a/rs/idl-meta/tests/fixture/mod.rs b/rs/idl-meta/tests/fixture/mod.rs new file mode 100644 index 000000000..2badbb51f --- /dev/null +++ b/rs/idl-meta/tests/fixture/mod.rs @@ -0,0 +1,484 @@ +use sails_idl_meta::{PrimitiveType::*, TypeDecl::*, TypeDef::*, *}; +use std::string::String; + +pub fn globals() -> Vec<(String, Option)> { + vec![ + ("sails".to_string(), Some("0.1.0".to_string())), + ("include".to_string(), Some("ownable.idl".to_string())), + ( + "include".to_string(), + Some("git://github.com/some_repo/tippable.idl".to_string()), + ), + ] +} + +pub fn enum_variants_type() -> Type { + Type { + name: "SomeType".to_string(), + type_params: vec![ + TypeParameter { + name: "T1".to_string(), + ty: None, + }, + TypeParameter { + name: "T2".to_string(), + ty: None, + }, + ], + def: TypeDef::Enum(EnumDef { + variants: vec![ + EnumVariant { + name: "Unit".to_string(), + def: StructDef { fields: vec![] }, + docs: vec!["Unit-like Variant".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "Tuple".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec!["Tuple-like Variant".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "TupleWithDocs".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: None, + type_decl: TypeDecl::option(Primitive(U32)), + docs: vec!["Some docs".to_string()], + annotations: vec![], + }, + StructField { + name: None, + type_decl: TypeDecl::tuple(vec![Primitive(U32), Primitive(U32)]), + docs: vec!["Some docs".to_string()], + annotations: vec![], + }, + ], + }, + docs: vec!["Tuple-like Variant with field docs".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "Struct".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: TypeDecl::option(Primitive(U32)), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: TypeDecl::tuple(vec![Primitive(U32), Primitive(U32)]), + docs: vec![], + annotations: vec![], + }, + ], + }, + docs: vec!["Struct-like Variant".to_string()], + annotations: vec![], + }, + EnumVariant { + name: "GenericStruct".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: TypeDecl::option(TypeDecl::named("T1".to_string())), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: TypeDecl::tuple(vec![ + TypeDecl::named("T2".to_string()), + TypeDecl::named("T2".to_string()), + ]), + docs: vec![], + annotations: vec![], + }, + ], + }, + docs: vec!["Generic Struct-like Variant".to_string()], + annotations: vec![], + }, + ], + }), + docs: vec!["SomeType Enum".to_string()], + annotations: vec![( + "rusttype".to_string(), + Some("sails-idl-meta::SomeType".to_string()), + )], + } +} + +pub fn counter_service() -> ServiceUnit { + ServiceUnit { + name: "Counter".to_string(), + extends: vec![], + funcs: vec![ + ServiceFunc { + name: "Add".to_string(), + params: vec![FuncParam { + name: "value".to_string(), + type_decl: Primitive(U32), + }], + output: Primitive(U32), + throws: None, + kind: FunctionKind::Command, + docs: vec!["Add a value to the counter".to_string()], + annotations: vec![], + }, + ServiceFunc { + name: "Sub".to_string(), + params: vec![FuncParam { + name: "value".to_string(), + type_decl: Primitive(U32), + }], + output: Primitive(U32), + throws: Some(Primitive(String)), + kind: FunctionKind::Command, + docs: vec!["Substract a value from the counter".to_string()], + annotations: vec![], + }, + ServiceFunc { + name: "Value".to_string(), + params: vec![], + output: Primitive(U32), + throws: None, + kind: FunctionKind::Query, + docs: vec!["Get the current value".to_string()], + annotations: vec![("query".to_string(), None)], + }, + ], + events: vec![ + ServiceEvent { + name: "Added".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec!["Emitted when a new value is added to the counter".to_string()], + annotations: vec![], + }, + ServiceEvent { + name: "Subtracted".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec!["Emitted when a value is subtracted from the counter".to_string()], + annotations: vec![], + }, + ], + types: vec![], + docs: vec![], + annotations: vec![], + } +} + +pub fn service_func() -> ServiceFunc { + ServiceFunc { + name: "DoThis".to_string(), + params: vec![ + FuncParam { + name: "p1".to_string(), + type_decl: Primitive(U32), + }, + FuncParam { + name: "p2".to_string(), + type_decl: Primitive(String), + }, + FuncParam { + name: "p3".to_string(), + type_decl: TypeDecl::tuple(vec![ + TypeDecl::option(Primitive(H160)), + TypeDecl::Named { + name: "NonZero".to_string(), + generics: vec![Primitive(U8)], + }, + ]), + }, + FuncParam { + name: "p4".to_string(), + type_decl: TypeDecl::named("TupleStruct".to_string()), + }, + ], + output: TypeDecl::tuple(vec![Primitive(String), Primitive(U32)]), + throws: Some(TypeDecl::tuple(vec![Primitive(String)])), + kind: FunctionKind::Command, + docs: vec!["Some func".to_string(), "With multiline doc".to_string()], + annotations: vec![], + } +} + +pub fn this_that_service() -> ServiceUnit { + ServiceUnit { + name: "ThisThat".to_string(), + extends: vec![], + funcs: vec![service_func()], + events: vec![], + types: vec![ + Type { + name: "DoThatParam".to_string(), + type_params: vec![], + def: Struct(StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: Primitive(U32), + docs: vec!["Parametr p1: u32".to_string()], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: Primitive(ActorId), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p3".to_string()), + type_decl: TypeDecl::named("ManyVariants".to_string()), + docs: vec![], + annotations: vec![], + }, + ], + }), + docs: vec![], + annotations: vec![], + }, + Type { + name: "ManyVariants".to_string(), + type_params: vec![], + def: Enum(EnumDef { + variants: vec![ + EnumVariant { + name: "One".to_string(), + def: StructDef { fields: vec![] }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Two".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Three".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: TypeDecl::option(Primitive(U32)), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Four".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: Some("a".to_string()), + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("b".to_string()), + type_decl: TypeDecl::option(Primitive(U16)), + docs: vec![], + annotations: vec![], + }, + ], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Five".to_string(), + def: StructDef { + fields: vec![ + StructField { + name: None, + type_decl: Primitive(String), + docs: vec![], + annotations: vec![], + }, + StructField { + name: None, + type_decl: Primitive(H256), + docs: vec![], + annotations: vec![( + "key".to_string(), + Some("value".to_string()), + )], + }, + ], + }, + docs: vec![], + annotations: vec![], + }, + EnumVariant { + name: "Six".to_string(), + def: StructDef { + fields: vec![StructField { + name: None, + type_decl: TypeDecl::tuple(vec![Primitive(U32)]), + docs: vec![], + annotations: vec![], + }], + }, + docs: vec![], + annotations: vec![], + }, + ], + }), + docs: vec![], + annotations: vec![], + }, + ], + docs: vec![], + annotations: vec![], + } +} + +pub fn ctor_func() -> CtorFunc { + CtorFunc { + name: "Create".to_string(), + params: vec![ + FuncParam { + name: "counter".to_string(), + type_decl: TypeDecl::option(Primitive(U32)), + }, + FuncParam { + name: "dog_position".to_string(), + type_decl: TypeDecl::option(TypeDecl::tuple(vec![Primitive(I32), Primitive(I32)])), + }, + ], + docs: vec![ + "Program constructor (called once at the very beginning of the program lifetime)" + .to_string(), + ], + annotations: vec![], + } +} + +pub fn program_unit() -> ProgramUnit { + ProgramUnit { + name: "Demo".to_string(), + ctors: vec![ + ctor_func(), + CtorFunc { + name: "Default".to_string(), + params: vec![], + docs: vec![ + "Another program constructor".to_string(), + "(called once at the very beginning of the program lifetime)".to_string(), + ], + annotations: vec![], + }, + ], + services: vec![ + ServiceExpo { + name: "Ping".to_string(), + route: None, + docs: vec![], + annotations: vec![], + }, + ServiceExpo { + name: "Counter".to_string(), + route: None, + docs: vec![], + annotations: vec![], + }, + ServiceExpo { + name: "Counter".to_string(), + route: Some("Counter2".to_string()), + docs: vec!["Another Counter service".to_owned()], + annotations: vec![], + }, + ], + types: vec![ + Type { + name: "DoThatParam".to_string(), + type_params: vec![], + def: Struct(StructDef { + fields: vec![ + StructField { + name: Some("p1".to_string()), + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p2".to_string()), + type_decl: Primitive(ActorId), + docs: vec![], + annotations: vec![], + }, + StructField { + name: Some("p3".to_string()), + type_decl: TypeDecl::named("ManyVariants".to_string()), + docs: vec![], + annotations: vec![], + }, + ], + }), + docs: vec![], + annotations: vec![], + }, + Type { + name: "TupleStruct".to_string(), + type_params: vec![], + def: Struct(StructDef { + fields: vec![StructField { + name: None, + type_decl: Primitive(U32), + docs: vec![], + annotations: vec![], + }], + }), + docs: vec![], + annotations: vec![], + }, + Type { + name: "UnitStruct".to_string(), + type_params: vec![], + def: Struct(StructDef { fields: vec![] }), + docs: vec![], + annotations: vec![], + }, + ], + docs: vec!["Demo Program".to_string()], + annotations: vec![], + } +} diff --git a/rs/idl-meta/tests/snapshots/templates__idl_service.snap b/rs/idl-meta/tests/snapshots/templates__idl_service.snap index fdd608671..bdc630fad 100644 --- a/rs/idl-meta/tests/snapshots/templates__idl_service.snap +++ b/rs/idl-meta/tests/snapshots/templates__idl_service.snap @@ -24,11 +24,11 @@ service Counter { } } -service ThisThat { +service ThisThat { functions { /// Some func /// With multiline doc - DoThis(p1: u32, p2: String, p3: (Option, NonZeroU8), p4: TupleStruct) -> (String, u32) throws (String); + DoThis(p1: u32, p2: String, p3: (Option, NonZero), p4: TupleStruct) -> (String, u32) throws (String); } types { struct DoThatParam { diff --git a/rs/idl-meta/tests/templates.rs b/rs/idl-meta/tests/templates.rs index abd341aa4..696700bf3 100644 --- a/rs/idl-meta/tests/templates.rs +++ b/rs/idl-meta/tests/templates.rs @@ -1,423 +1,11 @@ use askama::Template; use sails_idl_meta::*; -fn globals() -> Vec<(String, Option)> { - vec![ - ("sails".to_string(), Some("0.1.0".to_string())), - ("include".to_string(), Some("ownable.idl".to_string())), - ( - "include".to_string(), - Some("git://github.com/some_repo/tippable.idl".to_string()), - ), - ] -} - -fn enum_variants_type() -> Type { - use PrimitiveType::*; - use TypeDecl::*; - - Type { - name: "SomeType".to_string(), - type_params: vec![ - TypeParameter { - name: "T1".to_string(), - ty: None, - }, - TypeParameter { - name: "T2".to_string(), - ty: None, - }, - ], - def: TypeDef::Enum(EnumDef { - variants: vec![ - EnumVariant { - name: "Unit".to_string(), - def: StructDef { fields: vec![] }, - docs: vec!["Unit-like Variant".to_string()], - annotations: vec![], - }, - EnumVariant { - name: "Tuple".to_string(), - def: StructDef { - fields: vec![StructField { - name: None, - type_decl: Primitive(U32), - docs: vec![], - annotations: vec![], - }], - }, - docs: vec!["Tuple-like Variant".to_string()], - annotations: vec![], - }, - EnumVariant { - name: "TupleWithDocs".to_string(), - def: StructDef { - fields: vec![ - StructField { - name: None, - type_decl: TypeDecl::option(Primitive(U32)), - docs: vec!["Some docs".to_string()], - annotations: vec![], - }, - StructField { - name: None, - type_decl: Tuple(vec![Primitive(U32), Primitive(U32)]), - docs: vec!["Some docs".to_string()], - annotations: vec![], - }, - ], - }, - docs: vec!["Tuple-like Variant with field docs".to_string()], - annotations: vec![], - }, - EnumVariant { - name: "Struct".to_string(), - def: StructDef { - fields: vec![ - StructField { - name: Some("p1".to_string()), - type_decl: TypeDecl::option(Primitive(U32)), - docs: vec![], - annotations: vec![], - }, - StructField { - name: Some("p2".to_string()), - type_decl: Tuple(vec![Primitive(U32), Primitive(U32)]), - docs: vec![], - annotations: vec![], - }, - ], - }, - docs: vec!["Struct-like Variant".to_string()], - annotations: vec![], - }, - EnumVariant { - name: "GenericStruct".to_string(), - def: StructDef { - fields: vec![ - StructField { - name: Some("p1".to_string()), - type_decl: TypeDecl::option(Named("T1".to_string(), vec![])), - docs: vec![], - annotations: vec![], - }, - StructField { - name: Some("p2".to_string()), - type_decl: Tuple(vec![ - Named("T2".to_string(), vec![]), - Named("T2".to_string(), vec![]), - ]), - docs: vec![], - annotations: vec![], - }, - ], - }, - docs: vec!["Generic Struct-like Variant".to_string()], - annotations: vec![], - }, - ], - }), - docs: vec!["SomeType Enum".to_string()], - annotations: vec![( - "rusttype".to_string(), - Some("sails-idl-meta::SomeType".to_string()), - )], - } -} - -fn counter_service() -> ServiceUnit { - use PrimitiveType::*; - use TypeDecl::*; - - ServiceUnit { - name: "Counter".to_string(), - extends: vec![], - funcs: vec![ - ServiceFunc { - name: "Add".to_string(), - params: vec![FuncParam { - name: "value".to_string(), - type_decl: Primitive(U32), - }], - output: Primitive(U32), - throws: None, - kind: FunctionKind::Command, - docs: vec!["Add a value to the counter".to_string()], - annotations: vec![], - }, - ServiceFunc { - name: "Sub".to_string(), - params: vec![FuncParam { - name: "value".to_string(), - type_decl: Primitive(U32), - }], - output: Primitive(U32), - throws: Some(Primitive(String)), - kind: FunctionKind::Command, - docs: vec!["Substract a value from the counter".to_string()], - annotations: vec![], - }, - ServiceFunc { - name: "Value".to_string(), - params: vec![], - output: Primitive(U32), - throws: None, - kind: FunctionKind::Query, - docs: vec!["Get the current value".to_string()], - annotations: vec![("query".to_string(), None)], - }, - ], - events: vec![ - ServiceEvent { - name: "Added".to_string(), - def: StructDef { - fields: vec![StructField { - name: None, - type_decl: Primitive(U32), - docs: vec![], - annotations: vec![], - }], - }, - docs: vec!["Emitted when a new value is added to the counter".to_string()], - annotations: vec![], - }, - ServiceEvent { - name: "Subtracted".to_string(), - def: StructDef { - fields: vec![StructField { - name: None, - type_decl: Primitive(U32), - docs: vec![], - annotations: vec![], - }], - }, - docs: vec!["Emitted when a value is subtracted from the counter".to_string()], - annotations: vec![], - }, - ], - types: vec![], - docs: vec![], - annotations: vec![], - } -} - -fn this_that_service() -> ServiceUnit { - use PrimitiveType::*; - use TypeDecl::*; - use TypeDef::*; - - ServiceUnit { - name: "ThisThat ".to_string(), - extends: vec![], - funcs: vec![ServiceFunc { - name: "DoThis".to_string(), - params: vec![ - FuncParam { - name: "p1".to_string(), - type_decl: Primitive(U32), - }, - FuncParam { - name: "p2".to_string(), - type_decl: Primitive(String), - }, - FuncParam { - name: "p3".to_string(), - type_decl: Tuple(vec![ - TypeDecl::option(Primitive(H160)), - Named("NonZeroU8".to_string(), vec![]), - ]), - }, - FuncParam { - name: "p4".to_string(), - type_decl: Named("TupleStruct".to_string(), vec![]), - }, - ], - output: Tuple(vec![Primitive(String), Primitive(U32)]), - throws: Some(Tuple(vec![Primitive(String)])), - kind: FunctionKind::Command, - docs: vec!["Some func".to_string(), "With multiline doc".to_string()], - annotations: vec![], - }], - events: vec![], - types: vec![ - Type { - name: "DoThatParam".to_string(), - type_params: vec![], - def: Struct(StructDef { - fields: vec![ - StructField { - name: Some("p1".to_string()), - type_decl: Primitive(U32), - docs: vec!["Parametr p1: u32".to_string()], - annotations: vec![], - }, - StructField { - name: Some("p2".to_string()), - type_decl: Primitive(ActorId), - docs: vec![], - annotations: vec![], - }, - StructField { - name: Some("p3".to_string()), - type_decl: Named("ManyVariants".to_string(), vec![]), - docs: vec![], - annotations: vec![], - }, - ], - }), - docs: vec![], - annotations: vec![], - }, - Type { - name: "ManyVariants".to_string(), - type_params: vec![], - def: Enum(EnumDef { - variants: vec![ - EnumVariant { - name: "One".to_string(), - def: StructDef { fields: vec![] }, - docs: vec![], - annotations: vec![], - }, - EnumVariant { - name: "Two".to_string(), - def: StructDef { - fields: vec![StructField { - name: None, - type_decl: Primitive(U32), - docs: vec![], - annotations: vec![], - }], - }, - docs: vec![], - annotations: vec![], - }, - EnumVariant { - name: "Three".to_string(), - def: StructDef { - fields: vec![StructField { - name: None, - type_decl: TypeDecl::option(Primitive(U32)), - docs: vec![], - annotations: vec![], - }], - }, - docs: vec![], - annotations: vec![], - }, - EnumVariant { - name: "Four".to_string(), - def: StructDef { - fields: vec![ - StructField { - name: Some("a".to_string()), - type_decl: Primitive(U32), - docs: vec![], - annotations: vec![], - }, - StructField { - name: Some("b".to_string()), - type_decl: TypeDecl::option(Primitive(U16)), - docs: vec![], - annotations: vec![], - }, - ], - }, - docs: vec![], - annotations: vec![], - }, - EnumVariant { - name: "Five".to_string(), - def: StructDef { - fields: vec![ - StructField { - name: None, - type_decl: Primitive(String), - docs: vec![], - annotations: vec![], - }, - StructField { - name: None, - type_decl: Primitive(H256), - docs: vec![], - annotations: vec![( - "key".to_string(), - Some("value".to_string()), - )], - }, - ], - }, - docs: vec![], - annotations: vec![], - }, - EnumVariant { - name: "Six".to_string(), - def: StructDef { - fields: vec![StructField { - name: None, - type_decl: Tuple(vec![Primitive(U32)]), - docs: vec![], - annotations: vec![], - }], - }, - docs: vec![], - annotations: vec![], - }, - ], - }), - docs: vec![], - annotations: vec![], - }, - ], - docs: vec![], - annotations: vec![], - } -} - -fn program_unit() -> ProgramUnit { - use PrimitiveType::*; - use TypeDecl::*; - use TypeDef::*; - - ProgramUnit { - name: "Demo".to_string(), - ctors: vec![ - CtorFunc { - name: "Create".to_string(), - params: vec![FuncParam { name: "counter".to_string(), type_decl: TypeDecl::option(Primitive(U32)) }, - FuncParam { name: "dog_position".to_string(), type_decl: TypeDecl::option(Tuple(vec![Primitive(I32), Primitive(I32)])) }], - docs: vec!["Program constructor (called once at the very beginning of the program lifetime)".to_string()], - annotations: vec![], - }, - CtorFunc { - name: "Default".to_string(), - params: vec![], - docs: vec!["Another program constructor".to_string(), "(called once at the very beginning of the program lifetime)".to_string()], - annotations: vec![], - }, - ], - services: vec![ServiceExpo { name: "Ping".to_string(), route: None, docs: vec![], annotations: vec![] }, - ServiceExpo { name: "Counter".to_string(), route: None, docs: vec![], annotations: vec![] }, - ServiceExpo { name: "Counter".to_string(), route: Some("Counter2".to_string()), docs: vec!["Another Counter service".to_owned()], annotations: vec![] }], - types: vec![ - Type { name: "DoThatParam".to_string(), type_params: vec![], def: Struct(StructDef {fields: vec![ - StructField { name: Some("p1".to_string()), type_decl: Primitive(U32), docs: vec![], annotations: vec![] }, - StructField { name: Some("p2".to_string()), type_decl: Primitive(ActorId), docs: vec![], annotations: vec![] }, - StructField { name: Some("p3".to_string()), type_decl: Named("ManyVariants".to_string(), vec![]), docs: vec![], annotations: vec![] }, - ]}) , docs: vec![], annotations: vec![] }, - Type { name: "TupleStruct".to_string(), type_params: vec![], def: Struct(StructDef {fields: vec![ - StructField { name: None, type_decl: Primitive(U32), docs: vec![], annotations: vec![] }, - ]}) , docs: vec![], annotations: vec![] }, - Type { name: "UnitStruct".to_string(), type_params: vec![], def: Struct(StructDef {fields: vec![]}) , docs: vec![], annotations: vec![] }, - ], - docs: vec!["Demo Program".to_string()], - annotations: vec![], - } -} +mod fixture; #[test] fn type_enum() { - let ty = enum_variants_type(); + let ty = fixture::enum_variants_type(); let idl = ty.render().unwrap(); insta::assert_snapshot!(idl); } @@ -425,7 +13,7 @@ fn type_enum() { #[test] fn idl_globals() { let doc = IdlDoc { - globals: globals(), + globals: fixture::globals(), program: None, services: vec![], }; @@ -436,8 +24,8 @@ fn idl_globals() { #[test] fn idl_program() { let doc = IdlDoc { - globals: globals(), - program: Some(program_unit()), + globals: fixture::globals(), + program: Some(fixture::program_unit()), services: vec![], }; let idl = doc.render().unwrap(); @@ -447,9 +35,9 @@ fn idl_program() { #[test] fn idl_service() { let doc = IdlDoc { - globals: globals(), + globals: fixture::globals(), program: None, - services: vec![counter_service(), this_that_service()], + services: vec![fixture::counter_service(), fixture::this_that_service()], }; let idl = doc.render().unwrap(); insta::assert_snapshot!(idl); From 3c1beb52b0c32979a64a4f92480b14d52fb37c71 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Thu, 20 Nov 2025 12:24:49 +0100 Subject: [PATCH 3/3] feat: AST serde serialize/deserialize --- Cargo.lock | 2 + Cargo.toml | 4 +- rs/idl-meta/Cargo.toml | 7 + rs/idl-meta/src/ast.rs | 179 ++++++++- rs/idl-meta/tests/serde.rs | 48 +++ .../tests/snapshots/serde__idl_doc.snap | 369 ++++++++++++++++++ .../tests/snapshots/serde__program_unit.snap | 106 +++++ .../tests/snapshots/serde__service_unit.snap | 176 +++++++++ .../tests/snapshots/serde__type_enum.snap | 142 +++++++ 9 files changed, 1030 insertions(+), 3 deletions(-) create mode 100644 rs/idl-meta/tests/serde.rs create mode 100644 rs/idl-meta/tests/snapshots/serde__idl_doc.snap create mode 100644 rs/idl-meta/tests/snapshots/serde__program_unit.snap create mode 100644 rs/idl-meta/tests/snapshots/serde__service_unit.snap create mode 100644 rs/idl-meta/tests/snapshots/serde__type_enum.snap diff --git a/Cargo.lock b/Cargo.lock index db0bf7311..c47d4b90f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7250,6 +7250,8 @@ dependencies = [ "askama", "insta", "scale-info", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fba244161..c3bed5243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,8 +100,8 @@ proc-macro2 = { version = "1", default-features = false } rustdoc-types = "=0.36.0" quote = "1.0" scale-info = { version = "2.11", default-features = false } -serde = "1.0" -serde-json = { package = "serde_json", version = "1.0" } +serde = { version = "1.0", default-features = false } +serde-json = { package = "serde_json", version = "1.0", default-features = false } spin = { version = "0.9", default-features = false, features = ["spin_mutex"] } syn = "2.0" thiserror = { version = "2.0", default-features = false } diff --git a/rs/idl-meta/Cargo.toml b/rs/idl-meta/Cargo.toml index 10bd7c7d3..8aefe74be 100644 --- a/rs/idl-meta/Cargo.toml +++ b/rs/idl-meta/Cargo.toml @@ -12,6 +12,8 @@ rust-version.workspace = true [dependencies] askama = { workspace = true, optional = true } scale-info.workspace = true +serde = { workspace = true, features = ["derive"], optional = true } +serde-json = { workspace = true, features = ["alloc"], optional = true } [dev-dependencies] insta.workspace = true @@ -19,8 +21,13 @@ insta.workspace = true [features] default = ["ast"] ast = [] +serde = ["dep:serde", "dep:serde-json", "ast"] templates = ["dep:askama", "ast"] +[[test]] +name = "serde" +required-features = ["serde"] + [[test]] name = "templates" required-features = ["templates"] diff --git a/rs/idl-meta/src/ast.rs b/rs/idl-meta/src/ast.rs index 3c02b90ff..38cc83fc8 100644 --- a/rs/idl-meta/src/ast.rs +++ b/rs/idl-meta/src/ast.rs @@ -6,6 +6,8 @@ use alloc::{ vec::Vec, }; use core::fmt::{Display, Write}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; // -------------------------------- IDL model --------------------------------- @@ -21,9 +23,19 @@ use core::fmt::{Display, Write}; derive(askama::Template), template(path = "idl.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct IdlDoc { + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub globals: Vec<(String, Option)>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub program: Option, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub services: Vec, } @@ -40,12 +52,33 @@ pub struct IdlDoc { derive(askama::Template), template(path = "program.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ProgramUnit { pub name: String, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub ctors: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub services: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub types: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } @@ -56,10 +89,20 @@ pub struct ProgramUnit { /// - an optional low-level `route` (transport / path) used by the runtime, /// - may contain documentation comments and annotations. #[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ServiceExpo { pub name: String, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub route: Option, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } @@ -70,10 +113,20 @@ pub struct ServiceExpo { /// - `params` are the IDL-level arguments, /// - may contain documentation comments and annotations. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct CtorFunc { pub name: String, + #[cfg_attr(feature = "serde", serde(default))] pub params: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } @@ -91,13 +144,38 @@ pub struct CtorFunc { derive(askama::Template), template(path = "service.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ServiceUnit { pub name: String, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub extends: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub funcs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub events: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub types: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } @@ -109,18 +187,34 @@ pub struct ServiceUnit { /// - `is_query` marks read-only / query functions as defined by the spec; /// - may contain documentation comments and annotations. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ServiceFunc { pub name: String, + #[cfg_attr(feature = "serde", serde(default))] pub params: Vec, pub output: TypeDecl, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub throws: Option, pub kind: FunctionKind, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } /// Function kind based on mutability. #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "lowercase") +)] pub enum FunctionKind { #[default] Command, @@ -142,8 +236,10 @@ impl ServiceFunc { /// Stores the parameter name as written in IDL and its fully resolved type /// (`TypeDecl`), preserving declaration order. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct FuncParam { pub name: String, + #[cfg_attr(feature = "serde", serde(rename = "type"))] pub type_decl: TypeDecl, } @@ -171,6 +267,11 @@ pub type ServiceEvent = EnumVariant; /// - user-defined types with generics (`UserDefined`), /// - bare generic parameters (`T`). #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "lowercase", tag = "kind") +)] pub enum TypeDecl { /// Slice type `[T]`. Slice { item: Box }, @@ -185,10 +286,15 @@ pub enum TypeDecl { /// - generic type parameter (e.g. `T`) used in type definitions. Named { name: String, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] generics: Vec, }, /// Built-in primitive type from `PrimitiveType`. - Primitive(PrimitiveType), + #[cfg_attr(feature = "serde", serde(untagged))] + Primitive(#[cfg_attr(feature = "serde", serde(with = "serde_str"))] PrimitiveType), } impl TypeDecl { @@ -400,11 +506,25 @@ impl core::str::FromStr for PrimitiveType { derive(askama::Template), template(path = "type.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Type { pub name: String, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub type_params: Vec, + #[cfg_attr(feature = "serde", serde(flatten))] pub def: TypeDef, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } @@ -414,12 +534,14 @@ pub struct Type { /// - `ty` is an optional concrete type bound / substitution; `None` means that /// the parameter is left generic at this level. #[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TypeParameter { /// The name of the generic type parameter e.g. "T". pub name: String, /// The concrete type for the type parameter. /// /// `None` if the type parameter is skipped. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub ty: Option, } @@ -436,6 +558,11 @@ impl Display for TypeParameter { /// - `Struct` - record / tuple / unit structs; /// - `Enum` - tagged unions with variants that may carry payloads. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "lowercase", tag = "kind") +)] pub enum TypeDef { Struct(StructDef), Enum(EnumDef), @@ -453,7 +580,9 @@ pub enum TypeDef { derive(askama::Template), template(path = "struct_def.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct StructDef { + #[cfg_attr(feature = "serde", serde(default))] pub fields: Vec, } @@ -487,10 +616,21 @@ impl StructDef { derive(askama::Template), template(path = "field.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct StructField { + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub name: Option, + #[cfg_attr(feature = "serde", serde(rename = "type"))] pub type_decl: TypeDecl, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } @@ -499,7 +639,9 @@ pub struct StructField { /// Stores the ordered list of `EnumVariant` items that form a tagged union. /// Each variant may be unit-like, classic (named fields) or tuple-like. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct EnumDef { + #[cfg_attr(feature = "serde", serde(default))] pub variants: Vec, } @@ -514,9 +656,44 @@ pub struct EnumDef { derive(askama::Template), template(path = "variant.askama", escape = "none") )] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct EnumVariant { pub name: String, + #[cfg_attr(feature = "serde", serde(flatten))] pub def: StructDef, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub docs: Vec, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub annotations: Vec<(String, Option)>, } + +#[cfg(feature = "serde")] +mod serde_str { + use super::*; + use core::str::FromStr; + use serde::{Deserializer, Serializer}; + + pub(super) fn serialize(value: &T, serializer: S) -> Result + where + T: Display, + S: Serializer, + { + serializer.collect_str(value) + } + + pub(super) fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: FromStr, + ::Err: Display, + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + T::from_str(s.as_str()).map_err(serde::de::Error::custom) + } +} diff --git a/rs/idl-meta/tests/serde.rs b/rs/idl-meta/tests/serde.rs new file mode 100644 index 000000000..1c0ec2463 --- /dev/null +++ b/rs/idl-meta/tests/serde.rs @@ -0,0 +1,48 @@ +use sails_idl_meta::*; + +mod fixture; + +#[test] +fn type_enum() { + let ty = fixture::enum_variants_type(); + let serialized = serde_json::to_string_pretty(&ty).unwrap(); + let value: Type = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(ty, value); + insta::assert_snapshot!(serialized); +} + +#[test] +fn service_unit() { + let service = fixture::this_that_service(); + let serialized = serde_json::to_string_pretty(&service).unwrap(); + let value: ServiceUnit = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(service, value); + insta::assert_snapshot!(serialized); +} + +#[test] +fn program_unit() { + let prg = fixture::program_unit(); + let serialized = serde_json::to_string_pretty(&prg).unwrap(); + let value: ProgramUnit = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(prg, value); + insta::assert_snapshot!(serialized); +} + +#[test] +fn idl_doc() { + let doc = IdlDoc { + globals: fixture::globals(), + program: Some(fixture::program_unit()), + services: vec![fixture::counter_service(), fixture::this_that_service()], + }; + + let serialized = serde_json::to_string_pretty(&doc).unwrap(); + let value: IdlDoc = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(doc, value); + insta::assert_snapshot!(serialized); +} diff --git a/rs/idl-meta/tests/snapshots/serde__idl_doc.snap b/rs/idl-meta/tests/snapshots/serde__idl_doc.snap new file mode 100644 index 000000000..873c65196 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/serde__idl_doc.snap @@ -0,0 +1,369 @@ +--- +source: rs/idl-meta/tests/serde.rs +expression: serialized +--- +{ + "globals": [ + [ + "sails", + "0.1.0" + ], + [ + "include", + "ownable.idl" + ], + [ + "include", + "git://github.com/some_repo/tippable.idl" + ] + ], + "program": { + "name": "Demo", + "ctors": [ + { + "name": "Create", + "params": [ + { + "name": "counter", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u32" + ] + } + }, + { + "name": "dog_position", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + { + "kind": "tuple", + "types": [ + "i32", + "i32" + ] + } + ] + } + } + ], + "docs": [ + "Program constructor (called once at the very beginning of the program lifetime)" + ] + }, + { + "name": "Default", + "params": [], + "docs": [ + "Another program constructor", + "(called once at the very beginning of the program lifetime)" + ] + } + ], + "services": [ + { + "name": "Ping" + }, + { + "name": "Counter" + }, + { + "name": "Counter", + "route": "Counter2", + "docs": [ + "Another Counter service" + ] + } + ], + "types": [ + { + "name": "DoThatParam", + "kind": "struct", + "fields": [ + { + "name": "p1", + "type": "u32" + }, + { + "name": "p2", + "type": "ActorId" + }, + { + "name": "p3", + "type": { + "kind": "named", + "name": "ManyVariants" + } + } + ] + }, + { + "name": "TupleStruct", + "kind": "struct", + "fields": [ + { + "type": "u32" + } + ] + }, + { + "name": "UnitStruct", + "kind": "struct", + "fields": [] + } + ], + "docs": [ + "Demo Program" + ] + }, + "services": [ + { + "name": "Counter", + "funcs": [ + { + "name": "Add", + "params": [ + { + "name": "value", + "type": "u32" + } + ], + "output": "u32", + "kind": "command", + "docs": [ + "Add a value to the counter" + ] + }, + { + "name": "Sub", + "params": [ + { + "name": "value", + "type": "u32" + } + ], + "output": "u32", + "throws": "String", + "kind": "command", + "docs": [ + "Substract a value from the counter" + ] + }, + { + "name": "Value", + "params": [], + "output": "u32", + "kind": "query", + "docs": [ + "Get the current value" + ], + "annotations": [ + [ + "query", + null + ] + ] + } + ], + "events": [ + { + "name": "Added", + "fields": [ + { + "type": "u32" + } + ], + "docs": [ + "Emitted when a new value is added to the counter" + ] + }, + { + "name": "Subtracted", + "fields": [ + { + "type": "u32" + } + ], + "docs": [ + "Emitted when a value is subtracted from the counter" + ] + } + ] + }, + { + "name": "ThisThat", + "funcs": [ + { + "name": "DoThis", + "params": [ + { + "name": "p1", + "type": "u32" + }, + { + "name": "p2", + "type": "String" + }, + { + "name": "p3", + "type": { + "kind": "tuple", + "types": [ + { + "kind": "named", + "name": "Option", + "generics": [ + "H160" + ] + }, + { + "kind": "named", + "name": "NonZero", + "generics": [ + "u8" + ] + } + ] + } + }, + { + "name": "p4", + "type": { + "kind": "named", + "name": "TupleStruct" + } + } + ], + "output": { + "kind": "tuple", + "types": [ + "String", + "u32" + ] + }, + "throws": { + "kind": "tuple", + "types": [ + "String" + ] + }, + "kind": "command", + "docs": [ + "Some func", + "With multiline doc" + ] + } + ], + "types": [ + { + "name": "DoThatParam", + "kind": "struct", + "fields": [ + { + "name": "p1", + "type": "u32", + "docs": [ + "Parametr p1: u32" + ] + }, + { + "name": "p2", + "type": "ActorId" + }, + { + "name": "p3", + "type": { + "kind": "named", + "name": "ManyVariants" + } + } + ] + }, + { + "name": "ManyVariants", + "kind": "enum", + "variants": [ + { + "name": "One", + "fields": [] + }, + { + "name": "Two", + "fields": [ + { + "type": "u32" + } + ] + }, + { + "name": "Three", + "fields": [ + { + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u32" + ] + } + } + ] + }, + { + "name": "Four", + "fields": [ + { + "name": "a", + "type": "u32" + }, + { + "name": "b", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u16" + ] + } + } + ] + }, + { + "name": "Five", + "fields": [ + { + "type": "String" + }, + { + "type": "H256", + "annotations": [ + [ + "key", + "value" + ] + ] + } + ] + }, + { + "name": "Six", + "fields": [ + { + "type": { + "kind": "tuple", + "types": [ + "u32" + ] + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/rs/idl-meta/tests/snapshots/serde__program_unit.snap b/rs/idl-meta/tests/snapshots/serde__program_unit.snap new file mode 100644 index 000000000..4dc175343 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/serde__program_unit.snap @@ -0,0 +1,106 @@ +--- +source: rs/idl-meta/tests/serde.rs +expression: serialized +--- +{ + "name": "Demo", + "ctors": [ + { + "name": "Create", + "params": [ + { + "name": "counter", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u32" + ] + } + }, + { + "name": "dog_position", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + { + "kind": "tuple", + "types": [ + "i32", + "i32" + ] + } + ] + } + } + ], + "docs": [ + "Program constructor (called once at the very beginning of the program lifetime)" + ] + }, + { + "name": "Default", + "params": [], + "docs": [ + "Another program constructor", + "(called once at the very beginning of the program lifetime)" + ] + } + ], + "services": [ + { + "name": "Ping" + }, + { + "name": "Counter" + }, + { + "name": "Counter", + "route": "Counter2", + "docs": [ + "Another Counter service" + ] + } + ], + "types": [ + { + "name": "DoThatParam", + "kind": "struct", + "fields": [ + { + "name": "p1", + "type": "u32" + }, + { + "name": "p2", + "type": "ActorId" + }, + { + "name": "p3", + "type": { + "kind": "named", + "name": "ManyVariants" + } + } + ] + }, + { + "name": "TupleStruct", + "kind": "struct", + "fields": [ + { + "type": "u32" + } + ] + }, + { + "name": "UnitStruct", + "kind": "struct", + "fields": [] + } + ], + "docs": [ + "Demo Program" + ] +} diff --git a/rs/idl-meta/tests/snapshots/serde__service_unit.snap b/rs/idl-meta/tests/snapshots/serde__service_unit.snap new file mode 100644 index 000000000..9fb8cba55 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/serde__service_unit.snap @@ -0,0 +1,176 @@ +--- +source: rs/idl-meta/tests/serde.rs +expression: serialized +--- +{ + "name": "ThisThat", + "funcs": [ + { + "name": "DoThis", + "params": [ + { + "name": "p1", + "type": "u32" + }, + { + "name": "p2", + "type": "String" + }, + { + "name": "p3", + "type": { + "kind": "tuple", + "types": [ + { + "kind": "named", + "name": "Option", + "generics": [ + "H160" + ] + }, + { + "kind": "named", + "name": "NonZero", + "generics": [ + "u8" + ] + } + ] + } + }, + { + "name": "p4", + "type": { + "kind": "named", + "name": "TupleStruct" + } + } + ], + "output": { + "kind": "tuple", + "types": [ + "String", + "u32" + ] + }, + "throws": { + "kind": "tuple", + "types": [ + "String" + ] + }, + "kind": "command", + "docs": [ + "Some func", + "With multiline doc" + ] + } + ], + "types": [ + { + "name": "DoThatParam", + "kind": "struct", + "fields": [ + { + "name": "p1", + "type": "u32", + "docs": [ + "Parametr p1: u32" + ] + }, + { + "name": "p2", + "type": "ActorId" + }, + { + "name": "p3", + "type": { + "kind": "named", + "name": "ManyVariants" + } + } + ] + }, + { + "name": "ManyVariants", + "kind": "enum", + "variants": [ + { + "name": "One", + "fields": [] + }, + { + "name": "Two", + "fields": [ + { + "type": "u32" + } + ] + }, + { + "name": "Three", + "fields": [ + { + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u32" + ] + } + } + ] + }, + { + "name": "Four", + "fields": [ + { + "name": "a", + "type": "u32" + }, + { + "name": "b", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u16" + ] + } + } + ] + }, + { + "name": "Five", + "fields": [ + { + "type": "String" + }, + { + "type": "H256", + "annotations": [ + [ + "key", + "value" + ] + ] + } + ] + }, + { + "name": "Six", + "fields": [ + { + "type": { + "kind": "tuple", + "types": [ + "u32" + ] + } + } + ] + } + ] + } + ] +} diff --git a/rs/idl-meta/tests/snapshots/serde__type_enum.snap b/rs/idl-meta/tests/snapshots/serde__type_enum.snap new file mode 100644 index 000000000..7bd7fe651 --- /dev/null +++ b/rs/idl-meta/tests/snapshots/serde__type_enum.snap @@ -0,0 +1,142 @@ +--- +source: rs/idl-meta/tests/serde.rs +expression: serialized +--- +{ + "name": "SomeType", + "type_params": [ + { + "name": "T1" + }, + { + "name": "T2" + } + ], + "kind": "enum", + "variants": [ + { + "name": "Unit", + "fields": [], + "docs": [ + "Unit-like Variant" + ] + }, + { + "name": "Tuple", + "fields": [ + { + "type": "u32" + } + ], + "docs": [ + "Tuple-like Variant" + ] + }, + { + "name": "TupleWithDocs", + "fields": [ + { + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u32" + ] + }, + "docs": [ + "Some docs" + ] + }, + { + "type": { + "kind": "tuple", + "types": [ + "u32", + "u32" + ] + }, + "docs": [ + "Some docs" + ] + } + ], + "docs": [ + "Tuple-like Variant with field docs" + ] + }, + { + "name": "Struct", + "fields": [ + { + "name": "p1", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + "u32" + ] + } + }, + { + "name": "p2", + "type": { + "kind": "tuple", + "types": [ + "u32", + "u32" + ] + } + } + ], + "docs": [ + "Struct-like Variant" + ] + }, + { + "name": "GenericStruct", + "fields": [ + { + "name": "p1", + "type": { + "kind": "named", + "name": "Option", + "generics": [ + { + "kind": "named", + "name": "T1" + } + ] + } + }, + { + "name": "p2", + "type": { + "kind": "tuple", + "types": [ + { + "kind": "named", + "name": "T2" + }, + { + "kind": "named", + "name": "T2" + } + ] + } + } + ], + "docs": [ + "Generic Struct-like Variant" + ] + } + ], + "docs": [ + "SomeType Enum" + ], + "annotations": [ + [ + "rusttype", + "sails-idl-meta::SomeType" + ] + ] +}