diff --git a/src/commands/bindgen.rs b/src/commands/bindgen.rs index 6e0ad5f..87eac04 100644 --- a/src/commands/bindgen.rs +++ b/src/commands/bindgen.rs @@ -1,10 +1,10 @@ use std::io::Read; use std::{collections::HashMap, path::PathBuf}; -use crate::config::{BindingOptions, Config, KnownChain, TrpConfig}; +use crate::config::{BindingsTemplateConfig, Config, KnownChain, TrpConfig}; use clap::Args as ClapArgs; use miette::IntoDiagnostic; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Serialize, Serializer}; use tx3_lang::Protocol; use convert_case::{Case, Casing}; @@ -16,16 +16,10 @@ use zip::ZipArchive; #[derive(ClapArgs)] pub struct Args {} -/// Configuration structure for bindgen templates -#[derive(Debug, Deserialize)] -struct BindgenConfig { - protocol_files: Option>, -} - /// Structure returned by load_github_templates containing handlebars and optional config struct TemplateBundle { handlebars: Handlebars<'static>, - config: Option, + static_files: Vec<(String, String)> } fn make_helper(name: &'static str, f: F) -> impl handlebars::HelperDef + Send + Sync + 'static @@ -45,6 +39,72 @@ where } } +fn parse_type_from_string(type_str: &str) -> Result { + match type_str { + "Int" => Ok(tx3_lang::ir::Type::Int), + "Bool" => Ok(tx3_lang::ir::Type::Bool), + "Bytes" => Ok(tx3_lang::ir::Type::Bytes), + "Unit" => Ok(tx3_lang::ir::Type::Unit), + "Address" => Ok(tx3_lang::ir::Type::Address), + "UtxoRef" => Ok(tx3_lang::ir::Type::UtxoRef), + "AnyAsset" => Ok(tx3_lang::ir::Type::AnyAsset), + "Utxo" => Ok(tx3_lang::ir::Type::Utxo), + "Undefined" => Ok(tx3_lang::ir::Type::Undefined), + "List" => Ok(tx3_lang::ir::Type::List), + _ => { + // You would need to create a CustomId here. This depends on how CustomId is defined + // Ok(tx3_lang::ir::Type::Custom(CustomId { value: type_str.to_string() })) + Ok(tx3_lang::ir::Type::Custom(type_str.to_string())) // Keep your current implementation if CustomId is not accessible + } + } +} + +fn get_type_for_language(type_: &tx3_lang::ir::Type, language: &str) -> String { + match language { + "rust" => "ArgValue".to_string(), + "typescript" => match &type_ { + tx3_lang::ir::Type::Int => "bigint | number".to_string(), + tx3_lang::ir::Type::Bool => "boolean".to_string(), + tx3_lang::ir::Type::Bytes => "Uint8Array".to_string(), + tx3_lang::ir::Type::Unit => "void".to_string(), + tx3_lang::ir::Type::Address => "string".to_string(), + tx3_lang::ir::Type::UtxoRef => "string".to_string(), + tx3_lang::ir::Type::List => "any[]".to_string(), + tx3_lang::ir::Type::Custom(name) => name.clone(), + tx3_lang::ir::Type::AnyAsset => "string".to_string(), + tx3_lang::ir::Type::Utxo => "any".to_string(), + tx3_lang::ir::Type::Undefined => "any".to_string(), + }, + "python" => match &type_ { + tx3_lang::ir::Type::Int => "int".to_string(), + tx3_lang::ir::Type::Bool => "bool".to_string(), + tx3_lang::ir::Type::Bytes => "bytes".to_string(), + tx3_lang::ir::Type::Unit => "None".to_string(), + tx3_lang::ir::Type::List => "list[Any]".to_string(), + tx3_lang::ir::Type::Address => "str".to_string(), + tx3_lang::ir::Type::UtxoRef => "str".to_string(), + tx3_lang::ir::Type::Custom(name) => name.clone(), + tx3_lang::ir::Type::AnyAsset => "str".to_string(), + tx3_lang::ir::Type::Undefined => "Any".to_string(), + tx3_lang::ir::Type::Utxo => "Any".to_string(), + }, + "go" => match &type_ { + tx3_lang::ir::Type::Int => "int64".to_string(), + tx3_lang::ir::Type::Bool => "bool".to_string(), + tx3_lang::ir::Type::Bytes => "[]byte".to_string(), + tx3_lang::ir::Type::Unit => "struct{}".to_string(), + tx3_lang::ir::Type::Address => "string".to_string(), + tx3_lang::ir::Type::UtxoRef => "string".to_string(), + tx3_lang::ir::Type::List => "[]interface{}".to_string(), + tx3_lang::ir::Type::Custom(name) => name.clone(), + tx3_lang::ir::Type::AnyAsset => "string".to_string(), + tx3_lang::ir::Type::Utxo => "interface{}".to_string(), + tx3_lang::ir::Type::Undefined => "interface{}".to_string(), + }, + _ => "ArgValue".to_string(), // Default fallback + } +} + // Register any custom helpers here /// An array of helper functions for converting strings to various case styles. /// @@ -68,6 +128,33 @@ fn register_handlebars_helpers(handlebars: &mut Handlebars<'_>) { handlebars.register_helper(name, Box::new(make_helper(name, func))); } // Add more helpers as needed + + // Register helper to convert ir types to language types. + handlebars.register_helper("typeFor", Box::new( + |h: &Helper, _: &Handlebars, _: &Context, _: &mut RenderContext, out: &mut dyn Output| { + let type_param = h.param(0).ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("typeFor", 0))?; + let lang_param = h.param(1).ok_or_else(|| RenderErrorReason::ParamNotFoundForIndex("typeFor", 1))?; + + let type_str = type_param + .value() + .as_str() + .ok_or_else(|| RenderErrorReason::InvalidParamType("Expected type as string"))?; + + let type_ = parse_type_from_string(type_str) + .map_err(|_| RenderErrorReason::InvalidParamType("Failed to parse type"))?; + + + let language = lang_param + .value() + .as_str() + .ok_or_else(|| RenderErrorReason::InvalidParamType("Expected language as string"))?; + + let output_type = get_type_for_language(&type_, language); + + out.write(&output_type)?; + Ok(()) + } + )); } /// Loads Handlebars templates from a GitHub repository ZIP archive. @@ -81,7 +168,7 @@ fn register_handlebars_helpers(handlebars: &mut Handlebars<'_>) { /// 6. Registers each found template with Handlebars, using its path relative to `bindgen/` (without the `.hbs` extension) /// /// Returns a TemplateBundle containing the Handlebars registry and optional configuration. -async fn load_github_templates(github_url: &str) -> miette::Result { +async fn load_github_templates(github_url: &str, temp_dir: &TempDir, path: &str) -> miette::Result { // Parse GitHub URL let parts: Vec<&str> = github_url.split('/').collect(); if parts.len() < 2 { @@ -100,7 +187,7 @@ async fn load_github_templates(github_url: &str) -> miette::Result miette::Result miette::Result = None; + // Check for trix-bindgen.toml in the directory + // let toml_name = bindgen_path.join("trix-bindgen.toml").to_string_lossy().to_string(); + + // if let Ok(mut config_file) = archive.by_name(&toml_name) { + // let mut config_content = String::new(); + // config_file.read_to_string(&mut config_content).into_diagnostic()?; + + // config = toml::from_str::(&config_content) + // .into_diagnostic() + // .ok(); + // } + // Register handlebars templates let mut handlebars = Handlebars::new(); - let mut config: Option = None; + let mut static_files = Vec::new(); - for i in 0..archive.len() { + let bindgen_path_string = bindgen_path.to_string_lossy().to_string(); + let archive_bindgen_index = archive.index_for_name(&bindgen_path_string).unwrap_or(0); + + // Skip files that are not in the bindgen_path or are the bindgen_path itself + for i in archive_bindgen_index..archive.len() { let mut file = archive.by_index(i).into_diagnostic()?; let name = file.name().to_owned(); - // Check for trix-bindgen.toml in bindgen directory - if name.contains("bindgen") && name.ends_with("trix-bindgen.toml") { - let mut config_content = String::new(); - file.read_to_string(&mut config_content).into_diagnostic()?; + if !name.starts_with(&bindgen_path_string) { + break; // Stop processing if we reach a file outside the bindgen path + } - config = toml::from_str::(&config_content) - .into_diagnostic() - .ok(); + // If the file is a directory or its the trix-bindgen.toml, skip it + if file.is_dir() || name.ends_with("trix-bindgen.toml") { continue; } - if name.contains("bindgen") && name.ends_with(".hbs") { - // Remove everything before "bindgen/" and strip ".hbs" extension - let template_name = name - .split_once("bindgen/") - .map(|x| x.1) - .unwrap_or(&name) - .strip_suffix(".hbs") - .unwrap_or_else(|| name.split('/').next_back().unwrap_or(&name)); + // Remove everything before "bindgen/" and strip ".hbs" extension + let template_name = name + .strip_prefix(&bindgen_path_string) + .unwrap_or(&name); + + if name.ends_with(".hbs") { + let template_name = template_name.strip_suffix(".hbs").unwrap_or(&name); let mut template_content = String::new(); file.read_to_string(&mut template_content) @@ -163,12 +272,23 @@ async fn load_github_templates(github_url: &str) -> miette::Result); @@ -185,7 +305,7 @@ impl Serialize for BytesHex { #[derive(Serialize)] struct TxParameter { name: String, - type_name: String, + type_name: tx3_lang::ir::Type, } #[derive(Serialize)] @@ -208,6 +328,7 @@ struct HandlebarsData { transactions: Vec, headers: HashMap, env_vars: HashMap, + options: HashMap, } struct Job { @@ -217,11 +338,11 @@ struct Job { trp_endpoint: String, trp_headers: HashMap, env_args: HashMap, + options: HashMap, } fn generate_arguments( job: &Job, - get_type_for_field: fn(&tx3_lang::ir::Type) -> String, version: &str, ) -> miette::Result { let transactions = job @@ -236,7 +357,7 @@ fn generate_arguments( .iter() .map(|(key, type_)| TxParameter { name: key.as_str().to_case(Case::Camel), - type_name: get_type_for_field(type_), + type_name: type_.clone(), }) .collect(); @@ -271,64 +392,54 @@ fn generate_arguments( transactions, headers, env_vars, + options: job.options.clone(), }) } async fn execute_bindgen( job: &Job, - github_url: &str, - get_type_for_field: fn(&tx3_lang::ir::Type) -> String, + template_config: &BindingsTemplateConfig, version: &str, - binding_options: &Option, ) -> miette::Result<()> { - let template_bundle = load_github_templates(github_url).await?; + // Create a temporary directory to extract files + let temp_dir = TempDir::new().into_diagnostic()?; + let github_url = format!("{}/{}", &template_config.repo, template_config.r#ref.as_deref().unwrap_or("main")); + + let template_bundle = load_github_templates(&github_url, &temp_dir, &template_config.path).await?; // Create the destination directory if it doesn't exist std::fs::create_dir_all(&job.dest_path).into_diagnostic()?; - let handlebars_params = generate_arguments(job, get_type_for_field, version)?; - - let standalone = binding_options.as_ref().and_then(|opts| opts.standalone).unwrap_or(false); - - let all_files = template_bundle - .handlebars - .get_templates() - .keys() - .cloned() - .collect(); - - let templates_to_process = if standalone { - all_files - } else { - // If not standalone, use the config's protocol_files if available - template_bundle - .config - .as_ref() - .and_then(|c| c.protocol_files.clone()) - .unwrap_or_else(|| { - all_files - }) - }; - - // Process only the selected templates - for template_file in templates_to_process { - let template_name = template_file.strip_suffix(".hbs").unwrap_or(&template_file); - - if template_bundle - .handlebars - .get_template(template_name) - .is_some() - { - let template_content = template_bundle - .handlebars - .render(template_name, &handlebars_params) - .unwrap(); - let output_path = job.dest_path.join(&template_name); - std::fs::write(&output_path, template_content).unwrap(); - // println!("Generated file: {}", output_path.display()); + let handlebars_params = generate_arguments(job, version)?; + + let handlebars_template_iter = template_bundle.handlebars.get_templates().iter(); + + for (name, _) in handlebars_template_iter { + let template_content = template_bundle.handlebars.render(name, &handlebars_params).unwrap(); + if template_content.is_empty() { + // Skip empty templates + continue; + } + let output_path = job.dest_path.join(name); + if let Some(parent) = output_path.parent() { + // Create parent directories if they don't exist + std::fs::create_dir_all(parent).into_diagnostic()?; } + std::fs::write(&output_path, template_content).into_diagnostic()?; + // println!("Generated file: {}", output_path.display()); } + // Copy static files to the destination directory + for (src_path, file_destination) in &template_bundle.static_files { + let dest_path = job.dest_path.join(file_destination); + if let Some(parent) = dest_path.parent() { + std::fs::create_dir_all(parent).into_diagnostic()?; + } + std::fs::copy(src_path, dest_path).into_diagnostic()?; + // println!("Copied static file: {}", dest_path.display()); + } + + Ok(()) } @@ -353,101 +464,15 @@ pub async fn run(_args: Args, config: &Config) -> miette::Result<()> { trp_endpoint: profile.url.clone(), trp_headers: profile.headers.clone(), env_args: HashMap::new(), + options: bindgen.options.clone().unwrap_or_default(), }; - match bindgen.plugin.as_str() { - "rust" => { - execute_bindgen( - &job, - "tx3-lang/rust-sdk", - |_| "ArgValue".to_string(), - &config.protocol.version, - &bindgen.options, - ) - .await?; - println!("Rust bindgen successful"); - } - "typescript" => { - execute_bindgen( - &job, - "tx3-lang/web-sdk", - |ty| match ty { - tx3_lang::ir::Type::Int => "number".to_string(), - tx3_lang::ir::Type::Address => "string".to_string(), - tx3_lang::ir::Type::Bool => "boolean".to_string(), - tx3_lang::ir::Type::Bytes => "Uint8Array".to_string(), - tx3_lang::ir::Type::UtxoRef => "string".to_string(), - tx3_lang::ir::Type::List => "any[]".to_string(), - tx3_lang::ir::Type::Undefined => "any".to_string(), - tx3_lang::ir::Type::Unit => "void".to_string(), - tx3_lang::ir::Type::AnyAsset => "any".to_string(), - tx3_lang::ir::Type::Utxo => "any".to_string(), - tx3_lang::ir::Type::Custom(name) => name.clone(), - }, - &config.protocol.version, - &bindgen.options, - ) - .await?; - println!("Typescript bindgen successful"); - } - "python" => { - execute_bindgen( - &job, - "tx3-lang/python-sdk", - |ty| match ty { - tx3_lang::ir::Type::Int => "int".to_string(), - tx3_lang::ir::Type::Bool => "bool".to_string(), - tx3_lang::ir::Type::Bytes => "bytes".to_string(), - tx3_lang::ir::Type::Unit => "None".to_string(), - tx3_lang::ir::Type::List => "list[Any]".to_string(), - tx3_lang::ir::Type::Address => "str".to_string(), - tx3_lang::ir::Type::UtxoRef => "str".to_string(), - tx3_lang::ir::Type::Custom(name) => name.clone(), - tx3_lang::ir::Type::AnyAsset => "str".to_string(), - tx3_lang::ir::Type::Undefined => "Any".to_string(), - tx3_lang::ir::Type::Utxo => "Any".to_string(), - }, - &config.protocol.version, - &bindgen.options, - ) - .await?; - println!("Python bindgen successful"); - } - "go" => { - execute_bindgen( - &job, - "tx3-lang/go-sdk", - |ty| match ty { - tx3_lang::ir::Type::Int => "int64".to_string(), - tx3_lang::ir::Type::Bool => "bool".to_string(), - tx3_lang::ir::Type::Bytes => "Bytes".to_string(), - tx3_lang::ir::Type::Unit => "struct{}".to_string(), - tx3_lang::ir::Type::Address => "string".to_string(), - tx3_lang::ir::Type::UtxoRef => "string".to_string(), - tx3_lang::ir::Type::List => "[]interface{}".to_string(), - tx3_lang::ir::Type::Custom(name) => name.clone(), - tx3_lang::ir::Type::AnyAsset => "string".to_string(), - tx3_lang::ir::Type::Utxo => "interface{}".to_string(), - tx3_lang::ir::Type::Undefined => "interface{}".to_string(), - }, - &config.protocol.version, - &bindgen.options, - ) - .await?; - println!("Go bindgen successful"); - } - plugin => { - execute_bindgen( - &job, - plugin, - |_| "ArgValue".to_string(), // Default type for unknown plugins - &config.protocol.version, - &bindgen.options, - ) - .await?; - println!("{} bindgen successful", &plugin); - } - }; + execute_bindgen( + &job, + &bindgen.template, + &config.protocol.version, + ).await?; + println!("Bindgen successful"); } Ok(()) diff --git a/src/commands/init.rs b/src/commands/init.rs index 7d60742..7f2a78d 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::config::{BindingsConfig, Config, ProfilesConfig, ProtocolConfig, RegistryConfig}; +use crate::config::{BindingsConfig, BindingsTemplateConfig, Config, ProfilesConfig, ProtocolConfig, RegistryConfig}; use clap::Args as ClapArgs; use inquire::{Confirm, MultiSelect, Text}; use miette::IntoDiagnostic; @@ -130,7 +130,8 @@ pub fn run(args: Args, config: Option<&Config>) -> miette::Result<()> { .iter() .map(|binding| BindingsConfig { output_dir: PathBuf::from(format!("./gen/{}", binding.to_string().to_lowercase())), - plugin: binding.to_string().to_lowercase(), + plugin: None, // Deprecated + template: BindingsTemplateConfig::from_plugin(binding.to_lowercase().as_str()), options: None, }) .collect(), diff --git a/src/config.rs b/src/config.rs index 3a6e065..a9db415 100644 --- a/src/config.rs +++ b/src/config.rs @@ -197,21 +197,75 @@ impl From for U5cConfig { } #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BindingOptions { - pub standalone: Option, +pub struct BindingsTemplateConfig { + pub repo: String, + pub path: String, + pub r#ref: Option, // default: main +} + +impl Default for BindingsTemplateConfig { + fn default() -> Self { + Self { + repo: String::new(), + path: "bindgen".to_string(), + r#ref: None, + } + } +} + +impl BindingsTemplateConfig { + // Unify the creation of BindingsTemplateConfig from plugin name + pub fn from_plugin(plugin: &str) -> Self { + match plugin { + "typescript" => BindingsTemplateConfig { + repo: "tx3-lang/web-sdk".to_string(), + path: "bindgen/client-lib".to_string(), + r#ref: None, + }, + "rust" => BindingsTemplateConfig { + repo: "tx3-lang/rust-sdk".to_string(), + path: "bindgen".to_string(), + r#ref: None, + }, + "python" => BindingsTemplateConfig { + repo: "tx3-lang/python-sdk".to_string(), + path: "bindgen".to_string(), + r#ref: None, + }, + "go" => BindingsTemplateConfig { + repo: "tx3-lang/go-sdk".to_string(), + path: "bindgen".to_string(), + r#ref: None, + }, + _ => BindingsTemplateConfig::default() + } + } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BindingsConfig { - pub plugin: String, + // Deprecated field, use template instead + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin: Option, + #[serde(default)] + pub template: BindingsTemplateConfig, pub output_dir: PathBuf, - pub options: Option, + pub options: Option>, } impl Config { pub fn load(path: &PathBuf) -> miette::Result { let contents = std::fs::read_to_string(path).into_diagnostic()?; - let config = toml::from_str(&contents).into_diagnostic()?; + let mut config: Config = toml::from_str(&contents).into_diagnostic()?; + + // Post-process bindings to handle backward compatibility + // Eventually, this should be removed once deprecated plugin option is removed + for binding in &mut config.bindings { + if binding.template.repo.is_empty() && binding.plugin.is_some() { + binding.template = BindingsTemplateConfig::from_plugin(binding.plugin.as_ref().unwrap()); + } + } + Ok(config) }