diff --git a/bin/sozo/src/commands/build.rs b/bin/sozo/src/commands/build.rs index a2d0c18aa4..3da5620825 100644 --- a/bin/sozo/src/commands/build.rs +++ b/bin/sozo/src/commands/build.rs @@ -4,8 +4,8 @@ use anyhow::Result; use clap::{Args, Parser}; use colored::{ColoredString, Colorize}; use dojo_bindgen::{BuiltinPlugins, PluginManager}; -use dojo_world::local::{ResourceLocal, WorldLocal}; use dojo_world::ResourceType; +use dojo_world::local::{ResourceLocal, WorldLocal}; use scarb::core::{Config, Package, TargetKind}; use scarb::ops::CompileOpts; use scarb_ui::args::{FeaturesSpec, PackagesFilter}; @@ -38,6 +38,10 @@ pub struct BuildArgs { #[arg(help = "Generate Unreal Engine bindings.")] pub unrealengine: bool, + #[arg(long)] + #[arg(help = "Generate Go bindings.")] + pub golang: bool, + #[arg(long)] #[arg(help = "Output directory.", default_value = "bindings")] pub bindings_output: String, @@ -60,25 +64,25 @@ pub struct BuildArgs { pub struct StatOptions { #[arg(long = "stats.by-tag")] #[arg(help = "Sort the stats by tag.")] - #[arg(conflicts_with_all = ["stats.by-sierra-mb", "stats.by-sierra-felts", "stats.by-casm-felts"])] + #[arg(conflicts_with_all = ["sort_by_sierra_mb", "sort_by_sierra_felts", "sort_by_casm_felts"])] #[arg(default_value_t = false)] pub sort_by_tag: bool, #[arg(long = "stats.by-sierra-mb")] #[arg(help = "Sort the stats by Sierra file size in MB.")] - #[arg(conflicts_with_all = ["stats.by-tag", "stats.by-sierra-felts", "stats.by-casm-felts"])] + #[arg(conflicts_with_all = ["sort_by_tag", "sort_by_sierra_felts", "sort_by_casm_felts"])] #[arg(default_value_t = false)] pub sort_by_sierra_mb: bool, #[arg(long = "stats.by-sierra-felts")] #[arg(help = "Sort the stats by Sierra program size in felts.")] - #[arg(conflicts_with_all = ["stats.by-tag", "stats.by-sierra-mb", "stats.by-casm-felts"])] + #[arg(conflicts_with_all = ["sort_by_tag", "sort_by_sierra_mb", "sort_by_casm_felts"])] #[arg(default_value_t = false)] pub sort_by_sierra_felts: bool, #[arg(long = "stats.by-casm-felts")] #[arg(help = "Sort the stats by Casm bytecode size in felts.")] - #[arg(conflicts_with_all = ["stats.by-tag", "stats.by-sierra-mb", "stats.by-sierra-felts"])] + #[arg(conflicts_with_all = ["sort_by_tag", "sort_by_sierra_mb", "sort_by_sierra_felts"])] #[arg(default_value_t = false)] pub sort_by_casm_felts: bool, } @@ -138,6 +142,10 @@ impl BuildArgs { builtin_plugins.push(BuiltinPlugins::UnrealEngine); } + if self.golang { + builtin_plugins.push(BuiltinPlugins::Golang); + } + // Custom plugins are always empty for now. let bindgen = PluginManager { profile_name: ws.current_profile().expect("Profile expected").to_string(), @@ -223,6 +231,7 @@ impl Default for BuildArgs { recs: false, unity: false, unrealengine: false, + golang: false, bindings_output: "bindings".to_string(), stats: StatOptions::default(), packages: None, diff --git a/crates/dojo/bindgen/src/lib.rs b/crates/dojo/bindgen/src/lib.rs index c39325c7f9..322f83fbf0 100644 --- a/crates/dojo/bindgen/src/lib.rs +++ b/crates/dojo/bindgen/src/lib.rs @@ -17,6 +17,7 @@ use plugins::typescript::TypescriptPlugin; use plugins::typescript_v2::TypeScriptV2Plugin; use plugins::unity::UnityPlugin; use plugins::unrealengine::UnrealEnginePlugin; +use plugins::golang::GolangPlugin; use plugins::BuiltinPlugin; pub use plugins::BuiltinPlugins; @@ -105,6 +106,7 @@ impl PluginManager { BuiltinPlugins::UnrealEngine => Box::new(UnrealEnginePlugin::new()), BuiltinPlugins::TypeScriptV2 => Box::new(TypeScriptV2Plugin::new()), BuiltinPlugins::Recs => Box::new(TypescriptRecsPlugin::new()), + BuiltinPlugins::Golang => Box::new(GolangPlugin::new()), }; let files = builder.generate_code(&data).await?; diff --git a/crates/dojo/bindgen/src/plugins/golang/mod.rs b/crates/dojo/bindgen/src/plugins/golang/mod.rs new file mode 100644 index 0000000000..9778583391 --- /dev/null +++ b/crates/dojo/bindgen/src/plugins/golang/mod.rs @@ -0,0 +1,742 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use cainome::parser::tokens::{Composite, CompositeType, Function, Token}; +use convert_case::Casing; +use dojo_world::contracts::naming; + +use crate::error::BindgenResult; +use crate::plugins::BuiltinPlugin; +use crate::{DojoContract, DojoData, DojoModel}; + +pub struct GolangPlugin {} + +impl GolangPlugin { + pub fn new() -> Self { + Self {} + } + + // Maps cairo types to Go types + fn map_type(token: &Token) -> String { + match token.type_name().as_str() { + "bool" => "bool".to_string(), + "u8" => "uint8".to_string(), + "u16" => "uint16".to_string(), + "u32" => "uint32".to_string(), + "u64" => "uint64".to_string(), + "u128" => "*big.Int".to_string(), + "u256" => "*big.Int".to_string(), + "usize" => "uint".to_string(), + "felt252" => "*felt.Felt".to_string(), + "bytes31" => "[31]byte".to_string(), + "ClassHash" => "*felt.Felt".to_string(), + "ContractAddress" => "*felt.Felt".to_string(), + "ByteArray" => "[]byte".to_string(), + "array" => { + if let Token::Array(array) = token { + format!("[]{}", GolangPlugin::map_type(&array.inner)) + } else { + panic!("Invalid array token: {:?}", token); + } + } + "tuple" => { + if let Token::Tuple(tuple) = token { + // Go doesn't have native tuple support, so we'll create a struct + let fields = tuple + .inners + .iter() + .enumerate() + .map(|(i, t)| format!("Field{} {}", i + 1, GolangPlugin::map_type(t))) + .collect::>() + .join("; "); + format!("struct {{ {} }}", fields) + } 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(); + + if let Token::Composite(composite) = token { + // Special handling for Option types + if composite.type_path.contains("Option") { + // Extract the inner type from generic args + if let Some((_, inner_token)) = composite.generic_args.first() { + // For Option types, return a pointer to the inner type + return format!("*{}", GolangPlugin::map_type(inner_token)); + } + // Fallback to any if we can't determine the inner type + return "any".to_string(); + } + + if !composite.generic_args.is_empty() { + // Go doesn't support generics in the same way, we'll use any for now + type_name = format!("{}Generic", type_name); + } + } + + type_name + } + } + } + + fn generate_header() -> String { + format!( + "// Code generated by dojo-bindgen on {}. DO NOT EDIT.\n", + chrono::Utc::now().to_rfc2822() + ) + } + + fn generate_package_and_imports(package_name: &str) -> String { + format!( + "package {} + +import ( + \"context\" + \"fmt\" + \"math/big\" + \"time\" + + \"github.com/NethermindEth/juno/core/felt\" + \"github.com/NethermindEth/starknet.go/account\" + \"github.com/NethermindEth/starknet.go/rpc\" + \"github.com/NethermindEth/starknet.go/utils\" +)", + package_name + ) + } + + fn generate_model_types(models: &[&DojoModel], handled_tokens: &mut Vec) -> String { + let mut out = String::new(); + + for model in models { + let tokens = &model.tokens; + + for token in &tokens.structs { + if handled_tokens.iter().any(|t| t.type_name() == token.type_name()) { + continue; + } + + handled_tokens.push(token.to_composite().unwrap().to_owned()); + out += &GolangPlugin::format_struct(token.to_composite().unwrap()); + } + + for token in &tokens.enums { + if handled_tokens.iter().any(|t| t.type_name() == token.type_name()) { + continue; + } + + let composite = token.to_composite().unwrap(); + // Skip Option enums as we handle them as nullable pointers + if composite.type_path.contains("option::Option") { + continue; + } + + handled_tokens.push(composite.to_owned()); + out += &GolangPlugin::format_enum(composite); + } + + out += "\n"; + } + + out + } + + fn format_struct(token: &Composite) -> String { + let struct_name = token.type_name().to_case(convert_case::Case::Pascal); + let mut fields: Vec = Vec::new(); + + for field in &token.inners { + let field_name = field.name.to_case(convert_case::Case::Pascal); + let field_type = GolangPlugin::map_type(&field.token); + fields.push(format!("\t{} {} `json:\"{}\"`", field_name, field_type, field.name)); + } + + format!( + "// {} represents the {} struct +type {} struct {{ +{} +}} +", + struct_name, + token.type_path, + struct_name, + fields.join("\n") + ) + } + + fn format_enum(token: &Composite) -> String { + let enum_name = token.type_name().to_case(convert_case::Case::Pascal); + let mut out = String::new(); + + // Create the base enum type + out += &format!( + "// {} represents the {} enum\ntype {} int\n\nconst (\n", + enum_name, token.type_path, enum_name + ); + + // Create enum constants + for (idx, field) in token.inners.iter().enumerate() { + let variant_name = + format!("{}{}", enum_name, field.name.to_case(convert_case::Case::Pascal)); + if idx == 0 { + out += &format!("\t{} {} = iota\n", variant_name, enum_name); + } else { + out += &format!("\t{}\n", variant_name); + } + } + out += ")\n\n"; + + // Add String method + out += &format!("func (e {}) String() string {{\n\tswitch e {{\n", enum_name); + for field in &token.inners { + let variant_name = + format!("{}{}", enum_name, field.name.to_case(convert_case::Case::Pascal)); + out += &format!("\tcase {}:\n\t\treturn \"{}\"\n", variant_name, field.name); + } + out += &format!("\tdefault:\n\t\treturn \"Unknown{}\"\n\t}}\n}}\n", enum_name); + + out + } + + fn generate_client_struct(world_name: &str, contracts: &[&DojoContract]) -> String { + let client_name = format!("{}Client", world_name.to_case(convert_case::Case::Pascal)); + + // Contract fields + let contract_fields = contracts + .iter() + .map(|contract| { + let contract_name = GolangPlugin::formatted_contract_name(&contract.tag); + let field_name = contract_name.to_case(convert_case::Case::Camel); + format!("\t{} *{}Contract", field_name, contract_name) + }) + .collect::>() + .join("\n"); + + format!( + "// {} is the main client for interacting with the {} world +type {} struct {{ + account *account.Account + rpcClient *rpc.Provider + worldAddress string +{} +}}", + client_name, world_name, client_name, contract_fields + ) + } + + fn generate_contract_structs(contracts: &[&DojoContract]) -> String { + let mut out = String::new(); + + for contract in contracts { + let contract_name = GolangPlugin::formatted_contract_name(&contract.tag); + + out += &format!( + "// {}Contract provides methods to interact with the {} contract +type {}Contract struct {{ + address string + account *account.Account + client *rpc.Provider +}} +", + contract_name, contract.tag, contract_name + ); + } + + out + } + + fn generate_new_client_function(world_name: &str, contracts: &[&DojoContract]) -> String { + let client_name = format!("{}Client", world_name.to_case(convert_case::Case::Pascal)); + + let contract_initializations = contracts + .iter() + .map(|contract| { + let contract_name = GolangPlugin::formatted_contract_name(&contract.tag); + let field_name = contract_name.to_case(convert_case::Case::Camel); + format!( + "\tclient.{} = &{}Contract{{ + address: {}Address, + account: account, + client: rpcClient, + }}", + field_name, contract_name, field_name + ) + }) + .collect::>() + .join("\n\n"); + + let contract_addresses_params = contracts + .iter() + .map(|contract| { + let contract_name = GolangPlugin::formatted_contract_name(&contract.tag); + format!("{}Address string", contract_name.to_case(convert_case::Case::Camel)) + }) + .collect::>() + .join(", "); + + format!( + "// New{} creates a new client for interacting with the {} world +func New{}(rpcUrl string, account *account.Account, worldAddress string, {}) (*{}, error) {{ + rpcClient, err := rpc.NewProvider(rpcUrl) + if err != nil {{ + return nil, fmt.Errorf(\"failed to create RPC client: %w\", err) + }} + + client := &{}{{ + account: account, + rpcClient: rpcClient, + worldAddress: worldAddress, + }} + +{} + + return client, nil +}}", + client_name, + world_name, + client_name, + contract_addresses_params, + client_name, + client_name, + contract_initializations + ) + } + + fn generate_contract_methods( + contracts: &[&DojoContract], + handled_tokens: &[Composite], + ) -> String { + let mut out = String::new(); + + for contract in contracts { + let contract_name = GolangPlugin::formatted_contract_name(&contract.tag); + + for system in &contract.systems { + if let Ok(func) = system.to_function() { + out += + &GolangPlugin::format_system_method(&contract_name, func, handled_tokens); + out += "\n"; + } + } + } + + out + } + + fn format_system_method( + contract_name: &str, + system: &Function, + handled_tokens: &[Composite], + ) -> String { + let method_name = system.name.to_case(convert_case::Case::Pascal); + + // Generate method parameters + let params = system + .inputs + .iter() + .map(|(name, token)| { + let param_name = name.to_case(convert_case::Case::Camel); + let param_type = GolangPlugin::map_type(token); + format!("{} {}", param_name, param_type) + }) + .collect::>(); + + let params_str = if params.is_empty() { + "ctx context.Context".to_string() + } else { + format!("ctx context.Context, {}", params.join(", ")) + }; + + // Generate calldata for struct flattening (currently unused) + let _calldata = system + .inputs + .iter() + .map(|(name, token)| { + let param_name = name.to_case(convert_case::Case::Camel); + + // Check if this is a struct that needs to be flattened + match handled_tokens.iter().find(|t| t.type_name() == token.type_name()) { + Some(t) if t.r#type == CompositeType::Struct => { + // Flatten struct fields + t.inners + .iter() + .map(|field| { + format!( + "{}.{}", + param_name, + field.name.to_case(convert_case::Case::Pascal) + ) + }) + .collect::>() + .join(", ") + } + _ => param_name, + } + }) + .collect::>(); + + // Generate proper calldata based on parameter types + let calldata_params = system + .inputs + .iter() + .map(|(name, token)| { + let param_name = name.to_case(convert_case::Case::Camel); + match token.type_name().as_str() { + "felt252" | "ClassHash" | "ContractAddress" => param_name, + _ => format!("utils.MustHexToFelt({})", param_name), + } + }) + .collect::>(); + + let calldata_str = if calldata_params.is_empty() { + "[]*felt.Felt{}".to_string() + } else { + format!("[]*felt.Felt{{{}}}", calldata_params.join(", ")) + }; + + format!( + "// {} executes the {} system +func (c *{}Contract) {}({}) error {{ + tx, err := c.account.Execute(ctx, []rpc.FunctionCall{{{{ + ContractAddress: utils.MustHexToFelt(c.address), + EntryPointSelector: utils.GetSelectorFromNameFelt(\"{}\"), + Calldata: {}, + }}}}, account.ExecuteOptions{{MaxFee: big.NewInt(0)}}) + + if err != nil {{ + return fmt.Errorf(\"failed to execute %s: %w\", \"{}\", err) + }} + + // Wait for transaction receipt + _, err = c.client.WaitForTransaction(ctx, tx.TransactionHash, 8*time.Second) + if err != nil {{ + return fmt.Errorf(\"transaction failed: %w\", err) + }} + + return nil +}}", + method_name, + system.name, + contract_name, + method_name, + params_str, + system.name, + calldata_str, + system.name + ) + } + + fn formatted_contract_name(tag: &str) -> String { + // Always use the full tag to ensure uniqueness + let parts: Vec<&str> = tag.split('-').collect(); + // Convert each part to PascalCase and join + parts.iter().map(|p| naming::capitalize(p)).collect::>().join("") + } + + fn generate_code_content(data: &DojoData) -> String { + let mut handled_tokens = Vec::::new(); + let models = data.models.values().collect::>(); + let contracts = data.contracts.values().collect::>(); + + let package_name = data.world.name.to_case(convert_case::Case::Snake); + + let mut code = String::new(); + code += &GolangPlugin::generate_header(); + code += &GolangPlugin::generate_package_and_imports(&package_name); + code += "\n\n"; + code += &GolangPlugin::generate_model_types(&models, &mut handled_tokens); + code += "\n"; + code += &GolangPlugin::generate_contract_structs(&contracts); + code += "\n"; + code += &GolangPlugin::generate_client_struct(&data.world.name, &contracts); + code += "\n\n"; + code += &GolangPlugin::generate_new_client_function(&data.world.name, &contracts); + code += "\n\n"; + code += &GolangPlugin::generate_contract_methods(&contracts, &handled_tokens); + + code + } +} + +#[async_trait] +impl BuiltinPlugin for GolangPlugin { + async fn generate_code(&self, data: &DojoData) -> BindgenResult>> { + let code = GolangPlugin::generate_code_content(data); + + let mut out: HashMap> = HashMap::new(); + let output_path = Path::new(&format!("{}.go", data.world.name)).to_owned(); + + out.insert(output_path, code.as_bytes().to_vec()); + + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{DojoData, DojoWorld}; + use cainome::parser::tokens::{Composite, CompositeType, Token}; + + #[tokio::test] + async fn test_golang_plugin_generation() { + // Create a simple test data structure + let world = DojoWorld { name: "test_world".to_string() }; + + let data = DojoData { + world, + models: HashMap::new(), + contracts: HashMap::new(), + events: HashMap::new(), + }; + + let plugin = GolangPlugin::new(); + let result = plugin.generate_code(&data).await; + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 1); + + // Check that the generated file has the correct name + let (path, content) = files.iter().next().unwrap(); + assert_eq!(path.to_str().unwrap(), "test_world.go"); + + // Check that the content contains expected Go code + let content_str = String::from_utf8(content.clone()).unwrap(); + assert!(content_str.contains("package test_world")); + assert!(content_str.contains("import (")); + assert!(content_str.contains("type TestWorldClient struct")); + } + + #[tokio::test] + async fn test_golang_plugin_namespace_contracts() { + use crate::DojoContract; + + // Create test data with namespace conflicts + let world = DojoWorld { name: "test_world".to_string() }; + + let mut contracts = HashMap::new(); + + // Create contracts with namespace conflicts + contracts.insert( + "ns-c1".to_string(), + DojoContract { tag: "ns-c1".to_string(), tokens: Default::default(), systems: vec![] }, + ); + + contracts.insert( + "ns2-c1".to_string(), + DojoContract { tag: "ns2-c1".to_string(), tokens: Default::default(), systems: vec![] }, + ); + + let data = DojoData { world, models: HashMap::new(), contracts, events: HashMap::new() }; + + let plugin = GolangPlugin::new(); + let result = plugin.generate_code(&data).await; + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 1); + + let (_, content) = files.iter().next().unwrap(); + let content_str = String::from_utf8(content.clone()).unwrap(); + + // Check that both contracts have unique type names + assert!(content_str.contains("type NsC1Contract struct")); + assert!(content_str.contains("type Ns2C1Contract struct")); + + // Check that there are no duplicate type definitions + let ns_c1_count = content_str.matches("type NsC1Contract struct").count(); + let ns2_c1_count = content_str.matches("type Ns2C1Contract struct").count(); + assert_eq!(ns_c1_count, 1, "NsC1Contract should appear exactly once"); + assert_eq!(ns2_c1_count, 1, "Ns2C1Contract should appear exactly once"); + } + + #[tokio::test] + async fn test_golang_function_call_syntax() { + use crate::{DojoContract, Token}; + use cainome::parser::tokens::{CoreBasic, Function}; + + // Create test data with a contract and system + let world = DojoWorld { name: "test_world".to_string() }; + + let mut contracts = HashMap::new(); + + // Create a simple function token + let system_token = Token::Function(Function { + name: "test_system".to_string(), + inputs: vec![ + ( + "arg1".to_string(), + Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_owned() }), + ), + ( + "arg2".to_string(), + Token::CoreBasic(CoreBasic { type_path: "core::integer::u32".to_owned() }), + ), + ], + outputs: vec![], + named_outputs: vec![], + state_mutability: cainome::parser::tokens::StateMutability::External, + }); + + contracts.insert( + "test".to_string(), + DojoContract { + tag: "test".to_string(), + tokens: Default::default(), + systems: vec![system_token], + }, + ); + + let data = DojoData { world, models: HashMap::new(), contracts, events: HashMap::new() }; + + let plugin = GolangPlugin::new(); + let result = plugin.generate_code(&data).await; + + assert!(result.is_ok()); + let files = result.unwrap(); + let (_, content) = files.iter().next().unwrap(); + let content_str = String::from_utf8(content.clone()).unwrap(); + + // Check the function call syntax is correct + assert!(content_str.contains("[]rpc.FunctionCall{{")); + assert!(content_str.contains("ContractAddress: utils.MustHexToFelt(c.address),")); + assert!(content_str.contains("EntryPointSelector: utils.GetSelectorFromNameFelt(")); + assert!( + content_str + .contains("Calldata: []*felt.Felt{arg1, utils.MustHexToFelt(arg2)},") + ); + } + + #[test] + fn test_type_mappings() { + use cainome::parser::tokens::CoreBasic; + + // Test basic type mappings + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned() + })), + "*felt.Felt" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::byte_array::ByteArray".to_owned() + })), + "[]byte" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::bytes_31::bytes31".to_owned() + })), + "[31]byte" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::starknet::class_hash::ClassHash".to_owned() + })), + "*felt.Felt" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::starknet::contract_address::ContractAddress".to_owned() + })), + "*felt.Felt" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::u8".to_owned() + })), + "uint8" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::u16".to_owned() + })), + "uint16" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::u32".to_owned() + })), + "uint32" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::u64".to_owned() + })), + "uint64" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::u128".to_owned() + })), + "*big.Int" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::u256".to_owned() + })), + "*big.Int" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::bool".to_owned() + })), + "bool" + ); + assert_eq!( + GolangPlugin::map_type(&Token::CoreBasic(CoreBasic { + type_path: "core::integer::usize".to_owned() + })), + "uint" + ); + } + + #[test] + fn test_option_type_mapping() { + use cainome::parser::tokens::CoreBasic; + + // Create a mock Option type using CoreBasic token + let option_composite = Composite { + type_path: "core::option::Option::".to_string(), + generic_args: vec![( + "T".to_string(), + Token::CoreBasic(CoreBasic { type_path: "core::integer::u32".to_owned() }), + )], + r#type: CompositeType::Enum, + is_event: false, + alias: None, + inners: vec![], + }; + + let option_token = Token::Composite(option_composite); + assert_eq!(GolangPlugin::map_type(&option_token), "*uint32"); + } + + #[test] + fn test_contract_name_formatting() { + assert_eq!(GolangPlugin::formatted_contract_name("ns-c1"), "NsC1"); + assert_eq!(GolangPlugin::formatted_contract_name("ns2-c1"), "Ns2C1"); + assert_eq!(GolangPlugin::formatted_contract_name("ns-c2"), "NsC2"); + assert_eq!(GolangPlugin::formatted_contract_name("ns-c3"), "NsC3"); + assert_eq!(GolangPlugin::formatted_contract_name("simple"), "Simple"); + assert_eq!( + GolangPlugin::formatted_contract_name("my-awesome-contract"), + "MyAwesomeContract" + ); + + // Debug: print what we actually get + println!("ns-c1 -> {}", GolangPlugin::formatted_contract_name("ns-c1")); + println!("ns2-c1 -> {}", GolangPlugin::formatted_contract_name("ns2-c1")); + } +} diff --git a/crates/dojo/bindgen/src/plugins/mod.rs b/crates/dojo/bindgen/src/plugins/mod.rs index bae8ffd9af..d0bbd52d21 100644 --- a/crates/dojo/bindgen/src/plugins/mod.rs +++ b/crates/dojo/bindgen/src/plugins/mod.rs @@ -14,6 +14,7 @@ pub mod typescript; pub mod typescript_v2; pub mod unity; pub mod unrealengine; +pub mod golang; #[derive(Debug)] pub enum BuiltinPlugins { @@ -22,6 +23,7 @@ pub enum BuiltinPlugins { UnrealEngine, TypeScriptV2, Recs, + Golang, } impl fmt::Display for BuiltinPlugins { @@ -32,6 +34,7 @@ impl fmt::Display for BuiltinPlugins { BuiltinPlugins::UnrealEngine => write!(f, "unrealengine"), BuiltinPlugins::TypeScriptV2 => write!(f, "typescript_v2"), BuiltinPlugins::Recs => write!(f, "recs"), + BuiltinPlugins::Golang => write!(f, "golang"), } } } diff --git a/examples/simple/Scarb.lock b/examples/simple/Scarb.lock index 82ec7026f8..bb53299f3a 100644 --- a/examples/simple/Scarb.lock +++ b/examples/simple/Scarb.lock @@ -3,7 +3,7 @@ version = 1 [[package]] name = "dojo" -version = "1.5.0" +version = "1.5.1" dependencies = [ "dojo_plugin", ]