From 5af2bc695be959b724e5bb386169d950cd2d3d7a Mon Sep 17 00:00:00 2001 From: Larko <59736843+Larkooo@users.noreply.github.com> Date: Wed, 29 May 2024 15:25:26 -0400 Subject: [PATCH] feat(dojo-bindgen): support new layout types (#1954) * feat: new types in unity bindgen * feat: add types support to typescript codegen too * feat: handle token directly to get type name * chore: correct type names * chore: update to correct tuple types * fmt * Update mod.rs * Update mod.rs * Update mod.rs * feat: add support for complex enums to unity bindgen * feat: support generic args in typescript * fmt * feat: newer tuple type for c# * feat: value type name in record field * feat: generic rust like enum for unity bindgen * refacotr: optional generic args for record * feat: add suppport for generic enums for typescript v2 * feat: update test & disable typescript * fix: update unity systsem bindgen to use new felt getter #2008 * chore: clippy * clean uop --- bin/sozo/src/commands/build.rs | 21 ++- .../src/plugins/typescript/mod.rs | 89 +++++++--- .../src/plugins/typescript_v2/mod.rs | 157 +++++++++++------- crates/dojo-bindgen/src/plugins/unity/mod.rs | 127 +++++++++----- .../src/test_data/mocks/dojo_examples.ts | 100 ++++++----- 5 files changed, 317 insertions(+), 177 deletions(-) diff --git a/bin/sozo/src/commands/build.rs b/bin/sozo/src/commands/build.rs index d8112bb51f..e278e693d0 100644 --- a/bin/sozo/src/commands/build.rs +++ b/bin/sozo/src/commands/build.rs @@ -16,10 +16,11 @@ const CONTRACT_NAME_LABEL: &str = "Contract"; #[derive(Debug, Args)] pub struct BuildArgs { - #[arg(long)] - #[arg(help = "Generate Typescript bindings.")] - pub typescript: bool, - + // Should we deprecate typescript bindings codegen? + // Disabled due to lack of support in dojo.js + // #[arg(long)] + // #[arg(help = "Generate Typescript bindings.")] + // pub typescript: bool, #[arg(long)] #[arg(help = "Generate Typescript bindings.")] pub typescript_v2: bool, @@ -52,9 +53,11 @@ impl BuildArgs { trace!(?compile_info, "Compiled workspace."); let mut builtin_plugins = vec![]; - if self.typescript { - builtin_plugins.push(BuiltinPlugins::Typescript); - } + + // Disable typescript for now. Due to lack of support and maintenance in dojo.js + // if self.typescript { + // builtin_plugins.push(BuiltinPlugins::Typescript); + // } if self.typescript_v2 { builtin_plugins.push(BuiltinPlugins::TypeScriptV2); @@ -177,9 +180,9 @@ mod tests { let build_args = BuildArgs { bindings_output: "generated".to_string(), - typescript: false, + // typescript: false, unity: true, - typescript_v2: false, + typescript_v2: true, stats: true, }; let result = build_args.run(&config); diff --git a/crates/dojo-bindgen/src/plugins/typescript/mod.rs b/crates/dojo-bindgen/src/plugins/typescript/mod.rs index de2f11bc91..9b00739fe8 100644 --- a/crates/dojo-bindgen/src/plugins/typescript/mod.rs +++ b/crates/dojo-bindgen/src/plugins/typescript/mod.rs @@ -16,8 +16,8 @@ impl TypescriptPlugin { } // Maps cairo types to C#/Unity SDK defined types - fn map_type(type_name: &str) -> String { - match type_name { + fn map_type(token: &Token, generic_args: &Vec<(String, Token)>) -> String { + match token.type_name().as_str() { "bool" => "RecsType.Boolean".to_string(), "u8" => "RecsType.Number".to_string(), "u16" => "RecsType.Number".to_string(), @@ -30,8 +30,44 @@ impl TypescriptPlugin { "bytes31" => "RecsType.String".to_string(), "ClassHash" => "RecsType.BigInt".to_string(), "ContractAddress" => "RecsType.BigInt".to_string(), + "ByteArray" => "RecsType.String".to_string(), + "array" => { + if let Token::Array(array) = token { + format!("{}[]", TypescriptPlugin::map_type(&array.inner, generic_args)) + } else { + panic!("Invalid array token: {:?}", token); + } + } + "tuple" => { + if let Token::Tuple(tuple) = token { + let inners = tuple + .inners + .iter() + .map(|inner| TypescriptPlugin::map_type(inner, generic_args)) + .collect::>() + .join(", "); - _ => type_name.to_string(), + format!("[{}]", inners) + } else { + panic!("Invalid tuple token: {:?}", token); + } + } + "generic_arg" => { + if let Token::GenericArg(arg) = &token { + let arg_type = generic_args + .iter() + .find(|(name, _)| name == arg) + .unwrap_or_else(|| panic!("Generic arg not found: {}", arg)) + .1 + .clone(); + + TypescriptPlugin::map_type(&arg_type, generic_args) + } else { + panic!("Invalid generic arg token: {:?}", token); + } + } + + _ => token.type_name().to_string(), } } @@ -50,7 +86,7 @@ impl TypescriptPlugin { let mut fields = String::new(); for field in &token.inners { - let mapped = TypescriptPlugin::map_type(field.token.type_name().as_str()); + let mapped = TypescriptPlugin::map_type(&field.token, &token.generic_args); if mapped == field.token.type_name() { let token = handled_tokens .iter() @@ -93,24 +129,35 @@ export const {name}Definition = {{ // This will be formatted into a C# enum // Enum is mapped using index of cairo enum fn format_enum(token: &Composite) -> String { - let fields = token - .inners - .iter() - .map(|field| format!("{},", field.name,)) - .collect::>() - .join("\n "); + let name = token.type_name(); - format!( + let mut result = format!( " // Type definition for `{}` enum -export enum {} {{ - {} -}} -", - token.type_path, - token.type_name(), - fields - ) +type {} = ", + token.type_path, name + ); + + let mut variants = Vec::new(); + + for field in &token.inners { + let field_type = + TypescriptPlugin::map_type(&field.token, &token.generic_args).replace("()", ""); + + let variant_definition = if field_type.is_empty() { + // No associated data + format!("{{ type: '{}'; }}", field.name) + } else { + // With associated data + format!("{{ type: '{}'; data: {}; }}", field.name, field_type) + }; + + variants.push(variant_definition); + } + + result += &variants.join(" | "); + + result } // Token should be a model @@ -123,7 +170,7 @@ export enum {} {{ .inners .iter() .map(|field| { - let mapped = TypescriptPlugin::map_type(field.token.type_name().as_str()); + let mapped = TypescriptPlugin::map_type(&field.token, &model.generic_args); if mapped == field.token.type_name() { custom_types.push(format!("\"{}\"", field.token.type_name())); @@ -258,7 +305,7 @@ export function defineContractComponents(world: World) { fn format_system(system: &Function, handled_tokens: &[Composite]) -> String { fn map_type(token: &Token) -> String { match token { - Token::CoreBasic(t) => TypescriptPlugin::map_type(&t.type_name()) + Token::CoreBasic(_) => TypescriptPlugin::map_type(token, &vec![]) .replace("RecsType.", "") // types should be lowercased .to_lowercase(), diff --git a/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs b/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs index d894a340ad..7ae5d5b2a6 100644 --- a/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs +++ b/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use async_trait::async_trait; -use cainome::parser::tokens::{Composite, CompositeType, Function}; +use cainome::parser::tokens::{Composite, CompositeType, Function, Token}; use convert_case::Casing; use crate::error::BindgenResult; @@ -17,8 +17,8 @@ impl TypeScriptV2Plugin { } // Maps cairo types to TypeScript defined types - fn map_type(type_name: &str) -> String { - match type_name { + fn map_type(token: &Token) -> String { + match token.type_name().as_str() { "bool" => "boolean".to_string(), "u8" => "number".to_string(), "u16" => "number".to_string(), @@ -28,10 +28,57 @@ impl TypeScriptV2Plugin { "u256" => "bigint".to_string(), "usize" => "number".to_string(), "felt252" => "string".to_string(), + "bytes31" => "string".to_string(), "ClassHash" => "string".to_string(), "ContractAddress" => "string".to_string(), + "ByteArray" => "string".to_string(), + "array" => { + if let Token::Array(array) = token { + format!("{}[]", TypeScriptV2Plugin::map_type(&array.inner)) + } else { + panic!("Invalid array token: {:?}", token); + } + } + "tuple" => { + if let Token::Tuple(tuple) = token { + let inners = tuple + .inners + .iter() + .map(TypeScriptV2Plugin::map_type) + .collect::>() + .join(", "); + format!("[{}]", inners) + } else { + panic!("Invalid tuple token: {:?}", token); + } + } + "generic_arg" => { + if let Token::GenericArg(generic_arg) = &token { + generic_arg.clone() + } else { + panic!("Invalid generic_arg token: {:?}", token); + } + } + + _ => { + let mut type_name = token.type_name(); - _ => type_name.to_string(), + if let Token::Composite(composite) = token { + if !composite.generic_args.is_empty() { + type_name += &format!( + "<{}>", + composite + .generic_args + .iter() + .map(|(_, t)| TypeScriptV2Plugin::map_type(t)) + .collect::>() + .join(", ") + ) + } + } + + type_name + } } } @@ -116,36 +163,21 @@ function convertQueryToToriiClause(query: Query): Clause | undefined {{ for model in models { let tokens = &model.tokens; - for token in &tokens.enums { - handled_tokens.push(token.to_composite().unwrap().to_owned()); - } for token in &tokens.structs { - handled_tokens.push(token.to_composite().unwrap().to_owned()); - } - - let mut structs = tokens.structs.to_owned(); - structs.sort_by(|a, b| { - if a.to_composite() - .unwrap() - .inners - .iter() - .any(|field| field.token.type_name() == b.type_name()) - { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Less + if handled_tokens.iter().any(|t| t.type_name() == token.type_name()) { + continue; } - }); - for token in &structs { - out += TypeScriptV2Plugin::format_struct( - token.to_composite().unwrap(), - handled_tokens, - ) - .as_str(); + handled_tokens.push(token.to_composite().unwrap().to_owned()); + out += TypeScriptV2Plugin::format_struct(token.to_composite().unwrap()).as_str(); } for token in &tokens.enums { + if handled_tokens.iter().any(|t| t.type_name() == token.type_name()) { + continue; + } + + handled_tokens.push(token.to_composite().unwrap().to_owned()); out += TypeScriptV2Plugin::format_enum(token.to_composite().unwrap()).as_str(); } @@ -417,24 +449,13 @@ function convertQueryToToriiClause(query: Query): Clause | undefined {{ // Token should be a struct // This will be formatted into a TypeScript interface // using TypeScript defined types - fn format_struct(token: &Composite, handled_tokens: &[Composite]) -> String { + fn format_struct(token: &Composite) -> String { let mut native_fields: Vec = Vec::new(); for field in &token.inners { - let mapped = TypeScriptV2Plugin::map_type(field.token.type_name().as_str()); - if mapped == field.token.type_name() { - let token = handled_tokens - .iter() - .find(|t| t.type_name() == field.token.type_name()) - .unwrap_or_else(|| panic!("Token not found: {}", field.token.type_name())); - if token.r#type == CompositeType::Enum { - native_fields.push(format!("{}: {};", field.name, mapped)); - } else { - native_fields.push(format!("{}: {};", field.name, field.token.type_name())); - } - } else { - native_fields.push(format!("{}: {};", field.name, mapped)); - } + let mapped = TypeScriptV2Plugin::map_type(&field.token); + format!("{}: {};", field.name, mapped); + native_fields.push(format!("{}: {};", field.name, mapped)); } format!( @@ -454,24 +475,40 @@ export interface {name} {{ // This will be formatted into a C# enum // Enum is mapped using index of cairo enum fn format_enum(token: &Composite) -> String { - let fields = token - .inners - .iter() - .map(|field| format!("{},", field.name,)) - .collect::>() - .join("\n "); + let mut name = token.type_name(); + if !token.generic_args.is_empty() { + name += &format!( + "<{}>", + token.generic_args.iter().map(|(n, _)| n.clone()).collect::>().join(", ") + ) + } - format!( + let mut result = format!( " // Type definition for `{}` enum -export enum {} {{ - {} -}} -", - token.type_path, - token.type_name(), - fields - ) +type {} = ", + token.type_path, name + ); + + let mut variants = Vec::new(); + + for field in &token.inners { + let field_type = TypeScriptV2Plugin::map_type(&field.token).replace("()", ""); + + let variant_definition = if field_type.is_empty() { + // No associated data + format!("{{ type: '{}'; }}", field.name) + } else { + // With associated data + format!("{{ type: '{}'; data: {}; }}", field.name, field_type) + }; + + variants.push(variant_definition); + } + + result += &variants.join(" | "); + + result } // Formats a system into a JS method used by the contract class @@ -485,10 +522,10 @@ export enum {} {{ format!( "{}: {}", arg.0, - if TypeScriptV2Plugin::map_type(&arg.1.type_name()) == arg.1.type_name() { + if TypeScriptV2Plugin::map_type(&arg.1) == arg.1.type_name() { arg.1.type_name() } else { - TypeScriptV2Plugin::map_type(&arg.1.type_name()) + TypeScriptV2Plugin::map_type(&arg.1) } ) }) diff --git a/crates/dojo-bindgen/src/plugins/unity/mod.rs b/crates/dojo-bindgen/src/plugins/unity/mod.rs index addee69930..2827519351 100644 --- a/crates/dojo-bindgen/src/plugins/unity/mod.rs +++ b/crates/dojo-bindgen/src/plugins/unity/mod.rs @@ -16,8 +16,8 @@ impl UnityPlugin { } // Maps cairo types to C#/Unity SDK defined types - fn map_type(type_name: &str) -> String { - match type_name { + fn map_type(token: &Token) -> String { + match token.type_name().as_str() { "u8" => "byte".to_string(), "u16" => "ushort".to_string(), "u32" => "uint".to_string(), @@ -29,8 +29,54 @@ impl UnityPlugin { "bytes31" => "string".to_string(), "ClassHash" => "FieldElement".to_string(), "ContractAddress" => "FieldElement".to_string(), + "ByteArray" => "string".to_string(), + "array" => { + if let Token::Array(array) = token { + format!("{}[]", UnityPlugin::map_type(&array.inner)) + } else { + panic!("Invalid array token: {:?}", token); + } + } + "tuple" => { + if let Token::Tuple(tuple) = token { + let inners = tuple + .inners + .iter() + .map(UnityPlugin::map_type) + .collect::>() + .join(", "); + format!("({})", inners) + } else { + panic!("Invalid tuple token: {:?}", token); + } + } + "generic_arg" => { + if let Token::GenericArg(g) = &token { + g.clone() + } else { + panic!("Invalid generic arg token: {:?}", token); + } + } + + _ => { + let mut type_name = token.type_name().to_string(); + + if let Token::Composite(composite) = token { + if !composite.generic_args.is_empty() { + type_name += &format!( + "<{}>", + composite + .generic_args + .iter() + .map(|(_, t)| UnityPlugin::map_type(t)) + .collect::>() + .join(", ") + ) + } + } - _ => type_name.to_string(), + type_name + } } } @@ -48,13 +94,7 @@ impl UnityPlugin { let fields = token .inners .iter() - .map(|field| { - format!( - "public {} {};", - UnityPlugin::map_type(field.token.clone().type_name().as_str()), - field.name - ) - }) + .map(|field| format!("public {} {};", UnityPlugin::map_type(&field.token), field.name)) .collect::>() .join("\n "); @@ -76,24 +116,35 @@ public struct {} {{ // This will be formatted into a C# enum // Enum is mapped using index of cairo enum fn format_enum(token: &Composite) -> String { - let fields = token - .inners - .iter() - .map(|field| format!("{},", field.name,)) - .collect::>() - .join("\n "); + let mut name_with_generics = token.type_name(); + if !token.generic_args.is_empty() { + name_with_generics += &format!( + "<{}>", + token.generic_args.iter().map(|(n, _)| n.clone()).collect::>().join(", ") + ); + } - format!( + let mut result = format!( " // Type definition for `{}` enum -public enum {} {{ - {} -}} -", - token.type_path, - token.type_name(), - fields - ) +public abstract record {}() {{", + token.type_path, name_with_generics + ); + + for field in &token.inners { + let type_name = UnityPlugin::map_type(&field.token).replace(['(', ')'], ""); + + result += format!( + "\n public record {}({}) : {name_with_generics};", + field.name, + if type_name.is_empty() { type_name } else { format!("{} value", type_name) } + ) + .as_str(); + } + + result += "\n}\n"; + + result } // Token should be a model @@ -107,7 +158,7 @@ public enum {} {{ format!( "[ModelField(\"{}\")]\n public {} {};", field.name, - UnityPlugin::map_type(field.token.type_name().as_str()), + UnityPlugin::map_type(&field.token), field.name, ) }) @@ -183,19 +234,10 @@ public class {} : ModelInstance {{ // Handled tokens should be a list of all structs and enums used by the contract // Such as a set of referenced tokens from a model fn format_system(system: &Function, handled_tokens: &[Composite]) -> String { - fn map_type(token: &Token) -> String { - match token { - Token::CoreBasic(t) => UnityPlugin::map_type(&t.type_name()), - Token::Composite(t) => t.type_name().to_string(), - Token::Array(t) => format!("{}[]", map_type(&t.inner)), - _ => panic!("Unsupported token type: {:?}", token), - } - } - let args = system .inputs .iter() - .map(|arg| format!("{} {}", map_type(&arg.1), &arg.0)) + .map(|arg| format!("{} {}", UnityPlugin::map_type(&arg.1), &arg.0)) .collect::>() .join(", "); @@ -214,21 +256,18 @@ public class {} : ModelInstance {{ .inners .iter() .map(|field| { - format!( - "new FieldElement({}.{}).Inner()", - type_name, field.name - ) + format!("new FieldElement({}.{}).Inner", type_name, field.name) }) .collect::>() .join(",\n "), _ => { - format!("new FieldElement({}).Inner()", type_name) + format!("new FieldElement({}).Inner", type_name) } } } - None => match UnityPlugin::map_type(type_name).as_str() { - "FieldElement" => format!("{}.Inner()", type_name), - _ => format!("new FieldElement({}).Inner()", type_name), + None => match UnityPlugin::map_type(token).as_str() { + "FieldElement" => format!("{}.Inner", type_name), + _ => format!("new FieldElement({}).Inner", type_name), }, } }) diff --git a/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts b/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts index 0e807cd262..1c1c19c7bb 100644 --- a/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts +++ b/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts @@ -1,4 +1,4 @@ -// Generated by dojo-bindgen on Wed, 17 Apr 2024 07:58:49 +0000. Do not modify this file manually. +// Generated by dojo-bindgen on Tue, 28 May 2024 15:30:47 +0000. Do not modify this file manually. import { Account } from "starknet"; import { Clause, @@ -14,22 +14,6 @@ import { createManifestFromJson, } from "@dojoengine/core"; -// Type definition for `dojo_examples::models::EmoteMessage` struct -export interface EmoteMessage { - identity: string; - emote: Emote; -} - -// Type definition for `dojo_examples::models::Emote` enum -export enum Emote { - None, - Happy, - Sad, - Angry, - Love, -} - - // Type definition for `dojo_examples::models::Vec2` struct export interface Vec2 { x: number; @@ -42,6 +26,15 @@ export interface Position { vec: Vec2; } +// Type definition for `core::byte_array::ByteArray` struct +export interface ByteArray { + data: string[]; + pending_word: string; + pending_word_len: number; +} + +// Type definition for `core::option::Option::` enum +type Option = { type: 'Some'; data: A; } | { type: 'None'; } // Type definition for `dojo_examples::actions::actions::Moved` struct export interface Moved { @@ -50,14 +43,7 @@ export interface Moved { } // Type definition for `dojo_examples::models::Direction` enum -export enum Direction { - None, - Left, - Right, - Up, - Down, -} - +type Direction = { type: 'None'; } | { type: 'Left'; } | { type: 'Right'; } | { type: 'Up'; } | { type: 'Down'; } // Type definition for `dojo_examples::models::Moves` struct export interface Moves { @@ -66,13 +52,28 @@ export interface Moves { last_direction: Direction; } -// Type definition for `dojo_examples::models::Direction` enum -export enum Direction { - None, - Left, - Right, - Up, - Down, + +// Type definition for `dojo_examples::models::EmoteMessage` struct +export interface EmoteMessage { + identity: string; + emote: Emote; +} + +// Type definition for `dojo_examples::models::Emote` enum +type Emote = { type: 'None'; } | { type: 'Happy'; } | { type: 'Sad'; } | { type: 'Angry'; } | { type: 'Love'; } + +// Type definition for `dojo_examples::models::PlayerItem` struct +export interface PlayerItem { + item_id: number; + quantity: number; +} + +// Type definition for `dojo_examples::models::PlayerConfig` struct +export interface PlayerConfig { + player: string; + name: string; + items: PlayerItem[]; + favorite_item: Option; } @@ -129,15 +130,6 @@ class ActionsCalls extends BaseCalls { } } - async dojoResource(): Promise { - try { - await this.execute("dojo_resource", []) - } catch (error) { - console.error("Error executing dojoResource:", error); - throw error; - } - } - async spawn(): Promise { try { await this.execute("spawn", []) @@ -155,20 +147,42 @@ class ActionsCalls extends BaseCalls { throw error; } } + + async setPlayerConfig(name: string): Promise { + try { + await this.execute("set_player_config", [props.name.data, + props.name.pending_word, + props.name.pending_word_len]) + } catch (error) { + console.error("Error executing setPlayerConfig:", error); + throw error; + } + } + + async dojoResource(): Promise { + try { + await this.execute("dojo_resource", []) + } catch (error) { + console.error("Error executing dojoResource:", error); + throw error; + } + } } type Query = Partial<{ - EmoteMessage: ModelClause; Position: ModelClause; Moved: ModelClause; Moves: ModelClause; + EmoteMessage: ModelClause; + PlayerConfig: ModelClause; }>; type ResultMapping = { - EmoteMessage: EmoteMessage; Position: Position; Moved: Moved; Moves: Moves; + EmoteMessage: EmoteMessage; + PlayerConfig: PlayerConfig; }; type QueryResult = {