From 96dbf39be1ca4aaa9c037be879902085226f3c25 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 3 Apr 2025 17:20:08 -0700 Subject: [PATCH 01/88] add hyper-bindgen from commit: https://github.com/jaxs-ribs/hyper-bindgen/commit/858481b301c534acf9708cf849bfcdbab5c75dcf --- src/build/caller_utils_generator.rs | 786 +++++++++++++++++++++++ src/build/wit_generator.rs | 945 ++++++++++++++++++++++++++++ 2 files changed, 1731 insertions(+) create mode 100644 src/build/caller_utils_generator.rs create mode 100644 src/build/wit_generator.rs diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs new file mode 100644 index 00000000..8de9c0f9 --- /dev/null +++ b/src/build/caller_utils_generator.rs @@ -0,0 +1,786 @@ +use anyhow::{Context, Result, bail}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use toml::Value; +use walkdir::WalkDir; + +// Convert kebab-case to snake_case +pub fn to_snake_case(s: &str) -> String { + s.replace('-', "_") +} + +// Convert kebab-case to PascalCase +pub fn to_pascal_case(s: &str) -> String { + let parts = s.split('-'); + let mut result = String::new(); + + for part in parts { + if !part.is_empty() { + let mut chars = part.chars(); + if let Some(first_char) = chars.next() { + result.push(first_char.to_uppercase().next().unwrap()); + result.extend(chars); + } + } + } + + result +} + +// Find the world name in the world WIT file, prioritizing types-prefixed worlds +fn find_world_name(api_dir: &Path) -> Result { + let mut regular_world_name = None; + let mut types_world_name = None; + + // Look for world definition files + for entry in WalkDir::new(api_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { + if let Ok(content) = fs::read_to_string(path) { + if content.contains("world ") { + println!("Analyzing world definition file: {}", path.display()); + + // Extract the world name + let lines: Vec<&str> = content.lines().collect(); + + if let Some(world_line) = lines.iter().find(|line| line.trim().starts_with("world ")) { + println!("World line: {}", world_line); + + if let Some(world_name) = world_line.trim().split_whitespace().nth(1) { + let clean_name = world_name.trim_end_matches(" {"); + println!("Extracted world name: {}", clean_name); + + // Check if this is a types-prefixed world + if clean_name.starts_with("types-") { + types_world_name = Some(clean_name.to_string()); + println!("Found types world: {}", clean_name); + } else { + regular_world_name = Some(clean_name.to_string()); + println!("Found regular world: {}", clean_name); + } + } + } + } + } + } + } + + // Prioritize types-prefixed world if found + if let Some(types_name) = types_world_name { + return Ok(types_name); + } + + // If no types-prefixed world found, check if we have a regular world + if let Some(regular_name) = regular_world_name { + // Check if there's a corresponding types-prefixed world file + let types_name = format!("types-{}", regular_name); + let types_file = api_dir.join(format!("{}.wit", types_name)); + + if types_file.exists() { + println!("Found types world from file: {}", types_name); + return Ok(types_name); + } + + // Fall back to regular world but print a warning + println!("Warning: No types- world found, using regular world: {}", regular_name); + return Ok(regular_name); + } + + // If no world name is found, we should fail + bail!("No world name found in any WIT file. Cannot generate caller-utils without a world name.") +} + +// Convert WIT type to Rust type - IMPROVED with more Rust primitives +fn wit_type_to_rust(wit_type: &str) -> String { + match wit_type { + // Integer types + "s8" => "i8".to_string(), + "u8" => "u8".to_string(), + "s16" => "i16".to_string(), + "u16" => "u16".to_string(), + "s32" => "i32".to_string(), + "u32" => "u32".to_string(), + "s64" => "i64".to_string(), + "u64" => "u64".to_string(), + // Size types + "usize" => "usize".to_string(), + "isize" => "isize".to_string(), + // Floating point types + "f32" => "f32".to_string(), + "f64" => "f64".to_string(), + // Other primitive types + "string" => "String".to_string(), + "str" => "&str".to_string(), + "char" => "char".to_string(), + "bool" => "bool".to_string(), + "unit" => "()".to_string(), + // Special types + "address" => "WitAddress".to_string(), + // Common primitives that might be written differently in WIT + "i8" => "i8".to_string(), + "i16" => "i16".to_string(), + "i32" => "i32".to_string(), + "i64" => "i64".to_string(), + // Collection types with generics + t if t.starts_with("list<") => { + let inner_type = &t[5..t.len() - 1]; + format!("Vec<{}>", wit_type_to_rust(inner_type)) + }, + t if t.starts_with("option<") => { + let inner_type = &t[7..t.len() - 1]; + format!("Option<{}>", wit_type_to_rust(inner_type)) + }, + t if t.starts_with("result<") => { + let inner_part = &t[7..t.len() - 1]; + if let Some(comma_pos) = inner_part.find(',') { + let ok_type = &inner_part[..comma_pos].trim(); + let err_type = &inner_part[comma_pos + 1..].trim(); + format!("Result<{}, {}>", wit_type_to_rust(ok_type), wit_type_to_rust(err_type)) + } else { + format!("Result<{}, ()>", wit_type_to_rust(inner_part)) + } + }, + t if t.starts_with("tuple<") => { + let inner_types = &t[6..t.len() - 1]; + let rust_types: Vec = inner_types + .split(", ") + .map(|t| wit_type_to_rust(t)) + .collect(); + format!("({})", rust_types.join(", ")) + }, + // Handle map type if present + t if t.starts_with("map<") => { + let inner_part = &t[4..t.len() - 1]; + if let Some(comma_pos) = inner_part.find(',') { + let key_type = &inner_part[..comma_pos].trim(); + let value_type = &inner_part[comma_pos + 1..].trim(); + format!("HashMap<{}, {}>", wit_type_to_rust(key_type), wit_type_to_rust(value_type)) + } else { + // Fallback for malformed map type + format!("HashMap", wit_type_to_rust(inner_part)) + } + }, + // Custom types (in kebab-case) need to be converted to PascalCase + _ => to_pascal_case(wit_type).to_string(), + } +} + +// Generate default value for Rust type - IMPROVED with additional types +fn generate_default_value(rust_type: &str) -> String { + match rust_type { + // Integer types + "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" | "isize" | "usize" => "0".to_string(), + // Floating point types + "f32" | "f64" => "0.0".to_string(), + // String types + "String" => "String::new()".to_string(), + "&str" => "\"\"".to_string(), + // Other primitive types + "bool" => "false".to_string(), + "char" => "'\\0'".to_string(), + "()" => "()".to_string(), + // Collection types + t if t.starts_with("Vec<") => "Vec::new()".to_string(), + t if t.starts_with("Option<") => "None".to_string(), + t if t.starts_with("Result<") => { + // For Result, default to Ok with the default value of the success type + if let Some(success_type_end) = t.find(',') { + let success_type = &t[7..success_type_end]; + format!("Ok({})", generate_default_value(success_type)) + } else { + "Ok(())".to_string() + } + }, + t if t.starts_with("HashMap<") => "HashMap::new()".to_string(), + t if t.starts_with("(") => { + // Generate default tuple with default values for each element + let inner_part = t.trim_start_matches('(').trim_end_matches(')'); + let parts: Vec<_> = inner_part.split(", ").collect(); + let default_values: Vec<_> = parts.iter() + .map(|part| generate_default_value(part)) + .collect(); + format!("({})", default_values.join(", ")) + }, + // For custom types, assume they implement Default + _ => format!("{}::default()", rust_type), + } +} + +// Structure to represent a field in a WIT signature struct +struct SignatureField { + name: String, + wit_type: String, +} + +// Structure to represent a WIT signature struct +struct SignatureStruct { + function_name: String, + attr_type: String, + fields: Vec, +} + +// Find all interface imports in the world WIT file +fn find_interfaces_in_world(api_dir: &Path) -> Result> { + let mut interfaces = Vec::new(); + + // Find world definition files + for entry in WalkDir::new(api_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { + if let Ok(content) = fs::read_to_string(path) { + if content.contains("world ") { + println!("Analyzing world definition file: {}", path.display()); + + // Extract import statements + for line in content.lines() { + let line = line.trim(); + if line.starts_with("import ") && line.ends_with(";") { + let interface = line + .trim_start_matches("import ") + .trim_end_matches(";") + .trim(); + + interfaces.push(interface.to_string()); + println!(" Found interface import: {}", interface); + } + } + } + } + } + } + + Ok(interfaces) +} + +// Parse WIT file to extract function signatures and type definitions +fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec)> { + println!("Parsing WIT file: {}", file_path.display()); + + let content = fs::read_to_string(file_path) + .with_context(|| format!("Failed to read WIT file: {}", file_path.display()))?; + + let mut signatures = Vec::new(); + let mut type_names = Vec::new(); + + // Simple parser for WIT files to extract record definitions and types + let lines: Vec<_> = content.lines().collect(); + let mut i = 0; + + while i < lines.len() { + let line = lines[i].trim(); + + // Look for record definitions that aren't signature structs + if line.starts_with("record ") && !line.contains("-signature-") { + let record_name = line.trim_start_matches("record ").trim_end_matches(" {").trim(); + println!(" Found type: record {}", record_name); + type_names.push(record_name.to_string()); + } + // Look for variant definitions (enums) + else if line.starts_with("variant ") { + let variant_name = line.trim_start_matches("variant ").trim_end_matches(" {").trim(); + println!(" Found type: variant {}", variant_name); + type_names.push(variant_name.to_string()); + } + // Look for signature record definitions + else if line.starts_with("record ") && line.contains("-signature-") { + let record_name = line.trim_start_matches("record ").trim_end_matches(" {").trim(); + println!(" Found record: {}", record_name); + + // Extract function name and attribute type + let parts: Vec<_> = record_name.split("-signature-").collect(); + if parts.len() != 2 { + println!(" Unexpected record name format"); + i += 1; + continue; + } + + let function_name = parts[0].to_string(); + let attr_type = parts[1].to_string(); + + // Parse fields + let mut fields = Vec::new(); + i += 1; + + while i < lines.len() && !lines[i].trim().starts_with("}") { + let field_line = lines[i].trim(); + + // Skip comments and empty lines + if field_line.starts_with("//") || field_line.is_empty() { + i += 1; + continue; + } + + // Parse field definition + let field_parts: Vec<_> = field_line.split(':').collect(); + if field_parts.len() == 2 { + let field_name = field_parts[0].trim().to_string(); + let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); + + println!(" Field: {} -> {}", field_name, field_type); + fields.push(SignatureField { + name: field_name, + wit_type: field_type, + }); + } + + i += 1; + } + + signatures.push(SignatureStruct { + function_name, + attr_type, + fields, + }); + } + + i += 1; + } + + println!("Extracted {} signature structs and {} type definitions from {}", + signatures.len(), type_names.len(), file_path.display()); + Ok((signatures, type_names)) +} + +// Generate a Rust async function from a signature struct +fn generate_async_function(signature: &SignatureStruct) -> String { + // Convert function name from kebab-case to snake_case + let snake_function_name = to_snake_case(&signature.function_name); + + // Get pascal case version for the JSON request format + let pascal_function_name = to_pascal_case(&signature.function_name); + + // Function full name with attribute type + let full_function_name = format!("{}_{}_rpc", snake_function_name, signature.attr_type); + + // Extract parameters and return type + let mut params = Vec::new(); + let mut param_names = Vec::new(); + let mut return_type = "()".to_string(); + let mut target_param = ""; + + for field in &signature.fields { + let field_name_snake = to_snake_case(&field.name); + let rust_type = wit_type_to_rust(&field.wit_type); + + if field.name == "target" { + if field.wit_type == "string" { + target_param = "&str"; + } else { + // Use hyperware_process_lib::Address instead of WitAddress + target_param = "&Address"; + } + } else if field.name == "returning" { + return_type = rust_type; + } else { + params.push(format!("{}: {}", field_name_snake, rust_type)); + param_names.push(field_name_snake); + } + } + + // First parameter is always target + let all_params = if target_param.is_empty() { + params.join(", ") + } else { + format!("target: {}{}", target_param, if params.is_empty() { "" } else { ", " }) + ¶ms.join(", ") + }; + + // Wrap the return type in SendResult + let wrapped_return_type = format!("SendResult<{}>", return_type); + + // For HTTP endpoints, generate commented-out implementation + if signature.attr_type == "http" { + let default_value = generate_default_value(&return_type); + + // Add underscore prefix to all parameters for HTTP stubs + let all_params_with_underscore = if target_param.is_empty() { + params.iter() + .map(|param| { + let parts: Vec<&str> = param.split(':').collect(); + if parts.len() == 2 { + format!("_{}: {}", parts[0], parts[1]) + } else { + format!("_{}", param) + } + }) + .collect::>() + .join(", ") + } else { + let target_with_underscore = format!("_target: {}", target_param); + if params.is_empty() { + target_with_underscore + } else { + let params_with_underscore = params.iter() + .map(|param| { + let parts: Vec<&str> = param.split(':').collect(); + if parts.len() == 2 { + format!("_{}: {}", parts[0], parts[1]) + } else { + format!("_{}", param) + } + }) + .collect::>() + .join(", "); + format!("{}, {}", target_with_underscore, params_with_underscore) + } + }; + + return format!( + "/// Generated stub for `{}` {} RPC call\n/// HTTP endpoint - uncomment to implement\n// pub async fn {}({}) -> {} {{\n// // TODO: Implement HTTP endpoint\n// SendResult::Success({})\n// }}", + signature.function_name, + signature.attr_type, + full_function_name, + all_params_with_underscore, + wrapped_return_type, + default_value + ); + } + + // Format JSON parameters correctly + let json_params = if param_names.is_empty() { + // No parameters case + format!("json!({{\"{}\" : {{}}}})", pascal_function_name) + } else if param_names.len() == 1 { + // Single parameter case + format!("json!({{\"{}\": {}}})", pascal_function_name, param_names[0]) + } else { + // Multiple parameters case - use tuple format + format!("json!({{\"{}\": ({})}})", + pascal_function_name, + param_names.join(", ")) + }; + + // Generate function with implementation using send + format!( + "/// Generated stub for `{}` {} RPC call\npub async fn {}({}) -> {} {{\n let request = {};\n send::<{}>(&request, target, 30).await\n}}", + signature.function_name, + signature.attr_type, + full_function_name, + all_params, + wrapped_return_type, + json_params, + return_type + ) +} + +// Create the caller-utils crate with a single lib.rs file +fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { + // Path to the new crate + let caller_utils_dir = base_dir.join("caller-utils"); + println!("Creating caller-utils crate at {}", caller_utils_dir.display()); + + // Create directories + fs::create_dir_all(&caller_utils_dir)?; + fs::create_dir_all(caller_utils_dir.join("src"))?; + println!("Created project directory structure"); + + // Create Cargo.toml with updated dependencies + let cargo_toml = r#"[package] +name = "caller-utils" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +anyhow = "1.0" +hyperware_process_lib = { version = "1.0.4", features = ["logging"] } +process_macros = "0.1.0" +futures-util = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro" } +once_cell = "1.20.2" +futures = "0.3" +uuid = { version = "1.0" } +wit-bindgen = "0.41.0" + +[lib] +crate-type = ["cdylib", "lib"] +"#; + + fs::write(caller_utils_dir.join("Cargo.toml"), cargo_toml) + .with_context(|| "Failed to write caller-utils Cargo.toml")?; + + println!("Created Cargo.toml for caller-utils"); + + // Get the world name (preferably the types- version) + let world_name = find_world_name(api_dir)?; + println!("Using world name for code generation: {}", world_name); + + // Get all interfaces from the world file + let interface_imports = find_interfaces_in_world(api_dir)?; + + // Store all types from each interface + let mut interface_types: HashMap> = HashMap::new(); + + // Find all WIT files in the api directory to generate stubs + let mut wit_files = Vec::new(); + for entry in WalkDir::new(api_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { + // Exclude world definition files + if let Ok(content) = fs::read_to_string(path) { + if !content.contains("world ") { + wit_files.push(path.to_path_buf()); + } + } + } + } + + println!("Found {} WIT interface files", wit_files.len()); + + // Generate content for each module and collect types + let mut module_contents = HashMap::::new(); + + for wit_file in &wit_files { + // Extract the interface name from the file name + let interface_name = wit_file.file_stem().unwrap().to_string_lossy(); + let snake_interface_name = to_snake_case(&interface_name); + + println!("Processing interface: {} -> {}", interface_name, snake_interface_name); + + // Parse the WIT file to extract signature structs and types + match parse_wit_file(wit_file) { + Ok((signatures, types)) => { + // Store types for this interface + interface_types.insert(interface_name.to_string(), types); + + if signatures.is_empty() { + println!("No signatures found in {}", wit_file.display()); + continue; + } + + // Generate module content + let mut mod_content = String::new(); + + // Add function implementations + for signature in &signatures { + let function_impl = generate_async_function(signature); + mod_content.push_str(&function_impl); + mod_content.push_str("\n\n"); + } + + // Store the module content + module_contents.insert(snake_interface_name, mod_content); + + println!("Generated module content with {} function stubs", signatures.len()); + }, + Err(e) => { + println!("Error parsing WIT file {}: {}", wit_file.display(), e); + } + } + } + + // Create import statements for each interface using "hyperware::process::{interface_name}::*" + // Use a HashSet to track which interfaces we've already processed to avoid duplicates + let mut processed_interfaces = std::collections::HashSet::new(); + let mut interface_use_statements = Vec::new(); + + for interface_name in &interface_imports { + // Convert to snake case for module name + let snake_interface_name = to_snake_case(interface_name); + + // Only add the import if we haven't processed this interface yet + if processed_interfaces.insert(snake_interface_name.clone()) { + // Create wildcard import for this interface + interface_use_statements.push( + format!("pub use crate::hyperware::process::{}::*;", snake_interface_name) + ); + } + } + + // Create single lib.rs with all modules inline + let mut lib_rs = String::new(); + + // Updated wit_bindgen usage with explicit world name - FIXED: Removed unused imports + lib_rs.push_str("wit_bindgen::generate!({\n"); + lib_rs.push_str(" path: \"target/wit\",\n"); + lib_rs.push_str(&format!(" world: \"{}\",\n", world_name)); + lib_rs.push_str(" generate_unused_types: true,\n"); + lib_rs.push_str(" additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],\n"); + lib_rs.push_str("});\n\n"); + + lib_rs.push_str("/// Generated caller utilities for RPC function stubs\n\n"); + + // Add global imports + lib_rs.push_str("pub use hyperware_app_common::SendResult;\n"); + lib_rs.push_str("pub use hyperware_app_common::send;\n"); + lib_rs.push_str("use hyperware_process_lib::Address;\n"); + lib_rs.push_str("use serde_json::json;\n\n"); + + // Add interface use statements + if !interface_use_statements.is_empty() { + lib_rs.push_str("// Import types from each interface\n"); + for use_stmt in interface_use_statements { + lib_rs.push_str(&format!("{}\n", use_stmt)); + } + lib_rs.push_str("\n"); + } + + // Add all modules with their content + for (module_name, module_content) in module_contents { + lib_rs.push_str(&format!("/// Generated RPC stubs for the {} interface\n", module_name)); + lib_rs.push_str(&format!("pub mod {} {{\n", module_name)); + lib_rs.push_str(" use crate::*;\n\n"); + lib_rs.push_str(&format!(" {}\n", module_content.replace("\n", "\n "))); + lib_rs.push_str("}\n\n"); + } + + // Write lib.rs + let lib_rs_path = caller_utils_dir.join("src").join("lib.rs"); + println!("Writing lib.rs to {}", lib_rs_path.display()); + + fs::write(&lib_rs_path, lib_rs) + .with_context(|| format!("Failed to write lib.rs: {}", lib_rs_path.display()))?; + + println!("Created single lib.rs file with all modules inline"); + + // Create target/wit directory and copy all WIT files + let target_wit_dir = caller_utils_dir.join("target").join("wit"); + println!("Creating directory: {}", target_wit_dir.display()); + + // Remove the directory if it exists to ensure clean state + if target_wit_dir.exists() { + println!("Removing existing target/wit directory"); + fs::remove_dir_all(&target_wit_dir)?; + } + + fs::create_dir_all(&target_wit_dir)?; + + // Copy all WIT files to target/wit + for entry in WalkDir::new(api_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { + let file_name = path.file_name().unwrap(); + let target_path = target_wit_dir.join(file_name); + fs::copy(path, &target_path) + .with_context(|| format!("Failed to copy {} to {}", path.display(), target_path.display()))?; + println!("Copied {} to target/wit directory", file_name.to_string_lossy()); + } + } + + Ok(()) +} + +// Update workspace Cargo.toml to include the caller-utils crate +fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { + let workspace_cargo_toml = base_dir.join("Cargo.toml"); + println!("Updating workspace Cargo.toml at {}", workspace_cargo_toml.display()); + + if !workspace_cargo_toml.exists() { + println!("Workspace Cargo.toml not found at {}", workspace_cargo_toml.display()); + return Ok(()); + } + + let content = fs::read_to_string(&workspace_cargo_toml) + .with_context(|| format!("Failed to read workspace Cargo.toml: {}", workspace_cargo_toml.display()))?; + + // Parse the TOML content + let mut parsed_toml: Value = content.parse() + .with_context(|| "Failed to parse workspace Cargo.toml")?; + + // Check if there's a workspace section + if let Some(workspace) = parsed_toml.get_mut("workspace") { + if let Some(members) = workspace.get_mut("members") { + if let Some(members_array) = members.as_array_mut() { + // Check if caller-utils is already in the members list + let caller_utils_exists = members_array.iter().any(|m| { + m.as_str().map_or(false, |s| s == "caller-utils") + }); + + if !caller_utils_exists { + println!("Adding caller-utils to workspace members"); + members_array.push(Value::String("caller-utils".to_string())); + + // Write back the updated TOML + let updated_content = toml::to_string_pretty(&parsed_toml) + .with_context(|| "Failed to serialize updated workspace Cargo.toml")?; + + fs::write(&workspace_cargo_toml, updated_content) + .with_context(|| format!("Failed to write updated workspace Cargo.toml: {}", workspace_cargo_toml.display()))?; + + println!("Successfully updated workspace Cargo.toml"); + } else { + println!("caller-utils is already in workspace members"); + } + } + } + } + + Ok(()) +} + +// Add caller-utils as a dependency to hyperware:process crates +fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { + for project_path in projects { + let cargo_toml_path = project_path.join("Cargo.toml"); + println!("Adding caller-utils dependency to {}", cargo_toml_path.display()); + + let content = fs::read_to_string(&cargo_toml_path) + .with_context(|| format!("Failed to read project Cargo.toml: {}", cargo_toml_path.display()))?; + + let mut parsed_toml: Value = content.parse() + .with_context(|| format!("Failed to parse project Cargo.toml: {}", cargo_toml_path.display()))?; + + // Add caller-utils to dependencies if not already present + if let Some(dependencies) = parsed_toml.get_mut("dependencies") { + if let Some(deps_table) = dependencies.as_table_mut() { + if !deps_table.contains_key("caller-utils") { + deps_table.insert( + "caller-utils".to_string(), + Value::Table({ + let mut t = toml::map::Map::new(); + t.insert("path".to_string(), Value::String("../caller-utils".to_string())); + t + }) + ); + + // Write back the updated TOML + let updated_content = toml::to_string_pretty(&parsed_toml) + .with_context(|| format!("Failed to serialize updated project Cargo.toml: {}", cargo_toml_path.display()))?; + + fs::write(&cargo_toml_path, updated_content) + .with_context(|| format!("Failed to write updated project Cargo.toml: {}", cargo_toml_path.display()))?; + + println!("Successfully added caller-utils dependency"); + } else { + println!("caller-utils dependency already exists"); + } + } + } + } + + Ok(()) +} + +// Create caller-utils crate and integrate with the workspace +pub fn create_caller_utils(base_dir: &Path, api_dir: &Path, projects: &[PathBuf]) -> Result<()> { + // Step 1: Create the caller-utils crate + create_caller_utils_crate(api_dir, base_dir)?; + + // Step 2: Update workspace Cargo.toml + update_workspace_cargo_toml(base_dir)?; + + // Step 3: Add caller-utils dependency to each hyperware:process project + add_caller_utils_to_projects(projects)?; + + Ok(()) +} \ No newline at end of file diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs new file mode 100644 index 00000000..e5ad6949 --- /dev/null +++ b/src/build/wit_generator.rs @@ -0,0 +1,945 @@ +use anyhow::{Context, Result}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use syn::{self, Attribute, ImplItem, Item, Type}; +use walkdir::WalkDir; +use toml::Value; + +// Helper functions for naming conventions +fn to_kebab_case(s: &str) -> String { + // First, handle the case where the input has underscores + if s.contains('_') { + return s.replace('_', "-"); + } + + let mut result = String::with_capacity(s.len() + 5); // Extra capacity for hyphens + let chars: Vec = s.chars().collect(); + + for (i, &c) in chars.iter().enumerate() { + if c.is_uppercase() { + // Add hyphen if: + // 1. Not the first character + // 2. Previous character is lowercase + // 3. Or next character is lowercase (to handle acronyms like HTML) + if i > 0 && + (chars[i-1].is_lowercase() || + (i < chars.len() - 1 && chars[i+1].is_lowercase())) + { + result.push('-'); + } + result.push(c.to_lowercase().next().unwrap()); + } else { + result.push(c); + } + } + + result +} + +// Validates a name doesn't contain numbers or "stream" +fn validate_name(name: &str, kind: &str) -> Result<()> { + // Check for numbers + if name.chars().any(|c| c.is_digit(10)) { + anyhow::bail!("Error: {} name '{}' contains numbers, which is not allowed", kind, name); + } + + // Check for "stream" + if name.to_lowercase().contains("stream") { + anyhow::bail!("Error: {} name '{}' contains 'stream', which is not allowed", kind, name); + } + + Ok(()) +} + +// Remove "State" suffix from a name +fn remove_state_suffix(name: &str) -> String { + if name.ends_with("State") { + let len = name.len(); + return name[0..len-5].to_string(); + } + name.to_string() +} + +// Extract wit_world from the #[hyperprocess] attribute using the format in the debug representation +fn extract_wit_world(attrs: &[Attribute]) -> Result { + for attr in attrs { + if attr.path().is_ident("hyperprocess") { + // Convert attribute to string representation + let attr_str = format!("{:?}", attr); + println!("Attribute string: {}", attr_str); + + // Look for wit_world in the attribute string + if let Some(pos) = attr_str.find("wit_world") { + println!("Found wit_world at position {}", pos); + + // Find the literal value after wit_world by looking for lit: "value" + let lit_pattern = "lit: \""; + if let Some(lit_pos) = attr_str[pos..].find(lit_pattern) { + let start_pos = pos + lit_pos + lit_pattern.len(); + + // Find the closing quote of the literal + if let Some(quote_pos) = attr_str[start_pos..].find('\"') { + let world_name = &attr_str[start_pos..(start_pos + quote_pos)]; + println!("Extracted wit_world: {}", world_name); + return Ok(world_name.to_string()); + } + } + } + } + } + anyhow::bail!("wit_world not found in hyperprocess attribute") +} + +// Convert Rust type to WIT type, including downstream types +fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { + match ty { + Type::Path(type_path) => { + if type_path.path.segments.is_empty() { + return Ok("unknown".to_string()); + } + + let ident = &type_path.path.segments.last().unwrap().ident; + let type_name = ident.to_string(); + + match type_name.as_str() { + "i32" => Ok("s32".to_string()), + "u32" => Ok("u32".to_string()), + "i64" => Ok("s64".to_string()), + "u64" => Ok("u64".to_string()), + "f32" => Ok("f32".to_string()), + "f64" => Ok("f64".to_string()), + "String" => Ok("string".to_string()), + "bool" => Ok("bool".to_string()), + "Vec" => { + if let syn::PathArguments::AngleBracketed(args) = + &type_path.path.segments.last().unwrap().arguments + { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + let inner_type = rust_type_to_wit(inner_ty, used_types)?; + Ok(format!("list<{}>", inner_type)) + } else { + Ok("list".to_string()) + } + } else { + Ok("list".to_string()) + } + } + "Option" => { + if let syn::PathArguments::AngleBracketed(args) = + &type_path.path.segments.last().unwrap().arguments + { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + let inner_type = rust_type_to_wit(inner_ty, used_types)?; + Ok(format!("option<{}>", inner_type)) + } else { + Ok("option".to_string()) + } + } else { + Ok("option".to_string()) + } + } + "HashMap" | "BTreeMap" => { + if let syn::PathArguments::AngleBracketed(args) = + &type_path.path.segments.last().unwrap().arguments + { + if args.args.len() >= 2 { + if let (Some(syn::GenericArgument::Type(key_ty)), Some(syn::GenericArgument::Type(val_ty))) = + (args.args.first(), args.args.get(1)) { + let key_type = rust_type_to_wit(key_ty, used_types)?; + let val_type = rust_type_to_wit(val_ty, used_types)?; + // For HashMaps, we'll generate a list of tuples where each tuple contains a key and value + Ok(format!("list>", key_type, val_type)) + } else { + Ok("list>".to_string()) + } + } else { + Ok("list>".to_string()) + } + } else { + Ok("list>".to_string()) + } + } + custom => { + // Validate custom type name + validate_name(custom, "Type")?; + + // Convert custom type to kebab-case and add to used types + let kebab_custom = to_kebab_case(custom); + used_types.insert(kebab_custom.clone()); + Ok(kebab_custom) + } + } + } + Type::Reference(type_ref) => { + // Handle references by using the underlying type + rust_type_to_wit(&type_ref.elem, used_types) + } + Type::Tuple(type_tuple) => { + if type_tuple.elems.is_empty() { + // Empty tuple is unit in WIT + Ok("unit".to_string()) + } else { + // Create a tuple representation in WIT + let mut elem_types = Vec::new(); + for elem in &type_tuple.elems { + elem_types.push(rust_type_to_wit(elem, used_types)?); + } + Ok(format!("tuple<{}>", elem_types.join(", "))) + } + } + _ => Ok("unknown".to_string()), + } +} + +// Find all Rust files in a crate directory +fn find_rust_files(crate_path: &Path) -> Vec { + let mut rust_files = Vec::new(); + let src_dir = crate_path.join("src"); + + println!("Finding Rust files in {}", src_dir.display()); + + if !src_dir.exists() || !src_dir.is_dir() { + println!("No src directory found at {}", src_dir.display()); + return rust_files; + } + + for entry in WalkDir::new(src_dir) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + println!("Found Rust file: {}", path.display()); + rust_files.push(path.to_path_buf()); + } + } + + println!("Found {} Rust files", rust_files.len()); + rust_files +} + +// Collect type definitions (structs and enums) from a file +fn collect_type_definitions_from_file(file_path: &Path) -> Result> { + println!("Collecting type definitions from file: {}", file_path.display()); + + let content = fs::read_to_string(file_path) + .with_context(|| format!("Failed to read file: {}", file_path.display()))?; + + let ast = syn::parse_file(&content) + .with_context(|| format!("Failed to parse file: {}", file_path.display()))?; + + let mut type_defs = HashMap::new(); + + for item in &ast.items { + match item { + Item::Struct(item_struct) => { + // Validate struct name doesn't contain numbers or "stream" + let orig_name = item_struct.ident.to_string(); + + // Skip trying to validate if name contains "__" as these are likely internal types + if orig_name.contains("__") { + println!(" Skipping likely internal struct: {}", orig_name); + continue; + } + + match validate_name(&orig_name, "Struct") { + Ok(_) => { + // Use kebab-case for struct name + let name = to_kebab_case(&orig_name); + println!(" Found struct: {} -> {}", orig_name, name); + + let fields: Vec = match &item_struct.fields { + syn::Fields::Named(fields) => { + let mut used_types = HashSet::new(); + let mut field_strings = Vec::new(); + + for f in &fields.named { + if let Some(field_ident) = &f.ident { + // Validate field name doesn't contain digits + let field_orig_name = field_ident.to_string(); + + match validate_name(&field_orig_name, "Field") { + Ok(_) => { + // Convert field names to kebab-case + let field_name = to_kebab_case(&field_orig_name); + + // Skip if field conversion failed + if field_name.is_empty() { + println!(" Skipping field with empty name conversion"); + continue; + } + + let field_type = match rust_type_to_wit(&f.ty, &mut used_types) { + Ok(ty) => ty, + Err(e) => { + println!(" Error converting field type: {}", e); + "unknown".to_string() + } + }; + + println!(" Field: {} -> {}", field_name, field_type); + field_strings.push(format!(" {}: {}", field_name, field_type)); + }, + Err(e) => { + println!(" Skipping field with invalid name: {}", e); + continue; + } + } + } + } + + field_strings + } + _ => Vec::new(), + }; + + if !fields.is_empty() { + type_defs.insert( + name.clone(), + format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), + ); + } + }, + Err(e) => { + println!(" Skipping struct with invalid name: {}", e); + continue; + } + } + } + Item::Enum(item_enum) => { + // Validate enum name doesn't contain numbers or "stream" + let orig_name = item_enum.ident.to_string(); + + // Skip trying to validate if name contains "__" as these are likely internal types + if orig_name.contains("__") { + println!(" Skipping likely internal enum: {}", orig_name); + continue; + } + + match validate_name(&orig_name, "Enum") { + Ok(_) => { + // Use kebab-case for enum name + let name = to_kebab_case(&orig_name); + println!(" Found enum: {} -> {}", orig_name, name); + + let mut variants = Vec::new(); + let mut skip_enum = false; + + for v in &item_enum.variants { + let variant_orig_name = v.ident.to_string(); + + // Validate variant name + match validate_name(&variant_orig_name, "Enum variant") { + Ok(_) => { + match &v.fields { + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + let mut used_types = HashSet::new(); + + match rust_type_to_wit( + &fields.unnamed.first().unwrap().ty, + &mut used_types + ) { + Ok(ty) => { + // Use kebab-case for variant names and use parentheses for type + let variant_name = to_kebab_case(&variant_orig_name); + println!(" Variant: {} -> {}({})", variant_orig_name, variant_name, ty); + variants.push(format!(" {}({})", variant_name, ty)); + }, + Err(e) => { + println!(" Error converting variant type: {}", e); + skip_enum = true; + break; + } + } + } + syn::Fields::Unit => { + // Use kebab-case for variant names + let variant_name = to_kebab_case(&variant_orig_name); + println!(" Variant: {} -> {}", variant_orig_name, variant_name); + variants.push(format!(" {}", variant_name)); + }, + _ => { + println!(" Skipping complex variant: {}", variant_orig_name); + // Complex variants with multiple fields aren't directly supported in WIT + // For simplicity, we'll skip enums with complex variants + skip_enum = true; + break; + }, + } + }, + Err(e) => { + println!(" Skipping variant with invalid name: {}", e); + skip_enum = true; + break; + } + } + } + + if !skip_enum && !variants.is_empty() { + type_defs.insert( + name.clone(), + format!(" variant {} {{\n{}\n }}", name, variants.join(",\n")), + ); + } + }, + Err(e) => { + println!(" Skipping enum with invalid name: {}", e); + continue; + } + } + } + _ => {} + } + } + + println!("Collected {} type definitions from file", type_defs.len()); + Ok(type_defs) +} + +// Find all relevant Rust projects +fn find_rust_projects(base_dir: &Path) -> Vec { + let mut projects = Vec::new(); + println!("Scanning for Rust projects in {}", base_dir.display()); + + for entry in WalkDir::new(base_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + + if path.is_dir() && path != base_dir { + let cargo_toml = path.join("Cargo.toml"); + println!("Checking {}", cargo_toml.display()); + + if cargo_toml.exists() { + // Try to read and parse Cargo.toml + if let Ok(content) = fs::read_to_string(&cargo_toml) { + if let Ok(cargo_data) = content.parse::() { + // Check for the specific metadata + if let Some(metadata) = cargo_data + .get("package") + .and_then(|p| p.get("metadata")) + .and_then(|m| m.get("component")) + { + if let Some(package) = metadata.get("package") { + if let Some(package_str) = package.as_str() { + println!(" Found package.metadata.component.package = {:?}", package_str); + if package_str == "hyperware:process" { + println!(" Adding project: {}", path.display()); + projects.push(path.to_path_buf()); + } + } + } + } else { + println!(" No package.metadata.component metadata found"); + } + } + } + } + } + } + + println!("Found {} relevant Rust projects", projects.len()); + projects +} + +// Helper function to generate signature struct for specific attribute type +fn generate_signature_struct( + kebab_name: &str, + attr_type: &str, + method: &syn::ImplItemFn, + used_types: &mut HashSet, +) -> Result { + // Create signature struct name with attribute type + let signature_struct_name = format!("{}-signature-{}", kebab_name, attr_type); + + // Generate comment for this specific function + let comment = format!(" // Function signature for: {} ({})", kebab_name, attr_type); + + // Create struct fields that directly represent function parameters + let mut struct_fields = Vec::new(); + + // Add target parameter based on attribute type + if attr_type == "http" { + struct_fields.push(" target: string".to_string()); + } else { // remote or local + struct_fields.push(" target: address".to_string()); + } + + // Process function parameters (skip &self and &mut self) + for arg in &method.sig.inputs { + if let syn::FnArg::Typed(pat_type) = arg { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + // Skip &self and &mut self + if pat_ident.ident == "self" { + continue; + } + + // Get original param name and convert to kebab-case + let param_orig_name = pat_ident.ident.to_string(); + + // Validate parameter name + match validate_name(¶m_orig_name, "Parameter") { + Ok(_) => { + let param_name = to_kebab_case(¶m_orig_name); + + // Rust type to WIT type + match rust_type_to_wit(&pat_type.ty, used_types) { + Ok(param_type) => { + // Add field directly to the struct + struct_fields.push(format!(" {}: {}", param_name, param_type)); + }, + Err(e) => { + println!(" Error converting parameter type: {}", e); + // Use a placeholder type for this parameter + struct_fields.push(format!(" {}: unknown", param_name)); + } + } + }, + Err(e) => { + println!(" Skipping parameter with invalid name: {}", e); + // Use a placeholder for invalid parameter names + struct_fields.push(" invalid-param: unknown".to_string()); + } + } + } + } + } + + // Add return type field + match &method.sig.output { + syn::ReturnType::Type(_, ty) => { + match rust_type_to_wit(&*ty, used_types) { + Ok(return_type) => { + struct_fields.push(format!(" returning: {}", return_type)); + }, + Err(e) => { + println!(" Error converting return type: {}", e); + struct_fields.push(" returning: unknown".to_string()); + } + } + } + _ => { + // For unit return type + struct_fields.push(" returning: unit".to_string()); + } + } + + // Combine everything into a record definition + let record_def = format!( + "{}\n record {} {{\n{}\n }}", + comment, + signature_struct_name, + struct_fields.join(",\n") + ); + + Ok(record_def) +} + +// Helper trait to get TypePath from Type +trait AsTypePath { + fn as_type_path(&self) -> Option<&syn::TypePath>; +} + +impl AsTypePath for syn::Type { + fn as_type_path(&self) -> Option<&syn::TypePath> { + match self { + syn::Type::Path(tp) => Some(tp), + _ => None, + } + } +} + +// Process a single Rust project and generate WIT files +fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { + println!("\nProcessing project: {}", project_path.display()); + + // Find lib.rs for this project + let lib_rs = project_path.join("src").join("lib.rs"); + + if !lib_rs.exists() { + println!("No lib.rs found for project: {}", project_path.display()); + return Ok(None); + } + + // Find all Rust files in the project + let rust_files = find_rust_files(project_path); + + // Collect all type definitions from all Rust files + let mut all_type_defs = HashMap::new(); + for file_path in &rust_files { + match collect_type_definitions_from_file(file_path) { + Ok(file_type_defs) => { + for (name, def) in file_type_defs { + all_type_defs.insert(name, def); + } + }, + Err(e) => { + println!("Error collecting type definitions from {}: {}", file_path.display(), e); + // Continue with other files + } + } + } + + println!("Collected {} total type definitions", all_type_defs.len()); + + // Parse lib.rs to find the hyperprocess attribute and interface details + let lib_content = fs::read_to_string(&lib_rs) + .with_context(|| format!("Failed to read lib.rs for project: {}", project_path.display()))?; + + let ast = syn::parse_file(&lib_content) + .with_context(|| format!("Failed to parse lib.rs for project: {}", project_path.display()))?; + + let mut wit_world = None; + let mut interface_name = None; + let mut kebab_interface_name = None; + let mut impl_item_with_hyperprocess = None; + + println!("Scanning for impl blocks with hyperprocess attribute"); + for item in &ast.items { + if let Item::Impl(impl_item) = item { + // Check if this impl block has a #[hyperprocess] attribute + if let Some(attr) = impl_item.attrs.iter().find(|attr| attr.path().is_ident("hyperprocess")) { + println!("Found hyperprocess attribute"); + + // Extract the wit_world name + match extract_wit_world(&[attr.clone()]) { + Ok(world_name) => { + println!("Extracted wit_world: {}", world_name); + wit_world = Some(world_name); + + // Get the interface name from the impl type + interface_name = impl_item + .self_ty + .as_ref() + .as_type_path() + .map(|tp| { + if let Some(last_segment) = tp.path.segments.last() { + last_segment.ident.to_string() + } else { + "Unknown".to_string() + } + }); + + // Check for "State" suffix and remove it + if let Some(ref name) = interface_name { + // Validate the interface name + if let Err(e) = validate_name(name, "Interface") { + println!("Interface name validation failed: {}", e); + continue; + } + + // Remove State suffix if present + let base_name = remove_state_suffix(name); + + // Convert to kebab-case for file name and interface name + kebab_interface_name = Some(to_kebab_case(&base_name)); + + println!("Interface name: {:?}", interface_name); + println!("Base name: {}", base_name); + println!("Kebab interface name: {:?}", kebab_interface_name); + + // Save the impl item for later processing + impl_item_with_hyperprocess = Some(impl_item.clone()); + } + }, + Err(e) => println!("Failed to extract wit_world: {}", e), + } + } + } + } + + // Now generate the WIT content for the interface + if let (Some(ref iface_name), Some(ref kebab_name), Some(ref impl_item)) = + (&interface_name, &kebab_interface_name, &impl_item_with_hyperprocess) { + let mut signature_structs = Vec::new(); + let mut used_types = HashSet::new(); + + for item in &impl_item.items { + if let ImplItem::Fn(method) = item { + let method_name = method.sig.ident.to_string(); + println!(" Examining method: {}", method_name); + + // Check for attribute types + let has_remote = method.attrs.iter().any(|attr| attr.path().is_ident("remote")); + let has_local = method.attrs.iter().any(|attr| attr.path().is_ident("local")); + let has_http = method.attrs.iter().any(|attr| attr.path().is_ident("http")); + + if has_remote || has_local || has_http { + println!(" Has relevant attributes: remote={}, local={}, http={}", + has_remote, has_local, has_http); + + // Validate function name + match validate_name(&method_name, "Function") { + Ok(_) => { + // Convert function name to kebab-case + let kebab_name = to_kebab_case(&method_name); + println!(" Processing method: {} -> {}", method_name, kebab_name); + + // Generate a signature struct for each attribute type + if has_remote { + match generate_signature_struct(&kebab_name, "remote", method, &mut used_types) { + Ok(remote_struct) => signature_structs.push(remote_struct), + Err(e) => println!(" Error generating remote signature struct: {}", e), + } + } + + if has_local { + match generate_signature_struct(&kebab_name, "local", method, &mut used_types) { + Ok(local_struct) => signature_structs.push(local_struct), + Err(e) => println!(" Error generating local signature struct: {}", e), + } + } + + if has_http { + match generate_signature_struct(&kebab_name, "http", method, &mut used_types) { + Ok(http_struct) => signature_structs.push(http_struct), + Err(e) => println!(" Error generating HTTP signature struct: {}", e), + } + } + }, + Err(e) => { + println!(" Skipping method with invalid name: {}", e); + } + } + } else { + println!(" Skipping method without relevant attributes"); + } + } + } + + // Include all defined types, not just the ones used in interface functions + println!("Including all defined types ({})", all_type_defs.len()); + + // Convert all type definitions to a vector + let mut type_defs: Vec = all_type_defs.values().cloned().collect(); + + // Sort them for consistent output + type_defs.sort(); + + // Generate the final WIT content + if signature_structs.is_empty() { + println!("No functions found for interface {}", iface_name); + } else { + // Start with the interface comment + let mut content = " // This interface contains function signature definitions that will be used\n // by the hyper-bindgen macro to generate async function bindings.\n //\n // NOTE: This is currently a hacky workaround since WIT async functions are not\n // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware,\n // we should switch to using proper async WIT function signatures instead of\n // this struct-based approach with hyper-bindgen generating the async stubs.\n".to_string(); + + // Add standard imports + content.push_str("\n use standard.{address};\n\n"); + + // Add type definitions if any + if !type_defs.is_empty() { + content.push_str(&type_defs.join("\n\n")); + content.push_str("\n\n"); + } + + // Add signature structs + content.push_str(&signature_structs.join("\n\n")); + + // Wrap in interface block + let final_content = format!("interface {} {{\n{}\n}}\n", kebab_name, content); + println!("Generated interface content for {} with {} signature structs", iface_name, signature_structs.len()); + + // Write the interface file with kebab-case name + let interface_file = api_dir.join(format!("{}.wit", kebab_name)); + println!("Writing WIT file to {}", interface_file.display()); + + fs::write(&interface_file, &final_content) + .with_context(|| format!("Failed to write {}", interface_file.display()))?; + + println!("Successfully wrote WIT file"); + } + } + + if let (Some(_), Some(_), Some(kebab_iface)) = (wit_world, interface_name, kebab_interface_name) { + println!("Returning import statement for interface {}", kebab_iface); + // Use kebab-case interface name for import + Ok(Some(format!(" import {};", kebab_iface))) + } else { + println!("No valid interface found"); + Ok(None) + } +} + +// Generate WIT files from Rust code +pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec, Vec)> { + // Find all relevant Rust projects + let projects = find_rust_projects(base_dir); + let mut processed_projects = Vec::new(); + + if projects.is_empty() { + println!("No relevant Rust projects found."); + return Ok((Vec::new(), Vec::new())); + } + + // Process each project and collect world imports + let mut new_imports = Vec::new(); + let mut interfaces = Vec::new(); + + for project_path in &projects { + println!("Processing project: {}", project_path.display()); + + match process_rust_project(project_path, api_dir) { + Ok(Some(import)) => { + println!("Got import statement: {}", import); + new_imports.push(import.clone()); + + // Extract interface name from import statement + let interface_name = import + .trim_start_matches(" import ") + .trim_end_matches(";") + .to_string(); + + interfaces.push(interface_name); + processed_projects.push(project_path.clone()); + }, + Ok(None) => println!("No import statement generated"), + Err(e) => println!("Error processing project: {}", e), + } + } + + println!("Collected {} new imports", new_imports.len()); + + // Check for existing world definition files and update them + println!("Looking for existing world definition files"); + let mut updated_world = false; + + for entry in WalkDir::new(api_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { + println!("Checking WIT file: {}", path.display()); + + if let Ok(content) = fs::read_to_string(path) { + if content.contains("world ") { + println!("Found world definition file"); + + // Extract the world name and existing imports + let lines: Vec<&str> = content.lines().collect(); + let mut world_name = None; + let mut existing_imports = Vec::new(); + let mut include_line = " include process-v1;".to_string(); + + for line in &lines { + let trimmed = line.trim(); + + if trimmed.starts_with("world ") { + if let Some(name) = trimmed.split_whitespace().nth(1) { + world_name = Some(name.trim_end_matches(" {").to_string()); + } + } else if trimmed.starts_with("import ") { + existing_imports.push(trimmed.to_string()); + } else if trimmed.starts_with("include ") { + include_line = trimmed.to_string(); + } + } + + if let Some(world_name) = world_name { + println!("Extracted world name: {}", world_name); + + // Determine the include line based on world name + // If world name starts with "types-", use "include lib;" instead + if world_name.starts_with("types-") { + include_line = " include lib;".to_string(); + } else { + // Keep existing include or default to process-v1 + if !include_line.contains("include ") { + include_line = " include process-v1;".to_string(); + } + } + + // Combine existing imports with new imports + let mut all_imports = existing_imports.clone(); + + for import in &new_imports { + let import_stmt = import.trim(); + if !all_imports.iter().any(|i| i.trim() == import_stmt) { + all_imports.push(import_stmt.to_string()); + } + } + + // Make sure all imports have proper indentation + let all_imports_with_indent: Vec = all_imports + .iter() + .map(|import| { + if import.starts_with(" ") { + import.clone() + } else { + format!(" {}", import.trim()) + } + }) + .collect(); + + let imports_section = all_imports_with_indent.join("\n"); + + // Create updated world content with proper indentation + let world_content = format!( + "world {} {{\n{}\n {}\n}}", + world_name, + imports_section, + include_line.trim() + ); + + println!("Writing updated world definition to {}", path.display()); + // Write the updated world file + fs::write(path, world_content) + .with_context(|| format!("Failed to write updated world file: {}", path.display()))?; + + println!("Successfully updated world definition"); + updated_world = true; + } + } + } + } + } + + // If no world definitions were found, create a default one + if !updated_world && !new_imports.is_empty() { + // Define default world name + let default_world = "async-app-template-dot-os-v0"; + println!("No existing world definitions found, creating default with name: {}", default_world); + + // Create world content with process-v1 include and proper indentation for imports + let imports_with_indent: Vec = new_imports + .iter() + .map(|import| { + if import.starts_with(" ") { + import.clone() + } else { + format!(" {}", import.trim()) + } + }) + .collect(); + + // Determine include based on world name + let include_line = if default_world.starts_with("types-") { + "include lib;" + } else { + "include process-v1;" + }; + + let world_content = format!( + "world {} {{\n{}\n {}\n}}", + default_world, + imports_with_indent.join("\n"), + include_line + ); + + let world_file = api_dir.join(format!("{}.wit", default_world)); + println!("Writing default world definition to {}", world_file.display()); + + fs::write(&world_file, world_content) + .with_context(|| format!("Failed to write default world file: {}", world_file.display()))?; + + println!("Successfully created default world definition"); + } + + println!("WIT files generated successfully in the 'api' directory."); + Ok((processed_projects, interfaces)) +} \ No newline at end of file From 302aef0e0b8e2c7c9aca897e9b6baabc93228e98 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 3 Apr 2025 17:21:16 -0700 Subject: [PATCH 02/88] build: add minimal use of wit_generator & get it compiling --- Cargo.toml | 3 +- src/build/mod.rs | 14 + src/build/wit_generator.rs | 550 ++++++++++++++++++++------------- src/build_start_package/mod.rs | 2 + src/main.rs | 18 ++ src/run_tests/mod.rs | 4 + 6 files changed, 375 insertions(+), 216 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88c31954..7c6f1998 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,8 +52,7 @@ semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10.8" -syn = { version = "2.0", features = ["full", "visit", "extra-traits"] } -#syn = { version = "2.0", features = ["full", "visit"] } +syn = { version = "2.0", features = ["full", "visit", "parsing", "extra-traits"] } thiserror = "1.0" tokio = { version = "1.28", features = [ "macros", diff --git a/src/build/mod.rs b/src/build/mod.rs index 9e2382e3..b8059fc7 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -32,6 +32,8 @@ use crate::KIT_CACHE; mod rewrite; use rewrite::copy_and_rewrite_package; +mod wit_generator; + const PY_VENV_NAME: &str = "process_env"; const JAVASCRIPT_SRC_PATH: &str = "src/lib.js"; const PYTHON_SRC_PATH: &str = "src/lib.py"; @@ -1161,6 +1163,7 @@ async fn fetch_dependencies( include: &HashSet, exclude: &HashSet, rewrite: bool, + hyperapp: bool, force: bool, verbose: bool, ) -> Result<()> { @@ -1178,6 +1181,7 @@ async fn fetch_dependencies( vec![], // TODO: what about deps-of-deps? vec![], rewrite, + hyperapp, false, force, verbose, @@ -1215,6 +1219,7 @@ async fn fetch_dependencies( local_dep_deps, vec![], rewrite, + hyperapp, false, force, verbose, @@ -1531,6 +1536,7 @@ async fn compile_package( include: &HashSet, exclude: &HashSet, rewrite: bool, + hyperapp: bool, force: bool, verbose: bool, ignore_deps: bool, // for internal use; may cause problems when adding recursive deps @@ -1554,6 +1560,7 @@ async fn compile_package( include, exclude, rewrite, + hyperapp, force, verbose, ) @@ -1661,6 +1668,7 @@ pub async fn execute( local_dependencies: Vec, add_paths_to_api: Vec, rewrite: bool, + hyperapp: bool, reproducible: bool, force: bool, verbose: bool, @@ -1753,6 +1761,11 @@ pub async fn execute( copy_and_rewrite_package(package_dir)? }; + if hyperapp { + let api_dir = live_dir.join("api"); + let (_processed_projects, _interfaces) = wit_generator::generate_wit_files(&live_dir, &api_dir)?; + } + let ui_dirs = get_ui_dirs(&live_dir, &include, &exclude)?; if !no_ui && !ui_dirs.is_empty() { if !skip_deps_check { @@ -1779,6 +1792,7 @@ pub async fn execute( &include, &exclude, rewrite, + hyperapp, force, verbose, ignore_deps, diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index e5ad6949..43fffb6e 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1,10 +1,15 @@ -use anyhow::{Context, Result}; +//use anyhow::{Context}; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; + +use color_eyre::{ + eyre::{bail, WrapErr}, + Result, +}; use syn::{self, Attribute, ImplItem, Item, Type}; -use walkdir::WalkDir; use toml::Value; +use walkdir::WalkDir; // Helper functions for naming conventions fn to_kebab_case(s: &str) -> String { @@ -12,19 +17,19 @@ fn to_kebab_case(s: &str) -> String { if s.contains('_') { return s.replace('_', "-"); } - + let mut result = String::with_capacity(s.len() + 5); // Extra capacity for hyphens let chars: Vec = s.chars().collect(); - + for (i, &c) in chars.iter().enumerate() { if c.is_uppercase() { // Add hyphen if: // 1. Not the first character // 2. Previous character is lowercase // 3. Or next character is lowercase (to handle acronyms like HTML) - if i > 0 && - (chars[i-1].is_lowercase() || - (i < chars.len() - 1 && chars[i+1].is_lowercase())) + if i > 0 + && (chars[i - 1].is_lowercase() + || (i < chars.len() - 1 && chars[i + 1].is_lowercase())) { result.push('-'); } @@ -33,7 +38,7 @@ fn to_kebab_case(s: &str) -> String { result.push(c); } } - + result } @@ -41,14 +46,22 @@ fn to_kebab_case(s: &str) -> String { fn validate_name(name: &str, kind: &str) -> Result<()> { // Check for numbers if name.chars().any(|c| c.is_digit(10)) { - anyhow::bail!("Error: {} name '{}' contains numbers, which is not allowed", kind, name); + bail!( + "Error: {} name '{}' contains numbers, which is not allowed", + kind, + name + ); } - + // Check for "stream" if name.to_lowercase().contains("stream") { - anyhow::bail!("Error: {} name '{}' contains 'stream', which is not allowed", kind, name); + bail!( + "Error: {} name '{}' contains 'stream', which is not allowed", + kind, + name + ); } - + Ok(()) } @@ -56,7 +69,7 @@ fn validate_name(name: &str, kind: &str) -> Result<()> { fn remove_state_suffix(name: &str) -> String { if name.ends_with("State") { let len = name.len(); - return name[0..len-5].to_string(); + return name[0..len - 5].to_string(); } name.to_string() } @@ -68,16 +81,16 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { // Convert attribute to string representation let attr_str = format!("{:?}", attr); println!("Attribute string: {}", attr_str); - + // Look for wit_world in the attribute string if let Some(pos) = attr_str.find("wit_world") { println!("Found wit_world at position {}", pos); - + // Find the literal value after wit_world by looking for lit: "value" let lit_pattern = "lit: \""; if let Some(lit_pos) = attr_str[pos..].find(lit_pattern) { let start_pos = pos + lit_pos + lit_pattern.len(); - + // Find the closing quote of the literal if let Some(quote_pos) = attr_str[start_pos..].find('\"') { let world_name = &attr_str[start_pos..(start_pos + quote_pos)]; @@ -88,7 +101,7 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { } } } - anyhow::bail!("wit_world not found in hyperprocess attribute") + bail!("wit_world not found in hyperprocess attribute") } // Convert Rust type to WIT type, including downstream types @@ -98,10 +111,10 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Ok("s32".to_string()), "u32" => Ok("u32".to_string()), @@ -112,7 +125,7 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Ok("string".to_string()), "bool" => Ok("bool".to_string()), "Vec" => { - if let syn::PathArguments::AngleBracketed(args) = + if let syn::PathArguments::AngleBracketed(args) = &type_path.path.segments.last().unwrap().arguments { if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { @@ -144,8 +157,11 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result= 2 { - if let (Some(syn::GenericArgument::Type(key_ty)), Some(syn::GenericArgument::Type(val_ty))) = - (args.args.first(), args.args.get(1)) { + if let ( + Some(syn::GenericArgument::Type(key_ty)), + Some(syn::GenericArgument::Type(val_ty)), + ) = (args.args.first(), args.args.get(1)) + { let key_type = rust_type_to_wit(key_ty, used_types)?; let val_type = rust_type_to_wit(val_ty, used_types)?; // For HashMaps, we'll generate a list of tuples where each tuple contains a key and value @@ -163,7 +179,7 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { // Validate custom type name validate_name(custom, "Type")?; - + // Convert custom type to kebab-case and add to used types let kebab_custom = to_kebab_case(custom); used_types.insert(kebab_custom.clone()); @@ -196,111 +212,126 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Vec { let mut rust_files = Vec::new(); let src_dir = crate_path.join("src"); - + println!("Finding Rust files in {}", src_dir.display()); - + if !src_dir.exists() || !src_dir.is_dir() { println!("No src directory found at {}", src_dir.display()); return rust_files; } - - for entry in WalkDir::new(src_dir) - .into_iter() - .filter_map(Result::ok) - { + + for entry in WalkDir::new(src_dir).into_iter().filter_map(Result::ok) { let path = entry.path(); if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { println!("Found Rust file: {}", path.display()); rust_files.push(path.to_path_buf()); } } - + println!("Found {} Rust files", rust_files.len()); rust_files } // Collect type definitions (structs and enums) from a file fn collect_type_definitions_from_file(file_path: &Path) -> Result> { - println!("Collecting type definitions from file: {}", file_path.display()); - + println!( + "Collecting type definitions from file: {}", + file_path.display() + ); + let content = fs::read_to_string(file_path) .with_context(|| format!("Failed to read file: {}", file_path.display()))?; - + let ast = syn::parse_file(&content) .with_context(|| format!("Failed to parse file: {}", file_path.display()))?; - + let mut type_defs = HashMap::new(); - + for item in &ast.items { match item { Item::Struct(item_struct) => { // Validate struct name doesn't contain numbers or "stream" let orig_name = item_struct.ident.to_string(); - + // Skip trying to validate if name contains "__" as these are likely internal types if orig_name.contains("__") { println!(" Skipping likely internal struct: {}", orig_name); continue; } - + match validate_name(&orig_name, "Struct") { Ok(_) => { // Use kebab-case for struct name let name = to_kebab_case(&orig_name); println!(" Found struct: {} -> {}", orig_name, name); - + let fields: Vec = match &item_struct.fields { syn::Fields::Named(fields) => { let mut used_types = HashSet::new(); let mut field_strings = Vec::new(); - + for f in &fields.named { if let Some(field_ident) = &f.ident { // Validate field name doesn't contain digits let field_orig_name = field_ident.to_string(); - + match validate_name(&field_orig_name, "Field") { Ok(_) => { // Convert field names to kebab-case let field_name = to_kebab_case(&field_orig_name); - + // Skip if field conversion failed if field_name.is_empty() { println!(" Skipping field with empty name conversion"); continue; } - - let field_type = match rust_type_to_wit(&f.ty, &mut used_types) { + + let field_type = match rust_type_to_wit( + &f.ty, + &mut used_types, + ) { Ok(ty) => ty, Err(e) => { - println!(" Error converting field type: {}", e); + println!( + " Error converting field type: {}", + e + ); "unknown".to_string() } }; - - println!(" Field: {} -> {}", field_name, field_type); - field_strings.push(format!(" {}: {}", field_name, field_type)); - }, + + println!( + " Field: {} -> {}", + field_name, field_type + ); + field_strings.push(format!( + " {}: {}", + field_name, field_type + )); + } Err(e) => { - println!(" Skipping field with invalid name: {}", e); + println!( + " Skipping field with invalid name: {}", + e + ); continue; } } } } - + field_strings } _ => Vec::new(), }; - + if !fields.is_empty() { type_defs.insert( name.clone(), format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), ); } - }, + } Err(e) => { println!(" Skipping struct with invalid name: {}", e); continue; @@ -310,44 +341,56 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result { // Validate enum name doesn't contain numbers or "stream" let orig_name = item_enum.ident.to_string(); - + // Skip trying to validate if name contains "__" as these are likely internal types if orig_name.contains("__") { println!(" Skipping likely internal enum: {}", orig_name); continue; } - + match validate_name(&orig_name, "Enum") { Ok(_) => { // Use kebab-case for enum name let name = to_kebab_case(&orig_name); println!(" Found enum: {} -> {}", orig_name, name); - + let mut variants = Vec::new(); let mut skip_enum = false; - + for v in &item_enum.variants { let variant_orig_name = v.ident.to_string(); - + // Validate variant name match validate_name(&variant_orig_name, "Enum variant") { Ok(_) => { match &v.fields { - syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + syn::Fields::Unnamed(fields) + if fields.unnamed.len() == 1 => + { let mut used_types = HashSet::new(); - + match rust_type_to_wit( &fields.unnamed.first().unwrap().ty, - &mut used_types + &mut used_types, ) { Ok(ty) => { // Use kebab-case for variant names and use parentheses for type - let variant_name = to_kebab_case(&variant_orig_name); - println!(" Variant: {} -> {}({})", variant_orig_name, variant_name, ty); - variants.push(format!(" {}({})", variant_name, ty)); - }, + let variant_name = + to_kebab_case(&variant_orig_name); + println!( + " Variant: {} -> {}({})", + variant_orig_name, variant_name, ty + ); + variants.push(format!( + " {}({})", + variant_name, ty + )); + } Err(e) => { - println!(" Error converting variant type: {}", e); + println!( + " Error converting variant type: {}", + e + ); skip_enum = true; break; } @@ -356,18 +399,24 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result { // Use kebab-case for variant names let variant_name = to_kebab_case(&variant_orig_name); - println!(" Variant: {} -> {}", variant_orig_name, variant_name); + println!( + " Variant: {} -> {}", + variant_orig_name, variant_name + ); variants.push(format!(" {}", variant_name)); - }, + } _ => { - println!(" Skipping complex variant: {}", variant_orig_name); + println!( + " Skipping complex variant: {}", + variant_orig_name + ); // Complex variants with multiple fields aren't directly supported in WIT // For simplicity, we'll skip enums with complex variants skip_enum = true; break; - }, + } } - }, + } Err(e) => { println!(" Skipping variant with invalid name: {}", e); skip_enum = true; @@ -375,14 +424,18 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result { println!(" Skipping enum with invalid name: {}", e); continue; @@ -392,7 +445,7 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result {} } } - + println!("Collected {} type definitions from file", type_defs.len()); Ok(type_defs) } @@ -401,18 +454,18 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result Vec { let mut projects = Vec::new(); println!("Scanning for Rust projects in {}", base_dir.display()); - + for entry in WalkDir::new(base_dir) .max_depth(1) .into_iter() .filter_map(Result::ok) { let path = entry.path(); - + if path.is_dir() && path != base_dir { let cargo_toml = path.join("Cargo.toml"); println!("Checking {}", cargo_toml.display()); - + if cargo_toml.exists() { // Try to read and parse Cargo.toml if let Ok(content) = fs::read_to_string(&cargo_toml) { @@ -425,7 +478,10 @@ fn find_rust_projects(base_dir: &Path) -> Vec { { if let Some(package) = metadata.get("package") { if let Some(package_str) = package.as_str() { - println!(" Found package.metadata.component.package = {:?}", package_str); + println!( + " Found package.metadata.component.package = {:?}", + package_str + ); if package_str == "hyperware:process" { println!(" Adding project: {}", path.display()); projects.push(path.to_path_buf()); @@ -440,7 +496,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { } } } - + println!("Found {} relevant Rust projects", projects.len()); projects } @@ -454,20 +510,24 @@ fn generate_signature_struct( ) -> Result { // Create signature struct name with attribute type let signature_struct_name = format!("{}-signature-{}", kebab_name, attr_type); - + // Generate comment for this specific function - let comment = format!(" // Function signature for: {} ({})", kebab_name, attr_type); - + let comment = format!( + " // Function signature for: {} ({})", + kebab_name, attr_type + ); + // Create struct fields that directly represent function parameters let mut struct_fields = Vec::new(); - + // Add target parameter based on attribute type if attr_type == "http" { struct_fields.push(" target: string".to_string()); - } else { // remote or local + } else { + // remote or local struct_fields.push(" target: address".to_string()); } - + // Process function parameters (skip &self and &mut self) for arg in &method.sig.inputs { if let syn::FnArg::Typed(pat_type) = arg { @@ -476,28 +536,29 @@ fn generate_signature_struct( if pat_ident.ident == "self" { continue; } - + // Get original param name and convert to kebab-case let param_orig_name = pat_ident.ident.to_string(); - + // Validate parameter name match validate_name(¶m_orig_name, "Parameter") { Ok(_) => { let param_name = to_kebab_case(¶m_orig_name); - + // Rust type to WIT type match rust_type_to_wit(&pat_type.ty, used_types) { Ok(param_type) => { // Add field directly to the struct - struct_fields.push(format!(" {}: {}", param_name, param_type)); - }, + struct_fields + .push(format!(" {}: {}", param_name, param_type)); + } Err(e) => { println!(" Error converting parameter type: {}", e); // Use a placeholder type for this parameter struct_fields.push(format!(" {}: unknown", param_name)); } } - }, + } Err(e) => { println!(" Skipping parameter with invalid name: {}", e); // Use a placeholder for invalid parameter names @@ -507,26 +568,24 @@ fn generate_signature_struct( } } } - + // Add return type field match &method.sig.output { - syn::ReturnType::Type(_, ty) => { - match rust_type_to_wit(&*ty, used_types) { - Ok(return_type) => { - struct_fields.push(format!(" returning: {}", return_type)); - }, - Err(e) => { - println!(" Error converting return type: {}", e); - struct_fields.push(" returning: unknown".to_string()); - } + syn::ReturnType::Type(_, ty) => match rust_type_to_wit(&*ty, used_types) { + Ok(return_type) => { + struct_fields.push(format!(" returning: {}", return_type)); } - } + Err(e) => { + println!(" Error converting return type: {}", e); + struct_fields.push(" returning: unknown".to_string()); + } + }, _ => { // For unit return type struct_fields.push(" returning: unit".to_string()); } } - + // Combine everything into a record definition let record_def = format!( "{}\n record {} {{\n{}\n }}", @@ -534,7 +593,7 @@ fn generate_signature_struct( signature_struct_name, struct_fields.join(",\n") ); - + Ok(record_def) } @@ -555,18 +614,18 @@ impl AsTypePath for syn::Type { // Process a single Rust project and generate WIT files fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { println!("\nProcessing project: {}", project_path.display()); - + // Find lib.rs for this project let lib_rs = project_path.join("src").join("lib.rs"); - + if !lib_rs.exists() { println!("No lib.rs found for project: {}", project_path.display()); return Ok(None); } - + // Find all Rust files in the project let rust_files = find_rust_files(project_path); - + // Collect all type definitions from all Rust files let mut all_type_defs = HashMap::new(); for file_path in &rust_files { @@ -575,54 +634,66 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - println!("Error collecting type definitions from {}: {}", file_path.display(), e); + println!( + "Error collecting type definitions from {}: {}", + file_path.display(), + e + ); // Continue with other files } } } - + println!("Collected {} total type definitions", all_type_defs.len()); - + // Parse lib.rs to find the hyperprocess attribute and interface details - let lib_content = fs::read_to_string(&lib_rs) - .with_context(|| format!("Failed to read lib.rs for project: {}", project_path.display()))?; - - let ast = syn::parse_file(&lib_content) - .with_context(|| format!("Failed to parse lib.rs for project: {}", project_path.display()))?; - + let lib_content = fs::read_to_string(&lib_rs).with_context(|| { + format!( + "Failed to read lib.rs for project: {}", + project_path.display() + ) + })?; + + let ast = syn::parse_file(&lib_content).with_context(|| { + format!( + "Failed to parse lib.rs for project: {}", + project_path.display() + ) + })?; + let mut wit_world = None; let mut interface_name = None; let mut kebab_interface_name = None; let mut impl_item_with_hyperprocess = None; - + println!("Scanning for impl blocks with hyperprocess attribute"); for item in &ast.items { if let Item::Impl(impl_item) = item { // Check if this impl block has a #[hyperprocess] attribute - if let Some(attr) = impl_item.attrs.iter().find(|attr| attr.path().is_ident("hyperprocess")) { + if let Some(attr) = impl_item + .attrs + .iter() + .find(|attr| attr.path().is_ident("hyperprocess")) + { println!("Found hyperprocess attribute"); - + // Extract the wit_world name match extract_wit_world(&[attr.clone()]) { Ok(world_name) => { println!("Extracted wit_world: {}", world_name); wit_world = Some(world_name); - + // Get the interface name from the impl type - interface_name = impl_item - .self_ty - .as_ref() - .as_type_path() - .map(|tp| { - if let Some(last_segment) = tp.path.segments.last() { - last_segment.ident.to_string() - } else { - "Unknown".to_string() - } - }); - + interface_name = impl_item.self_ty.as_ref().as_type_path().map(|tp| { + if let Some(last_segment) = tp.path.segments.last() { + last_segment.ident.to_string() + } else { + "Unknown".to_string() + } + }); + // Check for "State" suffix and remove it if let Some(ref name) = interface_name { // Validate the interface name @@ -630,76 +701,111 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result println!("Failed to extract wit_world: {}", e), } } } } - + // Now generate the WIT content for the interface - if let (Some(ref iface_name), Some(ref kebab_name), Some(ref impl_item)) = - (&interface_name, &kebab_interface_name, &impl_item_with_hyperprocess) { + if let (Some(ref iface_name), Some(ref kebab_name), Some(ref impl_item)) = ( + &interface_name, + &kebab_interface_name, + &impl_item_with_hyperprocess, + ) { let mut signature_structs = Vec::new(); let mut used_types = HashSet::new(); - + for item in &impl_item.items { if let ImplItem::Fn(method) = item { let method_name = method.sig.ident.to_string(); println!(" Examining method: {}", method_name); - + // Check for attribute types - let has_remote = method.attrs.iter().any(|attr| attr.path().is_ident("remote")); - let has_local = method.attrs.iter().any(|attr| attr.path().is_ident("local")); + let has_remote = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("remote")); + let has_local = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("local")); let has_http = method.attrs.iter().any(|attr| attr.path().is_ident("http")); - + if has_remote || has_local || has_http { - println!(" Has relevant attributes: remote={}, local={}, http={}", - has_remote, has_local, has_http); - + println!( + " Has relevant attributes: remote={}, local={}, http={}", + has_remote, has_local, has_http + ); + // Validate function name match validate_name(&method_name, "Function") { Ok(_) => { // Convert function name to kebab-case let kebab_name = to_kebab_case(&method_name); println!(" Processing method: {} -> {}", method_name, kebab_name); - + // Generate a signature struct for each attribute type if has_remote { - match generate_signature_struct(&kebab_name, "remote", method, &mut used_types) { + match generate_signature_struct( + &kebab_name, + "remote", + method, + &mut used_types, + ) { Ok(remote_struct) => signature_structs.push(remote_struct), - Err(e) => println!(" Error generating remote signature struct: {}", e), + Err(e) => println!( + " Error generating remote signature struct: {}", + e + ), } } - + if has_local { - match generate_signature_struct(&kebab_name, "local", method, &mut used_types) { + match generate_signature_struct( + &kebab_name, + "local", + method, + &mut used_types, + ) { Ok(local_struct) => signature_structs.push(local_struct), - Err(e) => println!(" Error generating local signature struct: {}", e), + Err(e) => println!( + " Error generating local signature struct: {}", + e + ), } } - + if has_http { - match generate_signature_struct(&kebab_name, "http", method, &mut used_types) { + match generate_signature_struct( + &kebab_name, + "http", + method, + &mut used_types, + ) { Ok(http_struct) => signature_structs.push(http_struct), - Err(e) => println!(" Error generating HTTP signature struct: {}", e), + Err(e) => println!( + " Error generating HTTP signature struct: {}", + e + ), } } - }, + } Err(e) => { println!(" Skipping method with invalid name: {}", e); } @@ -709,51 +815,56 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result = all_type_defs.values().cloned().collect(); - + // Sort them for consistent output type_defs.sort(); - + // Generate the final WIT content if signature_structs.is_empty() { println!("No functions found for interface {}", iface_name); } else { // Start with the interface comment let mut content = " // This interface contains function signature definitions that will be used\n // by the hyper-bindgen macro to generate async function bindings.\n //\n // NOTE: This is currently a hacky workaround since WIT async functions are not\n // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware,\n // we should switch to using proper async WIT function signatures instead of\n // this struct-based approach with hyper-bindgen generating the async stubs.\n".to_string(); - + // Add standard imports content.push_str("\n use standard.{address};\n\n"); - + // Add type definitions if any if !type_defs.is_empty() { content.push_str(&type_defs.join("\n\n")); content.push_str("\n\n"); } - + // Add signature structs content.push_str(&signature_structs.join("\n\n")); - + // Wrap in interface block let final_content = format!("interface {} {{\n{}\n}}\n", kebab_name, content); - println!("Generated interface content for {} with {} signature structs", iface_name, signature_structs.len()); - + println!( + "Generated interface content for {} with {} signature structs", + iface_name, + signature_structs.len() + ); + // Write the interface file with kebab-case name let interface_file = api_dir.join(format!("{}.wit", kebab_name)); println!("Writing WIT file to {}", interface_file.display()); - + fs::write(&interface_file, &final_content) .with_context(|| format!("Failed to write {}", interface_file.display()))?; - + println!("Successfully wrote WIT file"); } } - - if let (Some(_), Some(_), Some(kebab_iface)) = (wit_world, interface_name, kebab_interface_name) { + + if let (Some(_), Some(_), Some(kebab_iface)) = (wit_world, interface_name, kebab_interface_name) + { println!("Returning import statement for interface {}", kebab_iface); // Use kebab-case interface name for import Ok(Some(format!(" import {};", kebab_iface))) @@ -768,67 +879,67 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { println!("Got import statement: {}", import); new_imports.push(import.clone()); - + // Extract interface name from import statement let interface_name = import .trim_start_matches(" import ") .trim_end_matches(";") .to_string(); - + interfaces.push(interface_name); processed_projects.push(project_path.clone()); - }, + } Ok(None) => println!("No import statement generated"), Err(e) => println!("Error processing project: {}", e), } } - + println!("Collected {} new imports", new_imports.len()); - + // Check for existing world definition files and update them println!("Looking for existing world definition files"); let mut updated_world = false; - + for entry in WalkDir::new(api_dir) .max_depth(1) .into_iter() .filter_map(Result::ok) { let path = entry.path(); - + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { println!("Checking WIT file: {}", path.display()); - + if let Ok(content) = fs::read_to_string(path) { if content.contains("world ") { println!("Found world definition file"); - + // Extract the world name and existing imports let lines: Vec<&str> = content.lines().collect(); let mut world_name = None; let mut existing_imports = Vec::new(); let mut include_line = " include process-v1;".to_string(); - + for line in &lines { let trimmed = line.trim(); - + if trimmed.starts_with("world ") { if let Some(name) = trimmed.split_whitespace().nth(1) { world_name = Some(name.trim_end_matches(" {").to_string()); @@ -839,10 +950,10 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec = all_imports .iter() @@ -875,9 +986,9 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec Result<(Vec = new_imports .iter() @@ -916,30 +1031,37 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec, add_paths_to_api: Vec, rewrite: bool, + hyperapp: bool, reproducible: bool, force: bool, verbose: bool, @@ -40,6 +41,7 @@ pub async fn execute( local_dependencies, add_paths_to_api, rewrite, + hyperapp, reproducible, force, verbose, diff --git a/src/main.rs b/src/main.rs index 87574074..758cde2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -235,6 +235,7 @@ async fn execute( .map(|s| PathBuf::from(s)) .collect(); let rewrite = matches.get_one::("REWRITE").unwrap(); + let hyperapp = matches.get_one::("HYPERAPP").unwrap(); let reproducible = matches.get_one::("REPRODUCIBLE").unwrap(); let force = matches.get_one::("FORCE").unwrap(); let verbose = matches.get_one::("VERBOSE").unwrap(); @@ -253,6 +254,7 @@ async fn execute( local_dependencies, add_paths_to_api, *rewrite, + *hyperapp, *reproducible, *force, *verbose, @@ -298,6 +300,7 @@ async fn execute( .map(|s| PathBuf::from(s)) .collect(); let rewrite = matches.get_one::("REWRITE").unwrap(); + let hyperapp = matches.get_one::("HYPERAPP").unwrap(); let reproducible = matches.get_one::("REPRODUCIBLE").unwrap(); let force = matches.get_one::("FORCE").unwrap(); let verbose = matches.get_one::("VERBOSE").unwrap(); @@ -316,6 +319,7 @@ async fn execute( local_dependencies, add_paths_to_api, *rewrite, + *hyperapp, *reproducible, *force, *verbose, @@ -758,6 +762,13 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .help("Rewrite the package (disables `Spawn!()`) [default: don't rewrite]") .required(false) ) + .arg(Arg::new("HYPERAPP") + .action(ArgAction::SetTrue) + .short('h') + .long("hyperapp") + .help("Build using the Hyperapp framework [default: don't use Hyperapp framework]") + .required(false) + ) .arg(Arg::new("REPRODUCIBLE") .action(ArgAction::SetTrue) .short('r') @@ -865,6 +876,13 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .help("Rewrite the package (disables `Spawn!()`) [default: don't rewrite]") .required(false) ) + .arg(Arg::new("HYPERAPP") + .action(ArgAction::SetTrue) + .short('h') + .long("hyperapp") + .help("Build using the Hyperapp framework [default: don't use Hyperapp framework]") + .required(false) + ) .arg(Arg::new("REPRODUCIBLE") .action(ArgAction::SetTrue) .short('r') diff --git a/src/run_tests/mod.rs b/src/run_tests/mod.rs index fbc6afca..cbdb1afd 100644 --- a/src/run_tests/mod.rs +++ b/src/run_tests/mod.rs @@ -355,6 +355,7 @@ async fn build_packages( let url = format!("http://localhost:{port}"); + // TODO: add hyperapp setting to tests.toml for dependency_package_path in &test.dependency_package_paths { let path = match expand_home_path(&dependency_package_path) { Some(p) => p, @@ -381,6 +382,7 @@ async fn build_packages( false, false, false, + false, ) .await?; debug!("Start {path:?}"); @@ -406,6 +408,7 @@ async fn build_packages( false, false, false, + false, ) .await?; } @@ -428,6 +431,7 @@ async fn build_packages( false, false, false, + false, ) .await?; } From 04b4b72483598f0a531e793beeb7808a1cb52a9d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 00:21:41 +0000 Subject: [PATCH 03/88] Format Rust code using rustfmt --- src/build/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index b8059fc7..509a784b 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1763,7 +1763,8 @@ pub async fn execute( if hyperapp { let api_dir = live_dir.join("api"); - let (_processed_projects, _interfaces) = wit_generator::generate_wit_files(&live_dir, &api_dir)?; + let (_processed_projects, _interfaces) = + wit_generator::generate_wit_files(&live_dir, &api_dir)?; } let ui_dirs = get_ui_dirs(&live_dir, &include, &exclude)?; From a4a7ea478ba51c3e16aa13f250fce05e601189e1 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 3 Apr 2025 17:27:50 -0700 Subject: [PATCH 04/88] build: remove `-h` flag that was interferring with `--help` --- src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 758cde2f..a49c9139 100644 --- a/src/main.rs +++ b/src/main.rs @@ -764,7 +764,6 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { ) .arg(Arg::new("HYPERAPP") .action(ArgAction::SetTrue) - .short('h') .long("hyperapp") .help("Build using the Hyperapp framework [default: don't use Hyperapp framework]") .required(false) @@ -878,7 +877,6 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { ) .arg(Arg::new("HYPERAPP") .action(ArgAction::SetTrue) - .short('h') .long("hyperapp") .help("Build using the Hyperapp framework [default: don't use Hyperapp framework]") .required(false) From 4ad1b14bd730c210040757e8eed4e25d70ba6955 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 7 Apr 2025 21:37:23 -0700 Subject: [PATCH 05/88] build: error out of bindgen rather than making non-working wit --- src/build/caller_utils_generator.rs | 433 ++++++++++++++++------------ src/build/wit_generator.rs | 81 +++--- 2 files changed, 295 insertions(+), 219 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 8de9c0f9..f6819a5e 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; @@ -14,7 +14,7 @@ pub fn to_snake_case(s: &str) -> String { pub fn to_pascal_case(s: &str) -> String { let parts = s.split('-'); let mut result = String::new(); - + for part in parts { if !part.is_empty() { let mut chars = part.chars(); @@ -24,7 +24,7 @@ pub fn to_pascal_case(s: &str) -> String { } } } - + result } @@ -32,7 +32,7 @@ pub fn to_pascal_case(s: &str) -> String { fn find_world_name(api_dir: &Path) -> Result { let mut regular_world_name = None; let mut types_world_name = None; - + // Look for world definition files for entry in WalkDir::new(api_dir) .max_depth(1) @@ -40,22 +40,24 @@ fn find_world_name(api_dir: &Path) -> Result { .filter_map(Result::ok) { let path = entry.path(); - + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { if let Ok(content) = fs::read_to_string(path) { if content.contains("world ") { println!("Analyzing world definition file: {}", path.display()); - + // Extract the world name let lines: Vec<&str> = content.lines().collect(); - - if let Some(world_line) = lines.iter().find(|line| line.trim().starts_with("world ")) { + + if let Some(world_line) = + lines.iter().find(|line| line.trim().starts_with("world ")) + { println!("World line: {}", world_line); - + if let Some(world_name) = world_line.trim().split_whitespace().nth(1) { let clean_name = world_name.trim_end_matches(" {"); println!("Extracted world name: {}", clean_name); - + // Check if this is a types-prefixed world if clean_name.starts_with("types-") { types_world_name = Some(clean_name.to_string()); @@ -70,28 +72,31 @@ fn find_world_name(api_dir: &Path) -> Result { } } } - + // Prioritize types-prefixed world if found if let Some(types_name) = types_world_name { return Ok(types_name); } - + // If no types-prefixed world found, check if we have a regular world if let Some(regular_name) = regular_world_name { // Check if there's a corresponding types-prefixed world file let types_name = format!("types-{}", regular_name); let types_file = api_dir.join(format!("{}.wit", types_name)); - + if types_file.exists() { println!("Found types world from file: {}", types_name); return Ok(types_name); } - + // Fall back to regular world but print a warning - println!("Warning: No types- world found, using regular world: {}", regular_name); + println!( + "Warning: No types- world found, using regular world: {}", + regular_name + ); return Ok(regular_name); } - + // If no world name is found, we should fail bail!("No world name found in any WIT file. Cannot generate caller-utils without a world name.") } @@ -108,9 +113,6 @@ fn wit_type_to_rust(wit_type: &str) -> String { "u32" => "u32".to_string(), "s64" => "i64".to_string(), "u64" => "u64".to_string(), - // Size types - "usize" => "usize".to_string(), - "isize" => "isize".to_string(), // Floating point types "f32" => "f32".to_string(), "f64" => "f64".to_string(), @@ -119,33 +121,32 @@ fn wit_type_to_rust(wit_type: &str) -> String { "str" => "&str".to_string(), "char" => "char".to_string(), "bool" => "bool".to_string(), - "unit" => "()".to_string(), + "_" => "()".to_string(), // Special types "address" => "WitAddress".to_string(), - // Common primitives that might be written differently in WIT - "i8" => "i8".to_string(), - "i16" => "i16".to_string(), - "i32" => "i32".to_string(), - "i64" => "i64".to_string(), // Collection types with generics t if t.starts_with("list<") => { let inner_type = &t[5..t.len() - 1]; format!("Vec<{}>", wit_type_to_rust(inner_type)) - }, + } t if t.starts_with("option<") => { let inner_type = &t[7..t.len() - 1]; format!("Option<{}>", wit_type_to_rust(inner_type)) - }, + } t if t.starts_with("result<") => { let inner_part = &t[7..t.len() - 1]; if let Some(comma_pos) = inner_part.find(',') { let ok_type = &inner_part[..comma_pos].trim(); let err_type = &inner_part[comma_pos + 1..].trim(); - format!("Result<{}, {}>", wit_type_to_rust(ok_type), wit_type_to_rust(err_type)) + format!( + "Result<{}, {}>", + wit_type_to_rust(ok_type), + wit_type_to_rust(err_type) + ) } else { format!("Result<{}, ()>", wit_type_to_rust(inner_part)) } - }, + } t if t.starts_with("tuple<") => { let inner_types = &t[6..t.len() - 1]; let rust_types: Vec = inner_types @@ -153,19 +154,7 @@ fn wit_type_to_rust(wit_type: &str) -> String { .map(|t| wit_type_to_rust(t)) .collect(); format!("({})", rust_types.join(", ")) - }, - // Handle map type if present - t if t.starts_with("map<") => { - let inner_part = &t[4..t.len() - 1]; - if let Some(comma_pos) = inner_part.find(',') { - let key_type = &inner_part[..comma_pos].trim(); - let value_type = &inner_part[comma_pos + 1..].trim(); - format!("HashMap<{}, {}>", wit_type_to_rust(key_type), wit_type_to_rust(value_type)) - } else { - // Fallback for malformed map type - format!("HashMap", wit_type_to_rust(inner_part)) - } - }, + } // Custom types (in kebab-case) need to be converted to PascalCase _ => to_pascal_case(wit_type).to_string(), } @@ -175,7 +164,9 @@ fn wit_type_to_rust(wit_type: &str) -> String { fn generate_default_value(rust_type: &str) -> String { match rust_type { // Integer types - "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" | "isize" | "usize" => "0".to_string(), + "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" | "isize" | "usize" => { + "0".to_string() + } // Floating point types "f32" | "f64" => "0.0".to_string(), // String types @@ -196,17 +187,18 @@ fn generate_default_value(rust_type: &str) -> String { } else { "Ok(())".to_string() } - }, - t if t.starts_with("HashMap<") => "HashMap::new()".to_string(), + } + //t if t.starts_with("HashMap<") => "HashMap::new()".to_string(), t if t.starts_with("(") => { // Generate default tuple with default values for each element let inner_part = t.trim_start_matches('(').trim_end_matches(')'); let parts: Vec<_> = inner_part.split(", ").collect(); - let default_values: Vec<_> = parts.iter() + let default_values: Vec<_> = parts + .iter() .map(|part| generate_default_value(part)) .collect(); format!("({})", default_values.join(", ")) - }, + } // For custom types, assume they implement Default _ => format!("{}::default()", rust_type), } @@ -228,7 +220,7 @@ struct SignatureStruct { // Find all interface imports in the world WIT file fn find_interfaces_in_world(api_dir: &Path) -> Result> { let mut interfaces = Vec::new(); - + // Find world definition files for entry in WalkDir::new(api_dir) .max_depth(1) @@ -236,12 +228,12 @@ fn find_interfaces_in_world(api_dir: &Path) -> Result> { .filter_map(Result::ok) { let path = entry.path(); - + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { if let Ok(content) = fs::read_to_string(path) { if content.contains("world ") { println!("Analyzing world definition file: {}", path.display()); - + // Extract import statements for line in content.lines() { let line = line.trim(); @@ -250,7 +242,7 @@ fn find_interfaces_in_world(api_dir: &Path) -> Result> { .trim_start_matches("import ") .trim_end_matches(";") .trim(); - + interfaces.push(interface.to_string()); println!(" Found interface import: {}", interface); } @@ -259,44 +251,53 @@ fn find_interfaces_in_world(api_dir: &Path) -> Result> { } } } - + Ok(interfaces) } // Parse WIT file to extract function signatures and type definitions fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec)> { println!("Parsing WIT file: {}", file_path.display()); - + let content = fs::read_to_string(file_path) .with_context(|| format!("Failed to read WIT file: {}", file_path.display()))?; - + let mut signatures = Vec::new(); let mut type_names = Vec::new(); - + // Simple parser for WIT files to extract record definitions and types let lines: Vec<_> = content.lines().collect(); let mut i = 0; - + while i < lines.len() { let line = lines[i].trim(); - + // Look for record definitions that aren't signature structs if line.starts_with("record ") && !line.contains("-signature-") { - let record_name = line.trim_start_matches("record ").trim_end_matches(" {").trim(); + let record_name = line + .trim_start_matches("record ") + .trim_end_matches(" {") + .trim(); println!(" Found type: record {}", record_name); type_names.push(record_name.to_string()); } // Look for variant definitions (enums) else if line.starts_with("variant ") { - let variant_name = line.trim_start_matches("variant ").trim_end_matches(" {").trim(); + let variant_name = line + .trim_start_matches("variant ") + .trim_end_matches(" {") + .trim(); println!(" Found type: variant {}", variant_name); type_names.push(variant_name.to_string()); } // Look for signature record definitions else if line.starts_with("record ") && line.contains("-signature-") { - let record_name = line.trim_start_matches("record ").trim_end_matches(" {").trim(); + let record_name = line + .trim_start_matches("record ") + .trim_end_matches(" {") + .trim(); println!(" Found record: {}", record_name); - + // Extract function name and attribute type let parts: Vec<_> = record_name.split("-signature-").collect(); if parts.len() != 2 { @@ -304,51 +305,55 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec i += 1; continue; } - + let function_name = parts[0].to_string(); let attr_type = parts[1].to_string(); - + // Parse fields let mut fields = Vec::new(); i += 1; - + while i < lines.len() && !lines[i].trim().starts_with("}") { let field_line = lines[i].trim(); - + // Skip comments and empty lines if field_line.starts_with("//") || field_line.is_empty() { i += 1; continue; } - + // Parse field definition let field_parts: Vec<_> = field_line.split(':').collect(); if field_parts.len() == 2 { let field_name = field_parts[0].trim().to_string(); let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); - + println!(" Field: {} -> {}", field_name, field_type); fields.push(SignatureField { name: field_name, wit_type: field_type, }); } - + i += 1; } - + signatures.push(SignatureStruct { function_name, attr_type, fields, }); } - + i += 1; } - - println!("Extracted {} signature structs and {} type definitions from {}", - signatures.len(), type_names.len(), file_path.display()); + + println!( + "Extracted {} signature structs and {} type definitions from {}", + signatures.len(), + type_names.len(), + file_path.display() + ); Ok((signatures, type_names)) } @@ -356,23 +361,23 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec fn generate_async_function(signature: &SignatureStruct) -> String { // Convert function name from kebab-case to snake_case let snake_function_name = to_snake_case(&signature.function_name); - + // Get pascal case version for the JSON request format let pascal_function_name = to_pascal_case(&signature.function_name); - + // Function full name with attribute type let full_function_name = format!("{}_{}_rpc", snake_function_name, signature.attr_type); - + // Extract parameters and return type let mut params = Vec::new(); let mut param_names = Vec::new(); let mut return_type = "()".to_string(); let mut target_param = ""; - + for field in &signature.fields { let field_name_snake = to_snake_case(&field.name); let rust_type = wit_type_to_rust(&field.wit_type); - + if field.name == "target" { if field.wit_type == "string" { target_param = "&str"; @@ -387,24 +392,29 @@ fn generate_async_function(signature: &SignatureStruct) -> String { param_names.push(field_name_snake); } } - + // First parameter is always target let all_params = if target_param.is_empty() { params.join(", ") } else { - format!("target: {}{}", target_param, if params.is_empty() { "" } else { ", " }) + ¶ms.join(", ") + format!( + "target: {}{}", + target_param, + if params.is_empty() { "" } else { ", " } + ) + ¶ms.join(", ") }; - + // Wrap the return type in SendResult let wrapped_return_type = format!("SendResult<{}>", return_type); - + // For HTTP endpoints, generate commented-out implementation if signature.attr_type == "http" { let default_value = generate_default_value(&return_type); - + // Add underscore prefix to all parameters for HTTP stubs let all_params_with_underscore = if target_param.is_empty() { - params.iter() + params + .iter() .map(|param| { let parts: Vec<&str> = param.split(':').collect(); if parts.len() == 2 { @@ -420,7 +430,8 @@ fn generate_async_function(signature: &SignatureStruct) -> String { if params.is_empty() { target_with_underscore } else { - let params_with_underscore = params.iter() + let params_with_underscore = params + .iter() .map(|param| { let parts: Vec<&str> = param.split(':').collect(); if parts.len() == 2 { @@ -434,7 +445,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { format!("{}, {}", target_with_underscore, params_with_underscore) } }; - + return format!( "/// Generated stub for `{}` {} RPC call\n/// HTTP endpoint - uncomment to implement\n// pub async fn {}({}) -> {} {{\n// // TODO: Implement HTTP endpoint\n// SendResult::Success({})\n// }}", signature.function_name, @@ -445,21 +456,26 @@ fn generate_async_function(signature: &SignatureStruct) -> String { default_value ); } - + // Format JSON parameters correctly let json_params = if param_names.is_empty() { // No parameters case format!("json!({{\"{}\" : {{}}}})", pascal_function_name) } else if param_names.len() == 1 { // Single parameter case - format!("json!({{\"{}\": {}}})", pascal_function_name, param_names[0]) + format!( + "json!({{\"{}\": {}}})", + pascal_function_name, param_names[0] + ) } else { // Multiple parameters case - use tuple format - format!("json!({{\"{}\": ({})}})", - pascal_function_name, - param_names.join(", ")) + format!( + "json!({{\"{}\": ({})}})", + pascal_function_name, + param_names.join(", ") + ) }; - + // Generate function with implementation using send format!( "/// Generated stub for `{}` {} RPC call\npub async fn {}({}) -> {} {{\n let request = {};\n send::<{}>(&request, target, 30).await\n}}", @@ -477,13 +493,16 @@ fn generate_async_function(signature: &SignatureStruct) -> String { fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { // Path to the new crate let caller_utils_dir = base_dir.join("caller-utils"); - println!("Creating caller-utils crate at {}", caller_utils_dir.display()); - + println!( + "Creating caller-utils crate at {}", + caller_utils_dir.display() + ); + // Create directories fs::create_dir_all(&caller_utils_dir)?; fs::create_dir_all(caller_utils_dir.join("src"))?; println!("Created project directory structure"); - + // Create Cargo.toml with updated dependencies let cargo_toml = r#"[package] name = "caller-utils" @@ -507,22 +526,22 @@ wit-bindgen = "0.41.0" [lib] crate-type = ["cdylib", "lib"] "#; - + fs::write(caller_utils_dir.join("Cargo.toml"), cargo_toml) .with_context(|| "Failed to write caller-utils Cargo.toml")?; - + println!("Created Cargo.toml for caller-utils"); - + // Get the world name (preferably the types- version) let world_name = find_world_name(api_dir)?; println!("Using world name for code generation: {}", world_name); - + // Get all interfaces from the world file let interface_imports = find_interfaces_in_world(api_dir)?; - + // Store all types from each interface let mut interface_types: HashMap> = HashMap::new(); - + // Find all WIT files in the api directory to generate stubs let mut wit_files = Vec::new(); for entry in WalkDir::new(api_dir) @@ -540,72 +559,79 @@ crate-type = ["cdylib", "lib"] } } } - + println!("Found {} WIT interface files", wit_files.len()); - + // Generate content for each module and collect types let mut module_contents = HashMap::::new(); - + for wit_file in &wit_files { // Extract the interface name from the file name let interface_name = wit_file.file_stem().unwrap().to_string_lossy(); let snake_interface_name = to_snake_case(&interface_name); - - println!("Processing interface: {} -> {}", interface_name, snake_interface_name); - + + println!( + "Processing interface: {} -> {}", + interface_name, snake_interface_name + ); + // Parse the WIT file to extract signature structs and types match parse_wit_file(wit_file) { Ok((signatures, types)) => { // Store types for this interface interface_types.insert(interface_name.to_string(), types); - + if signatures.is_empty() { println!("No signatures found in {}", wit_file.display()); continue; } - + // Generate module content let mut mod_content = String::new(); - + // Add function implementations for signature in &signatures { let function_impl = generate_async_function(signature); mod_content.push_str(&function_impl); mod_content.push_str("\n\n"); } - + // Store the module content module_contents.insert(snake_interface_name, mod_content); - - println!("Generated module content with {} function stubs", signatures.len()); - }, + + println!( + "Generated module content with {} function stubs", + signatures.len() + ); + } Err(e) => { println!("Error parsing WIT file {}: {}", wit_file.display(), e); } } } - + // Create import statements for each interface using "hyperware::process::{interface_name}::*" // Use a HashSet to track which interfaces we've already processed to avoid duplicates let mut processed_interfaces = std::collections::HashSet::new(); let mut interface_use_statements = Vec::new(); - + for interface_name in &interface_imports { // Convert to snake case for module name let snake_interface_name = to_snake_case(interface_name); - + // Only add the import if we haven't processed this interface yet if processed_interfaces.insert(snake_interface_name.clone()) { // Create wildcard import for this interface - interface_use_statements.push( - format!("pub use crate::hyperware::process::{}::*;", snake_interface_name) - ); + interface_use_statements.push(format!( + "pub use crate::hyperware::process::{}::*;", + snake_interface_name + )); } } - + // Create single lib.rs with all modules inline let mut lib_rs = String::new(); - + // Updated wit_bindgen usage with explicit world name - FIXED: Removed unused imports lib_rs.push_str("wit_bindgen::generate!({\n"); lib_rs.push_str(" path: \"target/wit\",\n"); @@ -613,15 +639,15 @@ crate-type = ["cdylib", "lib"] lib_rs.push_str(" generate_unused_types: true,\n"); lib_rs.push_str(" additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],\n"); lib_rs.push_str("});\n\n"); - + lib_rs.push_str("/// Generated caller utilities for RPC function stubs\n\n"); - + // Add global imports lib_rs.push_str("pub use hyperware_app_common::SendResult;\n"); lib_rs.push_str("pub use hyperware_app_common::send;\n"); lib_rs.push_str("use hyperware_process_lib::Address;\n"); lib_rs.push_str("use serde_json::json;\n\n"); - + // Add interface use statements if !interface_use_statements.is_empty() { lib_rs.push_str("// Import types from each interface\n"); @@ -630,37 +656,40 @@ crate-type = ["cdylib", "lib"] } lib_rs.push_str("\n"); } - + // Add all modules with their content for (module_name, module_content) in module_contents { - lib_rs.push_str(&format!("/// Generated RPC stubs for the {} interface\n", module_name)); + lib_rs.push_str(&format!( + "/// Generated RPC stubs for the {} interface\n", + module_name + )); lib_rs.push_str(&format!("pub mod {} {{\n", module_name)); lib_rs.push_str(" use crate::*;\n\n"); lib_rs.push_str(&format!(" {}\n", module_content.replace("\n", "\n "))); lib_rs.push_str("}\n\n"); } - + // Write lib.rs let lib_rs_path = caller_utils_dir.join("src").join("lib.rs"); println!("Writing lib.rs to {}", lib_rs_path.display()); - + fs::write(&lib_rs_path, lib_rs) .with_context(|| format!("Failed to write lib.rs: {}", lib_rs_path.display()))?; - + println!("Created single lib.rs file with all modules inline"); - + // Create target/wit directory and copy all WIT files let target_wit_dir = caller_utils_dir.join("target").join("wit"); println!("Creating directory: {}", target_wit_dir.display()); - + // Remove the directory if it exists to ensure clean state if target_wit_dir.exists() { println!("Removing existing target/wit directory"); fs::remove_dir_all(&target_wit_dir)?; } - + fs::create_dir_all(&target_wit_dir)?; - + // Copy all WIT files to target/wit for entry in WalkDir::new(api_dir) .max_depth(1) @@ -671,52 +700,75 @@ crate-type = ["cdylib", "lib"] if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { let file_name = path.file_name().unwrap(); let target_path = target_wit_dir.join(file_name); - fs::copy(path, &target_path) - .with_context(|| format!("Failed to copy {} to {}", path.display(), target_path.display()))?; - println!("Copied {} to target/wit directory", file_name.to_string_lossy()); + fs::copy(path, &target_path).with_context(|| { + format!( + "Failed to copy {} to {}", + path.display(), + target_path.display() + ) + })?; + println!( + "Copied {} to target/wit directory", + file_name.to_string_lossy() + ); } } - + Ok(()) } // Update workspace Cargo.toml to include the caller-utils crate fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { let workspace_cargo_toml = base_dir.join("Cargo.toml"); - println!("Updating workspace Cargo.toml at {}", workspace_cargo_toml.display()); - + println!( + "Updating workspace Cargo.toml at {}", + workspace_cargo_toml.display() + ); + if !workspace_cargo_toml.exists() { - println!("Workspace Cargo.toml not found at {}", workspace_cargo_toml.display()); + println!( + "Workspace Cargo.toml not found at {}", + workspace_cargo_toml.display() + ); return Ok(()); } - - let content = fs::read_to_string(&workspace_cargo_toml) - .with_context(|| format!("Failed to read workspace Cargo.toml: {}", workspace_cargo_toml.display()))?; - + + let content = fs::read_to_string(&workspace_cargo_toml).with_context(|| { + format!( + "Failed to read workspace Cargo.toml: {}", + workspace_cargo_toml.display() + ) + })?; + // Parse the TOML content - let mut parsed_toml: Value = content.parse() + let mut parsed_toml: Value = content + .parse() .with_context(|| "Failed to parse workspace Cargo.toml")?; - + // Check if there's a workspace section if let Some(workspace) = parsed_toml.get_mut("workspace") { if let Some(members) = workspace.get_mut("members") { if let Some(members_array) = members.as_array_mut() { // Check if caller-utils is already in the members list - let caller_utils_exists = members_array.iter().any(|m| { - m.as_str().map_or(false, |s| s == "caller-utils") - }); - + let caller_utils_exists = members_array + .iter() + .any(|m| m.as_str().map_or(false, |s| s == "caller-utils")); + if !caller_utils_exists { println!("Adding caller-utils to workspace members"); members_array.push(Value::String("caller-utils".to_string())); - + // Write back the updated TOML let updated_content = toml::to_string_pretty(&parsed_toml) .with_context(|| "Failed to serialize updated workspace Cargo.toml")?; - - fs::write(&workspace_cargo_toml, updated_content) - .with_context(|| format!("Failed to write updated workspace Cargo.toml: {}", workspace_cargo_toml.display()))?; - + + fs::write(&workspace_cargo_toml, updated_content).with_context(|| { + format!( + "Failed to write updated workspace Cargo.toml: {}", + workspace_cargo_toml.display() + ) + })?; + println!("Successfully updated workspace Cargo.toml"); } else { println!("caller-utils is already in workspace members"); @@ -724,7 +776,7 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { } } } - + Ok(()) } @@ -732,14 +784,25 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { for project_path in projects { let cargo_toml_path = project_path.join("Cargo.toml"); - println!("Adding caller-utils dependency to {}", cargo_toml_path.display()); - - let content = fs::read_to_string(&cargo_toml_path) - .with_context(|| format!("Failed to read project Cargo.toml: {}", cargo_toml_path.display()))?; - - let mut parsed_toml: Value = content.parse() - .with_context(|| format!("Failed to parse project Cargo.toml: {}", cargo_toml_path.display()))?; - + println!( + "Adding caller-utils dependency to {}", + cargo_toml_path.display() + ); + + let content = fs::read_to_string(&cargo_toml_path).with_context(|| { + format!( + "Failed to read project Cargo.toml: {}", + cargo_toml_path.display() + ) + })?; + + let mut parsed_toml: Value = content.parse().with_context(|| { + format!( + "Failed to parse project Cargo.toml: {}", + cargo_toml_path.display() + ) + })?; + // Add caller-utils to dependencies if not already present if let Some(dependencies) = parsed_toml.get_mut("dependencies") { if let Some(deps_table) = dependencies.as_table_mut() { @@ -748,18 +811,30 @@ fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { "caller-utils".to_string(), Value::Table({ let mut t = toml::map::Map::new(); - t.insert("path".to_string(), Value::String("../caller-utils".to_string())); + t.insert( + "path".to_string(), + Value::String("../caller-utils".to_string()), + ); t - }) + }), ); - + // Write back the updated TOML - let updated_content = toml::to_string_pretty(&parsed_toml) - .with_context(|| format!("Failed to serialize updated project Cargo.toml: {}", cargo_toml_path.display()))?; - - fs::write(&cargo_toml_path, updated_content) - .with_context(|| format!("Failed to write updated project Cargo.toml: {}", cargo_toml_path.display()))?; - + let updated_content = + toml::to_string_pretty(&parsed_toml).with_context(|| { + format!( + "Failed to serialize updated project Cargo.toml: {}", + cargo_toml_path.display() + ) + })?; + + fs::write(&cargo_toml_path, updated_content).with_context(|| { + format!( + "Failed to write updated project Cargo.toml: {}", + cargo_toml_path.display() + ) + })?; + println!("Successfully added caller-utils dependency"); } else { println!("caller-utils dependency already exists"); @@ -767,7 +842,7 @@ fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { } } } - + Ok(()) } @@ -775,12 +850,12 @@ fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { pub fn create_caller_utils(base_dir: &Path, api_dir: &Path, projects: &[PathBuf]) -> Result<()> { // Step 1: Create the caller-utils crate create_caller_utils_crate(api_dir, base_dir)?; - + // Step 2: Update workspace Cargo.toml update_workspace_cargo_toml(base_dir)?; - + // Step 3: Add caller-utils dependency to each hyperware:process project add_caller_utils_to_projects(projects)?; - + Ok(()) -} \ No newline at end of file +} diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 43fffb6e..4bf22a91 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1,10 +1,9 @@ -//use anyhow::{Context}; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; use color_eyre::{ - eyre::{bail, WrapErr}, + eyre::{bail, eyre, WrapErr}, Result, }; use syn::{self, Attribute, ImplItem, Item, Type}; @@ -109,13 +108,17 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { if type_path.path.segments.is_empty() { - return Ok("unknown".to_string()); + return Err(eyre!("Failed to parse path type: {ty:?}")); } let ident = &type_path.path.segments.last().unwrap().ident; let type_name = ident.to_string(); match type_name.as_str() { + "i8" => Ok("s8".to_string()), + "u8" => Ok("u8".to_string()), + "i16" => Ok("s16".to_string()), + "u16" => Ok("u16".to_string()), "i32" => Ok("s32".to_string()), "u32" => Ok("u32".to_string()), "i64" => Ok("s64".to_string()), @@ -132,10 +135,10 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result", inner_type)) } else { - Ok("list".to_string()) + Err(eyre!("Failed to parse Vec inner type")) } } else { - Ok("list".to_string()) + Err(eyre!("Failed to parse Vec inner type!")) } } "Option" => { @@ -146,36 +149,37 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result", inner_type)) } else { - Ok("option".to_string()) + Err(eyre!("Failed to parse Option inner type")) } } else { - Ok("option".to_string()) - } - } - "HashMap" | "BTreeMap" => { - if let syn::PathArguments::AngleBracketed(args) = - &type_path.path.segments.last().unwrap().arguments - { - if args.args.len() >= 2 { - if let ( - Some(syn::GenericArgument::Type(key_ty)), - Some(syn::GenericArgument::Type(val_ty)), - ) = (args.args.first(), args.args.get(1)) - { - let key_type = rust_type_to_wit(key_ty, used_types)?; - let val_type = rust_type_to_wit(val_ty, used_types)?; - // For HashMaps, we'll generate a list of tuples where each tuple contains a key and value - Ok(format!("list>", key_type, val_type)) - } else { - Ok("list>".to_string()) - } - } else { - Ok("list>".to_string()) - } - } else { - Ok("list>".to_string()) + Err(eyre!("Failed to parse Option inner type!")) } } + // TODO: fix and enable + //"HashMap" | "BTreeMap" => { + // if let syn::PathArguments::AngleBracketed(args) = + // &type_path.path.segments.last().unwrap().arguments + // { + // if args.args.len() >= 2 { + // if let ( + // Some(syn::GenericArgument::Type(key_ty)), + // Some(syn::GenericArgument::Type(val_ty)), + // ) = (args.args.first(), args.args.get(1)) + // { + // let key_type = rust_type_to_wit(key_ty, used_types)?; + // let val_type = rust_type_to_wit(val_ty, used_types)?; + // // For HashMaps, we'll generate a list of tuples where each tuple contains a key and value + // Ok(format!("list>", key_type, val_type)) + // } else { + // Ok("list>".to_string()) + // } + // } else { + // Ok("list>".to_string()) + // } + // } else { + // Ok("list>".to_string()) + // } + //} custom => { // Validate custom type name validate_name(custom, "Type")?; @@ -204,7 +208,7 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result", elem_types.join(", "))) } } - _ => Ok("unknown".to_string()), + _ => return Err(eyre!("Failed to parse type: {ty:?}")), } } @@ -296,7 +300,7 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result Result { println!(" Error converting parameter type: {}", e); - // Use a placeholder type for this parameter - struct_fields.push(format!(" {}: unknown", param_name)); + return Err(e); } } } Err(e) => { println!(" Skipping parameter with invalid name: {}", e); - // Use a placeholder for invalid parameter names - struct_fields.push(" invalid-param: unknown".to_string()); + return Err(e); } } } @@ -577,7 +578,7 @@ fn generate_signature_struct( } Err(e) => { println!(" Error converting return type: {}", e); - struct_fields.push(" returning: unknown".to_string()); + return Err(e); } }, _ => { From a0c9aeb4c14a3d8c2288e9093979b3e7ad43a5e8 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 8 Apr 2025 05:05:54 -0700 Subject: [PATCH 06/88] build: if no hyperapp api dir, make it --- src/build/mod.rs | 2 +- src/build/wit_generator.rs | 232 ++++++++++++++++++++++--------------- 2 files changed, 141 insertions(+), 93 deletions(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index 509a784b..577c5892 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1764,7 +1764,7 @@ pub async fn execute( if hyperapp { let api_dir = live_dir.join("api"); let (_processed_projects, _interfaces) = - wit_generator::generate_wit_files(&live_dir, &api_dir)?; + wit_generator::generate_wit_files(&live_dir, &api_dir, false)?; } let ui_dirs = get_ui_dirs(&live_dir, &include, &exclude)?; diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 4bf22a91..4971792a 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -613,7 +613,7 @@ impl AsTypePath for syn::Type { } // Process a single Rust project and generate WIT files -fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { +fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { println!("\nProcessing project: {}", project_path.display()); // Find lib.rs for this project @@ -864,60 +864,24 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result<(Vec, Vec)> { - // Find all relevant Rust projects - let projects = find_rust_projects(base_dir); - let mut processed_projects = Vec::new(); - - if projects.is_empty() { - println!("No relevant Rust projects found."); - return Ok((Vec::new(), Vec::new())); - } - - // Process each project and collect world imports - let mut new_imports = Vec::new(); - let mut interfaces = Vec::new(); - - for project_path in &projects { - println!("Processing project: {}", project_path.display()); - - match process_rust_project(project_path, api_dir) { - Ok(Some(import)) => { - println!("Got import statement: {}", import); - new_imports.push(import.clone()); - - // Extract interface name from import statement - let interface_name = import - .trim_start_matches(" import ") - .trim_end_matches(";") - .to_string(); - - interfaces.push(interface_name); - processed_projects.push(project_path.clone()); - } - Ok(None) => println!("No import statement generated"), - Err(e) => println!("Error processing project: {}", e), - } - } - - println!("Collected {} new imports", new_imports.len()); - - // Check for existing world definition files and update them - println!("Looking for existing world definition files"); - let mut updated_world = false; - +fn rewrite_wit( + api_dir: &Path, + new_imports: &Vec, + wit_worlds: &mut HashSet, + updated_world: &mut bool, +) -> Result<()> { for entry in WalkDir::new(api_dir) .max_depth(1) .into_iter() @@ -955,62 +919,146 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec = all_imports - .iter() - .map(|import| { - if import.starts_with(" ") { - import.clone() - } else { - format!(" {}", import.trim()) - } - }) - .collect(); - - let imports_section = all_imports_with_indent.join("\n"); - - // Create updated world content with proper indentation - let world_content = format!( - "world {} {{\n{}\n {}\n}}", - world_name, - imports_section, - include_line.trim() - ); - - println!("Writing updated world definition to {}", path.display()); - // Write the updated world file - fs::write(path, world_content).with_context(|| { - format!("Failed to write updated world file: {}", path.display()) - })?; - - println!("Successfully updated world definition"); - updated_world = true; + // Make sure all imports have proper indentation + let all_imports_with_indent: Vec = all_imports + .iter() + .map(|import| { + if import.starts_with(" ") { + import.clone() + } else { + format!(" {}", import.trim()) + } + }) + .collect(); + + let imports_section = all_imports_with_indent.join("\n"); + + // Create updated world content with proper indentation + let world_content = format!( + "world {} {{\n{}\n {}\n}}", + world_name, + imports_section, + include_line.trim() + ); + + println!("Writing updated world definition to {}", path.display()); + // Write the updated world file + fs::write(path, world_content).with_context(|| { + format!("Failed to write updated world file: {}", path.display()) + })?; + + println!("Successfully updated world definition"); + *updated_world = true; + } } } } } } + Ok(()) +} + +// Generate WIT files from Rust code +pub fn generate_wit_files( + base_dir: &Path, + api_dir: &Path, + is_recursive_call: bool, +) -> Result<(Vec, Vec)> { + fs::create_dir_all(&api_dir)?; + + // Find all relevant Rust projects + let projects = find_rust_projects(base_dir); + let mut processed_projects = Vec::new(); + + if projects.is_empty() { + println!("No relevant Rust projects found."); + return Ok((Vec::new(), Vec::new())); + } + + // Process each project and collect world imports + let mut new_imports = Vec::new(); + let mut interfaces = Vec::new(); + + let mut wit_worlds = HashSet::new(); + for project_path in &projects { + println!("Processing project: {}", project_path.display()); + + match process_rust_project(project_path, api_dir) { + Ok(Some((import, wit_world))) => { + println!("Got import statement: {}", import); + new_imports.push(import.clone()); + + // Extract interface name from import statement + let interface_name = import + .trim_start_matches(" import ") + .trim_end_matches(";") + .to_string(); + + interfaces.push(interface_name); + processed_projects.push(project_path.clone()); + + wit_worlds.insert(wit_world); + } + Ok(None) => println!("No import statement generated"), + Err(e) => println!("Error processing project: {}", e), + } + } + + println!("Collected {} new imports", new_imports.len()); + + // Check for existing world definition files and update them + println!("Looking for existing world definition files"); + let mut updated_world = false; + + rewrite_wit(api_dir, &new_imports, &mut wit_worlds, &mut updated_world)?; + + let rerun_rewrite_wit = !wit_worlds.is_empty(); + for wit_world in wit_worlds { + // Create a new file with the simple world definition + let new_file_path = api_dir.join(format!("{}.wit", wit_world)); + let simple_world_content = format!("world {} {{}}", wit_world); + + println!( + "Creating new world definition file: {}", + new_file_path.display() + ); + fs::write(&new_file_path, simple_world_content).with_context(|| { + format!( + "Failed to create new world file: {}", + new_file_path.display() + ) + })?; + + println!("Successfully created new world definition file"); + updated_world = true; + } + + if rerun_rewrite_wit && !is_recursive_call { + return generate_wit_files(base_dir, api_dir, true); + } // If no world definitions were found, create a default one if !updated_world && !new_imports.is_empty() { From 78a62c8a00715a8f4930cf9d3c8ef5355ca3fb9f Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 8 Apr 2025 20:22:27 -0700 Subject: [PATCH 07/88] build: get caller_utils working for deps --- src/build/caller_utils_generator.rs | 82 ++++++++++++-------------- src/build/mod.rs | 90 ++++++++++++++++++++--------- src/build/wit_generator.rs | 39 ++++++++++++- 3 files changed, 139 insertions(+), 72 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index f6819a5e..aaeed467 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -1,7 +1,12 @@ -use anyhow::{bail, Context, Result}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; + +use color_eyre::{ + eyre::{bail, WrapErr}, + Result, +}; + use toml::Value; use walkdir::WalkDir; @@ -29,9 +34,8 @@ pub fn to_pascal_case(s: &str) -> String { } // Find the world name in the world WIT file, prioritizing types-prefixed worlds -fn find_world_name(api_dir: &Path) -> Result { - let mut regular_world_name = None; - let mut types_world_name = None; +fn find_world_names(api_dir: &Path) -> Result> { + let mut world_names = Vec::new(); // Look for world definition files for entry in WalkDir::new(api_dir) @@ -60,11 +64,8 @@ fn find_world_name(api_dir: &Path) -> Result { // Check if this is a types-prefixed world if clean_name.starts_with("types-") { - types_world_name = Some(clean_name.to_string()); + world_names.push(clean_name.to_string()); println!("Found types world: {}", clean_name); - } else { - regular_world_name = Some(clean_name.to_string()); - println!("Found regular world: {}", clean_name); } } } @@ -73,32 +74,10 @@ fn find_world_name(api_dir: &Path) -> Result { } } - // Prioritize types-prefixed world if found - if let Some(types_name) = types_world_name { - return Ok(types_name); + if world_names.is_empty() { + bail!("No world name found in any WIT file. Cannot generate caller-utils without a world name.") } - - // If no types-prefixed world found, check if we have a regular world - if let Some(regular_name) = regular_world_name { - // Check if there's a corresponding types-prefixed world file - let types_name = format!("types-{}", regular_name); - let types_file = api_dir.join(format!("{}.wit", types_name)); - - if types_file.exists() { - println!("Found types world from file: {}", types_name); - return Ok(types_name); - } - - // Fall back to regular world but print a warning - println!( - "Warning: No types- world found, using regular world: {}", - regular_name - ); - return Ok(regular_name); - } - - // If no world name is found, we should fail - bail!("No world name found in any WIT file. Cannot generate caller-utils without a world name.") + Ok(world_names) } // Convert WIT type to Rust type - IMPROVED with more Rust primitives @@ -447,7 +426,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { }; return format!( - "/// Generated stub for `{}` {} RPC call\n/// HTTP endpoint - uncomment to implement\n// pub async fn {}({}) -> {} {{\n// // TODO: Implement HTTP endpoint\n// SendResult::Success({})\n// }}", + "// /// Generated stub for `{}` {} RPC call\n// /// HTTP endpoint - uncomment to implement\n// pub async fn {}({}) -> {} {{\n// // TODO: Implement HTTP endpoint\n// SendResult::Success({})\n// }}", signature.function_name, signature.attr_type, full_function_name, @@ -478,7 +457,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { // Generate function with implementation using send format!( - "/// Generated stub for `{}` {} RPC call\npub async fn {}({}) -> {} {{\n let request = {};\n send::<{}>(&request, target, 30).await\n}}", + "/// Generated stub for `{}` {} RPC call\npub async fn {}({}) -> {} {{\n let body = {};\n let body = serde_json::to_vec(&body).unwrap();\n let request = Request::to(target)\n .body(body);\n send::<{}>(request).await\n}}", signature.function_name, signature.attr_type, full_function_name, @@ -492,7 +471,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { // Create the caller-utils crate with a single lib.rs file fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { // Path to the new crate - let caller_utils_dir = base_dir.join("caller-utils"); + let caller_utils_dir = base_dir.join("crates").join("caller-utils"); println!( "Creating caller-utils crate at {}", caller_utils_dir.display() @@ -512,12 +491,11 @@ publish = false [dependencies] anyhow = "1.0" -hyperware_process_lib = { version = "1.0.4", features = ["logging"] } process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4e417e1" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } @@ -533,8 +511,22 @@ crate-type = ["cdylib", "lib"] println!("Created Cargo.toml for caller-utils"); // Get the world name (preferably the types- version) - let world_name = find_world_name(api_dir)?; - println!("Using world name for code generation: {}", world_name); + let world_names = find_world_names(api_dir)?; + println!("Using world names for code generation: {:?}", world_names); + let world_name = if world_names.len() == 0 { + "" + } else if world_names.len() == 1 { + &world_names[0] + } else { + let path = api_dir.join("types.wit"); + let mut content = "world types {\n".to_string(); + for world_name in world_names { + content.push_str(&format!(" include {world_name};\n")); + } + content.push_str("}\n"); + fs::write(&path, &content)?; + "types" + }; // Get all interfaces from the world file let interface_imports = find_interfaces_in_world(api_dir)?; @@ -632,7 +624,6 @@ crate-type = ["cdylib", "lib"] // Create single lib.rs with all modules inline let mut lib_rs = String::new(); - // Updated wit_bindgen usage with explicit world name - FIXED: Removed unused imports lib_rs.push_str("wit_bindgen::generate!({\n"); lib_rs.push_str(" path: \"target/wit\",\n"); lib_rs.push_str(&format!(" world: \"{}\",\n", world_name)); @@ -645,7 +636,8 @@ crate-type = ["cdylib", "lib"] // Add global imports lib_rs.push_str("pub use hyperware_app_common::SendResult;\n"); lib_rs.push_str("pub use hyperware_app_common::send;\n"); - lib_rs.push_str("use hyperware_process_lib::Address;\n"); + lib_rs.push_str("use hyperware_app_common::hyperware_process_lib as hyperware_process_lib;\n"); + lib_rs.push_str("use hyperware_process_lib::{Address, Request};\n"); lib_rs.push_str("use serde_json::json;\n\n"); // Add interface use statements @@ -752,11 +744,11 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { // Check if caller-utils is already in the members list let caller_utils_exists = members_array .iter() - .any(|m| m.as_str().map_or(false, |s| s == "caller-utils")); + .any(|m| m.as_str().map_or(false, |s| s == "crates/caller-utils")); if !caller_utils_exists { println!("Adding caller-utils to workspace members"); - members_array.push(Value::String("caller-utils".to_string())); + members_array.push(Value::String("crates/caller-utils".to_string())); // Write back the updated TOML let updated_content = toml::to_string_pretty(&parsed_toml) @@ -813,7 +805,7 @@ fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { let mut t = toml::map::Map::new(); t.insert( "path".to_string(), - Value::String("../caller-utils".to_string()), + Value::String("../crates/caller-utils".to_string()), ); t }), diff --git a/src/build/mod.rs b/src/build/mod.rs index 577c5892..8eb5044c 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -32,6 +32,7 @@ use crate::KIT_CACHE; mod rewrite; use rewrite::copy_and_rewrite_package; +mod caller_utils_generator; mod wit_generator; const PY_VENV_NAME: &str = "process_env"; @@ -1092,29 +1093,21 @@ async fn build_wit_dir( async fn compile_package_item( path: PathBuf, features: String, - apis: HashMap>, world: String, - wit_version: Option, + is_rust_process: bool, + is_py_process: bool, + is_js_process: bool, verbose: bool, ) -> Result<()> { - if path.is_dir() { - let is_rust_process = path.join(RUST_SRC_PATH).exists(); - let is_py_process = path.join(PYTHON_SRC_PATH).exists(); - let is_js_process = path.join(JAVASCRIPT_SRC_PATH).exists(); - if is_rust_process || is_py_process || is_js_process { - build_wit_dir(&path, &apis, wit_version).await?; - } - - if is_rust_process { - compile_rust_wasm_process(&path, &features, verbose).await?; - } else if is_py_process { - let python = get_python_version(None, None)? - .ok_or_else(|| eyre!("kit requires Python 3.10 or newer"))?; - compile_python_wasm_process(&path, &python, &world, verbose).await?; - } else if is_js_process { - let valid_node = get_newest_valid_node_version(None, None)?; - compile_javascript_wasm_process(&path, valid_node, &world, verbose).await?; - } + if is_rust_process { + compile_rust_wasm_process(&path, &features, verbose).await?; + } else if is_py_process { + let python = get_python_version(None, None)? + .ok_or_else(|| eyre!("kit requires Python 3.10 or newer"))?; + compile_python_wasm_process(&path, &python, &world, verbose).await?; + } else if is_js_process { + let valid_node = get_newest_valid_node_version(None, None)?; + compile_javascript_wasm_process(&path, valid_node, &world, verbose).await?; } Ok(()) } @@ -1539,6 +1532,7 @@ async fn compile_package( hyperapp: bool, force: bool, verbose: bool, + hyperapp_processed_projects: Option>, ignore_deps: bool, // for internal use; may cause problems when adding recursive deps ) -> Result<()> { let metadata = read_and_update_metadata(package_dir)?; @@ -1546,7 +1540,9 @@ async fn compile_package( let (mut apis, dependencies) = check_and_populate_dependencies(package_dir, &metadata, skip_deps_check, verbose).await?; + info!("dependencies: {dependencies:?}"); if !ignore_deps && !dependencies.is_empty() { + info!("fetching dependencies..."); fetch_dependencies( package_dir, &dependencies.iter().map(|s| s.to_string()).collect(), @@ -1564,7 +1560,7 @@ async fn compile_package( force, verbose, ) - .await?; + .await? } let wit_world = default_world @@ -1575,6 +1571,7 @@ async fn compile_package( let mut tasks = tokio::task::JoinSet::new(); let features = features.to_string(); + let mut to_compile = HashSet::new(); for entry in fs::read_dir(package_dir)? { let Ok(entry) = entry else { continue; @@ -1583,12 +1580,45 @@ async fn compile_package( if !is_cluded(&path, include, exclude) { continue; } + if !path.is_dir() { + continue; + } + + let is_rust_process = path.join(RUST_SRC_PATH).exists(); + let is_py_process = path.join(PYTHON_SRC_PATH).exists(); + let is_js_process = path.join(JAVASCRIPT_SRC_PATH).exists(); + if is_rust_process || is_py_process || is_js_process { + build_wit_dir(&path, &apis, metadata.properties.wit_version).await?; + } else { + continue; + } + to_compile.insert((path, is_rust_process, is_py_process, is_js_process)); + } + + // TODO: move process/target/wit -> target/wit + if !ignore_deps && !dependencies.is_empty() { + info!("{hyperapp_processed_projects:?}"); + if let Some(ref processed_projects) = hyperapp_processed_projects { + for processed_project in processed_projects { + let api_dir = processed_project.join("target").join("wit"); + info!("{processed_project:?} {api_dir:?}"); + caller_utils_generator::create_caller_utils( + package_dir, + &api_dir, + &[processed_project.clone()], + )?; + } + } + } + + for (path, is_rust_process, is_py_process, is_js_process) in to_compile { tasks.spawn(compile_package_item( path, features.clone(), - apis.clone(), wit_world.clone(), - metadata.properties.wit_version, + is_rust_process, + is_py_process, + is_js_process, verbose.clone(), )); } @@ -1761,11 +1791,18 @@ pub async fn execute( copy_and_rewrite_package(package_dir)? }; - if hyperapp { + let hyperapp_processed_projects = if !hyperapp { + None + } else { let api_dir = live_dir.join("api"); - let (_processed_projects, _interfaces) = + let (processed_projects, interfaces) = wit_generator::generate_wit_files(&live_dir, &api_dir, false)?; - } + if interfaces.is_empty() { + None + } else { + Some(processed_projects) + } + }; let ui_dirs = get_ui_dirs(&live_dir, &include, &exclude)?; if !no_ui && !ui_dirs.is_empty() { @@ -1796,6 +1833,7 @@ pub async fn execute( hyperapp, force, verbose, + hyperapp_processed_projects, ignore_deps, ) .await?; diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 4971792a..76a446bb 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -155,6 +155,29 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { + if let syn::PathArguments::AngleBracketed(args) = + &type_path.path.segments.last().unwrap().arguments + { + if args.args.len() >= 2 { + if let ( + Some(syn::GenericArgument::Type(ok_ty)), + Some(syn::GenericArgument::Type(err_ty)), + ) = (args.args.first(), args.args.get(1)) + { + let ok_type = rust_type_to_wit(ok_ty, used_types)?; + let err_type = rust_type_to_wit(err_ty, used_types)?; + Ok(format!("result<{}, {}>", ok_type, err_type)) + } else { + Err(eyre!("Failed to parse Result generic arguments")) + } + } else { + Err(eyre!("Result requires two type arguments")) + } + } else { + Err(eyre!("Failed to parse Result type arguments")) + } + } // TODO: fix and enable //"HashMap" | "BTreeMap" => { // if let syn::PathArguments::AngleBracketed(args) = @@ -920,7 +943,7 @@ fn rewrite_wit( println!("Extracted world name: {}", world_name); // Check if this world name matches the one we're looking for - if wit_worlds.remove(&world_name) { + if wit_worlds.remove(&world_name) || wit_worlds.contains(&world_name[6..]) { // Determine the include line based on world name // If world name starts with "types-", use "include lib;" instead if world_name.starts_with("types-") { @@ -1052,6 +1075,20 @@ pub fn generate_wit_files( ) })?; + let new_file_path = api_dir.join(format!("types-{}.wit", wit_world)); + let simple_world_content = format!("world types-{} {{}}", wit_world); + + println!( + "Creating new world definition file: {}", + new_file_path.display() + ); + fs::write(&new_file_path, simple_world_content).with_context(|| { + format!( + "Failed to create new world file: {}", + new_file_path.display() + ) + })?; + println!("Successfully created new world definition file"); updated_world = true; } From 39842593e3cd4288823da93bff3658d7e1fdd84a Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 8 Apr 2025 21:49:39 -0700 Subject: [PATCH 08/88] build: use latest macro --- src/build/caller_utils_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index aaeed467..84d6109b 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -495,7 +495,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4e417e1" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "f29952f" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } From 05f3f31c809300843f6207a565ff9ac484143cd2 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 9 Apr 2025 15:56:53 -0700 Subject: [PATCH 09/88] bump version to 1.1.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b5d746f..b2695d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2281,7 +2281,7 @@ dependencies = [ [[package]] name = "kit" -version = "1.0.2" +version = "1.1.0" dependencies = [ "alloy", "alloy-sol-macro", diff --git a/Cargo.toml b/Cargo.toml index 88c31954..6363bd78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kit" authors = ["Sybil Technologies AG"] -version = "1.0.2" +version = "1.1.0" edition = "2021" description = "Development toolkit for Hyperware" homepage = "https://hyperware.ai" From 66d43baf60fc59af34e5c34b67a6b1a2c38952ba Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 10 Apr 2025 10:03:37 -0700 Subject: [PATCH 10/88] allow tests to specify `hyperapp` bool --- src/run_tests/mod.rs | 6 +++--- src/run_tests/types.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/run_tests/mod.rs b/src/run_tests/mod.rs index cbdb1afd..3becc77e 100644 --- a/src/run_tests/mod.rs +++ b/src/run_tests/mod.rs @@ -378,7 +378,7 @@ async fn build_packages( dependency_package_paths.clone(), vec![], // TODO false, - false, + test.hyperapp.unwrap_or_default(), false, false, false, @@ -404,7 +404,7 @@ async fn build_packages( dependency_package_paths.clone(), vec![], // TODO false, - false, + test.hyperapp.unwrap_or_default(), false, false, false, @@ -427,7 +427,7 @@ async fn build_packages( dependency_package_paths.clone(), vec![], // TODO false, - false, + test.hyperapp.unwrap_or_default(), false, false, false, diff --git a/src/run_tests/types.rs b/src/run_tests/types.rs index 1735155e..b129fb17 100644 --- a/src/run_tests/types.rs +++ b/src/run_tests/types.rs @@ -32,6 +32,7 @@ pub struct Test { pub timeout_secs: u64, pub fakechain_router: u16, pub nodes: Vec, + pub hyperapp: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] From dca3ae8da2c428fbb44a30c15d1108027731b5b6 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 11 Apr 2025 22:58:15 -0700 Subject: [PATCH 11/88] build: consolidate target dir into one at package level --- src/build/mod.rs | 54 ++++++++++++++++++++++++++++++++++-------------- src/new/mod.rs | 6 +++--- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index 8eb5044c..3bfd4501 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -169,6 +169,23 @@ pub fn remove_missing_features(cargo_toml_path: &Path, features: Vec<&str>) -> R .collect()) } +#[instrument(level = "trace", skip_all)] +pub fn get_process_name(cargo_toml_path: &Path) -> Result { + let cargo_toml_content = fs::read_to_string(cargo_toml_path)?; + let cargo_toml: toml::Value = cargo_toml_content.parse()?; + + if let Some(process_name) = cargo_toml + .get("package") + .and_then(|p| p.get("name")) + .and_then(|n| n.as_str()) + { + let process_name = process_name.replace("_", "-"); + Ok(process_name.to_string()) + } else { + Err(eyre!("No package.name field in Cargo.toml at {cargo_toml_path:?}")) + } +} + /// Check if the first element is empty and there are no more elements #[instrument(level = "trace", skip_all)] fn is_only_empty_string(splitted: &Vec<&str>) -> bool { @@ -901,18 +918,22 @@ async fn compile_rust_wasm_process( features: &str, verbose: bool, ) -> Result<()> { + let Some(package_dir) = process_dir.parent() else { + return Err(eyre!("Could not derive package dir from process_dir ({process_dir:?}) parent")); + }; + let process_name = get_process_name(&process_dir.join("Cargo.toml"))?; info!("Compiling Rust Hyperware process in {:?}...", process_dir); // Paths - let wit_dir = process_dir.join("target").join("wit"); - let bindings_dir = process_dir + let wit_dir = package_dir.join("target").join("wit"); + let bindings_dir = package_dir .join("target") .join("bindings") - .join(process_dir.file_name().unwrap()); + .join(package_dir.file_name().unwrap()); fs::create_dir_all(&bindings_dir)?; // Check and download wasi_snapshot_preview1.wasm if it does not exist - let wasi_snapshot_file = process_dir + let wasi_snapshot_file = package_dir .join("target") .join("wasi_snapshot_preview1.wasm"); let wasi_snapshot_url = format!( @@ -935,6 +956,8 @@ async fn compile_rust_wasm_process( let mut args = vec![ "+stable", "build", + "-p", + &process_name, "--release", "--no-default-features", "--target", @@ -950,7 +973,7 @@ async fn compile_rust_wasm_process( } else { features.len() }; - let features = remove_missing_features(&process_dir.join("Cargo.toml"), features)?; + let features = remove_missing_features(&package_dir.join("Cargo.toml"), features)?; if !test_only && original_length != features.len() { info!( "process {:?} missing features; using {:?}", @@ -963,7 +986,7 @@ async fn compile_rust_wasm_process( args.push(&features); } let result = run_command( - Command::new("cargo").args(&args).current_dir(process_dir), + Command::new("cargo").args(&args).current_dir(package_dir), verbose, )?; @@ -981,9 +1004,9 @@ async fn compile_rust_wasm_process( // For use inside of process_dir // Run `wasm-tools component new`, putting output in pkg/ // and rewriting all `_`s to `-`s - // cargo hates `-`s and so outputs with `_`s; Kimap hates + // cargo hates `-`s and so outputs with `_`s; Hypermap hates // `_`s and so we convert to and enforce all `-`s - let wasm_file_name_cab = process_dir + let wasm_file_name_cab = package_dir .file_name() .and_then(|s| s.to_str()) .unwrap() @@ -993,7 +1016,7 @@ async fn compile_rust_wasm_process( let wasm_file_prefix = Path::new("target/wasm32-wasip1/release"); let wasm_file_cab = wasm_file_prefix.join(&format!("{wasm_file_name_cab}.wasm")); - let wasm_file_pkg = format!("../pkg/{wasm_file_name_hep}.wasm"); + let wasm_file_pkg = format!("pkg/{wasm_file_name_hep}.wasm"); let wasm_file_pkg = Path::new(&wasm_file_pkg); let wasi_snapshot_file = Path::new("target/wasi_snapshot_preview1.wasm"); @@ -1009,7 +1032,7 @@ async fn compile_rust_wasm_process( "--adapt", wasi_snapshot_file.to_str().unwrap(), ]) - .current_dir(process_dir), + .current_dir(package_dir), verbose, )?; @@ -1070,11 +1093,11 @@ async fn compile_and_copy_ui( #[instrument(level = "trace", skip_all)] async fn build_wit_dir( - process_dir: &Path, + package_dir: &Path, apis: &HashMap>, wit_version: Option, ) -> Result<()> { - let wit_dir = process_dir.join("target").join("wit"); + let wit_dir = package_dir.join("target").join("wit"); if wit_dir.exists() { fs::remove_dir_all(&wit_dir)?; } @@ -1569,6 +1592,8 @@ async fn compile_package( }) .to_string(); + build_wit_dir(&package_dir, &apis, metadata.properties.wit_version).await?; + let mut tasks = tokio::task::JoinSet::new(); let features = features.to_string(); let mut to_compile = HashSet::new(); @@ -1588,11 +1613,8 @@ async fn compile_package( let is_py_process = path.join(PYTHON_SRC_PATH).exists(); let is_js_process = path.join(JAVASCRIPT_SRC_PATH).exists(); if is_rust_process || is_py_process || is_js_process { - build_wit_dir(&path, &apis, metadata.properties.wit_version).await?; - } else { - continue; + to_compile.insert((path, is_rust_process, is_py_process, is_js_process)); } - to_compile.insert((path, is_rust_process, is_py_process, is_js_process)); } // TODO: move process/target/wit -> target/wit diff --git a/src/new/mod.rs b/src/new/mod.rs index 86b6013f..1b5f1e82 100644 --- a/src/new/mod.rs +++ b/src/new/mod.rs @@ -283,12 +283,12 @@ pub fn execute( if !is_hypermap_safe(&package_name, false) { let error = if !is_from_dir { eyre!( - "`package_name` '{}' must be Kimap safe (a-z, 0-9, - allowed).", + "`package_name` '{}' must be Hypermap safe (a-z, 0-9, - allowed).", package_name ) } else { eyre!( - "`package_name` (derived from given directory {:?}) '{}' must be Kimap safe (a-z, 0-9, - allowed).", + "`package_name` (derived from given directory {:?}) '{}' must be Hypermap safe (a-z, 0-9, - allowed).", new_dir, package_name, ) @@ -297,7 +297,7 @@ pub fn execute( } if !is_hypermap_safe(&publisher, true) { return Err(eyre!( - "`publisher` '{}' must be Kimap safe (a-z, 0-9, -, . allowed).", + "`publisher` '{}' must be Hypermap safe (a-z, 0-9, -, . allowed).", publisher )); } From 7884377105735616b281a7f38b907937ba339d92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 06:00:26 +0000 Subject: [PATCH 12/88] Format Rust code using rustfmt --- src/build/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index 3bfd4501..07696396 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -182,7 +182,9 @@ pub fn get_process_name(cargo_toml_path: &Path) -> Result { let process_name = process_name.replace("_", "-"); Ok(process_name.to_string()) } else { - Err(eyre!("No package.name field in Cargo.toml at {cargo_toml_path:?}")) + Err(eyre!( + "No package.name field in Cargo.toml at {cargo_toml_path:?}" + )) } } @@ -919,7 +921,9 @@ async fn compile_rust_wasm_process( verbose: bool, ) -> Result<()> { let Some(package_dir) = process_dir.parent() else { - return Err(eyre!("Could not derive package dir from process_dir ({process_dir:?}) parent")); + return Err(eyre!( + "Could not derive package dir from process_dir ({process_dir:?}) parent" + )); }; let process_name = get_process_name(&process_dir.join("Cargo.toml"))?; info!("Compiling Rust Hyperware process in {:?}...", process_dir); From 544ab348912327d45e4b5290b0cc20437303bc19 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 14 Apr 2025 17:22:34 -0700 Subject: [PATCH 13/88] move caller-utils into target --- src/build/caller_utils_generator.rs | 18 ++++++++---------- src/build/mod.rs | 19 ++++++------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 84d6109b..edcf9ce0 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -35,6 +35,7 @@ pub fn to_pascal_case(s: &str) -> String { // Find the world name in the world WIT file, prioritizing types-prefixed worlds fn find_world_names(api_dir: &Path) -> Result> { + println!("Looking in {api_dir:?} for world names..."); let mut world_names = Vec::new(); // Look for world definition files @@ -471,7 +472,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { // Create the caller-utils crate with a single lib.rs file fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { // Path to the new crate - let caller_utils_dir = base_dir.join("crates").join("caller-utils"); + let caller_utils_dir = base_dir.join("target").join("caller-utils"); println!( "Creating caller-utils crate at {}", caller_utils_dir.display() @@ -495,7 +496,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "f29952f" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "df8f395" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } @@ -744,11 +745,11 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { // Check if caller-utils is already in the members list let caller_utils_exists = members_array .iter() - .any(|m| m.as_str().map_or(false, |s| s == "crates/caller-utils")); + .any(|m| m.as_str().map_or(false, |s| s == "target/caller-utils")); if !caller_utils_exists { println!("Adding caller-utils to workspace members"); - members_array.push(Value::String("crates/caller-utils".to_string())); + members_array.push(Value::String("target/caller-utils".to_string())); // Write back the updated TOML let updated_content = toml::to_string_pretty(&parsed_toml) @@ -773,7 +774,7 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { } // Add caller-utils as a dependency to hyperware:process crates -fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { +pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { for project_path in projects { let cargo_toml_path = project_path.join("Cargo.toml"); println!( @@ -805,7 +806,7 @@ fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { let mut t = toml::map::Map::new(); t.insert( "path".to_string(), - Value::String("../crates/caller-utils".to_string()), + Value::String("../target/caller-utils".to_string()), ); t }), @@ -839,15 +840,12 @@ fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { } // Create caller-utils crate and integrate with the workspace -pub fn create_caller_utils(base_dir: &Path, api_dir: &Path, projects: &[PathBuf]) -> Result<()> { +pub fn create_caller_utils(base_dir: &Path, api_dir: &Path) -> Result<()> { // Step 1: Create the caller-utils crate create_caller_utils_crate(api_dir, base_dir)?; // Step 2: Update workspace Cargo.toml update_workspace_cargo_toml(base_dir)?; - // Step 3: Add caller-utils dependency to each hyperware:process project - add_caller_utils_to_projects(projects)?; - Ok(()) } diff --git a/src/build/mod.rs b/src/build/mod.rs index 07696396..d0748a54 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1621,19 +1621,12 @@ async fn compile_package( } } - // TODO: move process/target/wit -> target/wit - if !ignore_deps && !dependencies.is_empty() { - info!("{hyperapp_processed_projects:?}"); - if let Some(ref processed_projects) = hyperapp_processed_projects { - for processed_project in processed_projects { - let api_dir = processed_project.join("target").join("wit"); - info!("{processed_project:?} {api_dir:?}"); - caller_utils_generator::create_caller_utils( - package_dir, - &api_dir, - &[processed_project.clone()], - )?; - } + let api_dir = package_dir.join("target").join("wit"); + //info!("{processed_project:?} {api_dir:?}"); + if let Some(ref processed_projects) = hyperapp_processed_projects { + caller_utils_generator::create_caller_utils(package_dir, &api_dir)?; + for processed_project in processed_projects { + caller_utils_generator::add_caller_utils_to_projects(&[processed_project.clone()])?; } } From 7358ae61c8fa220baafec589d843b074f9356edc Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 14 Apr 2025 17:33:05 -0700 Subject: [PATCH 14/88] new: use updated path for wit bindgen --- src/new/templates/rust/no-ui/blank/blank/src/lib.rs | 2 +- src/new/templates/rust/no-ui/chat/chat/src/lib.rs | 6 ++++-- src/new/templates/rust/no-ui/chat/send/src/lib.rs | 10 +++++++--- src/new/templates/rust/no-ui/echo/echo/src/lib.rs | 2 +- .../rust/no-ui/fibonacci/fibonacci/src/lib.rs | 2 +- .../templates/rust/no-ui/fibonacci/number/src/lib.rs | 6 ++++-- .../rust/no-ui/file-transfer/download/src/lib.rs | 2 +- .../file-transfer/file-transfer-worker-api/src/lib.rs | 2 +- .../file-transfer/file-transfer-worker/src/lib.rs | 2 +- .../rust/no-ui/file-transfer/file-transfer/src/lib.rs | 2 +- .../rust/no-ui/file-transfer/list-files/src/lib.rs | 6 ++++-- src/new/templates/rust/ui/chat/chat/src/lib.rs | 2 +- 12 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/new/templates/rust/no-ui/blank/blank/src/lib.rs b/src/new/templates/rust/no-ui/blank/blank/src/lib.rs index d3ef2112..df935449 100644 --- a/src/new/templates/rust/no-ui/blank/blank/src/lib.rs +++ b/src/new/templates/rust/no-ui/blank/blank/src/lib.rs @@ -1,7 +1,7 @@ use hyperware_process_lib::{await_message, call_init, println, Address}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "process-v1", }); diff --git a/src/new/templates/rust/no-ui/chat/chat/src/lib.rs b/src/new/templates/rust/no-ui/chat/chat/src/lib.rs index 1a4988f4..f5089f9f 100644 --- a/src/new/templates/rust/no-ui/chat/chat/src/lib.rs +++ b/src/new/templates/rust/no-ui/chat/chat/src/lib.rs @@ -4,10 +4,12 @@ use crate::hyperware::process::chat::{ ChatMessage, Request as ChatRequest, Response as ChatResponse, SendRequest, }; use hyperware_process_lib::logging::{error, info, init_logging, Level}; -use hyperware_process_lib::{await_message, call_init, println, Address, Message, Request, Response}; +use hyperware_process_lib::{ + await_message, call_init, println, Address, Message, Request, Response, +}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "chat-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/chat/send/src/lib.rs b/src/new/templates/rust/no-ui/chat/send/src/lib.rs index 19942550..ea2a31a6 100644 --- a/src/new/templates/rust/no-ui/chat/send/src/lib.rs +++ b/src/new/templates/rust/no-ui/chat/send/src/lib.rs @@ -1,8 +1,12 @@ -use crate::hyperware::process::chat::{Request as ChatRequest, Response as ChatResponse, SendRequest}; -use hyperware_process_lib::{await_next_message_body, call_init, println, Address, Message, Request}; +use crate::hyperware::process::chat::{ + Request as ChatRequest, Response as ChatResponse, SendRequest, +}; +use hyperware_process_lib::{ + await_next_message_body, call_init, println, Address, Message, Request, +}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "chat-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize], diff --git a/src/new/templates/rust/no-ui/echo/echo/src/lib.rs b/src/new/templates/rust/no-ui/echo/echo/src/lib.rs index b2d367a6..842ddba7 100644 --- a/src/new/templates/rust/no-ui/echo/echo/src/lib.rs +++ b/src/new/templates/rust/no-ui/echo/echo/src/lib.rs @@ -2,7 +2,7 @@ use hyperware_process_lib::logging::{error, info, init_logging, Level}; use hyperware_process_lib::{await_message, call_init, println, Address, Message, Response}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "process-v1", }); diff --git a/src/new/templates/rust/no-ui/fibonacci/fibonacci/src/lib.rs b/src/new/templates/rust/no-ui/fibonacci/fibonacci/src/lib.rs index 22d97ee2..f9ba4376 100644 --- a/src/new/templates/rust/no-ui/fibonacci/fibonacci/src/lib.rs +++ b/src/new/templates/rust/no-ui/fibonacci/fibonacci/src/lib.rs @@ -5,7 +5,7 @@ use hyperware_process_lib::logging::{error, info, init_logging, Level}; use hyperware_process_lib::{await_message, call_init, Address, Message, Response}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "fibonacci-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/fibonacci/number/src/lib.rs b/src/new/templates/rust/no-ui/fibonacci/number/src/lib.rs index c5357da5..fd7d42a4 100644 --- a/src/new/templates/rust/no-ui/fibonacci/number/src/lib.rs +++ b/src/new/templates/rust/no-ui/fibonacci/number/src/lib.rs @@ -1,10 +1,12 @@ -use crate::hyperware::process::fibonacci::{Request as FibonacciRequest, Response as FibonacciResponse}; +use crate::hyperware::process::fibonacci::{ + Request as FibonacciRequest, Response as FibonacciResponse, +}; use hyperware_process_lib::{ await_next_message_body, call_init, println, Address, Message, Request, }; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "fibonacci-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize], diff --git a/src/new/templates/rust/no-ui/file-transfer/download/src/lib.rs b/src/new/templates/rust/no-ui/file-transfer/download/src/lib.rs index 50cf7d6e..2e99ea04 100644 --- a/src/new/templates/rust/no-ui/file-transfer/download/src/lib.rs +++ b/src/new/templates/rust/no-ui/file-transfer/download/src/lib.rs @@ -5,7 +5,7 @@ use hyperware_process_lib::{ }; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "file-transfer-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker-api/src/lib.rs b/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker-api/src/lib.rs index 1b6fa5b9..7d444673 100644 --- a/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker-api/src/lib.rs +++ b/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker-api/src/lib.rs @@ -5,7 +5,7 @@ use crate::hyperware::process::standard::Address as WitAddress; use hyperware_process_lib::{our_capabilities, spawn, Address, OnExit, Request, Response}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "file-transfer-worker-api-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker/src/lib.rs b/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker/src/lib.rs index aca5faef..bd87a312 100644 --- a/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker/src/lib.rs +++ b/src/new/templates/rust/no-ui/file-transfer/file-transfer-worker/src/lib.rs @@ -11,7 +11,7 @@ use hyperware_process_lib::{ }; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "file-transfer-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/file-transfer/file-transfer/src/lib.rs b/src/new/templates/rust/no-ui/file-transfer/file-transfer/src/lib.rs index 3a8a70c4..ec8e8715 100644 --- a/src/new/templates/rust/no-ui/file-transfer/file-transfer/src/lib.rs +++ b/src/new/templates/rust/no-ui/file-transfer/file-transfer/src/lib.rs @@ -14,7 +14,7 @@ use hyperware_process_lib::{ }; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "file-transfer-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/file-transfer/list-files/src/lib.rs b/src/new/templates/rust/no-ui/file-transfer/list-files/src/lib.rs index 66076f9f..284a3182 100644 --- a/src/new/templates/rust/no-ui/file-transfer/list-files/src/lib.rs +++ b/src/new/templates/rust/no-ui/file-transfer/list-files/src/lib.rs @@ -1,10 +1,12 @@ use crate::hyperware::process::file_transfer::{ Request as TransferRequest, Response as TransferResponse, }; -use hyperware_process_lib::{await_next_message_body, call_init, println, Address, Message, Request}; +use hyperware_process_lib::{ + await_next_message_body, call_init, println, Address, Message, Request, +}; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "file-transfer-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/ui/chat/chat/src/lib.rs b/src/new/templates/rust/ui/chat/chat/src/lib.rs index a8d4b455..9a56bb2a 100644 --- a/src/new/templates/rust/ui/chat/chat/src/lib.rs +++ b/src/new/templates/rust/ui/chat/chat/src/lib.rs @@ -14,7 +14,7 @@ use hyperware_process_lib::{ }; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "chat-template-dot-os-v0", generate_unused_types: true, additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], From 96b0f5cad4a815e79144586ede6e924dd6029463 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 14 Apr 2025 20:55:55 -0700 Subject: [PATCH 15/88] new: update tests --- .../chat/test/chat-test/chat-test/src/lib.rs | 30 ++++++++++++------- .../echo/test/echo-test/echo-test/src/lib.rs | 2 +- .../fibonacci-test/fibonacci-test/src/lib.rs | 30 ++++++++++++------- .../file-transfer-test/src/lib.rs | 2 +- .../chat/test/chat-test/chat-test/src/lib.rs | 30 ++++++++++++------- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/new/templates/rust/no-ui/chat/test/chat-test/chat-test/src/lib.rs b/src/new/templates/rust/no-ui/chat/test/chat-test/chat-test/src/lib.rs index c0ebfcfb..41dbdc88 100644 --- a/src/new/templates/rust/no-ui/chat/test/chat-test/chat-test/src/lib.rs +++ b/src/new/templates/rust/no-ui/chat/test/chat-test/chat-test/src/lib.rs @@ -1,18 +1,24 @@ -use crate::hyperware::process::chat::{ChatMessage, Request as ChatRequest, Response as ChatResponse, SendRequest}; -use crate::hyperware::process::tester::{Request as TesterRequest, Response as TesterResponse, RunRequest, FailResponse}; +use crate::hyperware::process::chat::{ + ChatMessage, Request as ChatRequest, Response as ChatResponse, SendRequest, +}; +use crate::hyperware::process::tester::{ + FailResponse, Request as TesterRequest, Response as TesterResponse, RunRequest, +}; -use hyperware_process_lib::{await_message, call_init, print_to_terminal, println, Address, ProcessId, Request, Response}; +use hyperware_process_lib::{ + await_message, call_init, print_to_terminal, println, Address, ProcessId, Request, Response, +}; mod tester_lib; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "chat-test-template-dot-os-v0", generate_unused_types: true, additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], }); -fn handle_message (our: &Address) -> anyhow::Result<()> { +fn handle_message(our: &Address) -> anyhow::Result<()> { let message = await_message().unwrap(); if !message.is_request() { @@ -60,15 +66,19 @@ fn handle_message (our: &Address) -> anyhow::Result<()> { target: node_names[1].clone(), message: message.clone(), })) - .send_and_await_response(15)?.unwrap(); + .send_and_await_response(15)? + .unwrap(); // Get history from receiver & test print_to_terminal(0, "chat_test: c"); let response = Request::new() .target(their_chat_address.clone()) .body(ChatRequest::History(our.node.clone())) - .send_and_await_response(15)?.unwrap(); - if response.is_request() { fail!("chat_test"); }; + .send_and_await_response(15)? + .unwrap(); + if response.is_request() { + fail!("chat_test"); + }; let ChatResponse::History(messages) = response.body().try_into()? else { fail!("chat_test"); }; @@ -96,12 +106,12 @@ fn init(our: Address) { loop { match handle_message(&our) { - Ok(()) => {}, + Ok(()) => {} Err(e) => { print_to_terminal(0, format!("chat_test: error: {e:?}").as_str()); fail!("chat_test"); - }, + } }; } } diff --git a/src/new/templates/rust/no-ui/echo/test/echo-test/echo-test/src/lib.rs b/src/new/templates/rust/no-ui/echo/test/echo-test/echo-test/src/lib.rs index 34d9f9cd..87c15c29 100644 --- a/src/new/templates/rust/no-ui/echo/test/echo-test/echo-test/src/lib.rs +++ b/src/new/templates/rust/no-ui/echo/test/echo-test/echo-test/src/lib.rs @@ -9,7 +9,7 @@ use hyperware_process_lib::{ mod tester_lib; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "echo-test-template-dot-os-v0", generate_unused_types: true, additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/no-ui/fibonacci/test/fibonacci-test/fibonacci-test/src/lib.rs b/src/new/templates/rust/no-ui/fibonacci/test/fibonacci-test/fibonacci-test/src/lib.rs index c006603a..dc990637 100644 --- a/src/new/templates/rust/no-ui/fibonacci/test/fibonacci-test/fibonacci-test/src/lib.rs +++ b/src/new/templates/rust/no-ui/fibonacci/test/fibonacci-test/fibonacci-test/src/lib.rs @@ -1,12 +1,16 @@ use crate::hyperware::process::fibonacci::{Request as FibRequest, Response as FibResponse}; -use crate::hyperware::process::tester::{Request as TesterRequest, Response as TesterResponse, RunRequest, FailResponse}; +use crate::hyperware::process::tester::{ + FailResponse, Request as TesterRequest, Response as TesterResponse, RunRequest, +}; -use hyperware_process_lib::{await_message, call_init, print_to_terminal, Address, ProcessId, Request, Response}; +use hyperware_process_lib::{ + await_message, call_init, print_to_terminal, Address, ProcessId, Request, Response, +}; mod tester_lib; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "fibonacci-test-template-dot-os-v0", generate_unused_types: true, additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], @@ -16,8 +20,11 @@ fn test_number(n: u32, address: &Address) -> anyhow::Result { let response = Request::new() .target(address) .body(FibRequest::Number(n)) - .send_and_await_response(15)?.unwrap(); - if response.is_request() { fail!("fibonacci_test"); }; + .send_and_await_response(15)? + .unwrap(); + if response.is_request() { + fail!("fibonacci_test"); + }; let FibResponse::Number(fib_number) = response.body().try_into()? else { fail!("fibonacci_test"); }; @@ -28,15 +35,18 @@ fn test_numbers(n: u32, n_trials: u32, address: &Address) -> anyhow::Result let response = Request::new() .target(address) .body(FibRequest::Numbers((n, n_trials))) - .send_and_await_response(15)?.unwrap(); - if response.is_request() { fail!("fibonacci_test"); }; + .send_and_await_response(15)? + .unwrap(); + if response.is_request() { + fail!("fibonacci_test"); + }; let FibResponse::Numbers((fib_number, _)) = response.body().try_into()? else { fail!("fibonacci_test"); }; Ok(fib_number) } -fn handle_message (our: &Address) -> anyhow::Result<()> { +fn handle_message(our: &Address) -> anyhow::Result<()> { let message = await_message().unwrap(); if !message.is_request() { @@ -93,12 +103,12 @@ fn init(our: Address) { loop { match handle_message(&our) { - Ok(()) => {}, + Ok(()) => {} Err(e) => { print_to_terminal(0, format!("fibonacci_test: error: {e:?}").as_str()); fail!("fibonacci_test"); - }, + } }; } } diff --git a/src/new/templates/rust/no-ui/file-transfer/test/file-transfer-test/file-transfer-test/src/lib.rs b/src/new/templates/rust/no-ui/file-transfer/test/file-transfer-test/file-transfer-test/src/lib.rs index 8a3ec5b3..19cee03e 100644 --- a/src/new/templates/rust/no-ui/file-transfer/test/file-transfer-test/file-transfer-test/src/lib.rs +++ b/src/new/templates/rust/no-ui/file-transfer/test/file-transfer-test/file-transfer-test/src/lib.rs @@ -17,7 +17,7 @@ use hyperware_process_lib::{ mod tester_lib; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "file-transfer-test-template-dot-os-v0", generate_unused_types: true, additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], diff --git a/src/new/templates/rust/ui/chat/test/chat-test/chat-test/src/lib.rs b/src/new/templates/rust/ui/chat/test/chat-test/chat-test/src/lib.rs index c0ebfcfb..41dbdc88 100644 --- a/src/new/templates/rust/ui/chat/test/chat-test/chat-test/src/lib.rs +++ b/src/new/templates/rust/ui/chat/test/chat-test/chat-test/src/lib.rs @@ -1,18 +1,24 @@ -use crate::hyperware::process::chat::{ChatMessage, Request as ChatRequest, Response as ChatResponse, SendRequest}; -use crate::hyperware::process::tester::{Request as TesterRequest, Response as TesterResponse, RunRequest, FailResponse}; +use crate::hyperware::process::chat::{ + ChatMessage, Request as ChatRequest, Response as ChatResponse, SendRequest, +}; +use crate::hyperware::process::tester::{ + FailResponse, Request as TesterRequest, Response as TesterResponse, RunRequest, +}; -use hyperware_process_lib::{await_message, call_init, print_to_terminal, println, Address, ProcessId, Request, Response}; +use hyperware_process_lib::{ + await_message, call_init, print_to_terminal, println, Address, ProcessId, Request, Response, +}; mod tester_lib; wit_bindgen::generate!({ - path: "target/wit", + path: "../target/wit", world: "chat-test-template-dot-os-v0", generate_unused_types: true, additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], }); -fn handle_message (our: &Address) -> anyhow::Result<()> { +fn handle_message(our: &Address) -> anyhow::Result<()> { let message = await_message().unwrap(); if !message.is_request() { @@ -60,15 +66,19 @@ fn handle_message (our: &Address) -> anyhow::Result<()> { target: node_names[1].clone(), message: message.clone(), })) - .send_and_await_response(15)?.unwrap(); + .send_and_await_response(15)? + .unwrap(); // Get history from receiver & test print_to_terminal(0, "chat_test: c"); let response = Request::new() .target(their_chat_address.clone()) .body(ChatRequest::History(our.node.clone())) - .send_and_await_response(15)?.unwrap(); - if response.is_request() { fail!("chat_test"); }; + .send_and_await_response(15)? + .unwrap(); + if response.is_request() { + fail!("chat_test"); + }; let ChatResponse::History(messages) = response.body().try_into()? else { fail!("chat_test"); }; @@ -96,12 +106,12 @@ fn init(our: Address) { loop { match handle_message(&our) { - Ok(()) => {}, + Ok(()) => {} Err(e) => { print_to_terminal(0, format!("chat_test: error: {e:?}").as_str()); fail!("chat_test"); - }, + } }; } } From e52652b2088a61ef512e38e185a2edaa9f678a6c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 16 Apr 2025 21:13:23 -0700 Subject: [PATCH 16/88] build: use Result instead of SendResult --- src/build/caller_utils_generator.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index edcf9ce0..05578caf 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -384,8 +384,8 @@ fn generate_async_function(signature: &SignatureStruct) -> String { ) + ¶ms.join(", ") }; - // Wrap the return type in SendResult - let wrapped_return_type = format!("SendResult<{}>", return_type); + // Wrap the return type in a Result<_, AppSendError> + let wrapped_return_type = format!("Result<{}, AppSendError>", return_type); // For HTTP endpoints, generate commented-out implementation if signature.attr_type == "http" { @@ -427,7 +427,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { }; return format!( - "// /// Generated stub for `{}` {} RPC call\n// /// HTTP endpoint - uncomment to implement\n// pub async fn {}({}) -> {} {{\n// // TODO: Implement HTTP endpoint\n// SendResult::Success({})\n// }}", + "// /// Generated stub for `{}` {} RPC call\n// /// HTTP endpoint - uncomment to implement\n// pub async fn {}({}) -> {} {{\n// // TODO: Implement HTTP endpoint\n// Ok({})\n// }}", signature.function_name, signature.attr_type, full_function_name, @@ -496,7 +496,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "df8f395" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "a485cd1" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } @@ -635,7 +635,7 @@ crate-type = ["cdylib", "lib"] lib_rs.push_str("/// Generated caller utilities for RPC function stubs\n\n"); // Add global imports - lib_rs.push_str("pub use hyperware_app_common::SendResult;\n"); + lib_rs.push_str("pub use hyperware_app_common::AppSendError;\n"); lib_rs.push_str("pub use hyperware_app_common::send;\n"); lib_rs.push_str("use hyperware_app_common::hyperware_process_lib as hyperware_process_lib;\n"); lib_rs.push_str("use hyperware_process_lib::{Address, Request};\n"); From 9818709db6fd3978443ca996d332c88a172f6981 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 16 Apr 2025 21:20:20 -0700 Subject: [PATCH 17/88] build: use latest hyperprocess-macro --- src/build/caller_utils_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 05578caf..fd13fcfe 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -496,7 +496,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "a485cd1" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "034d339" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } From 12207754edb2afbf357a88b715a26217005bd483 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:49:06 -0400 Subject: [PATCH 18/88] WIP --- src/build/wit_generator.rs | 345 +++++++++++++++++++++---------------- 1 file changed, 194 insertions(+), 151 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 76a446bb..1aee2799 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -259,10 +259,13 @@ fn find_rust_files(crate_path: &Path) -> Vec { rust_files } -// Collect type definitions (structs and enums) from a file -fn collect_type_definitions_from_file(file_path: &Path) -> Result> { +// Collect **only used** type definitions (structs and enums) from a file +fn collect_type_definitions_from_file( + file_path: &Path, + used_types: &HashSet, // Accept the set of used types +) -> Result> { println!( - "Collecting type definitions from file: {}", + "Collecting used type definitions from file: {}", // Updated message file_path.display() ); @@ -282,6 +285,7 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result Result { // Use kebab-case for struct name let name = to_kebab_case(&orig_name); - println!(" Found struct: {} -> {}", orig_name, name); + // --- Check if this type is used --- + if !used_types.contains(&name) { + // println!(" Skipping unused struct: {} -> {}", orig_name, name); // Optional debug log + continue; // Skip this struct if not in the used set + } + // --- End Check --- + + println!(" Found used struct: {} -> {}", orig_name, name); // Updated message + + + // Proceed with field processing only if the struct is used let fields: Vec = match &item_struct.fields { syn::Fields::Named(fields) => { - let mut used_types = HashSet::new(); + // Note: The `rust_type_to_wit` calls here still use a *local* `used_types` + // set for *recursive* type discovery *within this struct's definition*. + // This is necessary for correctly formatting types like list. + // The main `used_types` set (passed as argument) determines *if* this struct + // definition is included at all. + let mut local_used_types_for_fields = HashSet::new(); // Renamed for clarity let mut field_strings = Vec::new(); for f in &fields.named { if let Some(field_ident) = &f.ident { - // Validate field name doesn't contain digits let field_orig_name = field_ident.to_string(); - match validate_name(&field_orig_name, "Field") { Ok(_) => { - // Convert field names to kebab-case let field_name = to_kebab_case(&field_orig_name); - - // Skip if field conversion failed if field_name.is_empty() { println!(" Skipping field with empty name conversion"); continue; } + // This call populates `local_used_types_for_fields` if needed, + // but its primary goal here is WIT type string generation. let field_type = match rust_type_to_wit( &f.ty, - &mut used_types, + &mut local_used_types_for_fields, // Pass the local set ) { Ok(ty) => ty, Err(e) => { @@ -323,6 +339,7 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result Result Vec::new(), + _ => Vec::new(), // Handle tuple structs, unit structs if needed }; + // Add the struct definition only if it has fields (or adjust logic if empty records are valid) if !fields.is_empty() { type_defs.insert( name.clone(), format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), ); + } else { + println!(" Skipping used struct {} with no convertible fields", name); } } Err(e) => { + // Struct name validation failed, skip regardless of usage println!(" Skipping struct with invalid name: {}", e); continue; } @@ -369,7 +390,7 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result Result { // Use kebab-case for enum name let name = to_kebab_case(&orig_name); - println!(" Found enum: {} -> {}", orig_name, name); + // --- Check if this type is used --- + if !used_types.contains(&name) { + // println!(" Skipping unused enum: {} -> {}", orig_name, name); // Optional debug log + continue; // Skip this enum if not in the used set + } + // --- End Check --- + + println!(" Found used enum: {} -> {}", orig_name, name); // Updated message + + // Proceed with variant processing only if the enum is used let mut variants = Vec::new(); let mut skip_enum = false; for v in &item_enum.variants { let variant_orig_name = v.ident.to_string(); - - // Validate variant name match validate_name(&variant_orig_name, "Enum variant") { Ok(_) => { match &v.fields { syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { - let mut used_types = HashSet::new(); - + // Similar to structs, use a local set for inner type resolution + let mut local_used_types_for_variant = HashSet::new(); match rust_type_to_wit( &fields.unnamed.first().unwrap().ty, - &mut used_types, + &mut local_used_types_for_variant, // Pass local set ) { Ok(ty) => { - // Use kebab-case for variant names and use parentheses for type let variant_name = to_kebab_case(&variant_orig_name); println!( @@ -418,12 +445,12 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result { - // Use kebab-case for variant names let variant_name = to_kebab_case(&variant_orig_name); println!( " Variant: {} -> {}", @@ -433,24 +460,23 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result { println!( - " Skipping complex variant: {}", - variant_orig_name + " Skipping complex variant in used enum {}: {}", + name, variant_orig_name ); - // Complex variants with multiple fields aren't directly supported in WIT - // For simplicity, we'll skip enums with complex variants - skip_enum = true; + skip_enum = true; // Skip the whole enum if one variant is complex break; } } } Err(e) => { - println!(" Skipping variant with invalid name: {}", e); - skip_enum = true; + println!(" Skipping variant with invalid name in used enum {}: {}", name, e); + skip_enum = true; // Skip the whole enum if one variant name is invalid break; } } } + // Add the enum definition only if it wasn't skipped and has variants if !skip_enum && !variants.is_empty() { type_defs.insert( name.clone(), @@ -460,19 +486,22 @@ fn collect_type_definitions_from_file(file_path: &Path) -> Result { + // Enum name validation failed, skip regardless of usage println!(" Skipping enum with invalid name: {}", e); continue; } } } - _ => {} + _ => {} // Handle other top-level items like functions, impls, etc. if needed } } - println!("Collected {} type definitions from file", type_defs.len()); + println!("Collected {} used type definitions from this file", type_defs.len()); // Updated message Ok(type_defs) } @@ -650,29 +679,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - for (name, def) in file_type_defs { - all_type_defs.insert(name, def); - } - } - Err(e) => { - println!( - "Error collecting type definitions from {}: {}", - file_path.display(), - e - ); - // Continue with other files - } - } - } - - println!("Collected {} total type definitions", all_type_defs.len()); - - // Parse lib.rs to find the hyperprocess attribute and interface details + // Parse lib.rs to find the hyperprocess attribute and interface details first let lib_content = fs::read_to_string(&lib_rs).with_context(|| { format!( "Failed to read lib.rs for project: {}", @@ -723,7 +730,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result println!("Failed to extract wit_world: {}", e), @@ -746,112 +754,141 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - // Convert function name to kebab-case - let kebab_name = to_kebab_case(&method_name); - println!(" Processing method: {} -> {}", method_name, kebab_name); - - // Generate a signature struct for each attribute type - if has_remote { - match generate_signature_struct( - &kebab_name, - "remote", - method, - &mut used_types, - ) { - Ok(remote_struct) => signature_structs.push(remote_struct), - Err(e) => println!( - " Error generating remote signature struct: {}", - e - ), + // Prepare to collect signature structs and used types + let mut signature_structs = Vec::new(); + let mut used_types = HashSet::new(); // This will be populated now + + // Analyze the functions within the identified impl block (if found) + if let Some(ref impl_item) = &impl_item_with_hyperprocess { + if let Some(ref _kebab_name) = &kebab_interface_name { // Ensure kebab_name is available but acknowledge unused in this block + for item in &impl_item.items { + if let ImplItem::Fn(method) = item { + let method_name = method.sig.ident.to_string(); + println!(" Examining method: {}", method_name); + + // Check for attribute types + let has_remote = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("remote")); + let has_local = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("local")); + let has_http = method.attrs.iter().any(|attr| attr.path().is_ident("http")); + + if has_remote || has_local || has_http { + println!( + " Has relevant attributes: remote={}, local={}, http={}", + has_remote, has_local, has_http + ); + + // Validate function name + match validate_name(&method_name, "Function") { + Ok(_) => { + // Convert function name to kebab-case + let func_kebab_name = to_kebab_case(&method_name); // Use different var name + println!(" Processing method: {} -> {}", method_name, func_kebab_name); + + // Generate a signature struct for each attribute type + // This will populate `used_types` + if has_remote { + match generate_signature_struct( + &func_kebab_name, // Pass func kebab name + "remote", + method, + &mut used_types, // Pass the main set + ) { + Ok(remote_struct) => signature_structs.push(remote_struct), + Err(e) => println!( + " Error generating remote signature struct: {}", + e + ), + } } - } - if has_local { - match generate_signature_struct( - &kebab_name, - "local", - method, - &mut used_types, - ) { - Ok(local_struct) => signature_structs.push(local_struct), - Err(e) => println!( - " Error generating local signature struct: {}", - e - ), + if has_local { + match generate_signature_struct( + &func_kebab_name, // Pass func kebab name + "local", + method, + &mut used_types, // Pass the main set + ) { + Ok(local_struct) => signature_structs.push(local_struct), + Err(e) => println!( + " Error generating local signature struct: {}", + e + ), + } } - } - if has_http { - match generate_signature_struct( - &kebab_name, - "http", - method, - &mut used_types, - ) { - Ok(http_struct) => signature_structs.push(http_struct), - Err(e) => println!( - " Error generating HTTP signature struct: {}", - e - ), + if has_http { + match generate_signature_struct( + &func_kebab_name, // Pass func kebab name + "http", + method, + &mut used_types, // Pass the main set + ) { + Ok(http_struct) => signature_structs.push(http_struct), + Err(e) => println!( + " Error generating HTTP signature struct: {}", + e + ), + } } } + Err(e) => { + println!(" Skipping method with invalid name: {}", e); + } } - Err(e) => { - println!(" Skipping method with invalid name: {}", e); - } + } else { + println!(" Skipping method without relevant attributes"); } - } else { - println!(" Skipping method without relevant attributes"); } } } + } + println!("Identified {} used types from function signatures.", used_types.len()); - // Include all defined types, not just the ones used in interface functions - println!("Including all defined types ({})", all_type_defs.len()); + // Collect **only used** type definitions from all Rust files + let mut all_type_defs = HashMap::new(); // Now starts empty, filled by collector + for file_path in &rust_files { + // Pass the populated used_types set to the collector + match collect_type_definitions_from_file(file_path, &used_types) { + Ok(file_type_defs) => { + for (name, def) in file_type_defs { + // Since the collector only returns used types, we can insert directly + all_type_defs.insert(name, def); + } + } + Err(e) => { + println!( + "Error collecting type definitions from {}: {}", + file_path.display(), + e + ); + // Continue with other files + } + } + } + + println!("Collected {} used type definitions", all_type_defs.len()); - // Convert all type definitions to a vector - let mut type_defs: Vec = all_type_defs.values().cloned().collect(); + // Now generate the WIT content for the interface + if let (Some(ref iface_name), Some(ref kebab_name), Some(ref _impl_item)) = ( // impl_item no longer needed here + &interface_name, + &kebab_interface_name, + &impl_item_with_hyperprocess, // Keep this condition to ensure an interface was found + ) { + // No need to filter anymore, all_type_defs contains only used types + let mut type_defs: Vec = all_type_defs.into_values().collect(); // Collect values directly - // Sort them for consistent output - type_defs.sort(); + type_defs.sort(); // Sort for consistent output + println!("Including {} used type definitions", type_defs.len()); // Generate the final WIT content - if signature_structs.is_empty() { - println!("No functions found for interface {}", iface_name); + if signature_structs.is_empty() && type_defs.is_empty() { // Check both sigs and types + println!("No functions or used types found for interface {}", iface_name); } else { // Start with the interface comment let mut content = " // This interface contains function signature definitions that will be used\n // by the hyper-bindgen macro to generate async function bindings.\n //\n // NOTE: This is currently a hacky workaround since WIT async functions are not\n // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware,\n // we should switch to using proper async WIT function signatures instead of\n // this struct-based approach with hyper-bindgen generating the async stubs.\n".to_string(); @@ -865,15 +902,18 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result Date: Thu, 17 Apr 2025 16:50:45 +0000 Subject: [PATCH 19/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 41 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 1aee2799..8de1aa00 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -304,7 +304,6 @@ fn collect_type_definitions_from_file( println!(" Found used struct: {} -> {}", orig_name, name); // Updated message - // Proceed with field processing only if the struct is used let fields: Vec = match &item_struct.fields { syn::Fields::Named(fields) => { @@ -376,7 +375,7 @@ fn collect_type_definitions_from_file( format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), ); } else { - println!(" Skipping used struct {} with no convertible fields", name); + println!(" Skipping used struct {} with no convertible fields", name); } } Err(e) => { @@ -487,7 +486,7 @@ fn collect_type_definitions_from_file( ), ); } else { - println!(" Skipping used enum {} due to complex/invalid variants or no variants", name); + println!(" Skipping used enum {} due to complex/invalid variants or no variants", name); } } Err(e) => { @@ -501,7 +500,10 @@ fn collect_type_definitions_from_file( } } - println!("Collected {} used type definitions from this file", type_defs.len()); // Updated message + println!( + "Collected {} used type definitions from this file", + type_defs.len() + ); // Updated message Ok(type_defs) } @@ -760,7 +762,8 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { // Convert function name to kebab-case let func_kebab_name = to_kebab_case(&method_name); // Use different var name - println!(" Processing method: {} -> {}", method_name, func_kebab_name); + println!( + " Processing method: {} -> {}", + method_name, func_kebab_name + ); // Generate a signature struct for each attribute type // This will populate `used_types` @@ -848,7 +854,10 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result Result Result Date: Thu, 17 Apr 2025 10:59:42 -0700 Subject: [PATCH 20/88] build: fix target consolidation regression --- src/build/caller_utils_generator.rs | 2 +- src/build/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index fd13fcfe..a4ec673f 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -496,7 +496,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "034d339" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "47400ab" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } diff --git a/src/build/mod.rs b/src/build/mod.rs index d0748a54..7fe7198b 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1010,7 +1010,7 @@ async fn compile_rust_wasm_process( // and rewriting all `_`s to `-`s // cargo hates `-`s and so outputs with `_`s; Hypermap hates // `_`s and so we convert to and enforce all `-`s - let wasm_file_name_cab = package_dir + let wasm_file_name_cab = process_dir .file_name() .and_then(|s| s.to_str()) .unwrap() From b9b62dfbd3a1ace0febebf22506e6c75c8baac8a Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:11:35 -0400 Subject: [PATCH 21/88] updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b2c30b0a..03f07a72 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ src/new/includes.rs src/new/templates/**/Cargo.lock src/new/templates/**/*.wasm src/new/templates/**/*.zip +**/.DS_Store From c7775e04c23dcb3b761114dd2ec5ee6d13036afe Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 17 Apr 2025 15:30:44 -0700 Subject: [PATCH 22/88] build: de-hack-ify wit api generation --- src/build/mod.rs | 2 +- src/build/wit_generator.rs | 560 ++++++++++++++++++------------------- 2 files changed, 280 insertions(+), 282 deletions(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index 7fe7198b..0b6a760a 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1815,7 +1815,7 @@ pub async fn execute( } else { let api_dir = live_dir.join("api"); let (processed_projects, interfaces) = - wit_generator::generate_wit_files(&live_dir, &api_dir, false)?; + wit_generator::generate_wit_files(&live_dir, &api_dir)?; if interfaces.is_empty() { None } else { diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 8de1aa00..e9a832ae 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -519,38 +519,44 @@ fn find_rust_projects(base_dir: &Path) -> Vec { { let path = entry.path(); - if path.is_dir() && path != base_dir { - let cargo_toml = path.join("Cargo.toml"); - println!("Checking {}", cargo_toml.display()); - - if cargo_toml.exists() { - // Try to read and parse Cargo.toml - if let Ok(content) = fs::read_to_string(&cargo_toml) { - if let Ok(cargo_data) = content.parse::() { - // Check for the specific metadata - if let Some(metadata) = cargo_data - .get("package") - .and_then(|p| p.get("metadata")) - .and_then(|m| m.get("component")) - { - if let Some(package) = metadata.get("package") { - if let Some(package_str) = package.as_str() { - println!( - " Found package.metadata.component.package = {:?}", - package_str - ); - if package_str == "hyperware:process" { - println!(" Adding project: {}", path.display()); - projects.push(path.to_path_buf()); - } - } - } - } else { - println!(" No package.metadata.component metadata found"); - } - } - } - } + if !path.is_dir() || path == base_dir { + continue; + } + let cargo_toml = path.join("Cargo.toml"); + println!("Checking {}", cargo_toml.display()); + + if !cargo_toml.exists() { + continue; + } + // Try to read and parse Cargo.toml + let Ok(content) = fs::read_to_string(&cargo_toml) else { + continue; + }; + let Ok(cargo_data) = content.parse::() else { + continue; + }; + // Check for the specific metadata + let Some(metadata) = cargo_data + .get("package") + .and_then(|p| p.get("metadata")) + .and_then(|m| m.get("component")) + else { + println!(" No package.metadata.component metadata found"); + continue; + }; + let Some(package) = metadata.get("package") else { + continue; + }; + let Some(package_str) = package.as_str() else { + continue; + }; + println!( + " Found package.metadata.component.package = {:?}", + package_str + ); + if package_str == "hyperware:process" { + println!(" Adding project: {}", path.display()); + projects.push(path.to_path_buf()); } } @@ -703,55 +709,57 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - println!("Extracted wit_world: {}", world_name); - wit_world = Some(world_name); + let Item::Impl(impl_item) = item else { + continue; + }; + // Check if this impl block has a #[hyperprocess] attribute + if let Some(attr) = impl_item + .attrs + .iter() + .find(|attr| attr.path().is_ident("hyperprocess")) + { + println!("Found hyperprocess attribute"); + + // Extract the wit_world name + match extract_wit_world(&[attr.clone()]) { + Ok(world_name) => { + println!("Extracted wit_world: {}", world_name); + wit_world = Some(world_name); + + // Get the interface name from the impl type + interface_name = impl_item.self_ty.as_ref().as_type_path().map(|tp| { + if let Some(last_segment) = tp.path.segments.last() { + last_segment.ident.to_string() + } else { + "Unknown".to_string() + } + }); - // Get the interface name from the impl type - interface_name = impl_item.self_ty.as_ref().as_type_path().map(|tp| { - if let Some(last_segment) = tp.path.segments.last() { - last_segment.ident.to_string() - } else { - "Unknown".to_string() - } - }); - - // Check for "State" suffix and remove it - if let Some(ref name) = interface_name { - // Validate the interface name - if let Err(e) = validate_name(name, "Interface") { - println!("Interface name validation failed: {}", e); - continue; // Skip this impl block if validation fails - } + // Check for "State" suffix and remove it + let Some(ref name) = interface_name else { + continue; + }; + // Validate the interface name + if let Err(e) = validate_name(name, "Interface") { + println!("Interface name validation failed: {}", e); + continue; // Skip this impl block if validation fails + } - // Remove State suffix if present - let base_name = remove_state_suffix(name); + // Remove State suffix if present + let base_name = remove_state_suffix(name); - // Convert to kebab-case for file name and interface name - kebab_interface_name = Some(to_kebab_case(&base_name)); + // Convert to kebab-case for file name and interface name + kebab_interface_name = Some(to_kebab_case(&base_name)); - println!("Interface name: {:?}", interface_name); - println!("Base name: {}", base_name); - println!("Kebab interface name: {:?}", kebab_interface_name); + println!("Interface name: {:?}", interface_name); + println!("Base name: {}", base_name); + println!("Kebab interface name: {:?}", kebab_interface_name); - // Save the impl item for later processing - impl_item_with_hyperprocess = Some(impl_item.clone()); - break; // Assume only one hyperprocess impl block per lib.rs - } - } - Err(e) => println!("Failed to extract wit_world: {}", e), + // Save the impl item for later processing + impl_item_with_hyperprocess = Some(impl_item.clone()); + break; // Assume only one hyperprocess impl block per lib.rs } + Err(e) => println!("Failed to extract wit_world: {}", e), } } } @@ -765,91 +773,92 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - // Convert function name to kebab-case - let func_kebab_name = to_kebab_case(&method_name); // Use different var name - println!( - " Processing method: {} -> {}", - method_name, func_kebab_name - ); - - // Generate a signature struct for each attribute type - // This will populate `used_types` - if has_remote { - match generate_signature_struct( - &func_kebab_name, // Pass func kebab name - "remote", - method, - &mut used_types, // Pass the main set - ) { - Ok(remote_struct) => signature_structs.push(remote_struct), - Err(e) => println!( - " Error generating remote signature struct: {}", - e - ), - } - } + let ImplItem::Fn(method) = item else { + continue; + }; + let method_name = method.sig.ident.to_string(); + println!(" Examining method: {}", method_name); + + // Check for attribute types + let has_remote = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("remote")); + let has_local = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("local")); + let has_http = method.attrs.iter().any(|attr| attr.path().is_ident("http")); + + if has_remote || has_local || has_http { + println!( + " Has relevant attributes: remote={}, local={}, http={}", + has_remote, has_local, has_http + ); + + // Validate function name + match validate_name(&method_name, "Function") { + Ok(_) => { + // Convert function name to kebab-case + let func_kebab_name = to_kebab_case(&method_name); // Use different var name + println!( + " Processing method: {} -> {}", + method_name, func_kebab_name + ); - if has_local { - match generate_signature_struct( - &func_kebab_name, // Pass func kebab name - "local", - method, - &mut used_types, // Pass the main set - ) { - Ok(local_struct) => signature_structs.push(local_struct), - Err(e) => println!( - " Error generating local signature struct: {}", - e - ), - } + // Generate a signature struct for each attribute type + // This will populate `used_types` + if has_remote { + match generate_signature_struct( + &func_kebab_name, // Pass func kebab name + "remote", + method, + &mut used_types, // Pass the main set + ) { + Ok(remote_struct) => signature_structs.push(remote_struct), + Err(e) => println!( + " Error generating remote signature struct: {}", + e + ), } + } - if has_http { - match generate_signature_struct( - &func_kebab_name, // Pass func kebab name - "http", - method, - &mut used_types, // Pass the main set - ) { - Ok(http_struct) => signature_structs.push(http_struct), - Err(e) => println!( - " Error generating HTTP signature struct: {}", - e - ), - } + if has_local { + match generate_signature_struct( + &func_kebab_name, // Pass func kebab name + "local", + method, + &mut used_types, // Pass the main set + ) { + Ok(local_struct) => signature_structs.push(local_struct), + Err(e) => println!( + " Error generating local signature struct: {}", + e + ), } } - Err(e) => { - println!(" Skipping method with invalid name: {}", e); + + if has_http { + match generate_signature_struct( + &func_kebab_name, // Pass func kebab name + "http", + method, + &mut used_types, // Pass the main set + ) { + Ok(http_struct) => signature_structs.push(http_struct), + Err(e) => println!( + " Error generating HTTP signature struct: {}", + e + ), + } } } - } else { - println!(" Skipping method without relevant attributes"); + Err(e) => { + println!(" Skipping method with invalid name: {}", e); + } } + } else { + println!(" Skipping method without relevant attributes"); } } } @@ -950,7 +959,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result, updated_world: &mut bool, ) -> Result<()> { + // handle existing api files for entry in WalkDir::new(api_dir) .max_depth(1) .into_iter() @@ -973,101 +983,135 @@ fn rewrite_wit( if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { println!("Checking WIT file: {}", path.display()); - if let Ok(content) = fs::read_to_string(path) { - if content.contains("world ") { - println!("Found world definition file"); + let Ok(content) = fs::read_to_string(path) else { + continue; + }; + if !content.contains("world ") { + continue; + } + println!("Found world definition file"); - // Extract the world name and existing imports - let lines: Vec<&str> = content.lines().collect(); - let mut world_name = None; - let mut existing_imports = Vec::new(); - let mut include_line = " include process-v1;".to_string(); + // Extract the world name and existing imports + let lines: Vec<&str> = content.lines().collect(); + let mut world_name = None; + let mut existing_imports = Vec::new(); + let mut include_lines = HashSet::new(); - for line in &lines { - let trimmed = line.trim(); + for line in &lines { + let trimmed = line.trim(); - if trimmed.starts_with("world ") { - if let Some(name) = trimmed.split_whitespace().nth(1) { - world_name = Some(name.trim_end_matches(" {").to_string()); - } - } else if trimmed.starts_with("import ") { - existing_imports.push(trimmed.to_string()); - } else if trimmed.starts_with("include ") { - include_line = trimmed.to_string(); - } + if trimmed.starts_with("world ") { + if let Some(name) = trimmed.split_whitespace().nth(1) { + world_name = Some(name.trim_end_matches(" {").to_string()); } + } else if trimmed.starts_with("import ") { + existing_imports.push(trimmed.to_string()); + } else if trimmed.starts_with("include ") { + include_lines.insert(trimmed.to_string()); + } + } + + let Some(world_name) = world_name else { + continue; + }; + + println!("Extracted world name: {}", world_name); + + // Check if this world name matches the one we're looking for + if wit_worlds.remove(&world_name) || wit_worlds.contains(&world_name[6..]) { + let world_content = generate_wit_file( + &world_name, + new_imports, + &existing_imports, + &mut include_lines, + )?; + + println!("Writing updated world definition to {}", path.display()); + // Write the updated world file + fs::write(path, world_content).with_context(|| { + format!("Failed to write updated world file: {}", path.display()) + })?; + + println!("Successfully updated world definition"); + *updated_world = true; + } + } + } - if let Some(world_name) = world_name { - println!("Extracted world name: {}", world_name); + // handle non-existing api files + for wit_world in wit_worlds.iter() { + for prefix in ["", "types-"] { + let wit_world = format!("{prefix}{wit_world}"); + let world_content = + generate_wit_file(&wit_world, new_imports, &Vec::new(), &mut HashSet::new())?; + + let path = api_dir.join(format!("{wit_world}.wit")); + println!("Writing updated world definition to {}", path.display()); + // Write the updated world file + fs::write(&path, world_content).with_context(|| { + format!("Failed to write updated world file: {}", path.display()) + })?; + + println!("Successfully created new world definition for {wit_world}"); + } + *updated_world = true; + } - // Check if this world name matches the one we're looking for - if wit_worlds.remove(&world_name) || wit_worlds.contains(&world_name[6..]) { - // Determine the include line based on world name - // If world name starts with "types-", use "include lib;" instead - if world_name.starts_with("types-") { - include_line = " include lib;".to_string(); - } else { - // Keep existing include or default to process-v1 - if !include_line.contains("include ") { - include_line = " include process-v1;".to_string(); - } - } + Ok(()) +} - // Combine existing imports with new imports - let mut all_imports = existing_imports.clone(); +fn generate_wit_file( + world_name: &str, + new_imports: &Vec, + existing_imports: &Vec, + include_lines: &mut HashSet, +) -> Result { + // Determine the include line based on world name + // If world name starts with "types-", use "include lib;" instead + if world_name.starts_with("types-") { + if !include_lines.contains("include lib;") { + include_lines.insert("include lib;".to_string()); + } + } else { + // Keep existing include or default to process-v1 + if include_lines.is_empty() { + include_lines.insert("include process-v1;".to_string()); + } + } - for import in new_imports { - let import_stmt = import.trim(); - if !all_imports.iter().any(|i| i.trim() == import_stmt) { - all_imports.push(import_stmt.to_string()); - } - } + // Combine existing imports with new imports + let mut all_imports = existing_imports.clone(); - // Make sure all imports have proper indentation - let all_imports_with_indent: Vec = all_imports - .iter() - .map(|import| { - if import.starts_with(" ") { - import.clone() - } else { - format!(" {}", import.trim()) - } - }) - .collect(); + for import in new_imports { + let import_stmt = import.trim(); + if !all_imports.iter().any(|i| i.trim() == import_stmt) { + all_imports.push(import_stmt.to_string()); + } + } - let imports_section = all_imports_with_indent.join("\n"); + // Make sure all imports have proper indentation + let all_imports_with_indent: Vec = all_imports + .iter() + .map(|import| { + if import.starts_with(" ") { + import.clone() + } else { + format!(" {}", import.trim()) + } + }) + .collect(); - // Create updated world content with proper indentation - let world_content = format!( - "world {} {{\n{}\n {}\n}}", - world_name, - imports_section, - include_line.trim() - ); + let imports_section = all_imports_with_indent.join("\n"); - println!("Writing updated world definition to {}", path.display()); - // Write the updated world file - fs::write(path, world_content).with_context(|| { - format!("Failed to write updated world file: {}", path.display()) - })?; + // Create updated world content with proper indentation + let include_lines: String = include_lines.iter().map(|l| format!(" {l}\n")).collect(); + let world_content = format!("world {world_name} {{\n{imports_section}\n{include_lines}}}"); - println!("Successfully updated world definition"); - *updated_world = true; - } - } - } - } - } - } - Ok(()) + return Ok(world_content); } // Generate WIT files from Rust code -pub fn generate_wit_files( - base_dir: &Path, - api_dir: &Path, - is_recursive_call: bool, -) -> Result<(Vec, Vec)> { +pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec, Vec)> { fs::create_dir_all(&api_dir)?; // Find all relevant Rust projects @@ -1088,17 +1132,10 @@ pub fn generate_wit_files( println!("Processing project: {}", project_path.display()); match process_rust_project(project_path, api_dir) { - Ok(Some((import, wit_world))) => { - println!("Got import statement: {}", import); - new_imports.push(import.clone()); - - // Extract interface name from import statement - let interface_name = import - .trim_start_matches(" import ") - .trim_end_matches(";") - .to_string(); + Ok(Some((interface, wit_world))) => { + new_imports.push(format!(" import {interface};")); - interfaces.push(interface_name); + interfaces.push(interface); processed_projects.push(project_path.clone()); wit_worlds.insert(wit_world); @@ -1116,45 +1153,6 @@ pub fn generate_wit_files( rewrite_wit(api_dir, &new_imports, &mut wit_worlds, &mut updated_world)?; - let rerun_rewrite_wit = !wit_worlds.is_empty(); - for wit_world in wit_worlds { - // Create a new file with the simple world definition - let new_file_path = api_dir.join(format!("{}.wit", wit_world)); - let simple_world_content = format!("world {} {{}}", wit_world); - - println!( - "Creating new world definition file: {}", - new_file_path.display() - ); - fs::write(&new_file_path, simple_world_content).with_context(|| { - format!( - "Failed to create new world file: {}", - new_file_path.display() - ) - })?; - - let new_file_path = api_dir.join(format!("types-{}.wit", wit_world)); - let simple_world_content = format!("world types-{} {{}}", wit_world); - - println!( - "Creating new world definition file: {}", - new_file_path.display() - ); - fs::write(&new_file_path, simple_world_content).with_context(|| { - format!( - "Failed to create new world file: {}", - new_file_path.display() - ) - })?; - - println!("Successfully created new world definition file"); - updated_world = true; - } - - if rerun_rewrite_wit && !is_recursive_call { - return generate_wit_files(base_dir, api_dir, true); - } - // If no world definitions were found, create a default one if !updated_world && !new_imports.is_empty() { // Define default world name From 62a6af3c4fe2b369ca78e4fc8f72104eaff28cbe Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 23 Apr 2025 20:38:49 -0700 Subject: [PATCH 23/88] use most recent hyperprocess_macro (b6ad495) --- src/build/caller_utils_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index a4ec673f..ecd7f436 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -496,7 +496,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "47400ab" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "b6ad495" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } From 101552adfd99aa3a822349dd9ed681f4bddcce8b Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:10:16 -0400 Subject: [PATCH 24/88] added log/debugging for wit generation --- src/build/wit_generator.rs | 182 +++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 86 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index e9a832ae..2b571943 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -9,6 +9,7 @@ use color_eyre::{ use syn::{self, Attribute, ImplItem, Item, Type}; use toml::Value; use walkdir::WalkDir; +use tracing::{debug, info, instrument, warn}; // Helper functions for naming conventions fn to_kebab_case(s: &str) -> String { @@ -73,17 +74,19 @@ fn remove_state_suffix(name: &str) -> String { name.to_string() } + // Extract wit_world from the #[hyperprocess] attribute using the format in the debug representation +#[instrument(skip(attrs))] fn extract_wit_world(attrs: &[Attribute]) -> Result { for attr in attrs { if attr.path().is_ident("hyperprocess") { // Convert attribute to string representation let attr_str = format!("{:?}", attr); - println!("Attribute string: {}", attr_str); + debug!("Attribute string: {}", attr_str); // Look for wit_world in the attribute string if let Some(pos) = attr_str.find("wit_world") { - println!("Found wit_world at position {}", pos); + debug!("Found wit_world at position {}", pos); // Find the literal value after wit_world by looking for lit: "value" let lit_pattern = "lit: \""; @@ -93,7 +96,7 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { // Find the closing quote of the literal if let Some(quote_pos) = attr_str[start_pos..].find('\"') { let world_name = &attr_str[start_pos..(start_pos + quote_pos)]; - println!("Extracted wit_world: {}", world_name); + debug!("Extracted wit_world: {}", world_name); return Ok(world_name.to_string()); } } @@ -104,6 +107,7 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { } // Convert Rust type to WIT type, including downstream types +#[instrument(level = "debug", skip(ty, used_types))] fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { match ty { Type::Path(type_path) => { @@ -236,35 +240,37 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Vec { let mut rust_files = Vec::new(); let src_dir = crate_path.join("src"); - println!("Finding Rust files in {}", src_dir.display()); + debug!("Finding Rust files in {}", src_dir.display()); if !src_dir.exists() || !src_dir.is_dir() { - println!("No src directory found at {}", src_dir.display()); + warn!("No src directory found at {}", src_dir.display()); return rust_files; } for entry in WalkDir::new(src_dir).into_iter().filter_map(Result::ok) { let path = entry.path(); if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { - println!("Found Rust file: {}", path.display()); + debug!("Found Rust file: {}", path.display()); rust_files.push(path.to_path_buf()); } } - println!("Found {} Rust files", rust_files.len()); + debug!("Found {} Rust files", rust_files.len()); rust_files } // Collect **only used** type definitions (structs and enums) from a file +#[instrument(skip(used_types))] fn collect_type_definitions_from_file( file_path: &Path, used_types: &HashSet, // Accept the set of used types ) -> Result> { - println!( + debug!( "Collecting used type definitions from file: {}", // Updated message file_path.display() ); @@ -286,7 +292,7 @@ fn collect_type_definitions_from_file( // Skip trying to validate if name contains "__" as these are likely internal types if orig_name.contains("__") { // This skip can remain, as internal types are unlikely to be in `used_types` anyway - println!(" Skipping likely internal struct: {}", orig_name); + warn!(" Skipping likely internal struct: {}", orig_name); continue; } @@ -297,12 +303,12 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { - // println!(" Skipping unused struct: {} -> {}", orig_name, name); // Optional debug log - continue; // Skip this struct if not in the used set + // Skip this struct if not in the used set + continue; } // --- End Check --- - println!(" Found used struct: {} -> {}", orig_name, name); // Updated message + debug!(" Found used struct: {} -> {}", orig_name, name); // Updated message // Proceed with field processing only if the struct is used let fields: Vec = match &item_struct.fields { @@ -322,7 +328,7 @@ fn collect_type_definitions_from_file( Ok(_) => { let field_name = to_kebab_case(&field_orig_name); if field_name.is_empty() { - println!(" Skipping field with empty name conversion"); + warn!(" Skipping field with empty name conversion"); continue; } @@ -334,7 +340,7 @@ fn collect_type_definitions_from_file( ) { Ok(ty) => ty, Err(e) => { - println!( + warn!( " Error converting field type: {}", e ); @@ -343,7 +349,7 @@ fn collect_type_definitions_from_file( } }; - println!( + debug!( " Field: {} -> {}", field_name, field_type ); @@ -353,7 +359,7 @@ fn collect_type_definitions_from_file( )); } Err(e) => { - println!( + warn!( " Skipping field with invalid name: {}", e ); @@ -375,12 +381,12 @@ fn collect_type_definitions_from_file( format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), ); } else { - println!(" Skipping used struct {} with no convertible fields", name); + warn!(" Skipping used struct {} with no convertible fields", name); } } Err(e) => { // Struct name validation failed, skip regardless of usage - println!(" Skipping struct with invalid name: {}", e); + warn!(" Skipping struct with invalid name: {}", e); continue; } } @@ -391,7 +397,7 @@ fn collect_type_definitions_from_file( // Skip trying to validate if name contains "__" if orig_name.contains("__") { - println!(" Skipping likely internal enum: {}", orig_name); + warn!(" Skipping likely internal enum: {}", orig_name); continue; } @@ -402,12 +408,12 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { - // println!(" Skipping unused enum: {} -> {}", orig_name, name); // Optional debug log - continue; // Skip this enum if not in the used set + warn!(" Skipping unused enum: {} -> {}", orig_name, name); // Optional debug log + continue; // Skip this enum if not in the used set } // --- End Check --- - println!(" Found used enum: {} -> {}", orig_name, name); // Updated message + debug!(" Found used enum: {} -> {}", orig_name, name); // Updated message // Proceed with variant processing only if the enum is used let mut variants = Vec::new(); @@ -430,7 +436,7 @@ fn collect_type_definitions_from_file( Ok(ty) => { let variant_name = to_kebab_case(&variant_orig_name); - println!( + debug!( " Variant: {} -> {}({})", variant_orig_name, variant_name, ty ); @@ -440,7 +446,7 @@ fn collect_type_definitions_from_file( )); } Err(e) => { - println!( + warn!( " Error converting variant type: {}", e ); @@ -451,14 +457,14 @@ fn collect_type_definitions_from_file( } syn::Fields::Unit => { let variant_name = to_kebab_case(&variant_orig_name); - println!( + debug!( " Variant: {} -> {}", variant_orig_name, variant_name ); variants.push(format!(" {}", variant_name)); } _ => { - println!( + warn!( " Skipping complex variant in used enum {}: {}", name, variant_orig_name ); @@ -468,7 +474,7 @@ fn collect_type_definitions_from_file( } } Err(e) => { - println!(" Skipping variant with invalid name in used enum {}: {}", name, e); + warn!(" Skipping variant with invalid name in used enum {}: {}", name, e); skip_enum = true; // Skip the whole enum if one variant name is invalid break; } @@ -486,12 +492,12 @@ fn collect_type_definitions_from_file( ), ); } else { - println!(" Skipping used enum {} due to complex/invalid variants or no variants", name); + warn!(" Skipping used enum {} due to complex/invalid variants or no variants", name); } } Err(e) => { // Enum name validation failed, skip regardless of usage - println!(" Skipping enum with invalid name: {}", e); + warn!(" Skipping enum with invalid name: {}", e); continue; } } @@ -500,7 +506,7 @@ fn collect_type_definitions_from_file( } } - println!( + debug!( "Collected {} used type definitions from this file", type_defs.len() ); // Updated message @@ -508,9 +514,10 @@ fn collect_type_definitions_from_file( } // Find all relevant Rust projects +#[instrument] fn find_rust_projects(base_dir: &Path) -> Vec { let mut projects = Vec::new(); - println!("Scanning for Rust projects in {}", base_dir.display()); + debug!("Scanning for Rust projects in {}", base_dir.display()); for entry in WalkDir::new(base_dir) .max_depth(1) @@ -523,7 +530,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { continue; } let cargo_toml = path.join("Cargo.toml"); - println!("Checking {}", cargo_toml.display()); + debug!("Checking {}", cargo_toml.display()); if !cargo_toml.exists() { continue; @@ -541,7 +548,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { .and_then(|p| p.get("metadata")) .and_then(|m| m.get("component")) else { - println!(" No package.metadata.component metadata found"); + warn!(" No package.metadata.component metadata found"); continue; }; let Some(package) = metadata.get("package") else { @@ -550,21 +557,22 @@ fn find_rust_projects(base_dir: &Path) -> Vec { let Some(package_str) = package.as_str() else { continue; }; - println!( + debug!( " Found package.metadata.component.package = {:?}", package_str ); if package_str == "hyperware:process" { - println!(" Adding project: {}", path.display()); + info!(" Adding project: {}", path.display()); projects.push(path.to_path_buf()); } } - println!("Found {} relevant Rust projects", projects.len()); + debug!("Found {} relevant Rust projects", projects.len()); projects } // Helper function to generate signature struct for specific attribute type +#[instrument(skip(used_types))] fn generate_signature_struct( kebab_name: &str, attr_type: &str, @@ -616,13 +624,13 @@ fn generate_signature_struct( .push(format!(" {}: {}", param_name, param_type)); } Err(e) => { - println!(" Error converting parameter type: {}", e); + warn!(" Error converting parameter type: {}", e); return Err(e); } } } Err(e) => { - println!(" Skipping parameter with invalid name: {}", e); + warn!(" Skipping parameter with invalid name: {}", e); return Err(e); } } @@ -637,7 +645,7 @@ fn generate_signature_struct( struct_fields.push(format!(" returning: {}", return_type)); } Err(e) => { - println!(" Error converting return type: {}", e); + warn!(" Error converting return type: {}", e); return Err(e); } }, @@ -673,14 +681,15 @@ impl AsTypePath for syn::Type { } // Process a single Rust project and generate WIT files +#[instrument(skip_all)] fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { - println!("\nProcessing project: {}", project_path.display()); + info!("Processing project: {}", project_path.display()); // Find lib.rs for this project let lib_rs = project_path.join("src").join("lib.rs"); if !lib_rs.exists() { - println!("No lib.rs found for project: {}", project_path.display()); + warn!("No lib.rs found for project: {}", project_path.display()); return Ok(None); } @@ -707,7 +716,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { - println!("Extracted wit_world: {}", world_name); + debug!("Extracted wit_world: {}", world_name); wit_world = Some(world_name); // Get the interface name from the impl type @@ -741,7 +750,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result println!("Failed to extract wit_world: {}", e), + Err(e) => warn!("Failed to extract wit_world: {}", e), } } } @@ -777,7 +786,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result { // Convert function name to kebab-case let func_kebab_name = to_kebab_case(&method_name); // Use different var name - println!( + info!( " Processing method: {} -> {}", method_name, func_kebab_name ); @@ -816,7 +825,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(remote_struct), - Err(e) => println!( + Err(e) => warn!( " Error generating remote signature struct: {}", e ), @@ -831,7 +840,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(local_struct), - Err(e) => println!( + Err(e) => warn!( " Error generating local signature struct: {}", e ), @@ -846,7 +855,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(http_struct), - Err(e) => println!( + Err(e) => warn!( " Error generating HTTP signature struct: {}", e ), @@ -854,16 +863,16 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - println!(" Skipping method with invalid name: {}", e); + warn!(" Skipping method with invalid name: {}", e); } } } else { - println!(" Skipping method without relevant attributes"); + warn!(" Skipping method without relevant attributes"); } } } } - println!( + debug!( "Identified {} used types from function signatures.", used_types.len() ); @@ -880,7 +889,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - println!( + warn!( "Error collecting type definitions from {}: {}", file_path.display(), e @@ -890,7 +899,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result = all_type_defs.into_values().collect(); // Collect values directly type_defs.sort(); // Sort for consistent output - println!("Including {} used type definitions", type_defs.len()); + debug!("Including {} used type definitions", type_defs.len()); // Generate the final WIT content if signature_structs.is_empty() && type_defs.is_empty() { // Check both sigs and types - println!( + warn!( "No functions or used types found for interface {}", iface_name ); @@ -933,7 +942,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result, @@ -981,7 +990,7 @@ fn rewrite_wit( let path = entry.path(); if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { - println!("Checking WIT file: {}", path.display()); + debug!("Checking WIT file: {}", path.display()); let Ok(content) = fs::read_to_string(path) else { continue; @@ -989,7 +998,7 @@ fn rewrite_wit( if !content.contains("world ") { continue; } - println!("Found world definition file"); + debug!("Found world definition file"); // Extract the world name and existing imports let lines: Vec<&str> = content.lines().collect(); @@ -1015,7 +1024,7 @@ fn rewrite_wit( continue; }; - println!("Extracted world name: {}", world_name); + debug!("Extracted world name: {}", world_name); // Check if this world name matches the one we're looking for if wit_worlds.remove(&world_name) || wit_worlds.contains(&world_name[6..]) { @@ -1026,13 +1035,13 @@ fn rewrite_wit( &mut include_lines, )?; - println!("Writing updated world definition to {}", path.display()); + info!("Writing updated world definition to {}", path.display()); // Write the updated world file fs::write(path, world_content).with_context(|| { format!("Failed to write updated world file: {}", path.display()) })?; - println!("Successfully updated world definition"); + info!("Successfully updated world definition"); *updated_world = true; } } @@ -1046,13 +1055,13 @@ fn rewrite_wit( generate_wit_file(&wit_world, new_imports, &Vec::new(), &mut HashSet::new())?; let path = api_dir.join(format!("{wit_world}.wit")); - println!("Writing updated world definition to {}", path.display()); + info!("Writing updated world definition to {}", path.display()); // Write the updated world file fs::write(&path, world_content).with_context(|| { format!("Failed to write updated world file: {}", path.display()) })?; - println!("Successfully created new world definition for {wit_world}"); + info!("Successfully created new world definition for {wit_world}"); } *updated_world = true; } @@ -1111,6 +1120,7 @@ fn generate_wit_file( } // Generate WIT files from Rust code +#[instrument(skip(base_dir, api_dir))] pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec, Vec)> { fs::create_dir_all(&api_dir)?; @@ -1119,7 +1129,7 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec { @@ -1140,15 +1150,15 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec println!("No import statement generated"), - Err(e) => println!("Error processing project: {}", e), + Ok(None) => warn!("No import statement generated"), + Err(e) => warn!("Error processing project: {}", e), } } - println!("Collected {} new imports", new_imports.len()); + debug!("Collected {} new imports", new_imports.len()); // Check for existing world definition files and update them - println!("Looking for existing world definition files"); + debug!("Looking for existing world definition files"); let mut updated_world = false; rewrite_wit(api_dir, &new_imports, &mut wit_worlds, &mut updated_world)?; @@ -1157,7 +1167,7 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec Result<(Vec Date: Tue, 29 Apr 2025 14:10:44 -0400 Subject: [PATCH 25/88] added log/debugging for caller-utils generation --- src/build/caller_utils_generator.rs | 127 ++++++++++++++++++---------- 1 file changed, 80 insertions(+), 47 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index ecd7f436..358f9965 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -6,6 +6,7 @@ use color_eyre::{ eyre::{bail, WrapErr}, Result, }; +use tracing::{debug, info, instrument, warn}; use toml::Value; use walkdir::WalkDir; @@ -34,8 +35,9 @@ pub fn to_pascal_case(s: &str) -> String { } // Find the world name in the world WIT file, prioritizing types-prefixed worlds +#[instrument(skip(api_dir))] fn find_world_names(api_dir: &Path) -> Result> { - println!("Looking in {api_dir:?} for world names..."); + info!(dir = ?api_dir, "Looking for world names..."); let mut world_names = Vec::new(); // Look for world definition files @@ -49,7 +51,7 @@ fn find_world_names(api_dir: &Path) -> Result> { if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { if let Ok(content) = fs::read_to_string(path) { if content.contains("world ") { - println!("Analyzing world definition file: {}", path.display()); + debug!(file = %path.display(), "Analyzing potential world definition file"); // Extract the world name let lines: Vec<&str> = content.lines().collect(); @@ -57,16 +59,16 @@ fn find_world_names(api_dir: &Path) -> Result> { if let Some(world_line) = lines.iter().find(|line| line.trim().starts_with("world ")) { - println!("World line: {}", world_line); + debug!(line = %world_line, "Found world line"); if let Some(world_name) = world_line.trim().split_whitespace().nth(1) { let clean_name = world_name.trim_end_matches(" {"); - println!("Extracted world name: {}", clean_name); + debug!(name = %clean_name, "Extracted potential world name"); // Check if this is a types-prefixed world if clean_name.starts_with("types-") { world_names.push(clean_name.to_string()); - println!("Found types world: {}", clean_name); + info!(name = %clean_name, "Found types-prefixed world"); } } } @@ -82,6 +84,7 @@ fn find_world_names(api_dir: &Path) -> Result> { } // Convert WIT type to Rust type - IMPROVED with more Rust primitives +#[instrument(level = "trace")] fn wit_type_to_rust(wit_type: &str) -> String { match wit_type { // Integer types @@ -141,6 +144,7 @@ fn wit_type_to_rust(wit_type: &str) -> String { } // Generate default value for Rust type - IMPROVED with additional types +#[instrument(level = "trace")] fn generate_default_value(rust_type: &str) -> String { match rust_type { // Integer types @@ -185,12 +189,14 @@ fn generate_default_value(rust_type: &str) -> String { } // Structure to represent a field in a WIT signature struct +#[derive(Debug)] struct SignatureField { name: String, wit_type: String, } // Structure to represent a WIT signature struct +#[derive(Debug)] struct SignatureStruct { function_name: String, attr_type: String, @@ -198,7 +204,9 @@ struct SignatureStruct { } // Find all interface imports in the world WIT file +#[instrument(skip(api_dir))] fn find_interfaces_in_world(api_dir: &Path) -> Result> { + info!(dir = ?api_dir, "Finding interface imports in world definitions"); let mut interfaces = Vec::new(); // Find world definition files @@ -212,7 +220,7 @@ fn find_interfaces_in_world(api_dir: &Path) -> Result> { if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { if let Ok(content) = fs::read_to_string(path) { if content.contains("world ") { - println!("Analyzing world definition file: {}", path.display()); + debug!(file = %path.display(), "Analyzing world definition file for imports"); // Extract import statements for line in content.lines() { @@ -224,20 +232,21 @@ fn find_interfaces_in_world(api_dir: &Path) -> Result> { .trim(); interfaces.push(interface.to_string()); - println!(" Found interface import: {}", interface); + debug!(interface = %interface, "Found interface import"); } } } } } } - + info!(count = interfaces.len(), interfaces = ?interfaces, "Found interface imports"); Ok(interfaces) } // Parse WIT file to extract function signatures and type definitions +#[instrument(skip(file_path))] fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec)> { - println!("Parsing WIT file: {}", file_path.display()); + info!(file = %file_path.display(), "Parsing WIT file"); let content = fs::read_to_string(file_path) .with_context(|| format!("Failed to read WIT file: {}", file_path.display()))?; @@ -258,7 +267,7 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec .trim_start_matches("record ") .trim_end_matches(" {") .trim(); - println!(" Found type: record {}", record_name); + debug!(name = %record_name, "Found type definition (record)"); type_names.push(record_name.to_string()); } // Look for variant definitions (enums) @@ -267,7 +276,7 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec .trim_start_matches("variant ") .trim_end_matches(" {") .trim(); - println!(" Found type: variant {}", variant_name); + debug!(name = %variant_name, "Found type definition (variant)"); type_names.push(variant_name.to_string()); } // Look for signature record definitions @@ -276,18 +285,19 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec .trim_start_matches("record ") .trim_end_matches(" {") .trim(); - println!(" Found record: {}", record_name); + debug!(name = %record_name, "Found signature record"); // Extract function name and attribute type let parts: Vec<_> = record_name.split("-signature-").collect(); if parts.len() != 2 { - println!(" Unexpected record name format"); + warn!(name = %record_name, "Unexpected signature record name format, skipping"); i += 1; continue; } let function_name = parts[0].to_string(); let attr_type = parts[1].to_string(); + debug!(function = %function_name, attr_type = %attr_type, "Extracted function name and type"); // Parse fields let mut fields = Vec::new(); @@ -308,7 +318,7 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec let field_name = field_parts[0].trim().to_string(); let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); - println!(" Field: {} -> {}", field_name, field_type); + debug!(name = %field_name, wit_type = %field_type, "Found field"); fields.push(SignatureField { name: field_name, wit_type: field_type, @@ -328,11 +338,11 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec i += 1; } - println!( - "Extracted {} signature structs and {} type definitions from {}", - signatures.len(), - type_names.len(), - file_path.display() + info!( + file = %file_path.display(), + signatures = signatures.len(), + types = type_names.len(), + "Finished parsing WIT file" ); Ok((signatures, type_names)) } @@ -347,6 +357,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { // Function full name with attribute type let full_function_name = format!("{}_{}_rpc", snake_function_name, signature.attr_type); + debug!(name = %full_function_name, "Generating function stub"); // Extract parameters and return type let mut params = Vec::new(); @@ -357,6 +368,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { for field in &signature.fields { let field_name_snake = to_snake_case(&field.name); let rust_type = wit_type_to_rust(&field.wit_type); + debug!(field = %field.name, wit_type = %field.wit_type, rust_type = %rust_type, "Processing field"); if field.name == "target" { if field.wit_type == "string" { @@ -367,14 +379,17 @@ fn generate_async_function(signature: &SignatureStruct) -> String { } } else if field.name == "returning" { return_type = rust_type; + debug!(return_type = %return_type, "Identified return type"); } else { params.push(format!("{}: {}", field_name_snake, rust_type)); param_names.push(field_name_snake); + debug!(param_name = param_names.last().unwrap(), "Added parameter"); } } - // First parameter is always target + // First parameter is always target let all_params = if target_param.is_empty() { + warn!("No 'target' parameter found in signature for {}", full_function_name); params.join(", ") } else { format!( @@ -389,6 +404,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { // For HTTP endpoints, generate commented-out implementation if signature.attr_type == "http" { + debug!("Generating commented-out stub for HTTP endpoint"); let default_value = generate_default_value(&return_type); // Add underscore prefix to all parameters for HTTP stubs @@ -400,6 +416,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { if parts.len() == 2 { format!("_{}: {}", parts[0], parts[1]) } else { + warn!(param = %param, "Could not parse parameter for underscore prefix"); format!("_{}", param) } }) @@ -417,6 +434,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { if parts.len() == 2 { format!("_{}: {}", parts[0], parts[1]) } else { + warn!(param = %param, "Could not parse parameter for underscore prefix"); format!("_{}", param) } }) @@ -440,15 +458,18 @@ fn generate_async_function(signature: &SignatureStruct) -> String { // Format JSON parameters correctly let json_params = if param_names.is_empty() { // No parameters case + debug!("Generating JSON with no parameters"); format!("json!({{\"{}\" : {{}}}})", pascal_function_name) } else if param_names.len() == 1 { // Single parameter case + debug!(param = %param_names[0], "Generating JSON with single parameter"); format!( "json!({{\"{}\": {}}})", pascal_function_name, param_names[0] ) } else { // Multiple parameters case - use tuple format + debug!(params = ?param_names, "Generating JSON with multiple parameters (tuple)"); format!( "json!({{\"{}\": ({})}})", pascal_function_name, @@ -457,6 +478,7 @@ fn generate_async_function(signature: &SignatureStruct) -> String { }; // Generate function with implementation using send + debug!("Generating standard RPC stub implementation"); format!( "/// Generated stub for `{}` {} RPC call\npub async fn {}({}) -> {} {{\n let body = {};\n let body = serde_json::to_vec(&body).unwrap();\n let request = Request::to(target)\n .body(body);\n send::<{}>(request).await\n}}", signature.function_name, @@ -470,18 +492,19 @@ fn generate_async_function(signature: &SignatureStruct) -> String { } // Create the caller-utils crate with a single lib.rs file +#[instrument(skip(api_dir, base_dir))] fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { // Path to the new crate let caller_utils_dir = base_dir.join("target").join("caller-utils"); - println!( - "Creating caller-utils crate at {}", - caller_utils_dir.display() + info!( + path = %caller_utils_dir.display(), + "Creating caller-utils crate" ); // Create directories fs::create_dir_all(&caller_utils_dir)?; fs::create_dir_all(caller_utils_dir.join("src"))?; - println!("Created project directory structure"); + debug!("Created project directory structure"); // Create Cargo.toml with updated dependencies let cargo_toml = r#"[package] @@ -547,13 +570,16 @@ crate-type = ["cdylib", "lib"] // Exclude world definition files if let Ok(content) = fs::read_to_string(path) { if !content.contains("world ") { + debug!(file = %path.display(), "Adding WIT file for parsing"); wit_files.push(path.to_path_buf()); + } else { + debug!(file = %path.display(), "Skipping world definition WIT file"); } } } } - println!("Found {} WIT interface files", wit_files.len()); + info!(count = wit_files.len(), "Found WIT interface files for stub generation"); // Generate content for each module and collect types let mut module_contents = HashMap::::new(); @@ -563,9 +589,9 @@ crate-type = ["cdylib", "lib"] let interface_name = wit_file.file_stem().unwrap().to_string_lossy(); let snake_interface_name = to_snake_case(&interface_name); - println!( - "Processing interface: {} -> {}", - interface_name, snake_interface_name + info!( + interface = %interface_name, module = %snake_interface_name, file = %wit_file.display(), + "Processing interface" ); // Parse the WIT file to extract signature structs and types @@ -575,7 +601,7 @@ crate-type = ["cdylib", "lib"] interface_types.insert(interface_name.to_string(), types); if signatures.is_empty() { - println!("No signatures found in {}", wit_file.display()); + info!(file = %wit_file.display(), "No signature records found, skipping module generation for this file."); continue; } @@ -590,15 +616,15 @@ crate-type = ["cdylib", "lib"] } // Store the module content - module_contents.insert(snake_interface_name, mod_content); + module_contents.insert(snake_interface_name.clone(), mod_content); - println!( - "Generated module content with {} function stubs", - signatures.len() + info!( + interface = %interface_name, module = %snake_interface_name.as_str(), count = signatures.len(), + "Generated module content" ); } Err(e) => { - println!("Error parsing WIT file {}: {}", wit_file.display(), e); + warn!(file = %wit_file.display(), error = %e, "Error parsing WIT file, skipping"); } } } @@ -664,12 +690,13 @@ crate-type = ["cdylib", "lib"] // Write lib.rs let lib_rs_path = caller_utils_dir.join("src").join("lib.rs"); - println!("Writing lib.rs to {}", lib_rs_path.display()); + info!("Writing generated code to {}", lib_rs_path.display()); fs::write(&lib_rs_path, lib_rs) .with_context(|| format!("Failed to write lib.rs: {}", lib_rs_path.display()))?; - println!("Created single lib.rs file with all modules inline"); + + info!("Created single lib.rs file with all modules inline"); // Create target/wit directory and copy all WIT files let target_wit_dir = caller_utils_dir.join("target").join("wit"); @@ -713,15 +740,15 @@ crate-type = ["cdylib", "lib"] // Update workspace Cargo.toml to include the caller-utils crate fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { let workspace_cargo_toml = base_dir.join("Cargo.toml"); - println!( - "Updating workspace Cargo.toml at {}", - workspace_cargo_toml.display() + info!( + path = %workspace_cargo_toml.display(), + "Updating workspace Cargo.toml" ); if !workspace_cargo_toml.exists() { - println!( - "Workspace Cargo.toml not found at {}", - workspace_cargo_toml.display() + warn!( + path = %workspace_cargo_toml.display(), + "Workspace Cargo.toml not found, skipping update." ); return Ok(()); } @@ -762,9 +789,9 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { ) })?; - println!("Successfully updated workspace Cargo.toml"); + info!("Successfully updated workspace Cargo.toml"); } else { - println!("caller-utils is already in workspace members"); + debug!("Workspace Cargo.toml already up-to-date regarding caller-utils member."); } } } @@ -774,12 +801,15 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { } // Add caller-utils as a dependency to hyperware:process crates +#[instrument(skip(projects))] pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { + info!(count = projects.len(), "Adding caller-utils dependency to projects"); for project_path in projects { let cargo_toml_path = project_path.join("Cargo.toml"); - println!( - "Adding caller-utils dependency to {}", - cargo_toml_path.display() + debug!( + project = ?project_path.file_name().unwrap_or_default(), + path = %cargo_toml_path.display(), + "Processing project" ); let content = fs::read_to_string(&cargo_toml_path).with_context(|| { @@ -840,12 +870,15 @@ pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { } // Create caller-utils crate and integrate with the workspace +#[instrument(skip(base_dir, api_dir))] pub fn create_caller_utils(base_dir: &Path, api_dir: &Path) -> Result<()> { + info!("Starting caller-utils generation and integration process."); // Step 1: Create the caller-utils crate create_caller_utils_crate(api_dir, base_dir)?; // Step 2: Update workspace Cargo.toml update_workspace_cargo_toml(base_dir)?; + info!("Successfully finished caller-utils generation and integration."); Ok(()) } From 2197d119c58ee4a74768d55ea9a3462fb9dae852 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:11:15 +0000 Subject: [PATCH 26/88] Format Rust code using rustfmt --- src/build/caller_utils_generator.rs | 22 ++++++++++++++++------ src/build/wit_generator.rs | 26 +++++++++++--------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 358f9965..919dd12b 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -387,9 +387,12 @@ fn generate_async_function(signature: &SignatureStruct) -> String { } } - // First parameter is always target + // First parameter is always target let all_params = if target_param.is_empty() { - warn!("No 'target' parameter found in signature for {}", full_function_name); + warn!( + "No 'target' parameter found in signature for {}", + full_function_name + ); params.join(", ") } else { format!( @@ -579,7 +582,10 @@ crate-type = ["cdylib", "lib"] } } - info!(count = wit_files.len(), "Found WIT interface files for stub generation"); + info!( + count = wit_files.len(), + "Found WIT interface files for stub generation" + ); // Generate content for each module and collect types let mut module_contents = HashMap::::new(); @@ -695,7 +701,6 @@ crate-type = ["cdylib", "lib"] fs::write(&lib_rs_path, lib_rs) .with_context(|| format!("Failed to write lib.rs: {}", lib_rs_path.display()))?; - info!("Created single lib.rs file with all modules inline"); // Create target/wit directory and copy all WIT files @@ -791,7 +796,9 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { info!("Successfully updated workspace Cargo.toml"); } else { - debug!("Workspace Cargo.toml already up-to-date regarding caller-utils member."); + debug!( + "Workspace Cargo.toml already up-to-date regarding caller-utils member." + ); } } } @@ -803,7 +810,10 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { // Add caller-utils as a dependency to hyperware:process crates #[instrument(skip(projects))] pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { - info!(count = projects.len(), "Adding caller-utils dependency to projects"); + info!( + count = projects.len(), + "Adding caller-utils dependency to projects" + ); for project_path in projects { let cargo_toml_path = project_path.join("Cargo.toml"); debug!( diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 2b571943..c10c7a4f 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -8,8 +8,8 @@ use color_eyre::{ }; use syn::{self, Attribute, ImplItem, Item, Type}; use toml::Value; -use walkdir::WalkDir; use tracing::{debug, info, instrument, warn}; +use walkdir::WalkDir; // Helper functions for naming conventions fn to_kebab_case(s: &str) -> String { @@ -74,7 +74,6 @@ fn remove_state_suffix(name: &str) -> String { name.to_string() } - // Extract wit_world from the #[hyperprocess] attribute using the format in the debug representation #[instrument(skip(attrs))] fn extract_wit_world(attrs: &[Attribute]) -> Result { @@ -409,7 +408,7 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { warn!(" Skipping unused enum: {} -> {}", orig_name, name); // Optional debug log - continue; // Skip this enum if not in the used set + continue; // Skip this enum if not in the used set } // --- End Check --- @@ -825,10 +824,9 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(remote_struct), - Err(e) => warn!( - " Error generating remote signature struct: {}", - e - ), + Err(e) => { + warn!(" Error generating remote signature struct: {}", e) + } } } @@ -840,10 +838,9 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(local_struct), - Err(e) => warn!( - " Error generating local signature struct: {}", - e - ), + Err(e) => { + warn!(" Error generating local signature struct: {}", e) + } } } @@ -855,10 +852,9 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(http_struct), - Err(e) => warn!( - " Error generating HTTP signature struct: {}", - e - ), + Err(e) => { + warn!(" Error generating HTTP signature struct: {}", e) + } } } } From 85cbb8061d27eed2549a00256ce47e1e1a625dad Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:12:56 -0400 Subject: [PATCH 27/88] Fixed caller-utils --- src/build/caller_utils_generator.rs | 68 +++++++++++++---------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 919dd12b..5f0976e0 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -35,9 +35,9 @@ pub fn to_pascal_case(s: &str) -> String { } // Find the world name in the world WIT file, prioritizing types-prefixed worlds -#[instrument(skip(api_dir))] +#[instrument(level = "trace", skip_all)] fn find_world_names(api_dir: &Path) -> Result> { - info!(dir = ?api_dir, "Looking for world names..."); + debug!(dir = ?api_dir, "Looking for world names..."); let mut world_names = Vec::new(); // Look for world definition files @@ -68,7 +68,7 @@ fn find_world_names(api_dir: &Path) -> Result> { // Check if this is a types-prefixed world if clean_name.starts_with("types-") { world_names.push(clean_name.to_string()); - info!(name = %clean_name, "Found types-prefixed world"); + debug!(name = %clean_name, "Found types-prefixed world"); } } } @@ -84,7 +84,7 @@ fn find_world_names(api_dir: &Path) -> Result> { } // Convert WIT type to Rust type - IMPROVED with more Rust primitives -#[instrument(level = "trace")] +#[instrument(level = "trace", skip_all)] fn wit_type_to_rust(wit_type: &str) -> String { match wit_type { // Integer types @@ -144,7 +144,7 @@ fn wit_type_to_rust(wit_type: &str) -> String { } // Generate default value for Rust type - IMPROVED with additional types -#[instrument(level = "trace")] +#[instrument(level = "trace", skip_all)] fn generate_default_value(rust_type: &str) -> String { match rust_type { // Integer types @@ -204,9 +204,9 @@ struct SignatureStruct { } // Find all interface imports in the world WIT file -#[instrument(skip(api_dir))] +#[instrument(level = "trace", skip_all)] fn find_interfaces_in_world(api_dir: &Path) -> Result> { - info!(dir = ?api_dir, "Finding interface imports in world definitions"); + debug!(dir = ?api_dir, "Finding interface imports in world definitions"); let mut interfaces = Vec::new(); // Find world definition files @@ -239,14 +239,14 @@ fn find_interfaces_in_world(api_dir: &Path) -> Result> { } } } - info!(count = interfaces.len(), interfaces = ?interfaces, "Found interface imports"); + debug!(count = interfaces.len(), interfaces = ?interfaces, "Found interface imports"); Ok(interfaces) } // Parse WIT file to extract function signatures and type definitions -#[instrument(skip(file_path))] +#[instrument(level = "trace", skip_all)] fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec)> { - info!(file = %file_path.display(), "Parsing WIT file"); + debug!(file = %file_path.display(), "Parsing WIT file"); let content = fs::read_to_string(file_path) .with_context(|| format!("Failed to read WIT file: {}", file_path.display()))?; @@ -338,7 +338,7 @@ fn parse_wit_file(file_path: &Path) -> Result<(Vec, Vec i += 1; } - info!( + debug!( file = %file_path.display(), signatures = signatures.len(), types = type_names.len(), @@ -495,11 +495,11 @@ fn generate_async_function(signature: &SignatureStruct) -> String { } // Create the caller-utils crate with a single lib.rs file -#[instrument(skip(api_dir, base_dir))] +#[instrument(level = "trace", skip_all)] fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { // Path to the new crate let caller_utils_dir = base_dir.join("target").join("caller-utils"); - info!( + debug!( path = %caller_utils_dir.display(), "Creating caller-utils crate" ); @@ -535,11 +535,11 @@ crate-type = ["cdylib", "lib"] fs::write(caller_utils_dir.join("Cargo.toml"), cargo_toml) .with_context(|| "Failed to write caller-utils Cargo.toml")?; - println!("Created Cargo.toml for caller-utils"); + debug!("Created Cargo.toml for caller-utils"); // Get the world name (preferably the types- version) let world_names = find_world_names(api_dir)?; - println!("Using world names for code generation: {:?}", world_names); + debug!("Using world names for code generation: {:?}", world_names); let world_name = if world_names.len() == 0 { "" } else if world_names.len() == 1 { @@ -582,7 +582,7 @@ crate-type = ["cdylib", "lib"] } } - info!( + debug!( count = wit_files.len(), "Found WIT interface files for stub generation" ); @@ -595,7 +595,7 @@ crate-type = ["cdylib", "lib"] let interface_name = wit_file.file_stem().unwrap().to_string_lossy(); let snake_interface_name = to_snake_case(&interface_name); - info!( + debug!( interface = %interface_name, module = %snake_interface_name, file = %wit_file.display(), "Processing interface" ); @@ -607,7 +607,7 @@ crate-type = ["cdylib", "lib"] interface_types.insert(interface_name.to_string(), types); if signatures.is_empty() { - info!(file = %wit_file.display(), "No signature records found, skipping module generation for this file."); + debug!(file = %wit_file.display(), "No signature records found, skipping module generation for this file."); continue; } @@ -624,7 +624,7 @@ crate-type = ["cdylib", "lib"] // Store the module content module_contents.insert(snake_interface_name.clone(), mod_content); - info!( + debug!( interface = %interface_name, module = %snake_interface_name.as_str(), count = signatures.len(), "Generated module content" ); @@ -696,20 +696,19 @@ crate-type = ["cdylib", "lib"] // Write lib.rs let lib_rs_path = caller_utils_dir.join("src").join("lib.rs"); - info!("Writing generated code to {}", lib_rs_path.display()); + debug!("Writing generated code to {}", lib_rs_path.display()); fs::write(&lib_rs_path, lib_rs) .with_context(|| format!("Failed to write lib.rs: {}", lib_rs_path.display()))?; - info!("Created single lib.rs file with all modules inline"); // Create target/wit directory and copy all WIT files let target_wit_dir = caller_utils_dir.join("target").join("wit"); - println!("Creating directory: {}", target_wit_dir.display()); + debug!("Creating directory: {}", target_wit_dir.display()); // Remove the directory if it exists to ensure clean state if target_wit_dir.exists() { - println!("Removing existing target/wit directory"); + debug!("Removing existing target/wit directory"); fs::remove_dir_all(&target_wit_dir)?; } @@ -732,7 +731,7 @@ crate-type = ["cdylib", "lib"] target_path.display() ) })?; - println!( + debug!( "Copied {} to target/wit directory", file_name.to_string_lossy() ); @@ -743,9 +742,10 @@ crate-type = ["cdylib", "lib"] } // Update workspace Cargo.toml to include the caller-utils crate +#[instrument(level = "trace", skip_all)] fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { let workspace_cargo_toml = base_dir.join("Cargo.toml"); - info!( + debug!( path = %workspace_cargo_toml.display(), "Updating workspace Cargo.toml" ); @@ -780,7 +780,6 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { .any(|m| m.as_str().map_or(false, |s| s == "target/caller-utils")); if !caller_utils_exists { - println!("Adding caller-utils to workspace members"); members_array.push(Value::String("target/caller-utils".to_string())); // Write back the updated TOML @@ -794,7 +793,7 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { ) })?; - info!("Successfully updated workspace Cargo.toml"); + debug!("Successfully updated workspace Cargo.toml"); } else { debug!( "Workspace Cargo.toml already up-to-date regarding caller-utils member." @@ -808,12 +807,8 @@ fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { } // Add caller-utils as a dependency to hyperware:process crates -#[instrument(skip(projects))] +#[instrument(level = "trace", skip_all)] pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { - info!( - count = projects.len(), - "Adding caller-utils dependency to projects" - ); for project_path in projects { let cargo_toml_path = project_path.join("Cargo.toml"); debug!( @@ -868,9 +863,9 @@ pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { ) })?; - println!("Successfully added caller-utils dependency"); + debug!(project = ?project_path.file_name().unwrap_or_default(), "Successfully added caller-utils dependency"); } else { - println!("caller-utils dependency already exists"); + debug!(project = ?project_path.file_name().unwrap_or_default(), "caller-utils dependency already exists"); } } } @@ -880,15 +875,14 @@ pub fn add_caller_utils_to_projects(projects: &[PathBuf]) -> Result<()> { } // Create caller-utils crate and integrate with the workspace -#[instrument(skip(base_dir, api_dir))] +#[instrument(level = "trace", skip_all)] pub fn create_caller_utils(base_dir: &Path, api_dir: &Path) -> Result<()> { - info!("Starting caller-utils generation and integration process."); // Step 1: Create the caller-utils crate create_caller_utils_crate(api_dir, base_dir)?; // Step 2: Update workspace Cargo.toml update_workspace_cargo_toml(base_dir)?; - info!("Successfully finished caller-utils generation and integration."); + info!("Successfully created caller-utils and copied the imports"); Ok(()) } From dcea23771ec652573b895e3ecc6b9030cf766d2f Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:13:14 -0400 Subject: [PATCH 28/88] Fixed wit_generator --- src/build/wit_generator.rs | 74 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index c10c7a4f..dbaf5860 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -75,7 +75,7 @@ fn remove_state_suffix(name: &str) -> String { } // Extract wit_world from the #[hyperprocess] attribute using the format in the debug representation -#[instrument(skip(attrs))] +#[instrument(level = "trace", skip_all)] fn extract_wit_world(attrs: &[Attribute]) -> Result { for attr in attrs { if attr.path().is_ident("hyperprocess") { @@ -106,7 +106,7 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { } // Convert Rust type to WIT type, including downstream types -#[instrument(level = "debug", skip(ty, used_types))] +#[instrument(level = "trace", skip_all)] fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { match ty { Type::Path(type_path) => { @@ -239,7 +239,7 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Vec { let mut rust_files = Vec::new(); let src_dir = crate_path.join("src"); @@ -264,13 +264,13 @@ fn find_rust_files(crate_path: &Path) -> Vec { } // Collect **only used** type definitions (structs and enums) from a file -#[instrument(skip(used_types))] +#[instrument(level = "trace", skip_all)] fn collect_type_definitions_from_file( file_path: &Path, used_types: &HashSet, // Accept the set of used types ) -> Result> { debug!( - "Collecting used type definitions from file: {}", // Updated message + "Collecting used type definitions from file: {}", file_path.display() ); @@ -291,7 +291,7 @@ fn collect_type_definitions_from_file( // Skip trying to validate if name contains "__" as these are likely internal types if orig_name.contains("__") { // This skip can remain, as internal types are unlikely to be in `used_types` anyway - warn!(" Skipping likely internal struct: {}", orig_name); + warn!("Skipping likely internal struct: {}", orig_name); continue; } @@ -307,7 +307,7 @@ fn collect_type_definitions_from_file( } // --- End Check --- - debug!(" Found used struct: {} -> {}", orig_name, name); // Updated message + debug!(" Found used struct: {} -> {}", orig_name, name); // Proceed with field processing only if the struct is used let fields: Vec = match &item_struct.fields { @@ -327,7 +327,7 @@ fn collect_type_definitions_from_file( Ok(_) => { let field_name = to_kebab_case(&field_orig_name); if field_name.is_empty() { - warn!(" Skipping field with empty name conversion"); + warn!("Skipping field with empty name conversion"); continue; } @@ -340,7 +340,7 @@ fn collect_type_definitions_from_file( Ok(ty) => ty, Err(e) => { warn!( - " Error converting field type: {}", + "Error converting field type: {}", e ); // Propagate error if field type conversion fails @@ -407,12 +407,12 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { - warn!(" Skipping unused enum: {} -> {}", orig_name, name); // Optional debug log + warn!(" Skipping type not present in any function signature: {} -> {}", orig_name, name); // Optional debug log continue; // Skip this enum if not in the used set } // --- End Check --- - debug!(" Found used enum: {} -> {}", orig_name, name); // Updated message + debug!(" Found used enum: {} -> {}", orig_name, name); // Proceed with variant processing only if the enum is used let mut variants = Vec::new(); @@ -508,12 +508,12 @@ fn collect_type_definitions_from_file( debug!( "Collected {} used type definitions from this file", type_defs.len() - ); // Updated message + ); Ok(type_defs) } // Find all relevant Rust projects -#[instrument] +#[instrument(level = "trace", skip_all)] fn find_rust_projects(base_dir: &Path) -> Vec { let mut projects = Vec::new(); debug!("Scanning for Rust projects in {}", base_dir.display()); @@ -547,7 +547,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { .and_then(|p| p.get("metadata")) .and_then(|m| m.get("component")) else { - warn!(" No package.metadata.component metadata found"); + warn!(" No package.metadata.component metadata found in {}", cargo_toml.display()); // Added path context continue; }; let Some(package) = metadata.get("package") else { @@ -561,7 +561,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { package_str ); if package_str == "hyperware:process" { - info!(" Adding project: {}", path.display()); + debug!(" Adding project: {}", path.display()); // INFO -> DEBUG projects.push(path.to_path_buf()); } } @@ -571,7 +571,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { } // Helper function to generate signature struct for specific attribute type -#[instrument(skip(used_types))] +#[instrument(level = "trace", skip_all)] fn generate_signature_struct( kebab_name: &str, attr_type: &str, @@ -680,7 +680,7 @@ impl AsTypePath for syn::Type { } // Process a single Rust project and generate WIT files -#[instrument(skip_all)] +#[instrument(level = "trace", skip_all)] fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { info!("Processing project: {}", project_path.display()); @@ -759,9 +759,9 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { // Convert function name to kebab-case let func_kebab_name = to_kebab_case(&method_name); // Use different var name - info!( + debug!( " Processing method: {} -> {}", method_name, func_kebab_name ); @@ -863,7 +863,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result, wit_worlds: &mut HashSet, updated_world: &mut bool, ) -> Result<()> { + debug!("Rewriting WIT world files in {}", api_dir.display()); // handle existing api files for entry in WalkDir::new(api_dir) .max_depth(1) @@ -1031,13 +1032,13 @@ fn rewrite_wit( &mut include_lines, )?; - info!("Writing updated world definition to {}", path.display()); + debug!("Writing updated world definition to {}", path.display()); // Write the updated world file fs::write(path, world_content).with_context(|| { format!("Failed to write updated world file: {}", path.display()) })?; - info!("Successfully updated world definition"); + debug!("Successfully updated world definition"); // INFO -> DEBUG *updated_world = true; } } @@ -1051,13 +1052,13 @@ fn rewrite_wit( generate_wit_file(&wit_world, new_imports, &Vec::new(), &mut HashSet::new())?; let path = api_dir.join(format!("{wit_world}.wit")); - info!("Writing updated world definition to {}", path.display()); + debug!("Writing updated world definition to {}", path.display()); // Write the updated world file fs::write(&path, world_content).with_context(|| { format!("Failed to write updated world file: {}", path.display()) })?; - info!("Successfully created new world definition for {wit_world}"); + debug!("Successfully created new world definition for {wit_world}"); } *updated_world = true; } @@ -1116,8 +1117,10 @@ fn generate_wit_file( } // Generate WIT files from Rust code -#[instrument(skip(base_dir, api_dir))] +#[instrument(level = "trace", skip_all)] pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec, Vec)> { + // Keep INFO for start + info!("Generating WIT files..."); fs::create_dir_all(&api_dir)?; // Find all relevant Rust projects @@ -1135,7 +1138,6 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { @@ -1146,8 +1148,8 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec warn!("No import statement generated"), - Err(e) => warn!("Error processing project: {}", e), + Ok(None) => warn!("No import statement generated for project: {}", project_path.display()), // Add path context + Err(e) => warn!("Error processing project {}: {}", project_path.display(), e), // Add path context } } @@ -1195,7 +1197,7 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec Date: Tue, 29 Apr 2025 19:13:37 +0000 Subject: [PATCH 29/88] Format Rust code using rustfmt --- src/build/caller_utils_generator.rs | 1 - src/build/wit_generator.rs | 25 ++++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 5f0976e0..90e59f08 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -701,7 +701,6 @@ crate-type = ["cdylib", "lib"] fs::write(&lib_rs_path, lib_rs) .with_context(|| format!("Failed to write lib.rs: {}", lib_rs_path.display()))?; - // Create target/wit directory and copy all WIT files let target_wit_dir = caller_utils_dir.join("target").join("wit"); debug!("Creating directory: {}", target_wit_dir.display()); diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index dbaf5860..e582f83e 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -327,7 +327,9 @@ fn collect_type_definitions_from_file( Ok(_) => { let field_name = to_kebab_case(&field_orig_name); if field_name.is_empty() { - warn!("Skipping field with empty name conversion"); + warn!( + "Skipping field with empty name conversion" + ); continue; } @@ -339,10 +341,7 @@ fn collect_type_definitions_from_file( ) { Ok(ty) => ty, Err(e) => { - warn!( - "Error converting field type: {}", - e - ); + warn!("Error converting field type: {}", e); // Propagate error if field type conversion fails return Err(e); } @@ -407,7 +406,10 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { - warn!(" Skipping type not present in any function signature: {} -> {}", orig_name, name); // Optional debug log + warn!( + " Skipping type not present in any function signature: {} -> {}", + orig_name, name + ); // Optional debug log continue; // Skip this enum if not in the used set } // --- End Check --- @@ -547,7 +549,10 @@ fn find_rust_projects(base_dir: &Path) -> Vec { .and_then(|p| p.get("metadata")) .and_then(|m| m.get("component")) else { - warn!(" No package.metadata.component metadata found in {}", cargo_toml.display()); // Added path context + warn!( + " No package.metadata.component metadata found in {}", + cargo_toml.display() + ); // Added path context continue; }; let Some(package) = metadata.get("package") else { @@ -1138,7 +1143,6 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { new_imports.push(format!(" import {interface};")); @@ -1148,7 +1152,10 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec warn!("No import statement generated for project: {}", project_path.display()), // Add path context + Ok(None) => warn!( + "No import statement generated for project: {}", + project_path.display() + ), // Add path context Err(e) => warn!("Error processing project {}: {}", project_path.display(), e), // Add path context } } From c1c7a10effbfc57ceb6b09e877e1ac94574b1b25 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:33:03 -0400 Subject: [PATCH 30/88] added special case for #[init] --- src/build/wit_generator.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index e582f83e..2507aaad 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -801,12 +801,19 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { // Convert function name to kebab-case let func_kebab_name = to_kebab_case(&method_name); // Use different var name + debug!( " Processing method: {} -> {}", method_name, func_kebab_name ); - // Generate a signature struct for each attribute type + if has_init { + debug!( + " Found initialization function: {}", + method_name + ); + continue; + } // This will populate `used_types` if has_remote { match generate_signature_struct( @@ -868,7 +882,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Date: Wed, 30 Apr 2025 11:34:26 -0400 Subject: [PATCH 31/88] updated process_rust_project to have debug instead of info on entry --- src/build/wit_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 2507aaad..0ffd58ff 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -687,7 +687,7 @@ impl AsTypePath for syn::Type { // Process a single Rust project and generate WIT files #[instrument(level = "trace", skip_all)] fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { - info!("Processing project: {}", project_path.display()); + debug!("Processing project: {}", project_path.display()); // Find lib.rs for this project let lib_rs = project_path.join("src").join("lib.rs"); From 26423dd8dc5719e9c8688cc43c58184488976746 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:37:40 -0400 Subject: [PATCH 32/88] removed #[instrument(level = trace, skip_all)] from functions that don't return a Result --- src/build/caller_utils_generator.rs | 2 -- src/build/wit_generator.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 90e59f08..c450252e 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -84,7 +84,6 @@ fn find_world_names(api_dir: &Path) -> Result> { } // Convert WIT type to Rust type - IMPROVED with more Rust primitives -#[instrument(level = "trace", skip_all)] fn wit_type_to_rust(wit_type: &str) -> String { match wit_type { // Integer types @@ -144,7 +143,6 @@ fn wit_type_to_rust(wit_type: &str) -> String { } // Generate default value for Rust type - IMPROVED with additional types -#[instrument(level = "trace", skip_all)] fn generate_default_value(rust_type: &str) -> String { match rust_type { // Integer types diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 0ffd58ff..f37d9784 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -239,7 +239,6 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Vec { let mut rust_files = Vec::new(); let src_dir = crate_path.join("src"); @@ -515,7 +514,6 @@ fn collect_type_definitions_from_file( } // Find all relevant Rust projects -#[instrument(level = "trace", skip_all)] fn find_rust_projects(base_dir: &Path) -> Vec { let mut projects = Vec::new(); debug!("Scanning for Rust projects in {}", base_dir.display()); From 9df43dbf6373201e4a36d5d01af6f7c9750276df Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:55:00 -0400 Subject: [PATCH 33/88] consolidated all tracing examples to follow the same convention --- src/build/wit_generator.rs | 197 ++++++++++++++----------------------- 1 file changed, 72 insertions(+), 125 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index f37d9784..c3d89d47 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -81,11 +81,11 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { if attr.path().is_ident("hyperprocess") { // Convert attribute to string representation let attr_str = format!("{:?}", attr); - debug!("Attribute string: {}", attr_str); + debug!(attr_str = %attr_str, "Attribute string"); // Look for wit_world in the attribute string if let Some(pos) = attr_str.find("wit_world") { - debug!("Found wit_world at position {}", pos); + debug!(pos = %pos, "Found wit_world"); // Find the literal value after wit_world by looking for lit: "value" let lit_pattern = "lit: \""; @@ -95,7 +95,7 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { // Find the closing quote of the literal if let Some(quote_pos) = attr_str[start_pos..].find('\"') { let world_name = &attr_str[start_pos..(start_pos + quote_pos)]; - debug!("Extracted wit_world: {}", world_name); + debug!(wit_world = %world_name, "Extracted wit_world"); return Ok(world_name.to_string()); } } @@ -243,22 +243,22 @@ fn find_rust_files(crate_path: &Path) -> Vec { let mut rust_files = Vec::new(); let src_dir = crate_path.join("src"); - debug!("Finding Rust files in {}", src_dir.display()); + debug!(src_dir = %src_dir.display(), "Finding Rust files"); if !src_dir.exists() || !src_dir.is_dir() { - warn!("No src directory found at {}", src_dir.display()); + warn!(src_dir = %src_dir.display(), "No src directory found"); return rust_files; } for entry in WalkDir::new(src_dir).into_iter().filter_map(Result::ok) { let path = entry.path(); if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { - debug!("Found Rust file: {}", path.display()); + debug!(path = %path.display(), "Found Rust file"); rust_files.push(path.to_path_buf()); } } - debug!("Found {} Rust files", rust_files.len()); + debug!(count = %rust_files.len(), "Found Rust files"); rust_files } @@ -269,8 +269,8 @@ fn collect_type_definitions_from_file( used_types: &HashSet, // Accept the set of used types ) -> Result> { debug!( - "Collecting used type definitions from file: {}", - file_path.display() + file_path = %file_path.display(), + "Collecting used type definitions from file" ); let content = fs::read_to_string(file_path) @@ -290,7 +290,7 @@ fn collect_type_definitions_from_file( // Skip trying to validate if name contains "__" as these are likely internal types if orig_name.contains("__") { // This skip can remain, as internal types are unlikely to be in `used_types` anyway - warn!("Skipping likely internal struct: {}", orig_name); + warn!(name = %orig_name, "Skipping likely internal struct"); continue; } @@ -306,7 +306,7 @@ fn collect_type_definitions_from_file( } // --- End Check --- - debug!(" Found used struct: {} -> {}", orig_name, name); + debug!(original_name = %orig_name, kebab_name = %name, "Found used struct"); // Proceed with field processing only if the struct is used let fields: Vec = match &item_struct.fields { @@ -327,6 +327,7 @@ fn collect_type_definitions_from_file( let field_name = to_kebab_case(&field_orig_name); if field_name.is_empty() { warn!( + struct_name = %name, field_original_name = %field_orig_name, "Skipping field with empty name conversion" ); continue; @@ -340,7 +341,7 @@ fn collect_type_definitions_from_file( ) { Ok(ty) => ty, Err(e) => { - warn!("Error converting field type: {}", e); + warn!(struct_name = %name, field_name = %field_name, error = %e, "Error converting field type"); // Propagate error if field type conversion fails return Err(e); } @@ -356,10 +357,7 @@ fn collect_type_definitions_from_file( )); } Err(e) => { - warn!( - " Skipping field with invalid name: {}", - e - ); + warn!(struct_name = %name, error = %e, "Skipping field with invalid name"); // Decide if you want to continue or error out continue; } @@ -378,12 +376,12 @@ fn collect_type_definitions_from_file( format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), ); } else { - warn!(" Skipping used struct {} with no convertible fields", name); + warn!(name = %name, "Skipping used struct with no convertible fields"); } } Err(e) => { // Struct name validation failed, skip regardless of usage - warn!(" Skipping struct with invalid name: {}", e); + warn!(error = %e, "Skipping struct with invalid name"); continue; } } @@ -394,7 +392,7 @@ fn collect_type_definitions_from_file( // Skip trying to validate if name contains "__" if orig_name.contains("__") { - warn!(" Skipping likely internal enum: {}", orig_name); + warn!(name = %orig_name, "Skipping likely internal enum"); continue; } @@ -405,15 +403,12 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { - warn!( - " Skipping type not present in any function signature: {} -> {}", - orig_name, name - ); // Optional debug log + warn!(original_name = %orig_name, kebab_name = %name, "Skipping type not present in any function signature"); continue; // Skip this enum if not in the used set } // --- End Check --- - debug!(" Found used enum: {} -> {}", orig_name, name); + debug!(original_name = %orig_name, kebab_name = %name, "Found used enum"); // Proceed with variant processing only if the enum is used let mut variants = Vec::new(); @@ -436,20 +431,14 @@ fn collect_type_definitions_from_file( Ok(ty) => { let variant_name = to_kebab_case(&variant_orig_name); - debug!( - " Variant: {} -> {}({})", - variant_orig_name, variant_name, ty - ); + debug!(original_name = %variant_orig_name, kebab_name = %variant_name, ty_str = %ty, "Found enum variant with type"); variants.push(format!( " {}({})", variant_name, ty )); } Err(e) => { - warn!( - " Error converting variant type: {}", - e - ); + warn!(enum_name = %name, variant_name = %variant_orig_name, error = %e, "Error converting variant type"); // Propagate error if variant type conversion fails return Err(e); } @@ -457,24 +446,18 @@ fn collect_type_definitions_from_file( } syn::Fields::Unit => { let variant_name = to_kebab_case(&variant_orig_name); - debug!( - " Variant: {} -> {}", - variant_orig_name, variant_name - ); + debug!(original_name = %variant_orig_name, kebab_name = %variant_name, "Found unit enum variant"); variants.push(format!(" {}", variant_name)); } _ => { - warn!( - " Skipping complex variant in used enum {}: {}", - name, variant_orig_name - ); + warn!(enum_name = %name, variant_name = %variant_orig_name, "Skipping complex variant in used enum"); skip_enum = true; // Skip the whole enum if one variant is complex break; } } } Err(e) => { - warn!(" Skipping variant with invalid name in used enum {}: {}", name, e); + warn!(enum_name = %name, error = %e, "Skipping variant with invalid name in used enum"); skip_enum = true; // Skip the whole enum if one variant name is invalid break; } @@ -492,12 +475,12 @@ fn collect_type_definitions_from_file( ), ); } else { - warn!(" Skipping used enum {} due to complex/invalid variants or no variants", name); + warn!(name = %name, "Skipping used enum due to complex/invalid variants or no variants"); } } Err(e) => { // Enum name validation failed, skip regardless of usage - warn!(" Skipping enum with invalid name: {}", e); + warn!(error = %e, "Skipping enum with invalid name"); continue; } } @@ -507,8 +490,8 @@ fn collect_type_definitions_from_file( } debug!( - "Collected {} used type definitions from this file", - type_defs.len() + count = %type_defs.len(), file_path = %file_path.display(), + "Collected used type definitions from this file" ); Ok(type_defs) } @@ -516,7 +499,7 @@ fn collect_type_definitions_from_file( // Find all relevant Rust projects fn find_rust_projects(base_dir: &Path) -> Vec { let mut projects = Vec::new(); - debug!("Scanning for Rust projects in {}", base_dir.display()); + debug!(base_dir = %base_dir.display(), "Scanning for Rust projects"); for entry in WalkDir::new(base_dir) .max_depth(1) @@ -529,7 +512,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { continue; } let cargo_toml = path.join("Cargo.toml"); - debug!("Checking {}", cargo_toml.display()); + debug!(path = %cargo_toml.display(), "Checking path"); if !cargo_toml.exists() { continue; @@ -547,10 +530,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { .and_then(|p| p.get("metadata")) .and_then(|m| m.get("component")) else { - warn!( - " No package.metadata.component metadata found in {}", - cargo_toml.display() - ); // Added path context + warn!(path = %cargo_toml.display(), "No package.metadata.component metadata found"); continue; }; let Some(package) = metadata.get("package") else { @@ -559,17 +539,14 @@ fn find_rust_projects(base_dir: &Path) -> Vec { let Some(package_str) = package.as_str() else { continue; }; - debug!( - " Found package.metadata.component.package = {:?}", - package_str - ); + debug!(package = %package_str, "Found package.metadata.component.package"); if package_str == "hyperware:process" { - debug!(" Adding project: {}", path.display()); // INFO -> DEBUG + debug!(path = %path.display(), "Adding project"); projects.push(path.to_path_buf()); } } - debug!("Found {} relevant Rust projects", projects.len()); + debug!(count = %projects.len(), "Found relevant Rust projects"); projects } @@ -626,13 +603,13 @@ fn generate_signature_struct( .push(format!(" {}: {}", param_name, param_type)); } Err(e) => { - warn!(" Error converting parameter type: {}", e); + warn!(param_name = %param_name, error = %e, "Error converting parameter type"); return Err(e); } } } Err(e) => { - warn!(" Skipping parameter with invalid name: {}", e); + warn!(error = %e, "Skipping parameter with invalid name"); return Err(e); } } @@ -647,7 +624,7 @@ fn generate_signature_struct( struct_fields.push(format!(" returning: {}", return_type)); } Err(e) => { - warn!(" Error converting return type: {}", e); + warn!(struct_name = %signature_struct_name, error = %e, "Error converting return type"); return Err(e); } }, @@ -685,13 +662,13 @@ impl AsTypePath for syn::Type { // Process a single Rust project and generate WIT files #[instrument(level = "trace", skip_all)] fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { - debug!("Processing project: {}", project_path.display()); + debug!(project_path = %project_path.display(), "Processing project"); // Find lib.rs for this project let lib_rs = project_path.join("src").join("lib.rs"); if !lib_rs.exists() { - warn!("No lib.rs found for project: {}", project_path.display()); + warn!(project_path = %project_path.display(), "No lib.rs found for project"); return Ok(None); } @@ -734,7 +711,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - debug!("Extracted wit_world: {}", world_name); + debug!(wit_world = %world_name, "Extracted wit_world"); wit_world = Some(world_name); // Get the interface name from the impl type @@ -752,7 +729,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result Result Result {}", - method_name, func_kebab_name - ); + debug!(original_name = %method_name, kebab_name = %func_kebab_name, "Processing method"); if has_init { - debug!( - " Found initialization function: {}", - method_name - ); + debug!(method_name = %method_name, "Found initialization function"); continue; } // This will populate `used_types` @@ -842,7 +808,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(remote_struct), Err(e) => { - warn!(" Error generating remote signature struct: {}", e) + warn!(method_name = %method_name, error = %e, "Error generating remote signature struct"); } } } @@ -856,7 +822,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(local_struct), Err(e) => { - warn!(" Error generating local signature struct: {}", e) + warn!(method_name = %method_name, error = %e, "Error generating local signature struct"); } } } @@ -870,25 +836,23 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(http_struct), Err(e) => { - warn!(" Error generating HTTP signature struct: {}", e) + warn!(method_name = %method_name, error = %e, "Error generating HTTP signature struct"); } } } } Err(e) => { warn!(" Skipping method with invalid name: {}", e); + warn!(method_name = %method_name, error = %e, "Skipping method with invalid name"); } } } else { warn!(" Method {} does not have the [remote], [local], [http] or [init] attribute, it should not be in the Impl block", method_name); + warn!(method_name = %method_name, "Method missing required attribute ([remote], [local], [http], or [init])"); } } } } - debug!( - "Identified {} used types from function signatures.", - used_types.len() - ); // Collect **only used** type definitions from all Rust files let mut all_type_defs = HashMap::new(); // Now starts empty, filled by collector @@ -902,17 +866,13 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - warn!( - "Error collecting type definitions from {}: {}", - file_path.display(), - e - ); + warn!(file_path = %file_path.display(), error = %e, "Error collecting type definitions from file"); // Continue with other files } } } - debug!("Collected {} used type definitions", all_type_defs.len()); + debug!(count = %all_type_defs.len(), "Collected used type definitions"); // Now generate the WIT content for the interface if let (Some(ref iface_name), Some(ref kebab_name), Some(ref _impl_item)) = ( @@ -925,15 +885,11 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result = all_type_defs.into_values().collect(); // Collect values directly type_defs.sort(); // Sort for consistent output - debug!("Including {} used type definitions", type_defs.len()); // Generate the final WIT content if signature_structs.is_empty() && type_defs.is_empty() { // Check both sigs and types - warn!( - "No functions or used types found for interface {}", - iface_name - ); + warn!(interface_name = %iface_name, "No functions or used types found for interface"); } else { // Start with the interface comment let mut content = " // This interface contains function signature definitions that will be used\n // by the hyper-bindgen macro to generate async function bindings.\n //\n // NOTE: This is currently a hacky workaround since WIT async functions are not\n // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware,\n // we should switch to using proper async WIT function signatures instead of\n // this struct-based approach with hyper-bindgen generating the async stubs.\n".to_string(); @@ -955,16 +911,11 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result, updated_world: &mut bool, ) -> Result<()> { - debug!("Rewriting WIT world files in {}", api_dir.display()); + debug!(api_dir = %api_dir.display(), "Rewriting WIT world files"); // handle existing api files for entry in WalkDir::new(api_dir) .max_depth(1) @@ -1004,7 +956,7 @@ fn rewrite_wit( let path = entry.path(); if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { - debug!("Checking WIT file: {}", path.display()); + debug!(path = %path.display(), "Checking WIT file"); let Ok(content) = fs::read_to_string(path) else { continue; @@ -1038,7 +990,7 @@ fn rewrite_wit( continue; }; - debug!("Extracted world name: {}", world_name); + debug!(world_name = %world_name, "Extracted world name"); // Check if this world name matches the one we're looking for if wit_worlds.remove(&world_name) || wit_worlds.contains(&world_name[6..]) { @@ -1049,7 +1001,7 @@ fn rewrite_wit( &mut include_lines, )?; - debug!("Writing updated world definition to {}", path.display()); + debug!(path = %path.display(), "Writing updated world definition"); // Write the updated world file fs::write(path, world_content).with_context(|| { format!("Failed to write updated world file: {}", path.display()) @@ -1069,7 +1021,7 @@ fn rewrite_wit( generate_wit_file(&wit_world, new_imports, &Vec::new(), &mut HashSet::new())?; let path = api_dir.join(format!("{wit_world}.wit")); - debug!("Writing updated world definition to {}", path.display()); + debug!(path = %path.display(), wit_world = %wit_world, "Writing new world definition"); // Write the updated world file fs::write(&path, world_content).with_context(|| { format!("Failed to write updated world file: {}", path.display()) @@ -1164,15 +1116,16 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec warn!( - "No import statement generated for project: {}", - project_path.display() - ), // Add path context - Err(e) => warn!("Error processing project {}: {}", project_path.display(), e), // Add path context + Ok(None) => { + warn!(project_path = %project_path.display(), "No import statement generated for project"); + }, + Err(e) => { + warn!(project_path = %project_path.display(), error = %e, "Error processing project"); + }, } } - debug!("Collected {} new imports", new_imports.len()); + debug!(count = %new_imports.len(), "Collected number of new imports"); // Check for existing world definition files and update them debug!("Looking for existing world definition files"); @@ -1184,10 +1137,7 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec = new_imports @@ -1216,10 +1166,7 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Date: Wed, 30 Apr 2025 15:55:20 +0000 Subject: [PATCH 34/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index c3d89d47..1bbba5ff 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -774,14 +774,8 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { // Convert function name to kebab-case let func_kebab_name = to_kebab_case(&method_name); // Use different var name - + debug!(original_name = %method_name, kebab_name = %func_kebab_name, "Processing method"); if has_init { @@ -1118,10 +1112,10 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { warn!(project_path = %project_path.display(), "No import statement generated for project"); - }, + } Err(e) => { warn!(project_path = %project_path.display(), error = %e, "Error processing project"); - }, + } } } From f0a3835def634268d829341b78bd1009893318f5 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:07:25 -0400 Subject: [PATCH 35/88] Removed pesky debug line --- src/build/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index 0b6a760a..475ca861 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -671,7 +671,6 @@ fn get_most_recent_modified_time( return Err(eyre!("Didn't find required dirs: {must_exist_dirs:?}")); } - debug!("get_most_recent_modified_time: most_recent: {most_recent:?}, most_recent_excluded: {most_recent_excluded:?}"); Ok((most_recent, most_recent_excluded)) } From 63ebc3a018c8233f073764862b13de7c5edf2448 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:08:14 +0000 Subject: [PATCH 36/88] Format Rust code using rustfmt --- src/build/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/build/mod.rs b/src/build/mod.rs index 475ca861..e0fb2db7 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -671,7 +671,6 @@ fn get_most_recent_modified_time( return Err(eyre!("Didn't find required dirs: {must_exist_dirs:?}")); } - Ok((most_recent, most_recent_excluded)) } From 272cfd626b286fdd47801c3fcfa86c8e6e45d38c Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:09:14 -0400 Subject: [PATCH 37/88] fix --- src/build/wit_generator.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 1bbba5ff..56bc5746 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -65,6 +65,19 @@ fn validate_name(name: &str, kind: &str) -> Result<()> { Ok(()) } +// Check if a field name starts with an underscore, and if so, strip it and print a warning. +fn check_and_strip_leading_underscore(field_name: String) -> String { + if let Some(stripped) = field_name.strip_prefix('_') { + warn!(field_name = %field_name, + "This field prefixed with an underscore, which is not allowed in WIT. + Function signatures should not include unused parameters." + ); + stripped.to_string() + } else { + field_name + } +} + // Remove "State" suffix from a name fn remove_state_suffix(name: &str) -> String { if name.ends_with("State") { @@ -593,7 +606,8 @@ fn generate_signature_struct( // Validate parameter name match validate_name(¶m_orig_name, "Parameter") { Ok(_) => { - let param_name = to_kebab_case(¶m_orig_name); + let param_name = check_and_strip_leading_underscore(param_orig_name); + let param_name = to_kebab_case(¶m_name); // Rust type to WIT type match rust_type_to_wit(&pat_type.ty, used_types) { From 0adb4a7259f4853d2b6ce9ef73a41edff38a3338 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:16:27 -0400 Subject: [PATCH 38/88] Small fixes for clarity in debugging --- src/build/wit_generator.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 56bc5746..eed65e95 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -69,8 +69,7 @@ fn validate_name(name: &str, kind: &str) -> Result<()> { fn check_and_strip_leading_underscore(field_name: String) -> String { if let Some(stripped) = field_name.strip_prefix('_') { warn!(field_name = %field_name, - "This field prefixed with an underscore, which is not allowed in WIT. - Function signatures should not include unused parameters." + "field_name is prefixed with an underscore, which is not allowed in WIT. Function signatures should not include unused parameters." ); stripped.to_string() } else { @@ -405,7 +404,7 @@ fn collect_type_definitions_from_file( // Skip trying to validate if name contains "__" if orig_name.contains("__") { - warn!(name = %orig_name, "Skipping likely internal enum"); + debug!(name = %orig_name, "Skipping likely internal enum"); continue; } @@ -416,7 +415,7 @@ fn collect_type_definitions_from_file( // --- Check if this type is used --- if !used_types.contains(&name) { - warn!(original_name = %orig_name, kebab_name = %name, "Skipping type not present in any function signature"); + debug!(original_name = %orig_name, kebab_name = %name, "Skipping type not present in any function signature"); continue; // Skip this enum if not in the used set } // --- End Check --- From b54deaed187f40ce4afc708b287c152c7e58efe7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:16:55 +0000 Subject: [PATCH 39/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index eed65e95..6c1f3258 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -69,8 +69,8 @@ fn validate_name(name: &str, kind: &str) -> Result<()> { fn check_and_strip_leading_underscore(field_name: String) -> String { if let Some(stripped) = field_name.strip_prefix('_') { warn!(field_name = %field_name, - "field_name is prefixed with an underscore, which is not allowed in WIT. Function signatures should not include unused parameters." - ); + "field_name is prefixed with an underscore, which is not allowed in WIT. Function signatures should not include unused parameters." + ); stripped.to_string() } else { field_name From 29887138161ad47337c5028d2cf33b6df088af35 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:39:21 -0400 Subject: [PATCH 40/88] WIP for consolidating WIT generation, also attempted to fix result for handling Result<(), {something}> --- src/build/wit_generator.rs | 196 +++++++++++++++++++++++-------------- 1 file changed, 124 insertions(+), 72 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 6c1f3258..43a2bea7 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -11,6 +11,7 @@ use toml::Value; use tracing::{debug, info, instrument, warn}; use walkdir::WalkDir; + // Helper functions for naming conventions fn to_kebab_case(s: &str) -> String { // First, handle the case where the input has underscores @@ -116,6 +117,16 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { } bail!("wit_world not found in hyperprocess attribute") } +// Helper function to check if a WIT type name is a primitive or known built-in +fn is_wit_primitive_or_builtin(type_name: &str) -> bool { + matches!(type_name, + "s8" | "u8" | "s16" | "u16" | "s32" | "u32" | "s64" | "u64" | + "f32" | "f64" | "bool" | "char" | "string" | "address" + ) || type_name.starts_with("list<") + || type_name.starts_with("option<") + || type_name.starts_with("result<") + || type_name.starts_with("tuple<") +} // Convert Rust type to WIT type, including downstream types #[instrument(level = "trace", skip_all)] @@ -174,20 +185,41 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result= 2 { - if let ( - Some(syn::GenericArgument::Type(ok_ty)), - Some(syn::GenericArgument::Type(err_ty)), - ) = (args.args.first(), args.args.get(1)) - { - let ok_type = rust_type_to_wit(ok_ty, used_types)?; - let err_type = rust_type_to_wit(err_ty, used_types)?; - Ok(format!("result<{}, {}>", ok_type, err_type)) + if args.args.len() >= 1 { // Allow one or two args for Result + if let Some(syn::GenericArgument::Type(ok_ty)) = args.args.first() { + let ok_type_str = rust_type_to_wit(ok_ty, used_types)?; + + let err_type_str = if args.args.len() >= 2 { + if let Some(syn::GenericArgument::Type(err_ty)) = args.args.get(1) { + rust_type_to_wit(err_ty, used_types)? + } else { + // Should ideally not happen if len >= 2, but handle defensively + return Err(eyre!("Failed to parse Result second generic argument")); + } + } else { + // Only one type arg provided (e.g., Rust Result) + // Assume error type is empty tuple () + "tuple<>".to_string() + }; + + // --- BEGIN Idiomatic Result Formatting --- + let final_ok = if ok_type_str == "tuple<>" { "_" } else { &ok_type_str }; + let final_err = if err_type_str == "tuple<>" { "_" } else { &err_type_str }; + + let result_string = match (final_ok, final_err) { + ("_", "_") => "result".to_string(), // Shorthand: result + (ok, "_") => format!("result<{}>", ok), // Shorthand: result + ("_", err) => format!("result<_, {}>", err), // Explicit: result<_, E> + (ok, err) => format!("result<{}, {}>", ok, err), // Explicit: result + }; + Ok(result_string) + // --- END Idiomatic Result Formatting --- + } else { - Err(eyre!("Failed to parse Result generic arguments")) + Err(eyre!("Failed to parse Result first generic argument")) } } else { - Err(eyre!("Result requires two type arguments")) + Err(eyre!("Result requires at least one type argument in Rust")) } } else { Err(eyre!("Failed to parse Result type arguments")) @@ -235,8 +267,9 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { if type_tuple.elems.is_empty() { - // Empty tuple is unit in WIT - Ok("unit".to_string()) + debug!("Empty tuple is tuple<> in WIT"); + // Empty tuple is tuple<> in WIT + Ok("tuple<>".to_string()) } else { // Create a tuple representation in WIT let mut elem_types = Vec::new(); @@ -278,8 +311,8 @@ fn find_rust_files(crate_path: &Path) -> Vec { #[instrument(level = "trace", skip_all)] fn collect_type_definitions_from_file( file_path: &Path, - used_types: &HashSet, // Accept the set of used types -) -> Result> { + used_types: &mut HashSet, // Change to mutable, we will add to it +) -> Result> { // Keep the return type, we still build the definitions here debug!( file_path = %file_path.display(), "Collecting used type definitions from file" @@ -312,23 +345,24 @@ fn collect_type_definitions_from_file( let name = to_kebab_case(&orig_name); // --- Check if this type is used --- + // NOTE: This check remains important. We only generate definitions + // for types that are *initially* marked as used by a function signature. + // However, the recursive calls below will now add *their* dependencies + // to the main `used_types` set for the final verification step. if !used_types.contains(&name) { - // Skip this struct if not in the used set continue; } // --- End Check --- debug!(original_name = %orig_name, kebab_name = %name, "Found used struct"); - // Proceed with field processing only if the struct is used - let fields: Vec = match &item_struct.fields { + let fields: Result> = match &item_struct.fields { // Change to collect Result syn::Fields::Named(fields) => { // Note: The `rust_type_to_wit` calls here still use a *local* `used_types` // set for *recursive* type discovery *within this struct's definition*. // This is necessary for correctly formatting types like list. // The main `used_types` set (passed as argument) determines *if* this struct // definition is included at all. - let mut local_used_types_for_fields = HashSet::new(); // Renamed for clarity let mut field_strings = Vec::new(); for f in &fields.named { @@ -345,17 +379,15 @@ fn collect_type_definitions_from_file( continue; } - // This call populates `local_used_types_for_fields` if needed, - // but its primary goal here is WIT type string generation. + // Pass the main `used_types` set here let field_type = match rust_type_to_wit( &f.ty, - &mut local_used_types_for_fields, // Pass the local set + used_types, // Pass the main set ) { Ok(ty) => ty, Err(e) => { - warn!(struct_name = %name, field_name = %field_name, error = %e, "Error converting field type"); - // Propagate error if field type conversion fails - return Err(e); + // Propagate error immediately + return Err(e.wrap_err(format!("Failed to convert field '{}' in struct '{}'", field_name, name))); } }; @@ -370,29 +402,31 @@ fn collect_type_definitions_from_file( } Err(e) => { warn!(struct_name = %name, error = %e, "Skipping field with invalid name"); - // Decide if you want to continue or error out - continue; + continue; // Or return Err(e) if invalid field names should stop generation } } } } - field_strings + Ok(field_strings) // Wrap in Ok } - _ => Vec::new(), // Handle tuple structs, unit structs if needed + _ => Ok(Vec::new()), // Handle tuple structs, unit structs if needed }; - // Add the struct definition only if it has fields (or adjust logic if empty records are valid) - if !fields.is_empty() { - type_defs.insert( - name.clone(), - format!(" record {} {{\n{}\n }}", name, fields.join(",\n")), - ); - } else { - warn!(name = %name, "Skipping used struct with no convertible fields"); + match fields { + Ok(fields_vec) => { + if !fields_vec.is_empty() { + type_defs.insert( + name.clone(), + format!(" record {} {{\n{}\n }}", name, fields_vec.join(",\n")), + ); + } else { + warn!(name = %name, "Skipping used struct with no convertible fields"); + } + } + Err(e) => return Err(e), // Propagate error from field processing } } Err(e) => { - // Struct name validation failed, skip regardless of usage warn!(error = %e, "Skipping struct with invalid name"); continue; } @@ -418,8 +452,6 @@ fn collect_type_definitions_from_file( debug!(original_name = %orig_name, kebab_name = %name, "Skipping type not present in any function signature"); continue; // Skip this enum if not in the used set } - // --- End Check --- - debug!(original_name = %orig_name, kebab_name = %name, "Found used enum"); // Proceed with variant processing only if the enum is used @@ -430,29 +462,24 @@ fn collect_type_definitions_from_file( let variant_orig_name = v.ident.to_string(); match validate_name(&variant_orig_name, "Enum variant") { Ok(_) => { - match &v.fields { + match &v.fields { syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { - // Similar to structs, use a local set for inner type resolution - let mut local_used_types_for_variant = HashSet::new(); + // Pass the main `used_types` set here match rust_type_to_wit( &fields.unnamed.first().unwrap().ty, - &mut local_used_types_for_variant, // Pass local set + used_types, // Pass main set ) { Ok(ty) => { - let variant_name = - to_kebab_case(&variant_orig_name); + let variant_name = to_kebab_case(&variant_orig_name); debug!(original_name = %variant_orig_name, kebab_name = %variant_name, ty_str = %ty, "Found enum variant with type"); - variants.push(format!( - " {}({})", - variant_name, ty - )); + variants.push(format!(" {}({})", variant_name, ty)); } Err(e) => { warn!(enum_name = %name, variant_name = %variant_orig_name, error = %e, "Error converting variant type"); - // Propagate error if variant type conversion fails - return Err(e); + // Propagate error immediately + return Err(e.wrap_err(format!("Failed to convert variant '{}' in enum '{}'", variant_orig_name, name))); } } } @@ -462,8 +489,8 @@ fn collect_type_definitions_from_file( variants.push(format!(" {}", variant_name)); } _ => { - warn!(enum_name = %name, variant_name = %variant_orig_name, "Skipping complex variant in used enum"); - skip_enum = true; // Skip the whole enum if one variant is complex + warn!(enum_name = %name, variant_name = %variant_orig_name, "Skipping complex variant in used enum"); + skip_enum = true; break; } } @@ -478,13 +505,9 @@ fn collect_type_definitions_from_file( // Add the enum definition only if it wasn't skipped and has variants if !skip_enum && !variants.is_empty() { - type_defs.insert( + type_defs.insert( name.clone(), - format!( - " variant {} {{\n{}\n }}", - name, - variants.join(",\n") - ), + format!(" variant {} {{\n{}\n }}", name, variants.join(",\n")), ); } else { warn!(name = %name, "Skipping used enum due to complex/invalid variants or no variants"); @@ -505,7 +528,7 @@ fn collect_type_definitions_from_file( count = %type_defs.len(), file_path = %file_path.display(), "Collected used type definitions from this file" ); - Ok(type_defs) + Ok(type_defs) // Return the collected definitions for this file } // Find all relevant Rust projects @@ -641,9 +664,9 @@ fn generate_signature_struct( return Err(e); } }, - _ => { - // For unit return type - struct_fields.push(" returning: unit".to_string()); + syn::ReturnType::Default => { // This corresponds to -> () or no return type + // Use tuple<> for functions returning nothing (Rust unit type) + struct_fields.push(" returning: tuple<>".to_string()); } } @@ -765,7 +788,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { for (name, def) in file_type_defs { - // Since the collector only returns used types, we can insert directly + // Insert definition if successfully generated. We might insert the same + // definition multiple times if defined in multiple files; HashMap handles this. + // Crucially, we only insert if the struct/enum *itself* was initially in used_types. all_type_defs.insert(name, def); } } Err(e) => { - warn!(file_path = %file_path.display(), error = %e, "Error collecting type definitions from file"); - // Continue with other files + // Decide how to handle errors from collection: warn and continue, or bail? + // Bailing might be safer to ensure correctness. + warn!(file_path = %file_path.display(), error = %e, "Error collecting type definitions from file, WIT may be incomplete."); + // For stricter checking, you might uncomment the next line: + return Err(e.wrap_err(format!("Failed to collect type definitions from {}", file_path.display()))); } } } - debug!(count = %all_type_defs.len(), "Collected used type definitions"); + // The verification logic added previously now works correctly because + // `used_types` contains ALL custom type names encountered, both top-level and nested. + debug!(used_type_count = %used_types.len(), used_types = ?used_types, "Final set of used types for verification"); // Added debug + // Verify that all used types are either primitive/builtin or defined locally + let mut undefined_types = Vec::new(); + for used_type_name in &used_types { + // Check if the used type is a primitive/builtin OR if we found its definition locally + if !is_wit_primitive_or_builtin(used_type_name) && !all_type_defs.contains_key(used_type_name) { + undefined_types.push(used_type_name.clone()); + } + } + + // If there are undefined types, raise an error + if !undefined_types.is_empty() { + undefined_types.sort(); + bail!( + "WIT Generation Error in project '{}': Found types used (directly or indirectly) in function signatures \ + that are neither WIT built-ins nor defined locally within the scanned project files: {:?}. \ + Ensure definitions for these types (structs/enums) are present in the project's source code, \ + or adjust the function/type definitions to use only WIT-compatible types.", + project_path.display(), + undefined_types + ); + } // Now generate the WIT content for the interface if let (Some(ref iface_name), Some(ref kebab_name), Some(ref _impl_item)) = ( // impl_item no longer needed here From b7d08146d6792809072991317df2e4c30f87f502 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:39:51 +0000 Subject: [PATCH 41/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 111 +++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 34 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 43a2bea7..41369f6f 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -11,7 +11,6 @@ use toml::Value; use tracing::{debug, info, instrument, warn}; use walkdir::WalkDir; - // Helper functions for naming conventions fn to_kebab_case(s: &str) -> String { // First, handle the case where the input has underscores @@ -119,13 +118,25 @@ fn extract_wit_world(attrs: &[Attribute]) -> Result { } // Helper function to check if a WIT type name is a primitive or known built-in fn is_wit_primitive_or_builtin(type_name: &str) -> bool { - matches!(type_name, - "s8" | "u8" | "s16" | "u16" | "s32" | "u32" | "s64" | "u64" | - "f32" | "f64" | "bool" | "char" | "string" | "address" + matches!( + type_name, + "s8" | "u8" + | "s16" + | "u16" + | "s32" + | "u32" + | "s64" + | "u64" + | "f32" + | "f64" + | "bool" + | "char" + | "string" + | "address" ) || type_name.starts_with("list<") - || type_name.starts_with("option<") - || type_name.starts_with("result<") - || type_name.starts_with("tuple<") + || type_name.starts_with("option<") + || type_name.starts_with("result<") + || type_name.starts_with("tuple<") } // Convert Rust type to WIT type, including downstream types @@ -185,16 +196,21 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result= 1 { // Allow one or two args for Result + if args.args.len() >= 1 { + // Allow one or two args for Result if let Some(syn::GenericArgument::Type(ok_ty)) = args.args.first() { let ok_type_str = rust_type_to_wit(ok_ty, used_types)?; let err_type_str = if args.args.len() >= 2 { - if let Some(syn::GenericArgument::Type(err_ty)) = args.args.get(1) { + if let Some(syn::GenericArgument::Type(err_ty)) = + args.args.get(1) + { rust_type_to_wit(err_ty, used_types)? } else { // Should ideally not happen if len >= 2, but handle defensively - return Err(eyre!("Failed to parse Result second generic argument")); + return Err(eyre!( + "Failed to parse Result second generic argument" + )); } } else { // Only one type arg provided (e.g., Rust Result) @@ -203,18 +219,25 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result" { "_" } else { &ok_type_str }; - let final_err = if err_type_str == "tuple<>" { "_" } else { &err_type_str }; + let final_ok = if ok_type_str == "tuple<>" { + "_" + } else { + &ok_type_str + }; + let final_err = if err_type_str == "tuple<>" { + "_" + } else { + &err_type_str + }; let result_string = match (final_ok, final_err) { - ("_", "_") => "result".to_string(), // Shorthand: result + ("_", "_") => "result".to_string(), // Shorthand: result (ok, "_") => format!("result<{}>", ok), // Shorthand: result ("_", err) => format!("result<_, {}>", err), // Explicit: result<_, E> (ok, err) => format!("result<{}, {}>", ok, err), // Explicit: result }; Ok(result_string) // --- END Idiomatic Result Formatting --- - } else { Err(eyre!("Failed to parse Result first generic argument")) } @@ -312,7 +335,8 @@ fn find_rust_files(crate_path: &Path) -> Vec { fn collect_type_definitions_from_file( file_path: &Path, used_types: &mut HashSet, // Change to mutable, we will add to it -) -> Result> { // Keep the return type, we still build the definitions here +) -> Result> { + // Keep the return type, we still build the definitions here debug!( file_path = %file_path.display(), "Collecting used type definitions from file" @@ -356,7 +380,8 @@ fn collect_type_definitions_from_file( debug!(original_name = %orig_name, kebab_name = %name, "Found used struct"); - let fields: Result> = match &item_struct.fields { // Change to collect Result + let fields: Result> = match &item_struct.fields { + // Change to collect Result syn::Fields::Named(fields) => { // Note: The `rust_type_to_wit` calls here still use a *local* `used_types` // set for *recursive* type discovery *within this struct's definition*. @@ -381,8 +406,7 @@ fn collect_type_definitions_from_file( // Pass the main `used_types` set here let field_type = match rust_type_to_wit( - &f.ty, - used_types, // Pass the main set + &f.ty, used_types, // Pass the main set ) { Ok(ty) => ty, Err(e) => { @@ -417,10 +441,14 @@ fn collect_type_definitions_from_file( if !fields_vec.is_empty() { type_defs.insert( name.clone(), - format!(" record {} {{\n{}\n }}", name, fields_vec.join(",\n")), + format!( + " record {} {{\n{}\n }}", + name, + fields_vec.join(",\n") + ), ); } else { - warn!(name = %name, "Skipping used struct with no convertible fields"); + warn!(name = %name, "Skipping used struct with no convertible fields"); } } Err(e) => return Err(e), // Propagate error from field processing @@ -462,7 +490,7 @@ fn collect_type_definitions_from_file( let variant_orig_name = v.ident.to_string(); match validate_name(&variant_orig_name, "Enum variant") { Ok(_) => { - match &v.fields { + match &v.fields { syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { @@ -472,13 +500,17 @@ fn collect_type_definitions_from_file( used_types, // Pass main set ) { Ok(ty) => { - let variant_name = to_kebab_case(&variant_orig_name); + let variant_name = + to_kebab_case(&variant_orig_name); debug!(original_name = %variant_orig_name, kebab_name = %variant_name, ty_str = %ty, "Found enum variant with type"); - variants.push(format!(" {}({})", variant_name, ty)); + variants.push(format!( + " {}({})", + variant_name, ty + )); } Err(e) => { warn!(enum_name = %name, variant_name = %variant_orig_name, error = %e, "Error converting variant type"); - // Propagate error immediately + // Propagate error immediately return Err(e.wrap_err(format!("Failed to convert variant '{}' in enum '{}'", variant_orig_name, name))); } } @@ -489,7 +521,7 @@ fn collect_type_definitions_from_file( variants.push(format!(" {}", variant_name)); } _ => { - warn!(enum_name = %name, variant_name = %variant_orig_name, "Skipping complex variant in used enum"); + warn!(enum_name = %name, variant_name = %variant_orig_name, "Skipping complex variant in used enum"); skip_enum = true; break; } @@ -505,9 +537,13 @@ fn collect_type_definitions_from_file( // Add the enum definition only if it wasn't skipped and has variants if !skip_enum && !variants.is_empty() { - type_defs.insert( + type_defs.insert( name.clone(), - format!(" variant {} {{\n{}\n }}", name, variants.join(",\n")), + format!( + " variant {} {{\n{}\n }}", + name, + variants.join(",\n") + ), ); } else { warn!(name = %name, "Skipping used enum due to complex/invalid variants or no variants"); @@ -664,7 +700,8 @@ fn generate_signature_struct( return Err(e); } }, - syn::ReturnType::Default => { // This corresponds to -> () or no return type + syn::ReturnType::Default => { + // This corresponds to -> () or no return type // Use tuple<> for functions returning nothing (Rust unit type) struct_fields.push(" returning: tuple<>".to_string()); } @@ -889,7 +926,8 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { for (name, def) in file_type_defs { // Insert definition if successfully generated. We might insert the same @@ -901,9 +939,12 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { // Decide how to handle errors from collection: warn and continue, or bail? // Bailing might be safer to ensure correctness. - warn!(file_path = %file_path.display(), error = %e, "Error collecting type definitions from file, WIT may be incomplete."); - // For stricter checking, you might uncomment the next line: - return Err(e.wrap_err(format!("Failed to collect type definitions from {}", file_path.display()))); + warn!(file_path = %file_path.display(), error = %e, "Error collecting type definitions from file, WIT may be incomplete."); + // For stricter checking, you might uncomment the next line: + return Err(e.wrap_err(format!( + "Failed to collect type definitions from {}", + file_path.display() + ))); } } } @@ -916,11 +957,13 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Date: Wed, 30 Apr 2025 16:54:51 -0400 Subject: [PATCH 42/88] small change WIP --- src/build/wit_generator.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 41369f6f..588170cc 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -218,7 +218,6 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result".to_string() }; - // --- BEGIN Idiomatic Result Formatting --- let final_ok = if ok_type_str == "tuple<>" { "_" } else { @@ -237,7 +236,7 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result format!("result<{}, {}>", ok, err), // Explicit: result }; Ok(result_string) - // --- END Idiomatic Result Formatting --- + } else { Err(eyre!("Failed to parse Result first generic argument")) } From 111f558903b0432fcdb4b2df6912ad193b5c1920 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:55:11 +0000 Subject: [PATCH 43/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 588170cc..c0d8de41 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -236,7 +236,6 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result format!("result<{}, {}>", ok, err), // Explicit: result }; Ok(result_string) - } else { Err(eyre!("Failed to parse Result first generic argument")) } From 94f5597ab12b59a96f194c7a56992b3a0aeda86e Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:08:32 -0400 Subject: [PATCH 44/88] made small adjustments --- src/build/wit_generator.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index c0d8de41..d49570e7 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -381,11 +381,13 @@ fn collect_type_definitions_from_file( let fields: Result> = match &item_struct.fields { // Change to collect Result syn::Fields::Named(fields) => { - // Note: The `rust_type_to_wit` calls here still use a *local* `used_types` - // set for *recursive* type discovery *within this struct's definition*. - // This is necessary for correctly formatting types like list. - // The main `used_types` set (passed as argument) determines *if* this struct - // definition is included at all. + // The recursive calls to `rust_type_to_wit` below use the main `used_types` + // set (passed mutably to this function). This ensures that any nested custom + // types encountered within fields (e.g., the `T` in `list`) are added to + // the main set for the final verification step in `process_rust_project`. + // The initial check `if !used_types.contains(&name)` still determines + // if this specific struct's definition is generated based on direct usage + // in function signatures. let mut field_strings = Vec::new(); for f in &fields.named { @@ -423,8 +425,11 @@ fn collect_type_definitions_from_file( )); } Err(e) => { - warn!(struct_name = %name, error = %e, "Skipping field with invalid name"); - continue; // Or return Err(e) if invalid field names should stop generation + // Propagate the error instead of just warning and continuing + return Err(e.wrap_err(format!( + "Invalid field name '{}' found in struct '{}'", + field_orig_name, name + ))); } } } @@ -453,8 +458,11 @@ fn collect_type_definitions_from_file( } } Err(e) => { - warn!(error = %e, "Skipping struct with invalid name"); - continue; + // Return the error instead of just warning + return Err(e.wrap_err(format!( + "Invalid struct name '{}' found", + orig_name + ))); } } } From d4bc171c5d374d9417e3036c07eb8fd5bba90081 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:08:57 +0000 Subject: [PATCH 45/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index d49570e7..758a84bb 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -459,10 +459,9 @@ fn collect_type_definitions_from_file( } Err(e) => { // Return the error instead of just warning - return Err(e.wrap_err(format!( - "Invalid struct name '{}' found", - orig_name - ))); + return Err( + e.wrap_err(format!("Invalid struct name '{}' found", orig_name)) + ); } } } From 37b98cf0b22f30c9ded05b6417b952bdb4c38985 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 1 May 2025 11:12:02 -0400 Subject: [PATCH 46/88] Handles errors more neatly, still WIP (errors at least print out fine, but we need to handle this better) --- src/build/wit_generator.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 758a84bb..f65f1fbf 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1224,10 +1224,14 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { - warn!(project_path = %project_path.display(), "No import statement generated for project"); + bail!("No import statement generated for project {}", project_path.display()); } Err(e) => { - warn!(project_path = %project_path.display(), error = %e, "Error processing project"); + bail!( + "Error processing project {}: {}", + project_path.display(), + e + ); } } } From 3c908f314e5100e3f0b32a05975d75f7ac421b36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 17:37:35 +0000 Subject: [PATCH 47/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index f65f1fbf..29c74e0b 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1224,15 +1224,14 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { - bail!("No import statement generated for project {}", project_path.display()); - } - Err(e) => { bail!( - "Error processing project {}: {}", - project_path.display(), - e + "No import statement generated for project {}", + project_path.display() ); } + Err(e) => { + bail!("Error processing project {}: {}", project_path.display(), e); + } } } From a2bfe2fef52abc70fe8326c1dd3cbd02e0740371 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 1 May 2025 13:37:48 -0400 Subject: [PATCH 48/88] added support for Result<(), E>, this fix also dissalows implicit or empty tuple '()' return of a function --- src/build/wit_generator.rs | 126 +++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 29c74e0b..8f2eb274 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -69,7 +69,7 @@ fn validate_name(name: &str, kind: &str) -> Result<()> { fn check_and_strip_leading_underscore(field_name: String) -> String { if let Some(stripped) = field_name.strip_prefix('_') { warn!(field_name = %field_name, - "field_name is prefixed with an underscore, which is not allowed in WIT. Function signatures should not include unused parameters." + " Warning: Field name starts with an underscore ('_'), which is invalid in WIT. Stripping the underscore from WIT definition. Function signatures should only include parameters that are actually used." ); stripped.to_string() } else { @@ -196,39 +196,31 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result= 1 { - // Allow one or two args for Result - if let Some(syn::GenericArgument::Type(ok_ty)) = args.args.first() { + // Strictly enforce exactly two arguments for Result + if args.args.len() == 2 { + if let ( + Some(syn::GenericArgument::Type(ok_ty)), + Some(syn::GenericArgument::Type(err_ty)), + ) = (args.args.first(), args.args.get(1)) + { let ok_type_str = rust_type_to_wit(ok_ty, used_types)?; + let err_type_str = rust_type_to_wit(err_ty, used_types)?; - let err_type_str = if args.args.len() >= 2 { - if let Some(syn::GenericArgument::Type(err_ty)) = - args.args.get(1) - { - rust_type_to_wit(err_ty, used_types)? - } else { - // Should ideally not happen if len >= 2, but handle defensively - return Err(eyre!( - "Failed to parse Result second generic argument" - )); - } - } else { - // Only one type arg provided (e.g., Rust Result) - // Assume error type is empty tuple () - "tuple<>".to_string() - }; - - let final_ok = if ok_type_str == "tuple<>" { + // Map Rust's () (represented as "_") to WIT's _ in result<...> + let final_ok = if ok_type_str == "_" { + // Check for "_" "_" } else { &ok_type_str }; - let final_err = if err_type_str == "tuple<>" { + let final_err = if err_type_str == "_" { + // Check for "_" "_" } else { &err_type_str }; + // Format the WIT result string according to WIT conventions let result_string = match (final_ok, final_err) { ("_", "_") => "result".to_string(), // Shorthand: result (ok, "_") => format!("result<{}>", ok), // Shorthand: result @@ -237,10 +229,14 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result), found {}", + args.args.len() + )) } } else { Err(eyre!("Failed to parse Result type arguments")) @@ -286,11 +282,13 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Result<(), Error> + // tuple<> Type::Tuple(type_tuple) => { if type_tuple.elems.is_empty() { - debug!("Empty tuple is tuple<> in WIT"); - // Empty tuple is tuple<> in WIT - Ok("tuple<>".to_string()) + // Represent () as "_" for the caller to interpret based on context. + // It's valid within Result<_, E>, but invalid as a direct return type. + Ok("_".to_string()) } else { // Create a tuple representation in WIT let mut elem_types = Vec::new(); @@ -663,14 +661,16 @@ fn generate_signature_struct( continue; } - // Get original param name and convert to kebab-case + // Get original param name let param_orig_name = pat_ident.ident.to_string(); + let method_name_for_error = method.sig.ident.to_string(); // Get method name for error messages // Validate parameter name match validate_name(¶m_orig_name, "Parameter") { Ok(_) => { - let param_name = check_and_strip_leading_underscore(param_orig_name); - let param_name = to_kebab_case(¶m_name); + let stripped_param_name = + check_and_strip_leading_underscore(param_orig_name.clone()); // Clone needed + let param_name = to_kebab_case(&stripped_param_name); // Rust type to WIT type match rust_type_to_wit(&pat_type.ty, used_types) { @@ -680,14 +680,20 @@ fn generate_signature_struct( .push(format!(" {}: {}", param_name, param_type)); } Err(e) => { - warn!(param_name = %param_name, error = %e, "Error converting parameter type"); - return Err(e); + // Wrap parameter type conversion error with context + return Err(e.wrap_err(format!( + "Failed to convert type for parameter '{}' in function '{}'", + param_orig_name, method_name_for_error + ))); } } } Err(e) => { - warn!(error = %e, "Skipping parameter with invalid name"); - return Err(e); + // Wrap invalid parameter name error with context + return Err(e.wrap_err(format!( + "Invalid name for parameter '{}' in function '{}'", + param_orig_name, method_name_for_error + ))); } } } @@ -698,20 +704,36 @@ fn generate_signature_struct( match &method.sig.output { syn::ReturnType::Type(_, ty) => match rust_type_to_wit(&*ty, used_types) { Ok(return_type) => { + // Check if the return type is "_", which signifies a standalone () return type. + if return_type == "_" { + let method_name = method.sig.ident.to_string(); + bail!( + "Function '{}' returns '()', which is not directly supported in WIT signatures. \ + Consider returning a Result<(), YourErrorType> or another meaningful type.", + method_name + ); + } + // Add the valid return type field struct_fields.push(format!(" returning: {}", return_type)); } Err(e) => { - warn!(struct_name = %signature_struct_name, error = %e, "Error converting return type"); - return Err(e); + // Propagate *other* errors from return type conversion, wrapping them. + let method_name = method.sig.ident.to_string(); + return Err(e.wrap_err(format!( + "Failed to convert return type for function '{}'", + method_name + ))); } }, syn::ReturnType::Default => { - // This corresponds to -> () or no return type - // Use tuple<> for functions returning nothing (Rust unit type) - struct_fields.push(" returning: tuple<>".to_string()); + // Functions exposed via WIT must have an explicit return type. + let method_name = method.sig.ident.to_string(); + bail!( + "Function '{}' must have an explicit return type (e.g., '-> MyType' or '-> Result<(), YourErrorType>') to be exposed via WIT. Implicit return types are not allowed.", + method_name + ); } } - // Combine everything into a record definition let record_def = format!( "{}\n record {} {{\n{}\n }}", @@ -880,7 +902,8 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(remote_struct), Err(e) => { - warn!(method_name = %method_name, error = %e, "Error generating remote signature struct"); + // Error: Return the specific error from generate_signature_struct directly + return Err(e); } } } @@ -894,7 +917,8 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(local_struct), Err(e) => { - warn!(method_name = %method_name, error = %e, "Error generating local signature struct"); + // Error: Return the specific error from generate_signature_struct directly + return Err(e); } } } @@ -908,19 +932,25 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result signature_structs.push(http_struct), Err(e) => { - warn!(method_name = %method_name, error = %e, "Error generating HTTP signature struct"); + // Error: Return the specific error from generate_signature_struct directly + return Err(e); } } } } Err(e) => { - warn!(" Skipping method with invalid name: {}", e); - warn!(method_name = %method_name, error = %e, "Skipping method with invalid name"); + return Err(e.wrap_err(format!( + "Method: '{}' has invalid name, it should not include any numbers or the keyword 'stream'", + method_name + ))); } } } else { - warn!(" Method {} does not have the [remote], [local], [http] or [init] attribute, it should not be in the Impl block", method_name); - warn!(method_name = %method_name, "Method missing required attribute ([remote], [local], [http], or [init])"); + // Method lacks the required attributes, this is an error - Keep specific error message + return Err(eyre!( + "Method '{}' in the #[hyperprocess] impl block is missing a required attribute ([remote], [local], [http], or [init]). Only methods with these attributes should be included in this impl block.", + method_name + )); } } } From efd2ddc86a8868752990c05970a7e699472ba793 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 1 May 2025 15:23:20 -0400 Subject: [PATCH 49/88] added more expressive error handling for some cases, added support for nested types in function definitions --- src/build/wit_generator.rs | 841 ++++++++++++++++--------------------- 1 file changed, 369 insertions(+), 472 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 8f2eb274..0efc19e8 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -326,16 +326,20 @@ fn find_rust_files(crate_path: &Path) -> Vec { rust_files } -// Collect **only used** type definitions (structs and enums) from a file +// Searches a single file for a specific type definition (struct or enum) by its kebab-case name. +// If found, generates its WIT definition string and returns it along with any new custom type +// dependencies discovered within its fields/variants. #[instrument(level = "trace", skip_all)] -fn collect_type_definitions_from_file( +fn find_and_make_wit_type_def( file_path: &Path, - used_types: &mut HashSet, // Change to mutable, we will add to it -) -> Result> { - // Keep the return type, we still build the definitions here + target_kebab_type_name: &str, + global_used_types: &mut HashSet, // Track all used types globally +) -> Result)>> { + // Return: Ok(Some((wit_def, new_local_deps))), Ok(None), or Err debug!( file_path = %file_path.display(), - "Collecting used type definitions from file" + target_type = %target_kebab_type_name, + "Searching for type definition" ); let content = fs::read_to_string(file_path) @@ -344,230 +348,159 @@ fn collect_type_definitions_from_file( let ast = syn::parse_file(&content) .with_context(|| format!("Failed to parse file: {}", file_path.display()))?; - let mut type_defs = HashMap::new(); - for item in &ast.items { - match item { - Item::Struct(item_struct) => { - // Validate struct name doesn't contain numbers or "stream" - let orig_name = item_struct.ident.to_string(); - - // Skip trying to validate if name contains "__" as these are likely internal types - if orig_name.contains("__") { - // This skip can remain, as internal types are unlikely to be in `used_types` anyway - warn!(name = %orig_name, "Skipping likely internal struct"); - continue; - } + // Determine if the current item matches the target type name + let (is_target, item_kind, orig_name) = match item { + Item::Struct(s) => { + let name = s.ident.to_string(); + ( + to_kebab_case(&name) == target_kebab_type_name, + "Struct", + name, + ) + } + Item::Enum(e) => { + let name = e.ident.to_string(); + (to_kebab_case(&name) == target_kebab_type_name, "Enum", name) + } + _ => (false, "", String::new()), + }; - match validate_name(&orig_name, "Struct") { - Ok(_) => { - // Use kebab-case for struct name - let name = to_kebab_case(&orig_name); - - // --- Check if this type is used --- - // NOTE: This check remains important. We only generate definitions - // for types that are *initially* marked as used by a function signature. - // However, the recursive calls below will now add *their* dependencies - // to the main `used_types` set for the final verification step. - if !used_types.contains(&name) { - continue; - } - // --- End Check --- - - debug!(original_name = %orig_name, kebab_name = %name, "Found used struct"); - - let fields: Result> = match &item_struct.fields { - // Change to collect Result - syn::Fields::Named(fields) => { - // The recursive calls to `rust_type_to_wit` below use the main `used_types` - // set (passed mutably to this function). This ensures that any nested custom - // types encountered within fields (e.g., the `T` in `list`) are added to - // the main set for the final verification step in `process_rust_project`. - // The initial check `if !used_types.contains(&name)` still determines - // if this specific struct's definition is generated based on direct usage - // in function signatures. - let mut field_strings = Vec::new(); - - for f in &fields.named { - if let Some(field_ident) = &f.ident { - let field_orig_name = field_ident.to_string(); - match validate_name(&field_orig_name, "Field") { - Ok(_) => { - let field_name = to_kebab_case(&field_orig_name); - if field_name.is_empty() { - warn!( - struct_name = %name, field_original_name = %field_orig_name, - "Skipping field with empty name conversion" - ); - continue; - } - - // Pass the main `used_types` set here - let field_type = match rust_type_to_wit( - &f.ty, used_types, // Pass the main set - ) { - Ok(ty) => ty, - Err(e) => { - // Propagate error immediately - return Err(e.wrap_err(format!("Failed to convert field '{}' in struct '{}'", field_name, name))); - } - }; - - debug!( - " Field: {} -> {}", - field_name, field_type - ); - field_strings.push(format!( - " {}: {}", - field_name, field_type - )); - } - Err(e) => { - // Propagate the error instead of just warning and continuing - return Err(e.wrap_err(format!( - "Invalid field name '{}' found in struct '{}'", - field_orig_name, name - ))); - } - } - } + if is_target { + // Skip internal-looking types (can be adjusted) + if orig_name.contains("__") { + warn!(name = %orig_name, "Skipping definition search for likely internal type"); + return Ok(None); // Treat as not found for WIT purposes + } + // Validate the original Rust name + validate_name(&orig_name, item_kind)?; + + let kebab_name = target_kebab_type_name; // We know this matches + let mut local_dependencies = HashSet::new(); // Track deps discovered *by this type* + + // --- Generate Struct Definition --- + if let Item::Struct(item_struct) = item { + let fields_result: Result> = match &item_struct.fields { + syn::Fields::Named(fields) => { + let mut field_strings = Vec::new(); + for f in &fields.named { + if let Some(field_ident) = &f.ident { + let field_orig_name = field_ident.to_string(); + // Validate field name (allow underscore stripping) + let stripped_field_orig_name = + check_and_strip_leading_underscore(field_orig_name.clone()); + // Validate the potentially stripped name, adding context about the rules + validate_name(&stripped_field_orig_name, "Field")?; + + let field_kebab_name = to_kebab_case(&stripped_field_orig_name); + if field_kebab_name.is_empty() { + warn!(struct_name=%kebab_name, field_original_name=%field_orig_name, "Skipping field with empty kebab-case name"); + continue; } - Ok(field_strings) // Wrap in Ok - } - _ => Ok(Vec::new()), // Handle tuple structs, unit structs if needed - }; - match fields { - Ok(fields_vec) => { - if !fields_vec.is_empty() { - type_defs.insert( - name.clone(), - format!( - " record {} {{\n{}\n }}", - name, - fields_vec.join(",\n") - ), - ); - } else { - warn!(name = %name, "Skipping used struct with no convertible fields"); + // Convert field type. `rust_type_to_wit` adds any new custom types + // found within the field type (e.g., in list) to `global_used_types`. + let field_wit_type = rust_type_to_wit(&f.ty, global_used_types) + .wrap_err_with(|| format!("Failed to convert field '{}':'{:?}' in struct '{}'", field_orig_name, f.ty, orig_name))?; + + // If the resulting WIT type itself is custom, add it to *local* dependencies + // so the caller knows this struct definition depends on it. + if !is_wit_primitive_or_builtin(&field_wit_type) { + local_dependencies.insert(field_wit_type.clone()); } + + field_strings.push(format!(" {}: {}", field_kebab_name, field_wit_type)); } - Err(e) => return Err(e), // Propagate error from field processing } + Ok(field_strings) } - Err(e) => { - // Return the error instead of just warning - return Err( - e.wrap_err(format!("Invalid struct name '{}' found", orig_name)) - ); + // Handle Unit Structs as empty records + syn::Fields::Unit => Ok(Vec::new()), + // Decide how to handle Tuple Structs (e.g., error, skip, specific WIT representation?) + syn::Fields::Unnamed(_) => bail!("Tuple structs ('struct {} (...)') are not currently supported for WIT generation.", orig_name), + }; + + match fields_result { + Ok(fields_vec) => { + // Generate record definition (use {} for empty records) + let definition = if fields_vec.is_empty() { + format!(" record {} {{}}", kebab_name) + } else { + format!( + " record {} {{\n{}\n }}", + kebab_name, + fields_vec.join(",\n") + ) + }; + debug!(type_name = %kebab_name, "Generated record definition"); + return Ok(Some((definition, local_dependencies))); } + Err(e) => return Err(e), // Propagate field processing error } } - Item::Enum(item_enum) => { - // Validate enum name doesn't contain numbers or "stream" - let orig_name = item_enum.ident.to_string(); - - // Skip trying to validate if name contains "__" - if orig_name.contains("__") { - debug!(name = %orig_name, "Skipping likely internal enum"); - continue; - } - - match validate_name(&orig_name, "Enum") { - Ok(_) => { - // Use kebab-case for enum name - let name = to_kebab_case(&orig_name); - - // --- Check if this type is used --- - if !used_types.contains(&name) { - debug!(original_name = %orig_name, kebab_name = %name, "Skipping type not present in any function signature"); - continue; // Skip this enum if not in the used set - } - debug!(original_name = %orig_name, kebab_name = %name, "Found used enum"); - // Proceed with variant processing only if the enum is used - let mut variants = Vec::new(); - let mut skip_enum = false; - - for v in &item_enum.variants { - let variant_orig_name = v.ident.to_string(); - match validate_name(&variant_orig_name, "Enum variant") { - Ok(_) => { - match &v.fields { - syn::Fields::Unnamed(fields) - if fields.unnamed.len() == 1 => - { - // Pass the main `used_types` set here - match rust_type_to_wit( - &fields.unnamed.first().unwrap().ty, - used_types, // Pass main set - ) { - Ok(ty) => { - let variant_name = - to_kebab_case(&variant_orig_name); - debug!(original_name = %variant_orig_name, kebab_name = %variant_name, ty_str = %ty, "Found enum variant with type"); - variants.push(format!( - " {}({})", - variant_name, ty - )); - } - Err(e) => { - warn!(enum_name = %name, variant_name = %variant_orig_name, error = %e, "Error converting variant type"); - // Propagate error immediately - return Err(e.wrap_err(format!("Failed to convert variant '{}' in enum '{}'", variant_orig_name, name))); - } - } - } - syn::Fields::Unit => { - let variant_name = to_kebab_case(&variant_orig_name); - debug!(original_name = %variant_orig_name, kebab_name = %variant_name, "Found unit enum variant"); - variants.push(format!(" {}", variant_name)); - } - _ => { - warn!(enum_name = %name, variant_name = %variant_orig_name, "Skipping complex variant in used enum"); - skip_enum = true; - break; - } - } - } - Err(e) => { - warn!(enum_name = %name, error = %e, "Skipping variant with invalid name in used enum"); - skip_enum = true; // Skip the whole enum if one variant name is invalid - break; - } + // --- Generate Enum Definition --- + if let Item::Enum(item_enum) = item { + let mut variants_wit = Vec::new(); + let mut skip_enum = false; + + for v in &item_enum.variants { + let variant_orig_name = v.ident.to_string(); + // Validate variant name before proceeding + validate_name(&variant_orig_name, "Enum variant")?; + let variant_kebab_name = to_kebab_case(&variant_orig_name); + + match &v.fields { + // Variant with one unnamed field: T -> case(T) + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + // `rust_type_to_wit` adds new custom types to `global_used_types` + let type_result = rust_type_to_wit( + &fields.unnamed.first().unwrap().ty, + global_used_types, + ) + .wrap_err_with(|| format!("Failed to convert variant '{}' type in enum '{}'", variant_orig_name, orig_name))?; + + // Check if the variant's type is custom and add to local deps + if !is_wit_primitive_or_builtin(&type_result) { + local_dependencies.insert(type_result.clone()); } + variants_wit.push(format!(" {}({})", variant_kebab_name, type_result)); } - - // Add the enum definition only if it wasn't skipped and has variants - if !skip_enum && !variants.is_empty() { - type_defs.insert( - name.clone(), - format!( - " variant {} {{\n{}\n }}", - name, - variants.join(",\n") - ), - ); - } else { - warn!(name = %name, "Skipping used enum due to complex/invalid variants or no variants"); + // Unit variant: -> case + syn::Fields::Unit => { + variants_wit.push(format!(" {}", variant_kebab_name)); + } + // Variants with named fields or multiple unnamed fields are not directly supported by WIT variants + _ => { + warn!(enum_name = %kebab_name, variant_name = %variant_orig_name, "Skipping complex enum variant (only unit variants or single-type variants like 'MyVariant(MyType)' are supported)"); + skip_enum = true; + break; // Skip the whole enum if one variant is complex } } - Err(e) => { - // Enum name validation failed, skip regardless of usage - warn!(error = %e, "Skipping enum with invalid name"); - continue; - } + } + + // Only generate if not skipped and has convertible variants + if !skip_enum && !variants_wit.is_empty() { + let definition = format!( + " variant {} {{\n{}\n }}", + kebab_name, + variants_wit.join(",\n") + ); + debug!(type_name = %kebab_name, "Generated variant definition"); + return Ok(Some((definition, local_dependencies))); + } else { + // Treat as not found for WIT generation if skipped or empty + warn!(name = %kebab_name, "Skipping enum definition due to complex/invalid variants or no convertible variants"); + return Ok(None); } } - _ => {} // Handle other top-level items like functions, impls, etc. if needed + // Should not be reached if item is Struct or Enum and is_target is true + unreachable!("Target type matched but was neither struct nor enum?"); } } - debug!( - count = %type_defs.len(), file_path = %file_path.display(), - "Collected used type definitions from this file" - ); - Ok(type_defs) // Return the collected definitions for this file + // Target type definition was not found in this specific file + Ok(None) } // Find all relevant Rust projects @@ -689,11 +622,8 @@ fn generate_signature_struct( } } Err(e) => { - // Wrap invalid parameter name error with context - return Err(e.wrap_err(format!( - "Invalid name for parameter '{}' in function '{}'", - param_orig_name, method_name_for_error - ))); + // Return the error directly + return Err(e); } } } @@ -764,315 +694,282 @@ impl AsTypePath for syn::Type { fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result> { debug!(project_path = %project_path.display(), "Processing project"); - // Find lib.rs for this project + // --- 0. Setup & Find Project Files --- let lib_rs = project_path.join("src").join("lib.rs"); - if !lib_rs.exists() { - warn!(project_path = %project_path.display(), "No lib.rs found for project"); + warn!(project_path = %project_path.display(), "No lib.rs found, skipping project"); return Ok(None); } - - // Find all Rust files in the project let rust_files = find_rust_files(project_path); + if rust_files.is_empty() { + warn!(project_path=%project_path.display(), "No Rust files found in src/, skipping project"); + return Ok(None) + } + let lib_content = fs::read_to_string(&lib_rs) + .with_context(|| format!("Failed to read lib.rs for project: {}", project_path.display()))?; + let ast = syn::parse_file(&lib_content) + .with_context(|| format!("Failed to parse lib.rs for project: {}", project_path.display()))?; - // Parse lib.rs to find the hyperprocess attribute and interface details first - let lib_content = fs::read_to_string(&lib_rs).with_context(|| { - format!( - "Failed to read lib.rs for project: {}", - project_path.display() - ) - })?; - - let ast = syn::parse_file(&lib_content).with_context(|| { - format!( - "Failed to parse lib.rs for project: {}", - project_path.display() - ) - })?; - + // --- 1. Find Hyperprocess Impl Block & Extract Metadata --- let mut wit_world = None; - let mut interface_name = None; - let mut kebab_interface_name = None; + let mut interface_name = None; // Original Rust name (e.g., MyProcessState) + let mut kebab_interface_name = None; // Kebab-case name (e.g., my-process) let mut impl_item_with_hyperprocess = None; - debug!("Scanning for impl blocks with hyperprocess attribute"); + debug!("Scanning lib.rs for impl block with #[hyperprocess] attribute"); for item in &ast.items { - let Item::Impl(impl_item) = item else { - continue; - }; - // Check if this impl block has a #[hyperprocess] attribute - if let Some(attr) = impl_item - .attrs - .iter() - .find(|attr| attr.path().is_ident("hyperprocess")) - { - debug!("Found hyperprocess attribute"); - - // Extract the wit_world name - match extract_wit_world(&[attr.clone()]) { - Ok(world_name) => { - debug!(wit_world = %world_name, "Extracted wit_world"); - wit_world = Some(world_name); - - // Get the interface name from the impl type - interface_name = impl_item.self_ty.as_ref().as_type_path().map(|tp| { - if let Some(last_segment) = tp.path.segments.last() { - last_segment.ident.to_string() + if let Item::Impl(impl_item) = item { + if let Some(attr) = impl_item + .attrs + .iter() + .find(|a| a.path().is_ident("hyperprocess")) + { + debug!("Found #[hyperprocess] attribute"); + match extract_wit_world(&[attr.clone()]) { + Ok(world_name) => { + debug!(wit_world = %world_name, "Extracted wit_world"); + wit_world = Some(world_name); + + // Get the struct name from the 'impl MyStruct for ...' part + interface_name = impl_item.self_ty.as_ref().as_type_path().and_then(|tp| { + tp.path.segments.last().map(|seg| seg.ident.to_string()) + }); + + if let Some(ref name) = interface_name { + // Validate original name first + match validate_name(name, "Interface") { + Ok(_) => { + let base_name = remove_state_suffix(name); + kebab_interface_name = Some(to_kebab_case(&base_name)); + debug!(interface_name = %name, base_name = %base_name, kebab_name = ?kebab_interface_name, "Interface details"); + impl_item_with_hyperprocess = Some(impl_item.clone()); + break; // Found the target impl block + } + Err(e) => { + // Escalate errors for invalid interface names instead of just warning + return Err(e.wrap_err(format!("Invalid interface name '{}' in hyperprocess impl block", name))); + } + } } else { - "Unknown".to_string() + warn!("Could not extract interface name from hyperprocess impl block type"); + wit_world = None; // Reset world if name extraction failed + continue; } - }); - - // Check for "State" suffix and remove it - let Some(ref name) = interface_name else { - continue; - }; - // Validate the interface name - if let Err(e) = validate_name(name, "Interface") { - warn!(interface_name = %name, error = %e, "Interface name validation failed"); - continue; // Skip this impl block if validation fails } + Err(e) => warn!("Failed to extract wit_world from attribute: {}", e), // Continue searching if extraction fails + } + } + } + } - // Remove State suffix if present - let base_name = remove_state_suffix(name); + // Exit early if no valid hyperprocess impl block was identified + let Some(ref impl_item) = impl_item_with_hyperprocess else { + warn!(project_path=%project_path.display(), "No valid #[hyperprocess] impl block found in lib.rs"); + return Ok(None); + }; + // These unwraps are safe due to the check above and the logic ensuring they are set together + let kebab_name = kebab_interface_name.as_ref().unwrap(); + let current_wit_world = wit_world.as_ref().unwrap(); - // Convert to kebab-case for file name and interface name - kebab_interface_name = Some(to_kebab_case(&base_name)); - debug!(interface_name = ?interface_name, base_name = %base_name, kebab_name = ?kebab_interface_name, "Interface details"); + // --- 2. Collect Signatures & Initial Types --- + let mut signature_structs = Vec::new(); // Stores WIT string for each signature record + let mut global_used_types = HashSet::new(); // All custom WIT types encountered (kebab-case) - // Save the impl item for later processing - impl_item_with_hyperprocess = Some(impl_item.clone()); - break; // Assume only one hyperprocess impl block per lib.rs - } - Err(e) => warn!("Failed to extract wit_world: {}", e), + debug!("Analyzing functions in hyperprocess impl block"); + for item in &impl_item.items { + if let ImplItem::Fn(method) = item { + let method_name = method.sig.ident.to_string(); + debug!(method_name = %method_name, "Examining method"); + + let has_remote = method.attrs.iter().any(|a| a.path().is_ident("remote")); + let has_local = method.attrs.iter().any(|a| a.path().is_ident("local")); + let has_http = method.attrs.iter().any(|a| a.path().is_ident("http")); + let has_init = method.attrs.iter().any(|a| a.path().is_ident("init")); + + if has_remote || has_local || has_http || has_init { + debug!(remote=%has_remote, local=%has_local, http=%has_http, init=%has_init, "Method attributes found"); + // Validate original Rust function name + validate_name(&method_name, "Function")?; // Error early if name invalid + let func_kebab_name = to_kebab_case(&method_name); + + if has_init { + debug!(method_name = %method_name, "Found [init] function, skipping signature generation"); + continue; + } + + // Generate signature structs. `generate_signature_struct` calls `rust_type_to_wit`, + // which populates `global_used_types` with all custom types found in parameters/return types. + if has_remote { + let sig_struct = generate_signature_struct(&func_kebab_name, "remote", method, &mut global_used_types)?; + signature_structs.push(sig_struct); + } + if has_local { + let sig_struct = generate_signature_struct(&func_kebab_name, "local", method, &mut global_used_types)?; + signature_structs.push(sig_struct); + } + if has_http { + let sig_struct = generate_signature_struct(&func_kebab_name, "http", method, &mut global_used_types)?; + signature_structs.push(sig_struct); + } + } else { + // Method in hyperprocess impl lacks required attribute - Error + return Err(eyre!( + "Method '{}' in the #[hyperprocess] impl block is missing a required attribute ([remote], [local], [http], or [init]). Only methods with these attributes should be included.", + method_name + )); } } } + debug!(signature_count = %signature_structs.len(), initial_used_types = ?global_used_types, "Completed signature analysis"); - // Prepare to collect signature structs and used types - let mut signature_structs = Vec::new(); - let mut used_types = HashSet::new(); // This will be populated by signatures AND nested types - // Analyze the functions within the identified impl block (if found) - if let Some(ref impl_item) = &impl_item_with_hyperprocess { - if let Some(ref _kebab_name) = &kebab_interface_name { - // Ensure kebab_name is available but acknowledge unused in this block - for item in &impl_item.items { - let ImplItem::Fn(method) = item else { - continue; - }; - let method_name = method.sig.ident.to_string(); - debug!(method_name = %method_name, "Examining method"); - - // Check for attribute types - let has_remote = method - .attrs - .iter() - .any(|attr| attr.path().is_ident("remote")); - let has_local = method - .attrs - .iter() - .any(|attr| attr.path().is_ident("local")); - let has_http = method.attrs.iter().any(|attr| attr.path().is_ident("http")); - let has_init = method.attrs.iter().any(|attr| attr.path().is_ident("init")); - - if has_remote || has_local || has_http || has_init { - debug!(remote = %has_remote, local = %has_local, http = %has_http, init = %has_init, "Method attributes"); - - // Validate function name - match validate_name(&method_name, "Function") { - Ok(_) => { - // Convert function name to kebab-case - let func_kebab_name = to_kebab_case(&method_name); // Use different var name - - debug!(original_name = %method_name, kebab_name = %func_kebab_name, "Processing method"); - - if has_init { - debug!(method_name = %method_name, "Found initialization function"); - continue; - } - // This will populate `used_types` - if has_remote { - match generate_signature_struct( - &func_kebab_name, // Pass func kebab name - "remote", - method, - &mut used_types, // Pass the main set - ) { - Ok(remote_struct) => signature_structs.push(remote_struct), - Err(e) => { - // Error: Return the specific error from generate_signature_struct directly - return Err(e); - } - } - } + // --- 3. Resolve & Generate Type Definitions Iteratively --- + debug!("Starting iterative type definition resolution"); + let mut generated_type_defs = HashMap::new(); // Kebab-case name -> WIT definition string + let mut types_to_find_queue: Vec = global_used_types // Initialize queue + .iter() + .filter(|ty| !is_wit_primitive_or_builtin(ty)) // Only custom types + .cloned() + .collect(); + let mut processed_types = HashSet::new(); // Track types processed to avoid cycles/redundancy - if has_local { - match generate_signature_struct( - &func_kebab_name, // Pass func kebab name - "local", - method, - &mut used_types, // Pass the main set - ) { - Ok(local_struct) => signature_structs.push(local_struct), - Err(e) => { - // Error: Return the specific error from generate_signature_struct directly - return Err(e); - } - } - } + // Add primitives/builtins to processed_types initially + for ty in &global_used_types { + if is_wit_primitive_or_builtin(ty) { + processed_types.insert(ty.clone()); + } + } - if has_http { - match generate_signature_struct( - &func_kebab_name, // Pass func kebab name - "http", - method, - &mut used_types, // Pass the main set - ) { - Ok(http_struct) => signature_structs.push(http_struct), - Err(e) => { - // Error: Return the specific error from generate_signature_struct directly - return Err(e); - } - } - } - } - Err(e) => { - return Err(e.wrap_err(format!( - "Method: '{}' has invalid name, it should not include any numbers or the keyword 'stream'", - method_name - ))); - } - } - } else { - // Method lacks the required attributes, this is an error - Keep specific error message - return Err(eyre!( - "Method '{}' in the #[hyperprocess] impl block is missing a required attribute ([remote], [local], [http], or [init]). Only methods with these attributes should be included in this impl block.", - method_name - )); - } - } + + while let Some(type_name_to_find) = types_to_find_queue.pop() { + if processed_types.contains(&type_name_to_find) { + continue; // Already processed or known primitive/builtin } - } - // Collect **only used** type definitions from all Rust files - let mut all_type_defs = HashMap::new(); - for file_path in &rust_files { - // Pass the populated used_types set to the collector, making it mutable - // It will ADD nested types to `used_types` if they are custom. - match collect_type_definitions_from_file(file_path, &mut used_types) { - // Pass as mutable - Ok(file_type_defs) => { - for (name, def) in file_type_defs { - // Insert definition if successfully generated. We might insert the same - // definition multiple times if defined in multiple files; HashMap handles this. - // Crucially, we only insert if the struct/enum *itself* was initially in used_types. - all_type_defs.insert(name, def); + debug!(type_name = %type_name_to_find, "Attempting to find definition"); + let mut definition_found_in_project = false; + + // Search across all project files for the definition + for file_path in &rust_files { + match find_and_make_wit_type_def(file_path, &type_name_to_find, &mut global_used_types) { + Ok(Some((wit_definition, new_local_deps))) => { + debug!(type_name=%type_name_to_find, file_path=%file_path.display(), "Found definition"); + + // Store the definition. Check for duplicates across files. + if let Some(existing_def) = generated_type_defs.insert(type_name_to_find.clone(), wit_definition) { + // Simple string comparison might be too strict if formatting differs slightly. + // But good enough for a warning. + if existing_def != *generated_type_defs.get(&type_name_to_find).unwrap() { + warn!(type_name = %type_name_to_find, "Type definition found in multiple files with different generated content. Using the one from: {}", file_path.display()); + } + } + processed_types.insert(type_name_to_find.clone()); // Mark as processed + definition_found_in_project = true; + + // Add newly discovered dependencies from this type's definition to the queue + for dep in new_local_deps { + if !processed_types.contains(&dep) && !types_to_find_queue.contains(&dep) { + debug!(dependency = %dep, discovered_by = %type_name_to_find, "Adding new dependency to find queue"); + types_to_find_queue.push(dep); + } + } + // Found the definition for this type, stop searching files for it + break; } - } - Err(e) => { - // Decide how to handle errors from collection: warn and continue, or bail? - // Bailing might be safer to ensure correctness. - warn!(file_path = %file_path.display(), error = %e, "Error collecting type definitions from file, WIT may be incomplete."); - // For stricter checking, you might uncomment the next line: - return Err(e.wrap_err(format!( - "Failed to collect type definitions from {}", - file_path.display() - ))); + Ok(None) => continue, // Not in this file, check next file + Err(e) => { + return Err(e); // <-- CHANGE IS HERE + } } } + // If after checking all files, the definition wasn't found + if !definition_found_in_project { + debug!(type_name=%type_name_to_find, "Definition not found in any scanned file."); + // Mark as processed to avoid infinite loop. Verification step will catch this. + processed_types.insert(type_name_to_find.clone()); + } } + debug!("Finished iterative type definition resolution"); - // The verification logic added previously now works correctly because - // `used_types` contains ALL custom type names encountered, both top-level and nested. - debug!(used_type_count = %used_types.len(), used_types = ?used_types, "Final set of used types for verification"); // Added debug - // Verify that all used types are either primitive/builtin or defined locally + // --- 4. Verify All Used Types Have Definitions --- + debug!(final_used_types = ?global_used_types, found_definitions = ?generated_type_defs.keys(), "Starting final verification"); let mut undefined_types = Vec::new(); - for used_type_name in &used_types { - // Check if the used type is a primitive/builtin OR if we found its definition locally - if !is_wit_primitive_or_builtin(used_type_name) - && !all_type_defs.contains_key(used_type_name) - { + for used_type_name in &global_used_types { + if !is_wit_primitive_or_builtin(used_type_name) && !generated_type_defs.contains_key(used_type_name) { + warn!(type_name=%used_type_name, "Verification failed: Used type has no generated definition."); undefined_types.push(used_type_name.clone()); } } - // If there are undefined types, raise an error if !undefined_types.is_empty() { undefined_types.sort(); + // Use the original project path display for user-friendliness + let project_display = project_path.display(); bail!( "WIT Generation Error in project '{}': Found types used (directly or indirectly) in function signatures \ that are neither WIT built-ins nor defined locally within the scanned project files: {:?}. \ - Ensure definitions for these types (structs/enums) are present in the project's source code, \ - or adjust the function/type definitions to use only WIT-compatible types.", - project_path.display(), + Ensure definitions for these types (structs/enums) are present in the project's source code \ + (and not skipped due to errors/complexity), or adjust the function/type definitions.", + project_display, undefined_types ); } - // Now generate the WIT content for the interface - if let (Some(ref iface_name), Some(ref kebab_name), Some(ref _impl_item)) = ( - // impl_item no longer needed here - &interface_name, - &kebab_interface_name, - &impl_item_with_hyperprocess, // Keep this condition to ensure an interface was found - ) { - // No need to filter anymore, all_type_defs contains only used types - let mut type_defs: Vec = all_type_defs.into_values().collect(); // Collect values directly - - type_defs.sort(); // Sort for consistent output - - // Generate the final WIT content - if signature_structs.is_empty() && type_defs.is_empty() { - // Check both sigs and types - warn!(interface_name = %iface_name, "No functions or used types found for interface"); - } else { - // Start with the interface comment - let mut content = " // This interface contains function signature definitions that will be used\n // by the hyper-bindgen macro to generate async function bindings.\n //\n // NOTE: This is currently a hacky workaround since WIT async functions are not\n // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware,\n // we should switch to using proper async WIT function signatures instead of\n // this struct-based approach with hyper-bindgen generating the async stubs.\n".to_string(); - - // Add standard imports - content.push_str("\n use standard.{address};\n\n"); - - // Add type definitions if any - if !type_defs.is_empty() { - content.push_str(&type_defs.join("\n\n")); - content.push_str("\n\n"); - } - - // Add signature structs if any (moved after types for potentially better readability) - if !signature_structs.is_empty() { - content.push_str(&signature_structs.join("\n\n")); - } + debug!("Verification successful: All used types have definitions or are built-in."); - // Wrap in interface block - let final_content = - format!("interface {} {{\n{}\n}}\n", kebab_name, content.trim_end()); // Trim trailing whitespace - debug!(interface_name = %iface_name, signature_count = %signature_structs.len(), type_def_count = %type_defs.len(), "Generated interface content"); - // Write the interface file with kebab-case name - let interface_file = api_dir.join(format!("{}.wit", kebab_name)); - debug!(path = %interface_file.display(), "Writing WIT file"); + // --- 5. Generate Final WIT Interface File --- + let mut all_generated_defs: Vec = generated_type_defs.into_values().collect(); + all_generated_defs.sort(); // Sort type definitions for consistent output + signature_structs.sort(); // Sort signature records as well - fs::write(&interface_file, &final_content) - .with_context(|| format!("Failed to write {}", interface_file.display()))?; + if signature_structs.is_empty() && all_generated_defs.is_empty() { + warn!(interface_name = %interface_name.as_ref().unwrap(), "No attributed functions or used types found. No WIT interface file generated for this project."); + // No interface generated, but maybe the world file still needs creating/updating? + // Let's return None for the interface part, the main function handles world logic. + // We still need to return the world name though. + // Return None for interface means no `import` statement added later. + return Ok(None); // Indicate no interface content was generated + } else { + debug!(kebab_name=%kebab_name, "Generating final WIT content"); + let mut content = String::new(); + + // Add standard imports (can be refined based on actual needs) + content.push_str(" use standard.{address};\n"); // Assuming world includes 'standard' + + // Add type definitions + if !all_generated_defs.is_empty() { + content.push('\n'); // Separator + debug!(count=%all_generated_defs.len(), "Adding type definitions to interface"); + content.push_str(&all_generated_defs.join("\n\n")); + content.push('\n'); + } - debug!("Successfully wrote WIT file"); + // Add signature structs + if !signature_structs.is_empty() { + content.push('\n'); // Separator + debug!(count=%signature_structs.len(), "Adding signature structs to interface"); + content.push_str(&signature_structs.join("\n\n")); } - } else { - warn!("No valid hyperprocess interface found in lib.rs"); - } - // Return statement remains the same logic - if let (Some(wit_world), Some(_), Some(kebab_iface)) = - (wit_world, interface_name, kebab_interface_name) - { - debug!(interface = %kebab_iface, "Returning import statement for interface"); - // Use kebab-case interface name for import - Ok(Some((kebab_iface, wit_world))) - } else { - warn!("No valid interface found or wit_world extracted."); // Updated message - Ok(None) + // Wrap in interface block + let final_content = format!("interface {} {{\n{}\n}}\n", kebab_name, content.trim()); // Trim any trailing whitespace + debug!(interface_name = %interface_name.as_ref().unwrap(), signature_count = %signature_structs.len(), type_def_count = %all_generated_defs.len(), "Generated interface content"); + + // Write the interface file + let interface_file = api_dir.join(format!("{}.wit", kebab_name)); + debug!(path = %interface_file.display(), "Writing WIT file"); + fs::write(&interface_file, &final_content) + .with_context(|| format!("Failed to write WIT interface file: {}", interface_file.display()))?; + debug!("Successfully wrote WIT file"); + + // If content was generated, return the kebab name for the import statement + debug!(interface = %kebab_name, wit_world=%current_wit_world, "Returning import statement info"); + Ok(Some(( + kebab_name.to_string(), + current_wit_world.to_string(), + ))) } } From 94b28e86bf4ea2f91af013565fec5611a7c94fc2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 19:23:58 +0000 Subject: [PATCH 50/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 206 ++++++++++++++++++++++--------------- 1 file changed, 121 insertions(+), 85 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 0efc19e8..598d25b0 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -367,10 +367,10 @@ fn find_and_make_wit_type_def( }; if is_target { - // Skip internal-looking types (can be adjusted) + // Skip internal-looking types (can be adjusted) if orig_name.contains("__") { - warn!(name = %orig_name, "Skipping definition search for likely internal type"); - return Ok(None); // Treat as not found for WIT purposes + warn!(name = %orig_name, "Skipping definition search for likely internal type"); + return Ok(None); // Treat as not found for WIT purposes } // Validate the original Rust name validate_name(&orig_name, item_kind)?; @@ -446,25 +446,31 @@ fn find_and_make_wit_type_def( for v in &item_enum.variants { let variant_orig_name = v.ident.to_string(); - // Validate variant name before proceeding + // Validate variant name before proceeding validate_name(&variant_orig_name, "Enum variant")?; let variant_kebab_name = to_kebab_case(&variant_orig_name); match &v.fields { // Variant with one unnamed field: T -> case(T) syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { - // `rust_type_to_wit` adds new custom types to `global_used_types` + // `rust_type_to_wit` adds new custom types to `global_used_types` let type_result = rust_type_to_wit( &fields.unnamed.first().unwrap().ty, global_used_types, ) - .wrap_err_with(|| format!("Failed to convert variant '{}' type in enum '{}'", variant_orig_name, orig_name))?; - - // Check if the variant's type is custom and add to local deps + .wrap_err_with(|| { + format!( + "Failed to convert variant '{}' type in enum '{}'", + variant_orig_name, orig_name + ) + })?; + + // Check if the variant's type is custom and add to local deps if !is_wit_primitive_or_builtin(&type_result) { local_dependencies.insert(type_result.clone()); } - variants_wit.push(format!(" {}({})", variant_kebab_name, type_result)); + variants_wit + .push(format!(" {}({})", variant_kebab_name, type_result)); } // Unit variant: -> case syn::Fields::Unit => { @@ -494,8 +500,8 @@ fn find_and_make_wit_type_def( return Ok(None); } } - // Should not be reached if item is Struct or Enum and is_target is true - unreachable!("Target type matched but was neither struct nor enum?"); + // Should not be reached if item is Struct or Enum and is_target is true + unreachable!("Target type matched but was neither struct nor enum?"); } } @@ -703,12 +709,20 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { let base_name = remove_state_suffix(name); kebab_interface_name = Some(to_kebab_case(&base_name)); @@ -747,7 +761,10 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { // Escalate errors for invalid interface names instead of just warning - return Err(e.wrap_err(format!("Invalid interface name '{}' in hyperprocess impl block", name))); + return Err(e.wrap_err(format!( + "Invalid interface name '{}' in hyperprocess impl block", + name + ))); } } } else { @@ -771,7 +788,6 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result WIT definition string @@ -834,13 +864,12 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result { debug!(type_name=%type_name_to_find, file_path=%file_path.display(), "Found definition"); // Store the definition. Check for duplicates across files. - if let Some(existing_def) = generated_type_defs.insert(type_name_to_find.clone(), wit_definition) { - // Simple string comparison might be too strict if formatting differs slightly. - // But good enough for a warning. - if existing_def != *generated_type_defs.get(&type_name_to_find).unwrap() { + if let Some(existing_def) = + generated_type_defs.insert(type_name_to_find.clone(), wit_definition) + { + // Simple string comparison might be too strict if formatting differs slightly. + // But good enough for a warning. + if existing_def != *generated_type_defs.get(&type_name_to_find).unwrap() { warn!(type_name = %type_name_to_find, "Type definition found in multiple files with different generated content. Using the one from: {}", file_path.display()); - } + } } processed_types.insert(type_name_to_find.clone()); // Mark as processed definition_found_in_project = true; @@ -870,7 +902,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result continue, // Not in this file, check next file Err(e) => { - return Err(e); // <-- CHANGE IS HERE - } + return Err(e); // <-- CHANGE IS HERE + } } } - // If after checking all files, the definition wasn't found - if !definition_found_in_project { - debug!(type_name=%type_name_to_find, "Definition not found in any scanned file."); - // Mark as processed to avoid infinite loop. Verification step will catch this. - processed_types.insert(type_name_to_find.clone()); - } + // If after checking all files, the definition wasn't found + if !definition_found_in_project { + debug!(type_name=%type_name_to_find, "Definition not found in any scanned file."); + // Mark as processed to avoid infinite loop. Verification step will catch this. + processed_types.insert(type_name_to_find.clone()); + } } debug!("Finished iterative type definition resolution"); - // --- 4. Verify All Used Types Have Definitions --- debug!(final_used_types = ?global_used_types, found_definitions = ?generated_type_defs.keys(), "Starting final verification"); let mut undefined_types = Vec::new(); for used_type_name in &global_used_types { - if !is_wit_primitive_or_builtin(used_type_name) && !generated_type_defs.contains_key(used_type_name) { - warn!(type_name=%used_type_name, "Verification failed: Used type has no generated definition."); + if !is_wit_primitive_or_builtin(used_type_name) + && !generated_type_defs.contains_key(used_type_name) + { + warn!(type_name=%used_type_name, "Verification failed: Used type has no generated definition."); undefined_types.push(used_type_name.clone()); } } @@ -916,8 +949,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result = generated_type_defs.into_values().collect(); @@ -926,10 +958,10 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result Date: Thu, 1 May 2025 15:31:22 -0400 Subject: [PATCH 51/88] merge local and remote --- src/build/wit_generator.rs | 182 +++++++++++++++++++++---------------- 1 file changed, 104 insertions(+), 78 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 598d25b0..d7171d12 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -739,52 +739,52 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - debug!(wit_world = %world_name, "Extracted wit_world"); - wit_world = Some(world_name); - - // Get the struct name from the 'impl MyStruct for ...' part - interface_name = impl_item.self_ty.as_ref().as_type_path().and_then(|tp| { - tp.path.segments.last().map(|seg| seg.ident.to_string()) - }); - - if let Some(ref name) = interface_name { - // Validate original name first - match validate_name(name, "Interface") { - Ok(_) => { - let base_name = remove_state_suffix(name); - kebab_interface_name = Some(to_kebab_case(&base_name)); - debug!(interface_name = %name, base_name = %base_name, kebab_name = ?kebab_interface_name, "Interface details"); - impl_item_with_hyperprocess = Some(impl_item.clone()); - break; // Found the target impl block - } - Err(e) => { - // Escalate errors for invalid interface names instead of just warning - return Err(e.wrap_err(format!( - "Invalid interface name '{}' in hyperprocess impl block", - name - ))); - } - } - } else { - warn!("Could not extract interface name from hyperprocess impl block type"); - wit_world = None; // Reset world if name extraction failed - continue; + // Attempt to extract wit_world. Propagate error if extraction fails. + let world_name = extract_wit_world(&[attr.clone()]) + .wrap_err("Failed to extract wit_world from #[hyperprocess] attribute")?; + debug!(wit_world = %world_name, "Extracted wit_world"); + wit_world = Some(world_name); + + + // Get the struct name from the 'impl MyStruct for ...' part + interface_name = impl_item.self_ty.as_ref().as_type_path().and_then(|tp| { + tp.path.segments.last().map(|seg| seg.ident.to_string()) + }); + + if let Some(ref name) = interface_name { + // Validate original name first + match validate_name(name, "Interface") { + Ok(_) => { + let base_name = remove_state_suffix(name); + kebab_interface_name = Some(to_kebab_case(&base_name)); + debug!(interface_name = %name, base_name = %base_name, kebab_name = ?kebab_interface_name, "Interface details"); + impl_item_with_hyperprocess = Some(impl_item.clone()); + break; // Found the target impl block + } + Err(e) => { + // Escalate errors for invalid interface names instead of just warning + return Err(e.wrap_err(format!("Invalid interface name '{}' in hyperprocess impl block", name))); } } - Err(e) => warn!("Failed to extract wit_world from attribute: {}", e), // Continue searching if extraction fails + } else { + // If interface name couldn't be extracted, it's an error for this project. + bail!("Could not extract interface name from #[hyperprocess] impl block type: {:?}", impl_item.self_ty); + } + + } } } // Exit early if no valid hyperprocess impl block was identified let Some(ref impl_item) = impl_item_with_hyperprocess else { - warn!(project_path=%project_path.display(), "No valid #[hyperprocess] impl block found in lib.rs"); + // If we looped through everything and didn't find a block (and didn't error above), + // it means no #[hyperprocess] attribute was found at all. This is okay, just skip. + warn!(project_path=%project_path.display(), "No #[hyperprocess] impl block found in lib.rs, skipping project"); return Ok(None); }; - // These unwraps are safe due to the check above and the logic ensuring they are set together + // These unwraps are safe due to the checks above ensuring we error or break successfully let kebab_name = kebab_interface_name.as_ref().unwrap(); let current_wit_world = wit_world.as_ref().unwrap(); @@ -881,18 +881,18 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { + // Directly propagate errors from find_and_make_wit_type_def + match find_and_make_wit_type_def(file_path, &type_name_to_find, &mut global_used_types)? { + Some((wit_definition, new_local_deps)) => { + + debug!(type_name=%type_name_to_find, file_path=%file_path.display(), "Found definition"); // Store the definition. Check for duplicates across files. - if let Some(existing_def) = - generated_type_defs.insert(type_name_to_find.clone(), wit_definition) - { - // Simple string comparison might be too strict if formatting differs slightly. - // But good enough for a warning. - if existing_def != *generated_type_defs.get(&type_name_to_find).unwrap() { + if let Some(existing_def) = generated_type_defs.insert(type_name_to_find.clone(), wit_definition.clone()) { // Clone wit_definition here + // Simple string comparison might be too strict if formatting differs slightly. + // But good enough for a warning. + if existing_def != wit_definition { // Compare with the cloned value warn!(type_name = %type_name_to_find, "Type definition found in multiple files with different generated content. Using the one from: {}", file_path.display()); } } @@ -909,10 +909,9 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result continue, // Not in this file, check next file - Err(e) => { - return Err(e); // <-- CHANGE IS HERE - } + None => continue, // Not in this file, check next file + + } } // If after checking all files, the definition wasn't found @@ -957,12 +956,14 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result"); + warn!(interface_name = %name_for_warning, "No attributed functions or used types requiring definitions found. No WIT interface file generated for this project."); + + // Return the world name even if no interface content is generated, + // so the world file can still be updated/created if necessary. + // But signal that no *interface* was generated by returning None for the interface name part. + return Ok(Some((String::new(), current_wit_world.to_string()))); // Return empty string for interface name } else { debug!(kebab_name=%kebab_name, "Generating final WIT content"); let mut content = String::new(); @@ -1173,44 +1174,65 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec { - new_imports.push(format!(" import {interface};")); - - interfaces.push(interface); + // Only add import if an interface name was actually generated + if !interface.is_empty() { + new_imports.push(format!(" import {interface};")); + interfaces.push(interface); // Add to list of generated interfaces + } else { + // Log if processing succeeded but generated no interface content + debug!(project = %project_path.display(), world = %wit_world, "Project processed but generated no interface content (only types/no functions?)"); + } + // Always record the project path and the target world processed_projects.push(project_path.clone()); - wit_worlds.insert(wit_world); } + // Project was skipped intentionally (e.g., no lib.rs, no #[hyperprocess]) Ok(None) => { - bail!( - "No import statement generated for project {}", - project_path.display() - ); + debug!(project = %project_path.display(), "Project skipped during processing (e.g., no lib.rs or #[hyperprocess] found)"); + // Continue to the next project + continue; } + // An error occurred during processing Err(e) => { + // Propagate the error, stopping the entire generation process bail!("Error processing project {}: {}", project_path.display(), e); } } } + debug!(count = %new_imports.len(), "Collected number of new imports"); + if new_imports.is_empty() && wit_worlds.is_empty() { + info!("No WIT interfaces generated and no target WIT worlds identified across all projects."); + return Ok((processed_projects, interfaces)); // Return empty interfaces list + } else if new_imports.is_empty() { + info!("No new WIT interfaces generated, but target WIT world(s) identified: {:?}", wit_worlds); + // Proceed to rewrite world files even without new imports, as existing ones might need updates/creation. + } - // Check for existing world definition files and update them - debug!("Looking for existing world definition files"); - let mut updated_world = false; - rewrite_wit(api_dir, &new_imports, &mut wit_worlds, &mut updated_world)?; + // Update or create WIT world files + debug!("Processing WIT world files for: {:?}", wit_worlds); + let mut updated_world = false; // Track if any world file was written/updated - // If no world definitions were found, create a default one + + rewrite_wit(api_dir, &new_imports, &mut wit_worlds.clone(), &mut updated_world)?; // Pass a clone as rewrite_wit might modify it + + + // If no existing world matched and no new world files were created by rewrite_wit, + // AND we actually had imports to add, create a default world file. if !updated_world && !new_imports.is_empty() { + // Define default world name let default_world = "async-app-template-dot-os-v0"; - warn!(default_world = %default_world, "No existing world definitions found, creating default"); + warn!(default_world = %default_world, "No existing world definitions found or created for collected imports, creating default world file"); // Create world content with process-v1 include and proper indentation for imports let imports_with_indent: Vec = new_imports @@ -1231,12 +1253,12 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec Date: Thu, 1 May 2025 19:32:17 +0000 Subject: [PATCH 52/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 74 ++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index d7171d12..a8a24aac 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -745,15 +745,16 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { let base_name = remove_state_suffix(name); kebab_interface_name = Some(to_kebab_case(&base_name)); @@ -763,16 +764,16 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { // Escalate errors for invalid interface names instead of just warning - return Err(e.wrap_err(format!("Invalid interface name '{}' in hyperprocess impl block", name))); + return Err(e.wrap_err(format!( + "Invalid interface name '{}' in hyperprocess impl block", + name + ))); } } } else { // If interface name couldn't be extracted, it's an error for this project. bail!("Could not extract interface name from #[hyperprocess] impl block type: {:?}", impl_item.self_ty); - } - - } } } @@ -882,17 +883,20 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result { - - debug!(type_name=%type_name_to_find, file_path=%file_path.display(), "Found definition"); // Store the definition. Check for duplicates across files. - if let Some(existing_def) = generated_type_defs.insert(type_name_to_find.clone(), wit_definition.clone()) { // Clone wit_definition here - // Simple string comparison might be too strict if formatting differs slightly. - // But good enough for a warning. - if existing_def != wit_definition { // Compare with the cloned value + if let Some(existing_def) = generated_type_defs + .insert(type_name_to_find.clone(), wit_definition.clone()) + { + // Clone wit_definition here + // Simple string comparison might be too strict if formatting differs slightly. + // But good enough for a warning. + if existing_def != wit_definition { + // Compare with the cloned value warn!(type_name = %type_name_to_find, "Type definition found in multiple files with different generated content. Using the one from: {}", file_path.display()); } } @@ -910,8 +914,6 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result continue, // Not in this file, check next file - - } } // If after checking all files, the definition wasn't found @@ -960,9 +962,9 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result"); warn!(interface_name = %name_for_warning, "No attributed functions or used types requiring definitions found. No WIT interface file generated for this project."); - // Return the world name even if no interface content is generated, - // so the world file can still be updated/created if necessary. - // But signal that no *interface* was generated by returning None for the interface name part. + // Return the world name even if no interface content is generated, + // so the world file can still be updated/created if necessary. + // But signal that no *interface* was generated by returning None for the interface name part. return Ok(Some((String::new(), current_wit_world.to_string()))); // Return empty string for interface name } else { debug!(kebab_name=%kebab_name, "Generating final WIT content"); @@ -1186,8 +1188,8 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec Result<(Vec Result<(Vec Date: Thu, 1 May 2025 15:39:13 -0400 Subject: [PATCH 53/88] streamlined error-handling more --- src/build/wit_generator.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index a8a24aac..989d3fc1 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1234,24 +1234,12 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec = new_imports - .iter() - .map(|import| { - if import.starts_with(" ") { - import.clone() - } else { - format!(" {}", import.trim()) - } - }) - .collect(); // Determine include based on world name let include_line = if default_world.starts_with("types-") { @@ -1279,8 +1267,10 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Date: Thu, 1 May 2025 19:41:48 +0000 Subject: [PATCH 54/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 989d3fc1..39cdaebf 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1240,7 +1240,6 @@ pub fn generate_wit_files(base_dir: &Path, api_dir: &Path) -> Result<(Vec Result<(Vec Date: Mon, 5 May 2025 17:10:54 -0700 Subject: [PATCH 55/88] always run build.rs --- build.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 5c3ae138..1bdf8338 100644 --- a/build.rs +++ b/build.rs @@ -50,7 +50,10 @@ fn visit_dirs(dir: &Path, output_buffer: &mut Vec) -> io::Result<()> { let path = entry.path(); if path.is_dir() { let dir_name = path.file_name().and_then(|s| s.to_str()); - if dir_name == Some("home") || dir_name == Some("target") { + if dir_name == Some("home") + || dir_name == Some("target") + || dir_name == Some(".mypy_cache") + { continue; } visit_dirs(&path, output_buffer)?; @@ -106,6 +109,9 @@ fn add_branch_name(repo: &git2::Repository) -> anyhow::Result<()> { } fn main() -> anyhow::Result<()> { + // Always run this script + println!("cargo:rerun-if-changed=NULL"); + make_new_includes()?; // write version info into binary From b48a6848f9b601c9d3e22659139377cf9d030778 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 14 May 2025 10:25:37 -0700 Subject: [PATCH 56/88] change Optimism -> Base --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index a49c9139..7913b64b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -578,7 +578,7 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .arg(Arg::new("RPC_ENDPOINT") .action(ArgAction::Set) .long("rpc") - .help("Ethereum Optimism mainnet RPC endpoint (wss://)") + .help("Ethereum Base mainnet RPC endpoint (wss://)") .required(false) ) .arg(Arg::new("PERSIST") @@ -655,7 +655,7 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .arg(Arg::new("RPC_ENDPOINT") .action(ArgAction::Set) .long("rpc") - .help("Ethereum Optimism mainnet RPC endpoint (wss://)") + .help("Ethereum Base mainnet RPC endpoint (wss://)") .required(false) ) //.arg(Arg::new("PASSWORD") // TODO: with develop 0.8.0 @@ -1116,7 +1116,7 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .action(ArgAction::Set) .short('r') .long("rpc") - .help("Ethereum Optimism mainnet RPC endpoint (wss://)") + .help("Ethereum Base mainnet RPC endpoint (wss://)") .required(true) ) .arg(Arg::new("REAL") From f10770e2fc49dfdcf549ba747a3cf2e976912ebf Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 18 Jun 2025 11:26:09 -0700 Subject: [PATCH 57/88] setup: remove old, unused code --- src/setup/mod.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 0cf66ab4..16cba2a0 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -344,34 +344,9 @@ pub fn check_foundry_deps() -> Result> { if !is_command_installed("anvil")? { return Ok(vec![Dependency::Foundry]); } - // let (_, installed_datetime) = get_foundry_version()?; Ok(vec![]) } -#[instrument(level = "trace", skip_all)] -fn get_foundry_version() -> Result<(String, String)> { - let output = run_command(Command::new("bash").args(&["-c", "anvil --version"]), false)?; - let Some(output) = output else { - return Err(eyre!( - "failed to fetch foundry version: anvil --version failed" - )); - }; - let output: Vec<&str> = output.0.split('(').nth(1).unwrap().split(' ').collect(); - if output.len() != 2 { - return Err(eyre!( - "failed to fetch foundry version: unexpected output: {output:?}" - )); - } - Ok(( - output[0].trim().to_string(), - output[1] - .trim() - .strip_suffix(')') - .unwrap_or_else(|| output[1]) - .to_string(), - )) -} - /// install forge+anvil+others, could be separated into binary extractions from github releases. #[instrument(level = "trace", skip_all)] fn install_foundry(verbose: bool) -> Result<()> { From e23837d02e5ec65de89a4f0796068943f91c1594 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 21 Jun 2025 10:17:49 -0700 Subject: [PATCH 58/88] caller_utils_ts: add initial work --- src/build/caller_utils_ts_generator.rs | 483 +++++++++++++++++++++++++ src/build/mod.rs | 6 + 2 files changed, 489 insertions(+) create mode 100644 src/build/caller_utils_ts_generator.rs diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs new file mode 100644 index 00000000..c889d7b2 --- /dev/null +++ b/src/build/caller_utils_ts_generator.rs @@ -0,0 +1,483 @@ +use std::fs; +use std::path::Path; + +use color_eyre::{eyre::WrapErr, Result}; +use tracing::{debug, info, instrument, warn}; + +use walkdir::WalkDir; + +// Convert kebab-case to camelCase +pub fn to_camel_case(s: &str) -> String { + let parts: Vec<&str> = s.split('-').collect(); + if parts.is_empty() { + return String::new(); + } + + let mut result = parts[0].to_string(); + for part in &parts[1..] { + if !part.is_empty() { + let mut chars = part.chars(); + if let Some(first_char) = chars.next() { + result.push(first_char.to_uppercase().next().unwrap()); + result.extend(chars); + } + } + } + + result +} + +// Convert kebab-case to PascalCase +pub fn to_pascal_case(s: &str) -> String { + let parts = s.split('-'); + let mut result = String::new(); + + for part in parts { + if !part.is_empty() { + let mut chars = part.chars(); + if let Some(first_char) = chars.next() { + result.push(first_char.to_uppercase().next().unwrap()); + result.extend(chars); + } + } + } + + result +} + +// Convert WIT type to TypeScript type +fn wit_type_to_typescript(wit_type: &str) -> String { + match wit_type { + // Integer types - all become number in TypeScript + "s8" | "u8" | "s16" | "u16" | "s32" | "u32" | "s64" | "u64" => "number".to_string(), + // Floating point types + "f32" | "f64" => "number".to_string(), + // Other primitive types + "string" => "string".to_string(), + "bool" => "boolean".to_string(), + "_" => "void".to_string(), + // Special types + "address" => "string".to_string(), // Address would be a string in TypeScript + // Collection types with generics + t if t.starts_with("list<") => { + let inner_type = &t[5..t.len() - 1]; + // Special case for list which becomes number[] + if inner_type == "u8" { + "number[]".to_string() + } else { + format!("{}[]", wit_type_to_typescript(inner_type)) + } + } + t if t.starts_with("option<") => { + let inner_type = &t[7..t.len() - 1]; + format!("{} | null", wit_type_to_typescript(inner_type)) + } + t if t.starts_with("result<") => { + let inner_part = &t[7..t.len() - 1]; + if let Some(comma_pos) = inner_part.find(',') { + let ok_type = &inner_part[..comma_pos].trim(); + let err_type = &inner_part[comma_pos + 1..].trim(); + format!( + "{{ Ok: {} }} | {{ Err: {} }}", + wit_type_to_typescript(ok_type), + wit_type_to_typescript(err_type) + ) + } else { + format!( + "{{ Ok: {} }} | {{ Err: void }}", + wit_type_to_typescript(inner_part) + ) + } + } + t if t.starts_with("tuple<") => { + let inner_types = &t[6..t.len() - 1]; + let ts_types: Vec = inner_types + .split(", ") + .map(|t| wit_type_to_typescript(t)) + .collect(); + format!("[{}]", ts_types.join(", ")) + } + // Custom types (in kebab-case) need to be converted to PascalCase + _ => to_pascal_case(wit_type).to_string(), + } +} + +// Structure to represent a field in a WIT signature struct +#[derive(Debug)] +struct SignatureField { + name: String, + wit_type: String, +} + +// Structure to represent a WIT signature struct +#[derive(Debug)] +struct SignatureStruct { + function_name: String, + attr_type: String, + fields: Vec, +} + +// Parse WIT file to extract function signatures +#[instrument(level = "trace", skip_all)] +fn parse_wit_file(file_path: &Path) -> Result> { + debug!(file = %file_path.display(), "Parsing WIT file"); + + let content = fs::read_to_string(file_path) + .with_context(|| format!("Failed to read WIT file: {}", file_path.display()))?; + + let mut signatures = Vec::new(); + + // Simple parser for WIT files to extract record definitions + let lines: Vec<_> = content.lines().collect(); + let mut i = 0; + + while i < lines.len() { + let line = lines[i].trim(); + + // Look for signature record definitions + if line.starts_with("record ") && line.contains("-signature-") { + let record_name = line + .trim_start_matches("record ") + .trim_end_matches(" {") + .trim(); + debug!(name = %record_name, "Found signature record"); + + // Extract function name and attribute type + let parts: Vec<_> = record_name.split("-signature-").collect(); + if parts.len() != 2 { + warn!(name = %record_name, "Unexpected signature record name format, skipping"); + i += 1; + continue; + } + + let function_name = parts[0].to_string(); + let attr_type = parts[1].to_string(); + debug!(function = %function_name, attr_type = %attr_type, "Extracted function name and type"); + + // Parse fields + let mut fields = Vec::new(); + i += 1; + + while i < lines.len() && !lines[i].trim().starts_with("}") { + let field_line = lines[i].trim(); + + // Skip comments and empty lines + if field_line.starts_with("//") || field_line.is_empty() { + i += 1; + continue; + } + + // Parse field definition + let field_parts: Vec<_> = field_line.split(':').collect(); + if field_parts.len() == 2 { + let field_name = field_parts[0].trim().to_string(); + let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); + + debug!(name = %field_name, wit_type = %field_type, "Found field"); + fields.push(SignatureField { + name: field_name, + wit_type: field_type, + }); + } + + i += 1; + } + + signatures.push(SignatureStruct { + function_name, + attr_type, + fields, + }); + } + + i += 1; + } + + debug!( + file = %file_path.display(), + signatures = signatures.len(), + "Finished parsing WIT file" + ); + Ok(signatures) +} + +// Generate TypeScript interface and function from a signature struct +fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, String) { + // Convert function name from kebab-case to camelCase + let camel_function_name = to_camel_case(&signature.function_name); + let pascal_function_name = to_pascal_case(&signature.function_name); + + debug!(name = %camel_function_name, "Generating TypeScript function"); + + // Extract parameters and return type + let mut params = Vec::new(); + let mut param_names = Vec::new(); + let mut param_types = Vec::new(); + let mut return_type = "void".to_string(); + + for field in &signature.fields { + let field_name_camel = to_camel_case(&field.name); + let ts_type = wit_type_to_typescript(&field.wit_type); + debug!(field = %field.name, wit_type = %field.wit_type, ts_type = %ts_type, "Processing field"); + + if field.name == "target" { + // Skip target field as it's handled internally + continue; + } else if field.name == "returning" { + return_type = ts_type; + debug!(return_type = %return_type, "Identified return type"); + } else { + params.push(format!("{}: {}", field_name_camel, ts_type)); + param_names.push(field_name_camel); + param_types.push(ts_type); + } + } + + // Generate request interface + let request_interface = if param_names.is_empty() { + // No parameters case + format!( + "export interface {}Request {{\n {}: {{}}\n}}", + pascal_function_name, pascal_function_name + ) + } else if param_names.len() == 1 { + // Single parameter case + format!( + "export interface {}Request {{\n {}: {}\n}}", + pascal_function_name, pascal_function_name, param_types[0] + ) + } else { + // Multiple parameters case - use tuple format + format!( + "export interface {}Request {{\n {}: [{}]\n}}", + pascal_function_name, + pascal_function_name, + param_types.join(", ") + ) + }; + + // Generate response type alias + let response_type = format!( + "export type {}Response = {};", + pascal_function_name, return_type + ); + + // Generate function implementation + let function_params = params.join(", "); + + let data_construction = if param_names.is_empty() { + format!( + " const data: {}Request = {{\n {}: {{}},\n }};", + pascal_function_name, pascal_function_name + ) + } else if param_names.len() == 1 { + format!( + " const data: {}Request = {{\n {}: {},\n }};", + pascal_function_name, pascal_function_name, param_names[0] + ) + } else { + format!( + " const data: {}Request = {{\n {}: [{}],\n }};", + pascal_function_name, + pascal_function_name, + param_names.join(", ") + ) + }; + + let function_impl = format!( + "/**\n * {}\n{} * @returns Promise with result\n * @throws ApiError if the request fails\n */\nasync function {}({}): Promise<{}Response> {{\n{}\n\n return await apiRequest<{}Request, {}Response>('{}', 'POST', data);\n}}", + camel_function_name, + params.iter().map(|p| format!(" * @param {}", p)).collect::>().join("\n"), + camel_function_name, + function_params, + pascal_function_name, + data_construction, + pascal_function_name, + pascal_function_name, + camel_function_name + ); + + // Only return implementations for HTTP endpoints + if signature.attr_type == "http" { + (request_interface, response_type, function_impl) + } else { + debug!("Skipping non-HTTP endpoint"); + (String::new(), String::new(), String::new()) + } +} + +// Public entry point for creating TypeScript caller-utils +#[instrument(level = "trace", skip_all)] +pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result<()> { + // Path to the new TypeScript file + let ui_target_dir = base_dir.join("target").join("ui"); + let caller_utils_path = ui_target_dir.join("caller-utils.ts"); + + debug!( + path = %caller_utils_path.display(), + "Creating TypeScript caller-utils" + ); + + // Find all WIT files in the api directory + let mut wit_files = Vec::new(); + for entry in WalkDir::new(api_dir) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "wit") { + // Exclude world definition files + if let Ok(content) = fs::read_to_string(path) { + if !content.contains("world ") { + debug!(file = %path.display(), "Adding WIT file for parsing"); + wit_files.push(path.to_path_buf()); + } else { + debug!(file = %path.display(), "Skipping world definition WIT file"); + } + } + } + } + + debug!( + count = wit_files.len(), + "Found WIT interface files for TypeScript generation" + ); + + // Generate TypeScript content + let mut ts_content = String::new(); + + // Add the header with common utilities (always present) + ts_content.push_str("// Define a custom error type for API errors\n"); + ts_content.push_str("export class ApiError extends Error {\n"); + ts_content.push_str(" constructor(message: string, public readonly details?: unknown) {\n"); + ts_content.push_str(" super(message);\n"); + ts_content.push_str(" this.name = 'ApiError';\n"); + ts_content.push_str(" }\n"); + ts_content.push_str("}\n\n"); + + ts_content.push_str("// Parser for the Result-style responses\n"); + ts_content.push_str("// eslint-disable-next-line @typescript-eslint/no-explicit-any\n"); + ts_content.push_str("export function parseResultResponse(response: any): T {\n"); + ts_content.push_str( + " if ('Ok' in response && response.Ok !== undefined && response.Ok !== null) {\n", + ); + ts_content.push_str(" return response.Ok as T;\n"); + ts_content.push_str(" }\n\n"); + ts_content.push_str(" if ('Err' in response && response.Err !== undefined) {\n"); + ts_content.push_str(" throw new ApiError(`API returned an error`, response.Err);\n"); + ts_content.push_str(" }\n\n"); + ts_content.push_str(" throw new ApiError('Invalid API response format');\n"); + ts_content.push_str("}\n\n"); + + ts_content.push_str("/**\n"); + ts_content.push_str(" * Generic API request function\n"); + ts_content.push_str(" * @param endpoint - API endpoint\n"); + ts_content.push_str(" * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)\n"); + ts_content.push_str(" * @param data - Request data\n"); + ts_content.push_str(" * @returns Promise with parsed response data\n"); + ts_content.push_str(" * @throws ApiError if the request fails or response contains an error\n"); + ts_content.push_str(" */\n"); + ts_content.push_str("async function apiRequest(endpoint: string, method: string, data: T): Promise {\n"); + ts_content + .push_str(" const BASE_URL = import.meta.env.BASE_URL || window.location.origin;\n\n"); + ts_content.push_str(" const requestOptions: RequestInit = {\n"); + ts_content.push_str(" method: method,\n"); + ts_content.push_str(" headers: {\n"); + ts_content.push_str(" \"Content-Type\": \"application/json\",\n"); + ts_content.push_str(" },\n"); + ts_content.push_str(" };\n\n"); + ts_content.push_str(" // Only add body for methods that support it\n"); + ts_content.push_str(" if (method !== 'GET' && method !== 'HEAD') {\n"); + ts_content.push_str(" requestOptions.body = JSON.stringify(data);\n"); + ts_content.push_str(" }\n\n"); + ts_content.push_str(" const result = await fetch(`${BASE_URL}/api`, requestOptions);\n\n"); + ts_content.push_str(" if (!result.ok) {\n"); + ts_content + .push_str(" throw new ApiError(`HTTP request failed with status: ${result.status}`);\n"); + ts_content.push_str(" }\n\n"); + ts_content.push_str(" const jsonResponse = await result.json();\n"); + ts_content.push_str(" return parseResultResponse(jsonResponse);\n"); + ts_content.push_str("}\n\n"); + + // Collect all interfaces, types, and functions + let mut all_interfaces = Vec::new(); + let mut all_types = Vec::new(); + let mut all_functions = Vec::new(); + let mut function_names = Vec::new(); + + // Generate content for each WIT file + for wit_file in &wit_files { + match parse_wit_file(wit_file) { + Ok(signatures) => { + for signature in signatures { + let (interface_def, type_def, function_def) = + generate_typescript_function(&signature); + + if !interface_def.is_empty() { + all_interfaces.push(interface_def); + all_types.push(type_def); + all_functions.push(function_def); + function_names.push(to_camel_case(&signature.function_name)); + } + } + } + Err(e) => { + warn!(file = %wit_file.display(), error = %e, "Error parsing WIT file, skipping"); + } + } + } + + // If no HTTP functions were found, don't generate the file + if all_functions.is_empty() { + debug!("No HTTP functions found in WIT files, skipping TypeScript generation"); + return Ok(()); + } + + // Create directories only after we know we have HTTP functions + fs::create_dir_all(&ui_target_dir)?; + debug!("Created UI target directory structure"); + + // Add all collected definitions + if !all_interfaces.is_empty() { + ts_content.push_str("\n// API Interface Definitions\n\n"); + ts_content.push_str(&all_interfaces.join("\n\n")); + ts_content.push_str("\n\n"); + ts_content.push_str(&all_types.join("\n\n")); + ts_content.push_str("\n\n"); + } + + if !all_functions.is_empty() { + ts_content.push_str("// API Function Implementations\n\n"); + ts_content.push_str(&all_functions.join("\n\n")); + ts_content.push_str("\n\n"); + } + + // Add exports + if !function_names.is_empty() { + ts_content.push_str("// Export all API functions\n"); + ts_content.push_str("export {\n"); + for name in &function_names { + ts_content.push_str(&format!(" {},\n", name)); + } + ts_content.push_str("};\n"); + } + + // Write the TypeScript file + debug!( + "Writing generated TypeScript code to {}", + caller_utils_path.display() + ); + fs::write(&caller_utils_path, ts_content).with_context(|| { + format!( + "Failed to write caller-utils.ts: {}", + caller_utils_path.display() + ) + })?; + + info!( + "Successfully created TypeScript caller-utils at {}", + caller_utils_path.display() + ); + Ok(()) +} diff --git a/src/build/mod.rs b/src/build/mod.rs index e0fb2db7..fd9a7251 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -33,6 +33,7 @@ mod rewrite; use rewrite::copy_and_rewrite_package; mod caller_utils_generator; +mod caller_utils_ts_generator; mod wit_generator; const PY_VENV_NAME: &str = "process_env"; @@ -1814,6 +1815,11 @@ pub async fn execute( let api_dir = live_dir.join("api"); let (processed_projects, interfaces) = wit_generator::generate_wit_files(&live_dir, &api_dir)?; + + // generate ts bindings before building ui + let api_dir = live_dir.join("target").join("wit"); + caller_utils_ts_generator::create_typescript_caller_utils(&live_dir, &api_dir)?; + if interfaces.is_empty() { None } else { From ed22d3ba06437ea8db5488fa88ec362ce9767668 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 21 Jun 2025 11:12:14 -0700 Subject: [PATCH 59/88] caller_utils_ts: get it working well --- src/build/caller_utils_ts_generator.rs | 50 +++++++++++++++++--------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index c889d7b2..0b11258f 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -102,6 +102,22 @@ fn wit_type_to_typescript(wit_type: &str) -> String { } } +// Extract the inner type from a Result type for function returns +fn extract_result_ok_type(wit_type: &str) -> Option { + if wit_type.starts_with("result<") { + let inner_part = &wit_type[7..wit_type.len() - 1]; + if let Some(comma_pos) = inner_part.find(',') { + let ok_type = inner_part[..comma_pos].trim(); + Some(wit_type_to_typescript(ok_type)) + } else { + // Result with no error type + Some(wit_type_to_typescript(inner_part)) + } + } else { + None + } +} + // Structure to represent a field in a WIT signature struct #[derive(Debug)] struct SignatureField { @@ -213,7 +229,8 @@ fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, let mut params = Vec::new(); let mut param_names = Vec::new(); let mut param_types = Vec::new(); - let mut return_type = "void".to_string(); + let mut full_return_type = "void".to_string(); + let mut unwrapped_return_type = "void".to_string(); for field in &signature.fields { let field_name_camel = to_camel_case(&field.name); @@ -224,8 +241,14 @@ fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, // Skip target field as it's handled internally continue; } else if field.name == "returning" { - return_type = ts_type; - debug!(return_type = %return_type, "Identified return type"); + full_return_type = ts_type.clone(); + // Check if it's a Result type and extract the Ok type + if let Some(ok_type) = extract_result_ok_type(&field.wit_type) { + unwrapped_return_type = ok_type; + } else { + unwrapped_return_type = ts_type; + } + debug!(return_type = %unwrapped_return_type, "Identified return type"); } else { params.push(format!("{}: {}", field_name_camel, ts_type)); param_names.push(field_name_camel); @@ -256,10 +279,10 @@ fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, ) }; - // Generate response type alias + // Generate response type alias (using the full Result type) let response_type = format!( "export type {}Response = {};", - pascal_function_name, return_type + pascal_function_name, full_return_type ); // Generate function implementation @@ -284,16 +307,17 @@ fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, ) }; + // Function returns the unwrapped type since parseResultResponse extracts it let function_impl = format!( - "/**\n * {}\n{} * @returns Promise with result\n * @throws ApiError if the request fails\n */\nasync function {}({}): Promise<{}Response> {{\n{}\n\n return await apiRequest<{}Request, {}Response>('{}', 'POST', data);\n}}", + "/**\n * {}\n{} * @returns Promise with result\n * @throws ApiError if the request fails\n */\nexport async function {}({}): Promise<{}> {{\n{}\n\n return await apiRequest<{}Request, {}>('{}', 'POST', data);\n}}", camel_function_name, params.iter().map(|p| format!(" * @param {}", p)).collect::>().join("\n"), camel_function_name, function_params, - pascal_function_name, + unwrapped_return_type, // Use unwrapped type as the function return data_construction, pascal_function_name, - pascal_function_name, + unwrapped_return_type, // Pass unwrapped type to apiRequest, not Response type camel_function_name ); @@ -453,15 +477,7 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result ts_content.push_str("\n\n"); } - // Add exports - if !function_names.is_empty() { - ts_content.push_str("// Export all API functions\n"); - ts_content.push_str("export {\n"); - for name in &function_names { - ts_content.push_str(&format!(" {},\n", name)); - } - ts_content.push_str("};\n"); - } + // No need for explicit exports since functions are already exported inline // Write the TypeScript file debug!( From 6cafaf6fa29a6743c0b36f83314dc693b54bfc17 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 21 Jun 2025 14:39:00 -0700 Subject: [PATCH 60/88] build: fix ts caller-utils generation from fresh state --- src/build/caller_utils_ts_generator.rs | 3 ++- src/build/mod.rs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index 0b11258f..833c2eae 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -338,7 +338,8 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result let caller_utils_path = ui_target_dir.join("caller-utils.ts"); debug!( - path = %caller_utils_path.display(), + api_dir = %api_dir.display(), + call_utils_path = %caller_utils_path.display(), "Creating TypeScript caller-utils" ); diff --git a/src/build/mod.rs b/src/build/mod.rs index fd9a7251..9516ccb9 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1817,7 +1817,6 @@ pub async fn execute( wit_generator::generate_wit_files(&live_dir, &api_dir)?; // generate ts bindings before building ui - let api_dir = live_dir.join("target").join("wit"); caller_utils_ts_generator::create_typescript_caller_utils(&live_dir, &api_dir)?; if interfaces.is_empty() { From 9b0fb43fb3bf750cf0c0bfe4e9cb27c74df7d9fc Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 21 Jun 2025 16:20:12 -0700 Subject: [PATCH 61/88] update hyperware_app_common --- src/build/caller_utils_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index c450252e..f238bba1 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -520,7 +520,7 @@ process_macros = "0.1.0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "b6ad495" } +hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4c944b2" } once_cell = "1.20.2" futures = "0.3" uuid = { version = "1.0" } From b66b2934b9836168f05aa0e99bcff3044f604191 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 21 Jun 2025 17:19:23 -0700 Subject: [PATCH 62/88] build: pull hyperware_app_common version from process --- src/build/caller_utils_generator.rs | 162 ++++++++++++++++++++++++++-- 1 file changed, 156 insertions(+), 6 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index f238bba1..0e05f7aa 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use color_eyre::{ - eyre::{bail, WrapErr}, + eyre::{bail, eyre, WrapErr}, Result, }; use tracing::{debug, info, instrument, warn}; @@ -507,8 +507,13 @@ fn create_caller_utils_crate(api_dir: &Path, base_dir: &Path) -> Result<()> { fs::create_dir_all(caller_utils_dir.join("src"))?; debug!("Created project directory structure"); + // Get hyperware_app_common dependency from the process's Cargo.toml + let hyperware_dep = get_hyperware_app_common_dependency(base_dir)?; + debug!("Got hyperware_app_common dependency: {}", hyperware_dep); + // Create Cargo.toml with updated dependencies - let cargo_toml = r#"[package] + let cargo_toml = format!( + r#"[package] name = "caller-utils" version = "0.1.0" edition = "2021" @@ -518,17 +523,19 @@ publish = false anyhow = "1.0" process_macros = "0.1.0" futures-util = "0.3" -serde = { version = "1.0", features = ["derive"] } +serde = {{ version = "1.0", features = ["derive"] }} serde_json = "1.0" -hyperware_app_common = { git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4c944b2" } +hyperware_app_common = {} once_cell = "1.20.2" futures = "0.3" -uuid = { version = "1.0" } +uuid = {{ version = "1.0" }} wit-bindgen = "0.41.0" [lib] crate-type = ["cdylib", "lib"] -"#; +"#, + hyperware_dep + ); fs::write(caller_utils_dir.join("Cargo.toml"), cargo_toml) .with_context(|| "Failed to write caller-utils Cargo.toml")?; @@ -738,6 +745,149 @@ crate-type = ["cdylib", "lib"] Ok(()) } +// Get hyperware_app_common dependency from the process Cargo.toml files +#[instrument(level = "trace", skip_all)] +fn get_hyperware_app_common_dependency(base_dir: &Path) -> Result { + // First, read the workspace Cargo.toml to get the list of members + let workspace_cargo_toml_path = base_dir.join("Cargo.toml"); + debug!( + path = %workspace_cargo_toml_path.display(), + "Reading workspace Cargo.toml to get members" + ); + + let workspace_content = fs::read_to_string(&workspace_cargo_toml_path).with_context(|| { + format!( + "Failed to read workspace Cargo.toml: {}", + workspace_cargo_toml_path.display() + ) + })?; + + let workspace_toml: Value = workspace_content + .parse() + .with_context(|| "Failed to parse workspace Cargo.toml")?; + + // Get workspace members + let members = workspace_toml + .get("workspace") + .and_then(|w| w.get("members")) + .and_then(|m| m.as_array()) + .ok_or_else(|| eyre!("No workspace.members found in Cargo.toml"))?; + + let mut found_dependencies = HashMap::new(); + + // Check each member's Cargo.toml + for member in members { + if let Some(member_str) = member.as_str() { + // Skip generated directories like "target/" + if member_str.starts_with("target/") { + continue; + } + + let member_cargo_path = base_dir.join(member_str).join("Cargo.toml"); + if !member_cargo_path.exists() { + debug!( + "Member Cargo.toml not found: {}", + member_cargo_path.display() + ); + continue; + } + + debug!( + "Checking member: {} at {}", + member_str, + member_cargo_path.display() + ); + + let member_content = fs::read_to_string(&member_cargo_path).with_context(|| { + format!( + "Failed to read member Cargo.toml: {}", + member_cargo_path.display() + ) + })?; + + let member_toml: Value = member_content.parse().with_context(|| { + format!( + "Failed to parse member Cargo.toml: {}", + member_cargo_path.display() + ) + })?; + + // Look for hyperware_app_common in dependencies + if let Some(dependencies) = member_toml.get("dependencies") { + if let Some(hyperware_dep) = dependencies.get("hyperware_app_common") { + let dep_str = if let Some(table) = hyperware_dep.as_table() { + // Build inline table format manually + let mut parts = vec![]; + if let Some(git) = table.get("git").and_then(|v| v.as_str()) { + parts.push(format!("git = \"{}\"", git)); + } + if let Some(rev) = table.get("rev").and_then(|v| v.as_str()) { + parts.push(format!("rev = \"{}\"", rev)); + } + if let Some(branch) = table.get("branch").and_then(|v| v.as_str()) { + parts.push(format!("branch = \"{}\"", branch)); + } + if let Some(tag) = table.get("tag").and_then(|v| v.as_str()) { + parts.push(format!("tag = \"{}\"", tag)); + } + if let Some(version) = table.get("version").and_then(|v| v.as_str()) { + parts.push(format!("version = \"{}\"", version)); + } + if let Some(path) = table.get("path").and_then(|v| v.as_str()) { + parts.push(format!("path = \"{}\"", path)); + } + if let Some(features) = table.get("features").and_then(|v| v.as_array()) { + let features_str: Vec = features + .iter() + .filter_map(|f| f.as_str()) + .map(|f| format!("\"{}\"", f)) + .collect(); + parts.push(format!("features = [{}]", features_str.join(", "))); + } + format!("{{ {} }}", parts.join(", ")) + } else if let Some(str_dep) = hyperware_dep.as_str() { + format!("\"{}\"", str_dep) + } else { + continue; + }; + + debug!("Found hyperware_app_common in {}: {}", member_str, dep_str); + found_dependencies.insert(member_str.to_string(), dep_str); + } + } + } + } + + // Check that all found dependencies match + if found_dependencies.is_empty() { + // No dependencies found, use default + warn!("No hyperware_app_common dependencies found in any process, using default"); + return Ok( + r#"{ git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4c944b2" }"# + .to_string(), + ); + } + + let dep_values: Vec<_> = found_dependencies.values().collect(); + let first_dep = dep_values[0]; + + // Check if all dependencies are the same + for (process, dep) in &found_dependencies { + if dep != first_dep { + bail!( + "Conflicting hyperware_app_common versions found:\n Process '{}': {}\n Process '{}': {}\nAll processes must use the same version.", + found_dependencies.keys().next().unwrap(), + first_dep, + process, + dep + ); + } + } + + info!("Using hyperware_app_common dependency: {}", first_dep); + Ok(first_dep.clone()) +} + // Update workspace Cargo.toml to include the caller-utils crate #[instrument(level = "trace", skip_all)] fn update_workspace_cargo_toml(base_dir: &Path) -> Result<()> { From da5c10e2417d3e19fcd9d7a528d4d967d553dd7e Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 21 Jun 2025 17:25:51 -0700 Subject: [PATCH 63/88] build: refactor the version extraction --- src/build/caller_utils_generator.rs | 225 +++++++++++++--------------- 1 file changed, 107 insertions(+), 118 deletions(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 0e05f7aa..52eae7d1 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -745,147 +745,136 @@ crate-type = ["cdylib", "lib"] Ok(()) } -// Get hyperware_app_common dependency from the process Cargo.toml files -#[instrument(level = "trace", skip_all)] -fn get_hyperware_app_common_dependency(base_dir: &Path) -> Result { - // First, read the workspace Cargo.toml to get the list of members - let workspace_cargo_toml_path = base_dir.join("Cargo.toml"); - debug!( - path = %workspace_cargo_toml_path.display(), - "Reading workspace Cargo.toml to get members" - ); +// Format a TOML dependency value into an inline table string +fn format_toml_dependency(dep: &Value) -> Option { + match dep { + Value::Table(table) => { + let fields = [ + ("git", None), + ("rev", None), + ("branch", None), + ("tag", None), + ("version", None), + ("path", None), + ( + "features", + Some(|v: &Value| -> Option { + Some( + v.as_array()? + .iter() + .filter_map(|f| f.as_str()) + .map(|f| format!("\"{}\"", f)) + .collect::>() + .join(", ") + ) + }), + ), + ]; - let workspace_content = fs::read_to_string(&workspace_cargo_toml_path).with_context(|| { - format!( - "Failed to read workspace Cargo.toml: {}", - workspace_cargo_toml_path.display() - ) - })?; + let parts: Vec = fields + .iter() + .filter_map(|(key, formatter)| { + let value = table.get(*key)?; + if let Some(format_fn) = formatter { + Some(format!("{} = [{}]", key, format_fn(value)?)) + } else { + Some(format!("{} = \"{}\"", key, value.as_str()?)) + } + }) + .collect(); + + Some(format!("{{ {} }}", parts.join(", "))) + } + Value::String(s) => Some(format!("\"{}\"", s)), + _ => None, + } +} - let workspace_toml: Value = workspace_content +// Read and parse a Cargo.toml file +fn read_cargo_toml(path: &Path) -> Result { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read Cargo.toml: {}", path.display()))?; + content .parse() - .with_context(|| "Failed to parse workspace Cargo.toml")?; + .with_context(|| format!("Failed to parse Cargo.toml: {}", path.display())) +} - // Get workspace members +// Get hyperware_app_common dependency from the process Cargo.toml files +#[instrument(level = "trace", skip_all)] +fn get_hyperware_app_common_dependency(base_dir: &Path) -> Result { + const DEFAULT_DEP: &str = + r#"{ git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4c944b2" }"#; + + // Read workspace members + let workspace_toml = read_cargo_toml(&base_dir.join("Cargo.toml"))?; let members = workspace_toml .get("workspace") .and_then(|w| w.get("members")) .and_then(|m| m.as_array()) .ok_or_else(|| eyre!("No workspace.members found in Cargo.toml"))?; - let mut found_dependencies = HashMap::new(); - - // Check each member's Cargo.toml - for member in members { - if let Some(member_str) = member.as_str() { - // Skip generated directories like "target/" - if member_str.starts_with("target/") { - continue; - } + // Collect hyperware_app_common dependencies from all process members + let mut found_deps = HashMap::new(); - let member_cargo_path = base_dir.join(member_str).join("Cargo.toml"); - if !member_cargo_path.exists() { - debug!( - "Member Cargo.toml not found: {}", - member_cargo_path.display() - ); - continue; - } + for member in members.iter().filter_map(|m| m.as_str()) { + // Skip generated directories + if member.starts_with("target/") { + continue; + } + let member_cargo_path = base_dir.join(member).join("Cargo.toml"); + if !member_cargo_path.exists() { debug!( - "Checking member: {} at {}", - member_str, + "Member Cargo.toml not found: {}", member_cargo_path.display() ); + continue; + } - let member_content = fs::read_to_string(&member_cargo_path).with_context(|| { - format!( - "Failed to read member Cargo.toml: {}", - member_cargo_path.display() - ) - })?; + let member_toml = read_cargo_toml(&member_cargo_path)?; - let member_toml: Value = member_content.parse().with_context(|| { - format!( - "Failed to parse member Cargo.toml: {}", - member_cargo_path.display() - ) - })?; - - // Look for hyperware_app_common in dependencies - if let Some(dependencies) = member_toml.get("dependencies") { - if let Some(hyperware_dep) = dependencies.get("hyperware_app_common") { - let dep_str = if let Some(table) = hyperware_dep.as_table() { - // Build inline table format manually - let mut parts = vec![]; - if let Some(git) = table.get("git").and_then(|v| v.as_str()) { - parts.push(format!("git = \"{}\"", git)); - } - if let Some(rev) = table.get("rev").and_then(|v| v.as_str()) { - parts.push(format!("rev = \"{}\"", rev)); - } - if let Some(branch) = table.get("branch").and_then(|v| v.as_str()) { - parts.push(format!("branch = \"{}\"", branch)); - } - if let Some(tag) = table.get("tag").and_then(|v| v.as_str()) { - parts.push(format!("tag = \"{}\"", tag)); - } - if let Some(version) = table.get("version").and_then(|v| v.as_str()) { - parts.push(format!("version = \"{}\"", version)); - } - if let Some(path) = table.get("path").and_then(|v| v.as_str()) { - parts.push(format!("path = \"{}\"", path)); - } - if let Some(features) = table.get("features").and_then(|v| v.as_array()) { - let features_str: Vec = features - .iter() - .filter_map(|f| f.as_str()) - .map(|f| format!("\"{}\"", f)) - .collect(); - parts.push(format!("features = [{}]", features_str.join(", "))); - } - format!("{{ {} }}", parts.join(", ")) - } else if let Some(str_dep) = hyperware_dep.as_str() { - format!("\"{}\"", str_dep) - } else { - continue; - }; - - debug!("Found hyperware_app_common in {}: {}", member_str, dep_str); - found_dependencies.insert(member_str.to_string(), dep_str); - } - } + if let Some(dep) = member_toml + .get("dependencies") + .and_then(|d| d.get("hyperware_app_common")) + .and_then(format_toml_dependency) + { + debug!("Found hyperware_app_common in {}: {}", member, dep); + found_deps.insert(member.to_string(), dep); } } - // Check that all found dependencies match - if found_dependencies.is_empty() { - // No dependencies found, use default - warn!("No hyperware_app_common dependencies found in any process, using default"); - return Ok( - r#"{ git = "https://github.com/hyperware-ai/hyperprocess-macro", rev = "4c944b2" }"# - .to_string(), - ); - } + // Handle results + match found_deps.len() { + 0 => { + warn!("No hyperware_app_common dependencies found in any process, using default"); + Ok(DEFAULT_DEP.to_string()) + } + 1 => { + let dep = found_deps.values().next().unwrap(); + info!("Using hyperware_app_common dependency: {}", dep); + Ok(dep.clone()) + } + _ => { + // Ensure all dependencies match + let mut deps_iter = found_deps.values(); + let first_dep = deps_iter.next().unwrap(); + + for dep in deps_iter { + if dep != first_dep { + let (first_process, _) = + found_deps.iter().find(|(_, d)| *d == first_dep).unwrap(); + let (conflict_process, _) = found_deps.iter().find(|(_, d)| *d == dep).unwrap(); + bail!( + "Conflicting hyperware_app_common versions found:\n Process '{}': {}\n Process '{}': {}\nAll processes must use the same version.", + first_process, first_dep, conflict_process, dep + ); + } + } - let dep_values: Vec<_> = found_dependencies.values().collect(); - let first_dep = dep_values[0]; - - // Check if all dependencies are the same - for (process, dep) in &found_dependencies { - if dep != first_dep { - bail!( - "Conflicting hyperware_app_common versions found:\n Process '{}': {}\n Process '{}': {}\nAll processes must use the same version.", - found_dependencies.keys().next().unwrap(), - first_dep, - process, - dep - ); + info!("Using hyperware_app_common dependency: {}", first_dep); + Ok(first_dep.clone()) } } - - info!("Using hyperware_app_common dependency: {}", first_dep); - Ok(first_dep.clone()) } // Update workspace Cargo.toml to include the caller-utils crate From 38aec1cc1dab29f559101d2ce3d3520575028efc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:26:11 +0000 Subject: [PATCH 64/88] Format Rust code using rustfmt --- src/build/caller_utils_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index 52eae7d1..ef76d022 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -765,7 +765,7 @@ fn format_toml_dependency(dep: &Value) -> Option { .filter_map(|f| f.as_str()) .map(|f| format!("\"{}\"", f)) .collect::>() - .join(", ") + .join(", "), ) }), ), From d94c64e40318b7164145a6c50c7ab2b0bf6b3f1d Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 23 Jun 2025 15:45:57 -0700 Subject: [PATCH 65/88] start-package: fix some help text --- src/start_package/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/start_package/mod.rs b/src/start_package/mod.rs index ead1192e..c06e3503 100644 --- a/src/start_package/mod.rs +++ b/src/start_package/mod.rs @@ -236,7 +236,8 @@ pub async fn execute(package_dir: &Path, url: &str) -> Result<()> { .map_err(|e| { let e_string = e.to_string(); if e_string.contains("Failed with status code:") { - eyre!("{}\ncheck logs (default at {}) for full http response\n\nhint: is Kinode running at url {}?", e_string, KIT_LOG_PATH_DEFAULT, url) + eyre!("{e_string}\ncheck logs (default at {KIT_LOG_PATH_DEFAULT}) for full http response") + .with_suggestion(|| "is Hyperdrive running with `--expose-local` at url {url}?") } else { eyre!(e_string) } From 299874f69838ea3fac0384f5cf4ff70675edd013 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 25 Jun 2025 11:40:54 -0700 Subject: [PATCH 66/88] boot-fake-node: fix --- src/boot_fake_node/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/boot_fake_node/mod.rs b/src/boot_fake_node/mod.rs index 07f8549f..c309b034 100644 --- a/src/boot_fake_node/mod.rs +++ b/src/boot_fake_node/mod.rs @@ -405,6 +405,7 @@ pub fn run_runtime( format!("{port}"), "--verbosity".into(), format!("{verbosity}"), + "--expose-local".into(), ]; if !args.is_empty() { From c3ccbda60800c0215add3ffa34e5beca702fa47c Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 25 Jun 2025 11:41:47 -0700 Subject: [PATCH 67/88] boot*: add `--args` flag --- src/main.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7913b64b..84068641 100644 --- a/src/main.rs +++ b/src/main.rs @@ -153,6 +153,10 @@ async fn execute( let is_persist = matches.get_one::("PERSIST").unwrap(); let release = matches.get_one::("RELEASE").unwrap(); let verbosity = matches.get_one::("VERBOSITY").unwrap(); + let args = matches + .get_one::("ARGS") + .map(|s| s.split_whitespace().map(String::from).collect()) + .unwrap_or_else(|| vec![]); println!("boot_fake_node: {runtime_path:?}"); boot_fake_node::execute( @@ -167,7 +171,7 @@ async fn execute( *is_persist, *release, *verbosity, - vec![], + args, ) .await } @@ -184,6 +188,10 @@ async fn execute( // let password = matches.get_one::("PASSWORD").unwrap(); // TODO: with develop 0.8.0 let release = matches.get_one::("RELEASE").unwrap(); let verbosity = matches.get_one::("VERBOSITY").unwrap(); + let args = matches + .get_one::("ARGS") + .map(|s| s.split_whitespace().map(String::from).collect()) + .unwrap_or_else(|| vec![]); boot_real_node::execute( runtime_path, @@ -194,7 +202,7 @@ async fn execute( // password, // TODO: with develop 0.8.0 *release, *verbosity, - vec![], + args, ) .await } @@ -606,6 +614,13 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .default_value("0") .value_parser(value_parser!(u8)) ) + .arg(Arg::new("ARGS") + .action(ArgAction::Set) + .short('a') + .long("args") + .help("Additional arguments to pass to the node") + .required(false) + ) ) .subcommand(Command::new("boot-real-node") .about("Boot a real node") @@ -677,6 +692,13 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .default_value("0") .value_parser(value_parser!(u8)) ) + .arg(Arg::new("ARGS") + .action(ArgAction::Set) + .short('a') + .long("args") + .help("Additional arguments to pass to the node") + .required(false) + ) ) .subcommand(Command::new("build") .about("Build a Hyperware package") From a321e3cbd468d64f90dfe2ae8e34a7f94ee93d03 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sat, 21 Jun 2025 14:57:11 -0400 Subject: [PATCH 68/88] hotfix for WS --- src/build/wit_generator.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 39cdaebf..6550527c 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -803,9 +803,10 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Result Date: Sat, 21 Jun 2025 15:10:58 -0400 Subject: [PATCH 69/88] another hotfix --- src/build/caller_utils_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index ef76d022..d882f9ab 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -675,7 +675,7 @@ crate-type = ["cdylib", "lib"] lib_rs.push_str("pub use hyperware_app_common::AppSendError;\n"); lib_rs.push_str("pub use hyperware_app_common::send;\n"); lib_rs.push_str("use hyperware_app_common::hyperware_process_lib as hyperware_process_lib;\n"); - lib_rs.push_str("use hyperware_process_lib::{Address, Request};\n"); + lib_rs.push_str("pub use hyperware_process_lib::{Address, Request};\n"); lib_rs.push_str("use serde_json::json;\n\n"); // Add interface use statements From 92b0e1ba2e8eba3b8f61038665b1f2c293eff2e2 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:26:59 -0400 Subject: [PATCH 70/88] WIP --- src/build/wit_generator.rs | 106 ++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 6550527c..38d0f886 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -575,11 +575,32 @@ fn generate_signature_struct( let signature_struct_name = format!("{}-signature-{}", kebab_name, attr_type); // Generate comment for this specific function - let comment = format!( + let mut comment = format!( " // Function signature for: {} ({})", kebab_name, attr_type ); + // For HTTP endpoints, try to extract method and path from attribute + let mut has_explicit_path = false; + if attr_type == "http" { + if let Some((http_method, http_path)) = extract_http_info(&method.attrs)? { + comment.push_str(&format!("\n // HTTP: {} {}", http_method, http_path)); + // Check if path was explicitly defined (not just defaulted) + has_explicit_path = method.attrs.iter().any(|attr| { + if attr.path().is_ident("http") { + let attr_str = format!("{:?}", attr); + attr_str.contains("path") + } + else { + false + } + }); + } else { + // Default path if not specified + comment.push_str(&format!("\n // HTTP: POST /api/{}", kebab_name)); + } + } + // Create struct fields that directly represent function parameters let mut struct_fields = Vec::new(); @@ -591,6 +612,9 @@ fn generate_signature_struct( struct_fields.push(" target: address".to_string()); } + // Count non-self parameters + let mut param_count = 0; + // Process function parameters (skip &self and &mut self) for arg in &method.sig.inputs { if let syn::FnArg::Typed(pat_type) = arg { @@ -600,6 +624,8 @@ fn generate_signature_struct( continue; } + param_count += 1; + // Get original param name let param_orig_name = pat_ident.ident.to_string(); let method_name_for_error = method.sig.ident.to_string(); // Get method name for error messages @@ -636,6 +662,33 @@ fn generate_signature_struct( } } + // Validate parameter requirements + if param_count == 0 && attr_type == "http" && !has_explicit_path { + let method_name = method.sig.ident.to_string(); + bail!( + "\n\nERROR: HTTP handler '{}' is missing required configuration!\n\ + \n\ + HTTP handlers must EITHER:\n\ + 1. Have at least one parameter (beyond &self/&mut self), OR\n\ + 2. Define an explicit path in the attribute\n\ + \n\ + Examples of valid HTTP handlers:\n\ + \n\ + // Option 1: Handler with parameter (path will be /api/{})\n\ + #[http]\n\ + fn {}(&mut self, request: Request) -> Response {{ ... }}\n\ + \n\ + // Option 2: Handler with explicit path (no parameters needed)\n\ + #[http(path = \"/api/status\")]\n\ + fn {}(&mut self) -> Response {{ ... }}\n\ + \n\ + // Option 3: Handler with method and path\n\ + #[http(method = \"GET\", path = \"/api/users\")]\n\ + fn {}(&mut self) -> Vec {{ ... }}\n", + method_name, method_name, method_name, method_name, method_name + ); + } + // Add return type field match &method.sig.output { syn::ReturnType::Type(_, ty) => match rust_type_to_wit(&*ty, used_types) { @@ -681,6 +734,57 @@ fn generate_signature_struct( Ok(record_def) } +// Helper function to extract HTTP method and path from [http] attribute +#[instrument(level = "trace", skip_all)] +fn extract_http_info(attrs: &[Attribute]) -> Result> { + for attr in attrs { + if attr.path().is_ident("http") { + // Convert attribute to string representation for parsing + let attr_str = format!("{:?}", attr); + debug!(attr_str = %attr_str, "HTTP attribute string"); + + let mut method = None; + let mut path = None; + + // Look for method parameter + if let Some(method_pos) = attr_str.find("method") { + if let Some(eq_pos) = attr_str[method_pos..].find('=') { + let start_pos = method_pos + eq_pos + 1; + // Find the quoted value + if let Some(quote_start) = attr_str[start_pos..].find('"') { + let value_start = start_pos + quote_start + 1; + if let Some(quote_end) = attr_str[value_start..].find('"') { + method = Some(attr_str[value_start..value_start + quote_end].to_string()); + } + } + } + } + + // Look for path parameter + if let Some(path_pos) = attr_str.find("path") { + if let Some(eq_pos) = attr_str[path_pos..].find('=') { + let start_pos = path_pos + eq_pos + 1; + // Find the quoted value + if let Some(quote_start) = attr_str[start_pos..].find('"') { + let value_start = start_pos + quote_start + 1; + if let Some(quote_end) = attr_str[value_start..].find('"') { + path = Some(attr_str[value_start..value_start + quote_end].to_string()); + } + } + } + } + + // If we found at least one parameter, return the info + if method.is_some() || path.is_some() { + let final_method = method.unwrap_or_else(|| "POST".to_string()); + let final_path = path.unwrap_or_else(|| "/api".to_string()); + return Ok(Some((final_method, final_path))); + } + } + } + Ok(None) +} + // Helper trait to get TypePath from Type trait AsTypePath { fn as_type_path(&self) -> Option<&syn::TypePath>; From 1d880d29baa5af6947bb78da1d1d36791865d364 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:00:08 -0400 Subject: [PATCH 71/88] hotifx --- src/build/wit_generator.rs | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 38d0f886..d711ddac 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -662,32 +662,7 @@ fn generate_signature_struct( } } - // Validate parameter requirements - if param_count == 0 && attr_type == "http" && !has_explicit_path { - let method_name = method.sig.ident.to_string(); - bail!( - "\n\nERROR: HTTP handler '{}' is missing required configuration!\n\ - \n\ - HTTP handlers must EITHER:\n\ - 1. Have at least one parameter (beyond &self/&mut self), OR\n\ - 2. Define an explicit path in the attribute\n\ - \n\ - Examples of valid HTTP handlers:\n\ - \n\ - // Option 1: Handler with parameter (path will be /api/{})\n\ - #[http]\n\ - fn {}(&mut self, request: Request) -> Response {{ ... }}\n\ - \n\ - // Option 2: Handler with explicit path (no parameters needed)\n\ - #[http(path = \"/api/status\")]\n\ - fn {}(&mut self) -> Response {{ ... }}\n\ - \n\ - // Option 3: Handler with method and path\n\ - #[http(method = \"GET\", path = \"/api/users\")]\n\ - fn {}(&mut self) -> Vec {{ ... }}\n", - method_name, method_name, method_name, method_name, method_name - ); - } + // HTTP handlers no longer require parameters - they can have zero parameters // Add return type field match &method.sig.output { From b1ce5a50b57748cfc3821db00a6ef4c73efefbd0 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:00:15 -0400 Subject: [PATCH 72/88] removed old artifacts --- src/build/wit_generator.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index d711ddac..920e33d4 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -612,9 +612,6 @@ fn generate_signature_struct( struct_fields.push(" target: address".to_string()); } - // Count non-self parameters - let mut param_count = 0; - // Process function parameters (skip &self and &mut self) for arg in &method.sig.inputs { if let syn::FnArg::Typed(pat_type) = arg { @@ -624,8 +621,6 @@ fn generate_signature_struct( continue; } - param_count += 1; - // Get original param name let param_orig_name = pat_ident.ident.to_string(); let method_name_for_error = method.sig.ident.to_string(); // Get method name for error messages From d4f5bacb7bddbef3393f785d3e2b0415889ed78b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 21:01:28 +0000 Subject: [PATCH 73/88] Format Rust code using rustfmt --- src/build/wit_generator.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 920e33d4..a6fdfabf 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -590,8 +590,7 @@ fn generate_signature_struct( if attr.path().is_ident("http") { let attr_str = format!("{:?}", attr); attr_str.contains("path") - } - else { + } else { false } }); @@ -724,7 +723,8 @@ fn extract_http_info(attrs: &[Attribute]) -> Result> { if let Some(quote_start) = attr_str[start_pos..].find('"') { let value_start = start_pos + quote_start + 1; if let Some(quote_end) = attr_str[value_start..].find('"') { - method = Some(attr_str[value_start..value_start + quote_end].to_string()); + method = + Some(attr_str[value_start..value_start + quote_end].to_string()); } } } From 59e14c2f322491f150596b4cef5f79fcae0174ab Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Wed, 25 Jun 2025 15:21:23 -0700 Subject: [PATCH 74/88] build: improve ts generation --- src/build/caller_utils_ts_generator.rs | 232 +++++++++++++++++++++---- 1 file changed, 195 insertions(+), 37 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index 833c2eae..6d611c74 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -133,15 +133,38 @@ struct SignatureStruct { fields: Vec, } -// Parse WIT file to extract function signatures +// Structure to represent a WIT record +#[derive(Debug)] +struct WitRecord { + name: String, + fields: Vec, +} + +// Structure to represent a WIT variant +#[derive(Debug)] +struct WitVariant { + name: String, + cases: Vec, +} + +// Structure to hold all parsed WIT types +struct WitTypes { + signatures: Vec, + records: Vec, + variants: Vec, +} + +// Parse WIT file to extract function signatures, records, and variants #[instrument(level = "trace", skip_all)] -fn parse_wit_file(file_path: &Path) -> Result> { +fn parse_wit_file(file_path: &Path) -> Result { debug!(file = %file_path.display(), "Parsing WIT file"); let content = fs::read_to_string(file_path) .with_context(|| format!("Failed to read WIT file: {}", file_path.display()))?; let mut signatures = Vec::new(); + let mut records = Vec::new(); + let mut variants = Vec::new(); // Simple parser for WIT files to extract record definitions let lines: Vec<_> = content.lines().collect(); @@ -150,59 +173,134 @@ fn parse_wit_file(file_path: &Path) -> Result> { while i < lines.len() { let line = lines[i].trim(); - // Look for signature record definitions - if line.starts_with("record ") && line.contains("-signature-") { + // Look for record definitions + if line.starts_with("record ") { let record_name = line .trim_start_matches("record ") .trim_end_matches(" {") .trim(); - debug!(name = %record_name, "Found signature record"); + + if record_name.contains("-signature-") { + // This is a signature record + debug!(name = %record_name, "Found signature record"); + + // Extract function name and attribute type + let parts: Vec<_> = record_name.split("-signature-").collect(); + if parts.len() != 2 { + warn!(name = %record_name, "Unexpected signature record name format, skipping"); + i += 1; + continue; + } - // Extract function name and attribute type - let parts: Vec<_> = record_name.split("-signature-").collect(); - if parts.len() != 2 { - warn!(name = %record_name, "Unexpected signature record name format, skipping"); + let function_name = parts[0].to_string(); + let attr_type = parts[1].to_string(); + debug!(function = %function_name, attr_type = %attr_type, "Extracted function name and type"); + + // Parse fields + let mut fields = Vec::new(); + i += 1; + + while i < lines.len() && !lines[i].trim().starts_with("}") { + let field_line = lines[i].trim(); + + // Skip comments and empty lines + if field_line.starts_with("//") || field_line.is_empty() { + i += 1; + continue; + } + + // Parse field definition + let field_parts: Vec<_> = field_line.split(':').collect(); + if field_parts.len() == 2 { + let field_name = field_parts[0].trim().to_string(); + let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); + + debug!(name = %field_name, wit_type = %field_type, "Found field"); + fields.push(SignatureField { + name: field_name, + wit_type: field_type, + }); + } + + i += 1; + } + + signatures.push(SignatureStruct { + function_name, + attr_type, + fields, + }); + } else { + // This is a regular record + debug!(name = %record_name, "Found record"); + + // Parse fields + let mut fields = Vec::new(); i += 1; - continue; - } - let function_name = parts[0].to_string(); - let attr_type = parts[1].to_string(); - debug!(function = %function_name, attr_type = %attr_type, "Extracted function name and type"); + while i < lines.len() && !lines[i].trim().starts_with("}") { + let field_line = lines[i].trim(); - // Parse fields - let mut fields = Vec::new(); + // Skip comments and empty lines + if field_line.starts_with("//") || field_line.is_empty() { + i += 1; + continue; + } + + // Parse field definition + let field_parts: Vec<_> = field_line.split(':').collect(); + if field_parts.len() == 2 { + let field_name = field_parts[0].trim().to_string(); + let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); + + debug!(name = %field_name, wit_type = %field_type, "Found field"); + fields.push(SignatureField { + name: field_name, + wit_type: field_type, + }); + } + + i += 1; + } + + records.push(WitRecord { + name: record_name.to_string(), + fields, + }); + } + } + // Look for variant definitions + else if line.starts_with("variant ") { + let variant_name = line + .trim_start_matches("variant ") + .trim_end_matches(" {") + .trim(); + debug!(name = %variant_name, "Found variant"); + + // Parse cases + let mut cases = Vec::new(); i += 1; while i < lines.len() && !lines[i].trim().starts_with("}") { - let field_line = lines[i].trim(); + let case_line = lines[i].trim(); // Skip comments and empty lines - if field_line.starts_with("//") || field_line.is_empty() { + if case_line.starts_with("//") || case_line.is_empty() { i += 1; continue; } - // Parse field definition - let field_parts: Vec<_> = field_line.split(':').collect(); - if field_parts.len() == 2 { - let field_name = field_parts[0].trim().to_string(); - let field_type = field_parts[1].trim().trim_end_matches(',').to_string(); - - debug!(name = %field_name, wit_type = %field_type, "Found field"); - fields.push(SignatureField { - name: field_name, - wit_type: field_type, - }); - } + // Parse case - just the name, ignoring any associated data for now + let case_name = case_line.trim_end_matches(',').to_string(); + debug!(case = %case_name, "Found variant case"); + cases.push(case_name); i += 1; } - signatures.push(SignatureStruct { - function_name, - attr_type, - fields, + variants.push(WitVariant { + name: variant_name.to_string(), + cases, }); } @@ -212,9 +310,49 @@ fn parse_wit_file(file_path: &Path) -> Result> { debug!( file = %file_path.display(), signatures = signatures.len(), + records = records.len(), + variants = variants.len(), "Finished parsing WIT file" ); - Ok(signatures) + Ok(WitTypes { + signatures, + records, + variants, + }) +} + +// Generate TypeScript interface from a WIT record +fn generate_typescript_interface(record: &WitRecord) -> String { + let interface_name = to_pascal_case(&record.name); + let mut fields = Vec::new(); + + for field in &record.fields { + let field_name = to_camel_case(&field.name); + let ts_type = wit_type_to_typescript(&field.wit_type); + fields.push(format!(" {}: {};", field_name, ts_type)); + } + + format!( + "export interface {} {{\n{}\n}}", + interface_name, + fields.join("\n") + ) +} + +// Generate TypeScript type from a WIT variant +fn generate_typescript_variant(variant: &WitVariant) -> String { + let type_name = to_pascal_case(&variant.name); + let cases: Vec = variant + .cases + .iter() + .map(|case| format!("\"{}\"", to_pascal_case(case))) + .collect(); + + format!( + "export type {} = {};", + type_name, + cases.join(" | ") + ) } // Generate TypeScript interface and function from a signature struct @@ -430,12 +568,25 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result let mut all_types = Vec::new(); let mut all_functions = Vec::new(); let mut function_names = Vec::new(); + let mut custom_types = Vec::new(); // For records and variants // Generate content for each WIT file for wit_file in &wit_files { match parse_wit_file(wit_file) { - Ok(signatures) => { - for signature in signatures { + Ok(wit_types) => { + // Process custom types (records and variants) + for record in &wit_types.records { + let interface_def = generate_typescript_interface(record); + custom_types.push(interface_def); + } + + for variant in &wit_types.variants { + let type_def = generate_typescript_variant(variant); + custom_types.push(type_def); + } + + // Process function signatures + for signature in &wit_types.signatures { let (interface_def, type_def, function_def) = generate_typescript_function(&signature); @@ -463,6 +614,13 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result fs::create_dir_all(&ui_target_dir)?; debug!("Created UI target directory structure"); + // Add custom types (records and variants) first + if !custom_types.is_empty() { + ts_content.push_str("\n// Custom Types from WIT definitions\n\n"); + ts_content.push_str(&custom_types.join("\n\n")); + ts_content.push_str("\n\n"); + } + // Add all collected definitions if !all_interfaces.is_empty() { ts_content.push_str("\n// API Interface Definitions\n\n"); From 9c6d940a235cfaf8a8ef8330a6088ac95f0b309d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 22:21:47 +0000 Subject: [PATCH 75/88] Format Rust code using rustfmt --- src/build/caller_utils_ts_generator.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index 6d611c74..67e46154 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -179,7 +179,7 @@ fn parse_wit_file(file_path: &Path) -> Result { .trim_start_matches("record ") .trim_end_matches(" {") .trim(); - + if record_name.contains("-signature-") { // This is a signature record debug!(name = %record_name, "Found signature record"); @@ -348,11 +348,7 @@ fn generate_typescript_variant(variant: &WitVariant) -> String { .map(|case| format!("\"{}\"", to_pascal_case(case))) .collect(); - format!( - "export type {} = {};", - type_name, - cases.join(" | ") - ) + format!("export type {} = {};", type_name, cases.join(" | ")) } // Generate TypeScript interface and function from a signature struct @@ -568,7 +564,7 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result let mut all_types = Vec::new(); let mut all_functions = Vec::new(); let mut function_names = Vec::new(); - let mut custom_types = Vec::new(); // For records and variants + let mut custom_types = Vec::new(); // For records and variants // Generate content for each WIT file for wit_file in &wit_files { @@ -579,7 +575,7 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result let interface_def = generate_typescript_interface(record); custom_types.push(interface_def); } - + for variant in &wit_types.variants { let type_def = generate_typescript_variant(variant); custom_types.push(type_def); From e1b8fc4d7cc713841ae7d1fa52945022e1406496 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 26 Jun 2025 17:13:52 -0700 Subject: [PATCH 76/88] build: generate ts bindings for parameter-less #[http] --- src/build/caller_utils_ts_generator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index 67e46154..d72ab994 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -394,7 +394,7 @@ fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, let request_interface = if param_names.is_empty() { // No parameters case format!( - "export interface {}Request {{\n {}: {{}}\n}}", + "export interface {}Request {{\n {}: null\n}}", pascal_function_name, pascal_function_name ) } else if param_names.len() == 1 { @@ -424,7 +424,7 @@ fn generate_typescript_function(signature: &SignatureStruct) -> (String, String, let data_construction = if param_names.is_empty() { format!( - " const data: {}Request = {{\n {}: {{}},\n }};", + " const data: {}Request = {{\n {}: null,\n }};", pascal_function_name, pascal_function_name ) } else if param_names.len() == 1 { From 531f660352376393a32952793a17c4605fb721ad Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 27 Jun 2025 05:59:17 -0700 Subject: [PATCH 77/88] build: generalize wit_generator --- src/build/wit_generator.rs | 49 ++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index a6fdfabf..039daf63 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -139,6 +139,41 @@ fn is_wit_primitive_or_builtin(type_name: &str) -> bool { || type_name.starts_with("tuple<") } +// Extract custom type names from a WIT type string (e.g., "list" -> ["foo-bar"]) +fn extract_custom_types_from_wit_type(wit_type: &str) -> Vec { + let mut custom_types = Vec::new(); + + // Skip if it's a primitive type + if is_wit_primitive_or_builtin(wit_type) && !wit_type.contains('<') { + return custom_types; + } + + // Handle composite types like list, option, result, tuple + if let Some(start) = wit_type.find('<') { + if let Some(end) = wit_type.rfind('>') { + let inner = &wit_type[start + 1..end]; + + // Split by comma to handle multiple type parameters + for part in inner.split(',') { + let trimmed = part.trim(); + if !trimmed.is_empty() && trimmed != "_" && !is_wit_primitive_or_builtin(trimmed) { + // Recursively extract from nested types + if trimmed.contains('<') { + custom_types.extend(extract_custom_types_from_wit_type(trimmed)); + } else { + custom_types.push(trimmed.to_string()); + } + } + } + } + } else if !is_wit_primitive_or_builtin(wit_type) { + // It's a non-composite custom type + custom_types.push(wit_type.to_string()); + } + + custom_types +} + // Convert Rust type to WIT type, including downstream types #[instrument(level = "trace", skip_all)] fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { @@ -403,10 +438,10 @@ fn find_and_make_wit_type_def( let field_wit_type = rust_type_to_wit(&f.ty, global_used_types) .wrap_err_with(|| format!("Failed to convert field '{}':'{:?}' in struct '{}'", field_orig_name, f.ty, orig_name))?; - // If the resulting WIT type itself is custom, add it to *local* dependencies - // so the caller knows this struct definition depends on it. - if !is_wit_primitive_or_builtin(&field_wit_type) { - local_dependencies.insert(field_wit_type.clone()); + // Extract any custom types from the field type and add them to local dependencies + // For example, from "list" we extract "participant-info" + for custom_type in extract_custom_types_from_wit_type(&field_wit_type) { + local_dependencies.insert(custom_type); } field_strings.push(format!(" {}: {}", field_kebab_name, field_wit_type)); @@ -465,9 +500,9 @@ fn find_and_make_wit_type_def( ) })?; - // Check if the variant's type is custom and add to local deps - if !is_wit_primitive_or_builtin(&type_result) { - local_dependencies.insert(type_result.clone()); + // Extract any custom types from the variant type and add them to local dependencies + for custom_type in extract_custom_types_from_wit_type(&type_result) { + local_dependencies.insert(custom_type); } variants_wit .push(format!(" {}({})", variant_kebab_name, type_result)); From 0f7544fc412f579a339cc49b379d60a81e787426 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 17 Jul 2025 15:33:16 -0700 Subject: [PATCH 78/88] build: fix compiler warnings --- src/build/wit_generator.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 039daf63..15ead1b4 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -616,19 +616,9 @@ fn generate_signature_struct( ); // For HTTP endpoints, try to extract method and path from attribute - let mut has_explicit_path = false; if attr_type == "http" { if let Some((http_method, http_path)) = extract_http_info(&method.attrs)? { comment.push_str(&format!("\n // HTTP: {} {}", http_method, http_path)); - // Check if path was explicitly defined (not just defaulted) - has_explicit_path = method.attrs.iter().any(|attr| { - if attr.path().is_ident("http") { - let attr_str = format!("{:?}", attr); - attr_str.contains("path") - } else { - false - } - }); } else { // Default path if not specified comment.push_str(&format!("\n // HTTP: POST /api/{}", kebab_name)); From 69082ea56b2b49d504438e9bb90ff4df074aa580 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 05:48:38 -0700 Subject: [PATCH 79/88] publish: add safe --- Cargo.lock | 7 ++ src/main.rs | 17 ++++- src/publish/mod.rs | 166 +++++++++++++++++++++++++++++++++------------ 3 files changed, 143 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2581b55f..b993ec06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,6 +2316,7 @@ dependencies = [ "tracing-appender", "tracing-error", "tracing-subscriber", + "urlencoding", "walkdir", "wit-bindgen", "zip", @@ -4314,6 +4315,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/src/main.rs b/src/main.rs index 84068641..495b022b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -403,6 +403,9 @@ async fn execute( .and_then(|kp| Some(PathBuf::from(kp))); let ledger = matches.get_one::("LEDGER").unwrap(); let trezor = matches.get_one::("TREZOR").unwrap(); + let safe = matches + .get_one::("SAFE") + .and_then(|gs| Some(gs.as_str())); let rpc_uri = matches.get_one::("RPC_URI").unwrap(); let real = matches.get_one::("REAL").unwrap(); let unpublish = matches.get_one::("UNPUBLISH").unwrap(); @@ -421,6 +424,7 @@ async fn execute( keystore_path, ledger, trezor, + safe, rpc_uri, real, unpublish, @@ -1110,21 +1114,28 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .action(ArgAction::Set) .short('k') .long("keystore-path") - .help("Path to private key keystore (choose 1 of `k`, `l`, `t`)") // TODO: add link to docs? + .help("Path to private key keystore (choose 1 of `k`, `l`, `t`, `s`)") // TODO: add link to docs? .required(false) ) .arg(Arg::new("LEDGER") .action(ArgAction::SetTrue) .short('l') .long("ledger") - .help("Use Ledger private key (choose 1 of `k`, `l`, `t`)") + .help("Use Ledger private key (choose 1 of `k`, `l`, `t`, `s`)") .required(false) ) .arg(Arg::new("TREZOR") .action(ArgAction::SetTrue) .short('t') .long("trezor") - .help("Use Trezor private key (choose 1 of `k`, `l`, `t`)") + .help("Use Trezor private key (choose 1 of `k`, `l`, `t`, `s`)") + .required(false) + ) + .arg(Arg::new("SAFE") + .action(ArgAction::Set) + .short('s') + .long("safe") + .help("Safe contract address (choose 1 of `k`, `l`, `t`, `s`)") .required(false) ) .arg(Arg::new("URI") diff --git a/src/publish/mod.rs b/src/publish/mod.rs index ef72f055..8c6be900 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -308,40 +308,20 @@ async fn prepare_hypermap_put( Ok((to, call)) } + #[instrument(level = "trace", skip_all)] -pub async fn execute( +pub async fn build_tx( package_dir: &Path, metadata_uri: &str, - keystore_path: Option, - ledger: &bool, - trezor: &bool, - rpc_uri: &str, + provider: &RootProvider, real: &bool, unpublish: &bool, + wallet_address: Address, + chain_id: u64, gas_limit: u64, max_priority_fee_per_gas: Option, max_fee_per_gas: Option, - mock: &bool, -) -> Result<()> { - if !package_dir.join("pkg").exists() { - return Err(eyre!( - "Required `pkg/` dir not found within given input dir {:?} (or cwd, if none given). Please re-run targeting a package.", - package_dir, - )); - } - - let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; - let (wallet_address, wallet) = match (keystore_path, *ledger, *trezor) { - (Some(ref kp), false, false) => read_keystore(kp)?, - (None, true, false) => read_ledger(chain_id).await?, - (None, false, true) => read_trezor(chain_id).await?, - _ => { - return Err(eyre!( - "Must supply one and only one of `--keystore_path`, `--ledger`, or `--trezor`" - )) - } - }; - +) -> Result<(String, Address, Vec, TransactionRequest)> { let metadata = read_and_update_metadata(package_dir)?; let name = metadata.name.clone().unwrap(); @@ -361,9 +341,6 @@ pub async fn execute( let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; check_pkg_hash(&metadata, package_dir, metadata_uri)?; - let ws = WsConnect::new(rpc_uri); - let provider: RootProvider = ProviderBuilder::default().on_ws(ws).await?; - let hypermap = Address::from_str(if *real { REAL_KIMAP_ADDRESS } else { @@ -410,7 +387,7 @@ pub async fn execute( let tx = TransactionRequest::default() .to(to) - .input(TransactionInput::new(call.into())) + .input(TransactionInput::new(call.clone().into())) .nonce(nonce) .with_chain_id(chain_id) .with_gas_limit(gas_limit) @@ -419,21 +396,122 @@ pub async fn execute( ) .with_max_fee_per_gas(max_fee_per_gas.unwrap_or_else(|| suggested_max_fee_per_gas)); - let tx_envelope = tx.build(&wallet).await?; - let tx_encoded = tx_envelope.encoded_2718(); - if *mock { - info!( - "{} {name} tx mock successful", - if *unpublish { "unpublish" } else { "publish" } - ); + Ok((name, to, call, tx)) +} + +#[instrument(level = "trace", skip_all)] +pub async fn execute( + package_dir: &Path, + metadata_uri: &str, + keystore_path: Option, + ledger: &bool, + trezor: &bool, + safe: Option<&str>, + rpc_uri: &str, + real: &bool, + unpublish: &bool, + gas_limit: u64, + max_priority_fee_per_gas: Option, + max_fee_per_gas: Option, + mock: &bool, +) -> Result<()> { + if !package_dir.join("pkg").exists() { + return Err(eyre!( + "Required `pkg/` dir not found within given input dir {:?} (or cwd, if none given). Please re-run targeting a package.", + package_dir, + )); + } + + let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; + + // Check if using Gnosis Safe mode + let is_safe_mode = safe.is_some(); + + let (wallet_address, wallet) = if is_safe_mode { + // In Safe mode, we don't need a wallet for signing + // Parse the Safe address provided by the user + let safe_address = Address::from_str(safe.unwrap())?; + (safe_address, None) } else { - let tx = provider.send_raw_transaction(&tx_encoded).await?; - let tx_hash = format!("{:?}", tx.tx_hash()); - let link = make_remote_link(&format!("https://basescan.org/tx/{tx_hash}"), &tx_hash); - info!( - "{} {name} tx sent: {link}", - if *unpublish { "unpublish" } else { "publish" } - ); + // Traditional wallet mode + let (addr, wallet) = match (keystore_path, *ledger, *trezor) { + (Some(ref kp), false, false) => read_keystore(kp)?, + (None, true, false) => read_ledger(chain_id).await?, + (None, false, true) => read_trezor(chain_id).await?, + _ => { + return Err(eyre!( + "Must supply one and only one of `--keystore_path`, `--ledger`, `--trezor`, or `--safe`" + )) + } + }; + (addr, Some(wallet)) + }; + + let ws = WsConnect::new(rpc_uri); + let provider: RootProvider = ProviderBuilder::default().on_ws(ws).await?; + + let (name, to, call, tx) = build_tx( + package_dir, + metadata_uri, + &provider, + real, + unpublish, + wallet_address, + chain_id, + gas_limit, + max_priority_fee_per_gas, + max_fee_per_gas, + ).await?; + + if is_safe_mode { + // Generate Safe transaction data + let tx_data = hex::encode(call); + + // TODO: can we get URL working? If so this is by far preferable + //// Create Safe App URL - always use Base chain (8453) + //let safe_url = format!( + // "https://app.safe.global/base:{}/transactions/tx?safe={}&to={}&value=0&data=0x{}", + // wallet_address, + // wallet_address, + // to, + // tx_data + //); + + info!("=== Gnosis Safe Transaction Data ==="); + // TODO: can we get URL working? If so this is by far preferable + //info!("Safe App URL (click or copy):"); + //info!("{}", make_remote_link(&safe_url, &safe_url)); + //info!(""); + info!("Manual Steps:"); + info!("1. Go to your Safe at https://app.safe.global"); + info!("2. Click \"New Transaction\" → \"Transaction Builder\""); + info!("3. Enter the contract address in \"Enter Address or ENS Name\": {}", to); + info!("4. Toggle \"Custom data\""); + info!("5. Put in \"ETH value\": 0"); + info!("6. Paste the transaction data in \"Data (Hex encoded)\": 0x{}", tx_data); + info!("7. \"Add new transaction\" -> \"Create Batch\" -> \"Simulate\""); + info!("8. If simulation passes, \"Send Batch\""); + info!("9. Collect signatures from other Safe owners"); + info!("10. Execute once threshold is reached (transaction only goes live in this final step)"); + } else { + // Traditional wallet signing flow + let wallet = wallet.unwrap(); + let tx_envelope = tx.build(&wallet).await?; + let tx_encoded = tx_envelope.encoded_2718(); + if *mock { + info!( + "{} {name} tx mock successful", + if *unpublish { "unpublish" } else { "publish" } + ); + } else { + let tx = provider.send_raw_transaction(&tx_encoded).await?; + let tx_hash = format!("{:?}", tx.tx_hash()); + let link = make_remote_link(&format!("https://basescan.org/tx/{tx_hash}"), &tx_hash); + info!( + "{} {name} tx sent: {link}", + if *unpublish { "unpublish" } else { "publish" } + ); + } } Ok(()) } From 9f19d4058b0eb48009d0835a95c89c3a36065da5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:49:07 +0000 Subject: [PATCH 80/88] Format Rust code using rustfmt --- src/publish/mod.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/publish/mod.rs b/src/publish/mod.rs index 8c6be900..7df5d874 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -308,7 +308,6 @@ async fn prepare_hypermap_put( Ok((to, call)) } - #[instrument(level = "trace", skip_all)] pub async fn build_tx( package_dir: &Path, @@ -461,7 +460,8 @@ pub async fn execute( gas_limit, max_priority_fee_per_gas, max_fee_per_gas, - ).await?; + ) + .await?; if is_safe_mode { // Generate Safe transaction data @@ -485,14 +485,22 @@ pub async fn execute( info!("Manual Steps:"); info!("1. Go to your Safe at https://app.safe.global"); info!("2. Click \"New Transaction\" → \"Transaction Builder\""); - info!("3. Enter the contract address in \"Enter Address or ENS Name\": {}", to); + info!( + "3. Enter the contract address in \"Enter Address or ENS Name\": {}", + to + ); info!("4. Toggle \"Custom data\""); info!("5. Put in \"ETH value\": 0"); - info!("6. Paste the transaction data in \"Data (Hex encoded)\": 0x{}", tx_data); + info!( + "6. Paste the transaction data in \"Data (Hex encoded)\": 0x{}", + tx_data + ); info!("7. \"Add new transaction\" -> \"Create Batch\" -> \"Simulate\""); info!("8. If simulation passes, \"Send Batch\""); info!("9. Collect signatures from other Safe owners"); - info!("10. Execute once threshold is reached (transaction only goes live in this final step)"); + info!( + "10. Execute once threshold is reached (transaction only goes live in this final step)" + ); } else { // Traditional wallet signing flow let wallet = wallet.unwrap(); From 2db75af26757a7bee9b2f3bba94b59f0807c0c12 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 09:04:28 -0700 Subject: [PATCH 81/88] publish: move some logic around --- Cargo.lock | 7 ------ src/publish/mod.rs | 54 +++++++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b993ec06..2581b55f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,7 +2316,6 @@ dependencies = [ "tracing-appender", "tracing-error", "tracing-subscriber", - "urlencoding", "walkdir", "wit-bindgen", "zip", @@ -4315,12 +4314,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf-8" version = "0.7.6" diff --git a/src/publish/mod.rs b/src/publish/mod.rs index 7df5d874..a2447846 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -310,8 +310,10 @@ async fn prepare_hypermap_put( #[instrument(level = "trace", skip_all)] pub async fn build_tx( - package_dir: &Path, metadata_uri: &str, + metadata_hash: &str, + name: &str, + publisher: &str, provider: &RootProvider, real: &bool, unpublish: &bool, @@ -320,26 +322,7 @@ pub async fn build_tx( gas_limit: u64, max_priority_fee_per_gas: Option, max_fee_per_gas: Option, -) -> Result<(String, Address, Vec, TransactionRequest)> { - let metadata = read_and_update_metadata(package_dir)?; - - let name = metadata.name.clone().unwrap(); - let publisher = metadata.properties.publisher.clone(); - - if !is_hypermap_safe(&name, false) { - return Err(eyre!( - "The App Store requires package names have only lowercase letters, digits, and `-`s" - )); - } - if !is_hypermap_safe(&publisher, true) { - return Err(eyre!( - "The App Store requires publisher names have only lowercase letters, digits, `-`s, and `.`s" - )); - } - - let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; - check_pkg_hash(&metadata, package_dir, metadata_uri)?; - +) -> Result<(Address, Vec, TransactionRequest)> { let hypermap = Address::from_str(if *real { REAL_KIMAP_ADDRESS } else { @@ -367,7 +350,7 @@ pub async fn build_tx( prepare_hypermap_put( multicall, - name.clone(), + name.to_string(), &publisher, hypermap, &provider, @@ -395,7 +378,7 @@ pub async fn build_tx( ) .with_max_fee_per_gas(max_fee_per_gas.unwrap_or_else(|| suggested_max_fee_per_gas)); - Ok((name, to, call, tx)) + Ok((to, call, tx)) } #[instrument(level = "trace", skip_all)] @@ -421,6 +404,25 @@ pub async fn execute( )); } + let metadata = read_and_update_metadata(package_dir)?; + + let name = metadata.name.clone().unwrap(); + let publisher = metadata.properties.publisher.clone(); + + if !is_hypermap_safe(&name, false) { + return Err(eyre!( + "The App Store requires package names have only lowercase letters, digits, and `-`s" + )); + } + if !is_hypermap_safe(&publisher, true) { + return Err(eyre!( + "The App Store requires publisher names have only lowercase letters, digits, `-`s, and `.`s" + )); + } + + let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; + check_pkg_hash(&metadata, package_dir, metadata_uri)?; + let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; // Check if using Gnosis Safe mode @@ -449,9 +451,11 @@ pub async fn execute( let ws = WsConnect::new(rpc_uri); let provider: RootProvider = ProviderBuilder::default().on_ws(ws).await?; - let (name, to, call, tx) = build_tx( - package_dir, + let (to, call, tx) = build_tx( metadata_uri, + &metadata_hash, + &name, + &publisher, &provider, real, unpublish, From b4e6d8dc8b83e8454364ff6ef6274ecf72909395 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 09:13:57 -0700 Subject: [PATCH 82/88] publish: change some var names --- src/publish/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/publish/mod.rs b/src/publish/mod.rs index a2447846..e99a2484 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -425,10 +425,9 @@ pub async fn execute( let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; - // Check if using Gnosis Safe mode - let is_safe_mode = safe.is_some(); + let is_safe_tx = safe.is_some(); - let (wallet_address, wallet) = if is_safe_mode { + let (wallet_address, wallet) = if is_safe_tx { // In Safe mode, we don't need a wallet for signing // Parse the Safe address provided by the user let safe_address = Address::from_str(safe.unwrap())?; @@ -467,7 +466,7 @@ pub async fn execute( ) .await?; - if is_safe_mode { + if is_safe_tx { // Generate Safe transaction data let tx_data = hex::encode(call); @@ -481,7 +480,7 @@ pub async fn execute( // tx_data //); - info!("=== Gnosis Safe Transaction Data ==="); + info!("=== Safe Transaction Data ==="); // TODO: can we get URL working? If so this is by far preferable //info!("Safe App URL (click or copy):"); //info!("{}", make_remote_link(&safe_url, &safe_url)); From 592903b30e35a98d79007d0067eb7282d8a74e9d Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 09:14:11 -0700 Subject: [PATCH 83/88] publish: be less strict on unpublish checks --- src/publish/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/publish/mod.rs b/src/publish/mod.rs index e99a2484..216bfa6c 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -421,7 +421,9 @@ pub async fn execute( } let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; - check_pkg_hash(&metadata, package_dir, metadata_uri)?; + if !unpublish { + check_pkg_hash(&metadata, package_dir, metadata_uri)?; + } let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; From b44ce6fbd6382b46c013ed97590931acd99812fb Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 5 Aug 2025 08:59:34 -0700 Subject: [PATCH 84/88] fix arg passing to node --- src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 495b022b..e727081e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -620,9 +620,9 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { ) .arg(Arg::new("ARGS") .action(ArgAction::Set) - .short('a') - .long("args") - .help("Additional arguments to pass to the node") + .num_args(1..) + .last(true) // Collect everything after -- + .help("Additional arguments to pass to the node (i.e. to Hyperdrive)") .required(false) ) ) @@ -698,9 +698,9 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { ) .arg(Arg::new("ARGS") .action(ArgAction::Set) - .short('a') - .long("args") - .help("Additional arguments to pass to the node") + .num_args(1..) + .last(true) // Collect everything after -- + .help("Additional arguments to pass to the node (i.e. to Hyperdrive)") .required(false) ) ) From ac854016d85191d6ab9dcea9a2466b65dd3ef236 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 5 Aug 2025 09:53:21 -0700 Subject: [PATCH 85/88] fix rewrite flags --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index e727081e..77a69475 100644 --- a/src/main.rs +++ b/src/main.rs @@ -785,7 +785,7 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .arg(Arg::new("REWRITE") .action(ArgAction::SetTrue) .long("rewrite") - .help("Rewrite the package (disables `Spawn!()`) [default: don't rewrite]") + .help("Rewrite the package (enables `Spawn!()`) [default: don't rewrite]") .required(false) ) .arg(Arg::new("HYPERAPP") @@ -897,8 +897,8 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { ) .arg(Arg::new("REWRITE") .action(ArgAction::SetTrue) - .long("no-rewrite") - .help("Rewrite the package (disables `Spawn!()`) [default: don't rewrite]") + .long("rewrite") + .help("Rewrite the package (enables `Spawn!()`) [default: don't rewrite]") .required(false) ) .arg(Arg::new("HYPERAPP") From eca05befe475a9856aa2140b89d9f6020b0ab683 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 5 Aug 2025 10:06:35 -0700 Subject: [PATCH 86/88] tweak publish helptext --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 77a69475..50957dd2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -404,7 +404,7 @@ async fn execute( let ledger = matches.get_one::("LEDGER").unwrap(); let trezor = matches.get_one::("TREZOR").unwrap(); let safe = matches - .get_one::("SAFE") + .get_one::("SAFE_CONTRACT_ADDRESS") .and_then(|gs| Some(gs.as_str())); let rpc_uri = matches.get_one::("RPC_URI").unwrap(); let real = matches.get_one::("REAL").unwrap(); @@ -1131,11 +1131,11 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .help("Use Trezor private key (choose 1 of `k`, `l`, `t`, `s`)") .required(false) ) - .arg(Arg::new("SAFE") + .arg(Arg::new("SAFE_CONTRACT_ADDRESS") .action(ArgAction::Set) .short('s') .long("safe") - .help("Safe contract address (choose 1 of `k`, `l`, `t`, `s`)") + .help("Create transaction for Safe (choose 1 of `k`, `l`, `t`, `s`)") .required(false) ) .arg(Arg::new("URI") From 22ae16096e24eeff53f7376c1de6368ed3555c15 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 5 Aug 2025 10:08:57 -0700 Subject: [PATCH 87/88] run-test: remove TODO comment that was done --- src/run_tests/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/run_tests/mod.rs b/src/run_tests/mod.rs index 3becc77e..6e86b664 100644 --- a/src/run_tests/mod.rs +++ b/src/run_tests/mod.rs @@ -355,7 +355,6 @@ async fn build_packages( let url = format!("http://localhost:{port}"); - // TODO: add hyperapp setting to tests.toml for dependency_package_path in &test.dependency_package_paths { let path = match expand_home_path(&dependency_package_path) { Some(p) => p, From 35c4d6a15cdbbd656e9ff4cadbf513e7801da094 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 5 Aug 2025 10:11:35 -0700 Subject: [PATCH 88/88] bump version (breaking change: all machine-generated files in `target/` including wit!) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2581b55f..38c16e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2282,7 +2282,7 @@ dependencies = [ [[package]] name = "kit" -version = "2.1.0" +version = "3.0.0" dependencies = [ "alloy", "alloy-sol-macro", diff --git a/Cargo.toml b/Cargo.toml index 027fe2c8..72a7c7b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kit" authors = ["Sybil Technologies AG"] -version = "2.1.0" +version = "3.0.0" edition = "2021" description = "Development toolkit for Hyperware" homepage = "https://hyperware.ai"