diff --git a/lib/ai/escalayer/models.v b/lib/ai/escalayer/models.v index af6ae894..fb9f6672 100644 --- a/lib/ai/escalayer/models.v +++ b/lib/ai/escalayer/models.v @@ -11,6 +11,22 @@ pub mut: max_tokens int } +// Create model configs +const claude_3_sonnet = escalayer.ModelConfig{ + name: 'anthropic/claude-3.7-sonnet' + provider: 'anthropic' + temperature: 0.7 + max_tokens: 25000 +} + +const gpt4 = escalayer.ModelConfig{ + name: 'gpt-4' + provider: 'openai' + temperature: 0.7 + max_tokens: 25000 +} + + // Call an AI model using OpenRouter fn call_ai_model(prompt string, model ModelConfig)! string { // Get OpenAI client (configured for OpenRouter) diff --git a/lib/ai/mcp/README2.md b/lib/ai/mcp/README2.md new file mode 100644 index 00000000..7fa38724 --- /dev/null +++ b/lib/ai/mcp/README2.md @@ -0,0 +1,3 @@ + + +If logic is implemented in mcp module, than structure with folders logic and mcp, where logic residers in /logic and mcp related code (like tool and prompt handlers and server code) in /mcp \ No newline at end of file diff --git a/lib/ai/mcp/backend_interface.v b/lib/ai/mcp/backend_interface.v index 4ba0fe24..5e3938b4 100644 --- a/lib/ai/mcp/backend_interface.v +++ b/lib/ai/mcp/backend_interface.v @@ -23,6 +23,9 @@ interface Backend { tool_get(name string) !Tool tool_list() ![]Tool tool_call(name string, arguments map[string]json2.Any) !ToolCallResult + + // Sampling methods + sampling_create_message(params map[string]json2.Any) !SamplingCreateMessageResult mut: resource_subscribe(uri string) ! resource_unsubscribe(uri string) ! diff --git a/lib/ai/mcp/backend_memory.v b/lib/ai/mcp/backend_memory.v index 349caad6..9e922a96 100644 --- a/lib/ai/mcp/backend_memory.v +++ b/lib/ai/mcp/backend_memory.v @@ -18,12 +18,17 @@ pub mut: // Tool related fields tools map[string]Tool tool_handlers map[string]ToolHandler + + // Sampling related fields + sampling_handler SamplingHandler } pub type ToolHandler = fn (arguments map[string]json2.Any) !ToolCallResult pub type PromptHandler = fn (arguments []string) ![]PromptMessage +pub type SamplingHandler = fn (params map[string]json2.Any) !SamplingCreateMessageResult + fn (b &MemoryBackend) resource_exists(uri string) !bool { return uri in b.resources } @@ -151,3 +156,30 @@ fn (b &MemoryBackend) tool_call(name string, arguments map[string]json2.Any) !To } } } + +// Sampling related methods + +fn (b &MemoryBackend) sampling_create_message(params map[string]json2.Any) !SamplingCreateMessageResult { + // Check if a sampling handler is registered + if isnil(b.sampling_handler) { + // Return a default implementation that just echoes back a message + // indicating that no sampling handler is registered + return SamplingCreateMessageResult{ + model: 'default' + stop_reason: 'endTurn' + role: 'assistant' + content: MessageContent{ + typ: 'text' + text: 'Sampling is not configured on this server. Please register a sampling handler.' + } + } + } + + // Call the sampling handler with the provided parameters + return b.sampling_handler(params)! +} + +// register_sampling_handler registers a handler for sampling requests +pub fn (mut b MemoryBackend) register_sampling_handler(handler SamplingHandler) { + b.sampling_handler = handler +} diff --git a/lib/ai/mcp/cmd/mcp.v b/lib/ai/mcp/cmd/mcp.v index fe6c7396..ee109d23 100644 --- a/lib/ai/mcp/cmd/mcp.v +++ b/lib/ai/mcp/cmd/mcp.v @@ -7,6 +7,7 @@ import freeflowuniverse.herolib.osal // import freeflowuniverse.herolib.ai.mcp.mcpgen // import freeflowuniverse.herolib.ai.mcp.baobab import freeflowuniverse.herolib.ai.mcp.rhai.mcp as rhai_mcp +import freeflowuniverse.herolib.ai.mcp.rust fn main() { do() or { panic(err) } @@ -69,6 +70,7 @@ mcp cmd_mcp.add_command(rhai_mcp.command) + cmd_mcp.add_command(rust.command) // cmd_mcp.add_command(baobab.command) // cmd_mcp.add_command(vcode.command) cmd_mcp.add_command(cmd_inspector) @@ -89,4 +91,4 @@ fn cmd_inspector_execute(cmd cli.Command) ! { } else { osal.exec(cmd: 'npx @modelcontextprotocol/inspector')! } -} +} \ No newline at end of file diff --git a/lib/ai/mcp/factory.v b/lib/ai/mcp/factory.v index ae538a1d..3fe615bc 100644 --- a/lib/ai/mcp/factory.v +++ b/lib/ai/mcp/factory.v @@ -41,7 +41,10 @@ pub fn new_server(backend Backend, params ServerParams) !&Server { // Tool handlers 'tools/list': server.tools_list_handler, - 'tools/call': server.tools_call_handler + 'tools/call': server.tools_call_handler, + + // Sampling handlers + 'sampling/createMessage': server.sampling_create_message_handler } })! diff --git a/lib/ai/mcp/generics.v b/lib/ai/mcp/generics.v index 4dde2f1b..99aff54b 100644 --- a/lib/ai/mcp/generics.v +++ b/lib/ai/mcp/generics.v @@ -2,22 +2,22 @@ module mcp pub fn result_to_mcp_tool_contents[T](result T) []ToolContent { - return [result_to_mcp_tool_content(result)] + return [result_to_mcp_tool_content[T](result)] } pub fn result_to_mcp_tool_content[T](result T) ToolContent { - return $if T is string { - ToolContent{ + $if T is string { + return ToolContent{ typ: 'text' text: result.str() } } $else $if T is int { - ToolContent{ + return ToolContent{ typ: 'number' number: result.int() } } $else $if T is bool { - ToolContent{ + return ToolContent{ typ: 'boolean' boolean: result.bool() } diff --git a/lib/ai/mcp/handler_sampling.v b/lib/ai/mcp/handler_sampling.v new file mode 100644 index 00000000..ce0d8053 --- /dev/null +++ b/lib/ai/mcp/handler_sampling.v @@ -0,0 +1,145 @@ +module mcp + +import time +import os +import log +import x.json2 +import json +import freeflowuniverse.herolib.schemas.jsonrpc + +// Sampling related structs + +pub struct MessageContent { +pub: + typ string @[json: 'type'] + text string + data string + mimetype string @[json: 'mimeType'] +} + +pub struct Message { +pub: + role string + content MessageContent +} + +pub struct ModelHint { +pub: + name string +} + +pub struct ModelPreferences { +pub: + hints []ModelHint + cost_priority f32 @[json: 'costPriority'] + speed_priority f32 @[json: 'speedPriority'] + intelligence_priority f32 @[json: 'intelligencePriority'] +} + +pub struct SamplingCreateMessageParams { +pub: + messages []Message + model_preferences ModelPreferences @[json: 'modelPreferences'] + system_prompt string @[json: 'systemPrompt'] + include_context string @[json: 'includeContext'] + temperature f32 + max_tokens int @[json: 'maxTokens'] + stop_sequences []string @[json: 'stopSequences'] + metadata map[string]json2.Any +} + +pub struct SamplingCreateMessageResult { +pub: + model string + stop_reason string @[json: 'stopReason'] + role string + content MessageContent +} + +// sampling_create_message_handler handles the sampling/createMessage request +// This request is used to request LLM completions through the client +fn (mut s Server) sampling_create_message_handler(data string) !string { + // Decode the request + request_map := json2.raw_decode(data)!.as_map() + id := request_map['id'].int() + params_map := request_map['params'].as_map() + + // Validate required parameters + if 'messages' !in params_map { + return jsonrpc.new_error_response(id, missing_required_argument('messages')).encode() + } + + if 'maxTokens' !in params_map { + return jsonrpc.new_error_response(id, missing_required_argument('maxTokens')).encode() + } + + // Call the backend to handle the sampling request + result := s.backend.sampling_create_message(params_map) or { + return jsonrpc.new_error_response(id, sampling_error(err.msg())).encode() + } + + // Create a success response with the result + response := jsonrpc.new_response(id, json.encode(result)) + return response.encode() +} + +// Helper function to convert JSON messages to our Message struct format +fn parse_messages(messages_json json2.Any) ![]Message { + messages_arr := messages_json.arr() + mut result := []Message{cap: messages_arr.len} + + for msg_json in messages_arr { + msg_map := msg_json.as_map() + + if 'role' !in msg_map { + return error('Missing role in message') + } + + if 'content' !in msg_map { + return error('Missing content in message') + } + + role := msg_map['role'].str() + content_map := msg_map['content'].as_map() + + if 'type' !in content_map { + return error('Missing type in message content') + } + + typ := content_map['type'].str() + mut text := '' + mut data := '' + mut mimetype := '' + + if typ == 'text' { + if 'text' !in content_map { + return error('Missing text in text content') + } + text = content_map['text'].str() + } else if typ == 'image' { + if 'data' !in content_map { + return error('Missing data in image content') + } + data = content_map['data'].str() + + if 'mimeType' !in content_map { + return error('Missing mimeType in image content') + } + mimetype = content_map['mimeType'].str() + } else { + return error('Unsupported content type: ${typ}') + } + + result << Message{ + role: role + content: MessageContent{ + typ: typ + text: text + data: data + mimetype: mimetype + } + } + } + + return result +} diff --git a/lib/ai/mcp/model_error.v b/lib/ai/mcp/model_error.v index 8c2e17c7..38a423f9 100644 --- a/lib/ai/mcp/model_error.v +++ b/lib/ai/mcp/model_error.v @@ -33,3 +33,10 @@ fn tool_not_found(name string) jsonrpc.RPCError { message: 'Tool not found: ${name}' } } + +fn sampling_error(message string) jsonrpc.RPCError { + return jsonrpc.RPCError{ + code: -32603 // Internal error + message: 'Sampling error: ${message}' + } +} diff --git a/lib/ai/mcp/rhai/logic/logic.v b/lib/ai/mcp/rhai/logic/logic.v index 32ee4db7..175cde40 100644 --- a/lib/ai/mcp/rhai/logic/logic.v +++ b/lib/ai/mcp/rhai/logic/logic.v @@ -6,11 +6,15 @@ import freeflowuniverse.herolib.ai.utils import os pub fn generate_rhai_wrapper(name string, source_path string) !string { - prompt := rhai_wrapper_generation_prompt(name, source_path) or {panic(err)} + // Detect source package and module information + source_pkg_info := rust.detect_source_package(source_path)! + source_code := rust.read_source_code(source_path)! + prompt := rhai_wrapper_generation_prompt(name, source_code, source_pkg_info)! return run_wrapper_generation_task(prompt, RhaiGen{ name: name dir: source_path - }) or {panic(err)} + source_pkg_info: source_pkg_info + })! } // Runs the task to generate Rhai wrappers @@ -56,11 +60,11 @@ pub fn run_wrapper_generation_task(prompt_content string, gen RhaiGen) !string { } // Define a Rhai wrapper generator function for Container functions -pub fn rhai_wrapper_generation_prompt(name string, source_code string) !string { +pub fn rhai_wrapper_generation_prompt(name string, source_code string, source_pkg_info rust.SourcePackageInfo) !string { current_dir := os.dir(@FILE) - example_rhai := os.read_file('${current_dir}/prompts/example_script.md') or {panic(err)} - wrapper_md := os.read_file('${current_dir}/prompts/wrapper.md') or {panic(err)} - errors_md := os.read_file('${current_dir}/prompts/errors.md') or {panic(err)} + example_rhai := os.read_file('${current_dir}/prompts/example_script.md')! + wrapper_md := os.read_file('${current_dir}/prompts/wrapper.md')! + errors_md := os.read_file('${current_dir}/prompts/errors.md')! // Load all required template and guide files guides := os.read_file('/Users/timurgordon/code/git.ourworld.tf/herocode/sal/aiprompts/rhaiwrapping_classicai.md')! @@ -120,6 +124,12 @@ pub fn write_rhai_wrapper_module(wrapper WrapperModule, name string, path string os.write_file('${project_dir}/src/lib.rs', wrapper.lib_rs) or { return error('Failed to write lib.rs: ${err}') } + } else { + // Use default lib.rs template if none provided + lib_rs_content := $tmpl('./templates/lib.rs') + os.write_file('${project_dir}/src/lib.rs', lib_rs_content) or { + return error('Failed to write lib.rs: ${err}') + } } // Write the wrapper.rs file @@ -141,6 +151,12 @@ pub fn write_rhai_wrapper_module(wrapper WrapperModule, name string, path string os.write_file('${examples_dir}/example.rs', wrapper.example_rs) or { return error('Failed to write example.rs: ${err}') } + } else { + // Use default example.rs template if none provided + example_rs_content := $tmpl('./templates/example.rs') + os.write_file('${examples_dir}/example.rs', example_rs_content) or { + return error('Failed to write example.rs: ${err}') + } } // Write the engine.rs file if provided @@ -199,39 +215,26 @@ fn extract_module_name(code string) string { struct RhaiGen { name string dir string + source_pkg_info rust.SourcePackageInfo } // Process the AI response and compile the generated code -fn (gen RhaiGen) process_rhai_wrappers(response string)! string { - // Extract code blocks from the response - code_blocks := extract_code_blocks(response) or { - return err - } - - name := gen.name - - // Create a WrapperModule struct with the extracted content - wrapper := WrapperModule{ - lib_rs: $tmpl('./templates/lib.rs') - wrapper_rs: code_blocks.wrapper_rs - example_rs: $tmpl('./templates/example.rs') - engine_rs: code_blocks.engine_rs +pub fn (gen RhaiGen) process_rhai_wrappers(input string) !string { + blocks := extract_code_blocks(input)! + source_pkg_info := gen.source_pkg_info + // Create the module structure + mod := WrapperModule{ + lib_rs: blocks.lib_rs + engine_rs: blocks.engine_rs + example_rhai: blocks.example_rhai generic_wrapper_rs: $tmpl('./templates/generic_wrapper.rs') - cargo_toml: $tmpl('./templates/cargo.toml') - example_rhai: code_blocks.example_rhai + wrapper_rs: blocks.wrapper_rs } - // Create the wrapper module - project_dir := write_rhai_wrapper_module(wrapper, gen.name, gen.dir) or { - return error('Failed to create wrapper module: ${err}') - } + // Write the module files + project_dir := write_rhai_wrapper_module(mod, gen.name, gen.dir)! - // Build and run the project - build_output, run_output := rust.run_example(project_dir, 'example') or { - return err - } - - return format_success_message(project_dir, build_output, run_output) + return project_dir } // CodeBlocks struct to hold extracted code blocks @@ -239,6 +242,7 @@ struct CodeBlocks { wrapper_rs string engine_rs string example_rhai string + lib_rs string } // Extract code blocks from the AI response @@ -266,10 +270,17 @@ fn extract_code_blocks(response string)! CodeBlocks { } } + // Extract lib.rs content + lib_rs_content := utils.extract_code_block(response, 'lib.rs', 'rust') + if lib_rs_content == '' { + return error('Failed to extract lib.rs content from response. Please ensure your code is properly formatted inside a code block that starts with ```rust\n// lib.rs and ends with ```') + } + return CodeBlocks{ wrapper_rs: wrapper_rs_content engine_rs: engine_rs_content example_rhai: example_rhai_content + lib_rs: lib_rs_content } } diff --git a/lib/ai/mcp/rhai/logic/logic_sampling.v b/lib/ai/mcp/rhai/logic/logic_sampling.v new file mode 100644 index 00000000..356b77f0 --- /dev/null +++ b/lib/ai/mcp/rhai/logic/logic_sampling.v @@ -0,0 +1,260 @@ +module logic + +import freeflowuniverse.herolib.ai.escalayer +import freeflowuniverse.herolib.lang.rust +import freeflowuniverse.herolib.ai.utils +import os + +// pub fn generate_rhai_wrapper_sampling(name string, source_path string) !string { +// prompt := rhai_wrapper_generation_prompt(name, source_path) or {panic(err)} +// return run_wrapper_generation_task_sampling(prompt, RhaiGen{ +// name: name +// dir: source_path +// }) or {panic(err)} +// } + +// // Runs the task to generate Rhai wrappers +// pub fn run_wrapper_generation_task_sampling(prompt_content string, gen RhaiGen) !string { +// // Create a new task +// mut task := escalayer.new_task( +// name: 'rhai_wrapper_creator.escalayer' +// description: 'Create Rhai wrappers for Rust functions that follow builder pattern and create examples corresponding to the provided example file' +// ) + +// // Create model configs +// sonnet_model := escalayer.ModelConfig{ +// name: 'anthropic/claude-3.7-sonnet' +// provider: 'anthropic' +// temperature: 0.7 +// max_tokens: 25000 +// } + +// gpt4_model := escalayer.ModelConfig{ +// name: 'gpt-4' +// provider: 'openai' +// temperature: 0.7 +// max_tokens: 25000 +// } + +// // Create a prompt function that returns the prepared content +// prompt_function := fn [prompt_content] (input string) string { +// return prompt_content +// } + +// // Define a single unit task that handles everything +// task.new_unit_task( +// name: 'create_rhai_wrappers' +// prompt_function: prompt_function +// callback_function: gen.process_rhai_wrappers +// base_model: sonnet_model +// retry_model: gpt4_model +// retry_count: 1 +// ) + +// // Initiate the task +// return task.initiate('') +// } + +// @[params] +// pub struct WrapperModule { +// pub: +// lib_rs string +// example_rs string +// engine_rs string +// cargo_toml string +// example_rhai string +// generic_wrapper_rs string +// wrapper_rs string +// } + +// // functions is a list of function names that AI should extract and pass in +// pub fn write_rhai_wrapper_module(wrapper WrapperModule, name string, path string)! string { + +// // Define project directory paths +// project_dir := '${path}/rhai' + +// // Create the project using cargo new --lib +// if os.exists(project_dir) { +// os.rmdir_all(project_dir) or { +// return error('Failed to clean existing project directory: ${err}') +// } +// } + +// // Run cargo new --lib to create the project +// os.chdir(path) or { +// return error('Failed to change directory to base directory: ${err}') +// } + +// cargo_new_result := os.execute('cargo new --lib rhai') +// if cargo_new_result.exit_code != 0 { +// return error('Failed to create new library project: ${cargo_new_result.output}') +// } + +// // Create examples directory +// examples_dir := '${project_dir}/examples' +// os.mkdir_all(examples_dir) or { +// return error('Failed to create examples directory: ${err}') +// } + +// // Write the lib.rs file +// if wrapper.lib_rs != '' { +// os.write_file('${project_dir}/src/lib.rs', wrapper.lib_rs) or { +// return error('Failed to write lib.rs: ${err}') +// } +// } + +// // Write the wrapper.rs file +// if wrapper.wrapper_rs != '' { +// os.write_file('${project_dir}/src/wrapper.rs', wrapper.wrapper_rs) or { +// return error('Failed to write wrapper.rs: ${err}') +// } +// } + +// // Write the generic wrapper.rs file +// if wrapper.generic_wrapper_rs != '' { +// os.write_file('${project_dir}/src/generic_wrapper.rs', wrapper.generic_wrapper_rs) or { +// return error('Failed to write generic wrapper.rs: ${err}') +// } +// } + +// // Write the example.rs file +// if wrapper.example_rs != '' { +// os.write_file('${examples_dir}/example.rs', wrapper.example_rs) or { +// return error('Failed to write example.rs: ${err}') +// } +// } + +// // Write the engine.rs file if provided +// if wrapper.engine_rs != '' { +// os.write_file('${project_dir}/src/engine.rs', wrapper.engine_rs) or { +// return error('Failed to write engine.rs: ${err}') +// } +// } + +// // Write the Cargo.toml file +// os.write_file('${project_dir}/Cargo.toml', wrapper.cargo_toml) or { +// return error('Failed to write Cargo.toml: ${err}') +// } + +// // Write the example.rhai file +// os.write_file('${examples_dir}/example.rhai', wrapper.example_rhai) or { +// return error('Failed to write example.rhai: ${err}') +// } + +// return project_dir +// } + + + +// // Extract module name from wrapper code +// fn extract_module_name(code string) string { +// lines := code.split('\n') + +// for line in lines { +// // Look for pub mod or mod declarations +// if line.contains('pub mod ') || line.contains('mod ') { +// // Extract module name +// mut parts := []string{} +// if line.contains('pub mod ') { +// parts = line.split('pub mod ') +// } else { +// parts = line.split('mod ') +// } + +// if parts.len > 1 { +// // Extract the module name and remove any trailing characters +// mut name := parts[1].trim_space() +// // Remove any trailing { or ; or whitespace +// name = name.trim_right('{').trim_right(';').trim_space() +// if name != '' { +// return name +// } +// } +// } +// } + +// return '' +// } + +// // RhaiGen struct for generating Rhai wrappers +// struct RhaiGen { +// name string +// dir string +// } + +// // Process the AI response and compile the generated code +// fn (gen RhaiGen) process_rhai_wrappers(response string)! string { +// // Extract code blocks from the response +// code_blocks := extract_code_blocks(response) or { +// return err +// } + +// name := gen.name + +// // Create a WrapperModule struct with the extracted content +// wrapper := WrapperModule{ +// lib_rs: $tmpl('./templates/lib.rs') +// wrapper_rs: code_blocks.wrapper_rs +// example_rs: $tmpl('./templates/example.rs') +// engine_rs: code_blocks.engine_rs +// generic_wrapper_rs: $tmpl('./templates/generic_wrapper.rs') +// cargo_toml: $tmpl('./templates/cargo.toml') +// example_rhai: code_blocks.example_rhai +// } + +// // Create the wrapper module +// project_dir := write_rhai_wrapper_module(wrapper, gen.name, gen.dir) or { +// return error('Failed to create wrapper module: ${err}') +// } + +// // Build and run the project +// build_output, run_output := rust.run_example(project_dir, 'example') or { +// return err +// } + +// return format_success_message(project_dir, build_output, run_output) +// } + +// // CodeBlocks struct to hold extracted code blocks +// struct CodeBlocks { +// wrapper_rs string +// engine_rs string +// example_rhai string +// } + +// // Extract code blocks from the AI response +// fn extract_code_blocks(response string)! CodeBlocks { +// // Extract wrapper.rs content +// wrapper_rs_content := utils.extract_code_block(response, 'wrapper.rs', 'rust') +// if wrapper_rs_content == '' { +// return error('Failed to extract wrapper.rs content from response. Please ensure your code is properly formatted inside a code block that starts with ```rust\n// wrapper.rs and ends with ```') +// } + +// // Extract engine.rs content +// mut engine_rs_content := utils.extract_code_block(response, 'engine.rs', 'rust') +// if engine_rs_content == '' { +// // Try to extract from the response without explicit language marker +// engine_rs_content = utils.extract_code_block(response, 'engine.rs', '') +// } + +// // Extract example.rhai content +// mut example_rhai_content := utils.extract_code_block(response, 'example.rhai', 'rhai') +// if example_rhai_content == '' { +// // Try to extract from the response without explicit language marker +// example_rhai_content = utils.extract_code_block(response, 'example.rhai', '') +// if example_rhai_content == '' { +// return error('Failed to extract example.rhai content from response. Please ensure your code is properly formatted inside a code block that starts with ```rhai\n// example.rhai and ends with ```') +// } +// } + +// return CodeBlocks{ +// wrapper_rs: wrapper_rs_content +// engine_rs: engine_rs_content +// example_rhai: example_rhai_content +// } +// } + +// // Format success message +// fn format_success_message(project_dir string, build_output string, run_output string) string { +// return 'Successfully generated Rhai wrappers and ran the example!\n\nProject created at: ${project_dir}\n\nBuild output:\n${build_output}\n\nRun output:\n${run_output}' +// } diff --git a/lib/ai/mcp/rhai/logic/prompts/main.md b/lib/ai/mcp/rhai/logic/prompts/main.md index b5b73092..1f67f838 100644 --- a/lib/ai/mcp/rhai/logic/prompts/main.md +++ b/lib/ai/mcp/rhai/logic/prompts/main.md @@ -25,9 +25,9 @@ IMPORTANT NOTES: - rhai = "1.21.0" - serde = { version = "1.0", features = ["derive"] } - serde_json = "1.0" - - sal = { path = "../../../" } + - @{source_pkg_info.name} = { path = "@{source_pkg_info.path}" } -3. For the wrapper: `use sal::@{name};` this way you can access the module functions and objects with @{name}:: +3. For the wrapper: `use @{source_pkg_info.name}::@{source_pkg_info.module};` this way you can access the module functions and objects with @{source_pkg_info.module}:: 4. The generic_wrapper.rs file will be hardcoded into the package, you can use code from there. @@ -95,7 +95,5 @@ IMPORTANT NOTES: @{engine} MOST IMPORTANT: -import package being wrapped as `use sal::` +import package being wrapped as `use @{source_pkg_info.name}::@{source_pkg_info.module}` your engine create function is called `create_rhai_engine` - -``` diff --git a/lib/ai/mcp/rhai/logic/templates/cargo.toml b/lib/ai/mcp/rhai/logic/templates/cargo.toml index 1c665cb5..5849e1b9 100644 --- a/lib/ai/mcp/rhai/logic/templates/cargo.toml +++ b/lib/ai/mcp/rhai/logic/templates/cargo.toml @@ -7,4 +7,4 @@ edition = "2021" rhai = "1.21.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sal = { path = "../../../" } \ No newline at end of file +@{source_pkg_info.name} = { path = "@{source_pkg_info.path}" } \ No newline at end of file diff --git a/lib/ai/mcp/rhai/mcp/prompts.v b/lib/ai/mcp/rhai/mcp/prompts.v index 10fe9d62..2805b240 100644 --- a/lib/ai/mcp/rhai/mcp/prompts.v +++ b/lib/ai/mcp/rhai/mcp/prompts.v @@ -30,8 +30,9 @@ pub fn rhai_wrapper_prompt_handler(arguments []string) ![]mcp.PromptMessage { // Extract the module name from the directory path (last component) name := rust.extract_module_name_from_path(source_path) +source_pkg_info := rust.detect_source_package(source_path)! -result := logic.rhai_wrapper_generation_prompt(name, source_code)! +result := logic.rhai_wrapper_generation_prompt(name, source_code, source_pkg_info)! return [mcp.PromptMessage{ role: 'assistant' content: mcp.PromptContent{ diff --git a/lib/ai/mcp/rust/command.v b/lib/ai/mcp/rust/command.v new file mode 100644 index 00000000..000c615e --- /dev/null +++ b/lib/ai/mcp/rust/command.v @@ -0,0 +1,21 @@ +module rust + +import cli + +pub const command := cli.Command{ + sort_flags: true + name: 'rust' + description: 'Rust language tools command' + commands: [ + cli.Command{ + name: 'start' + execute: cmd_start + description: 'start the Rust MCP server' + } + ] +} + +fn cmd_start(cmd cli.Command) ! { + mut server := new_mcp_server()! + server.start()! +} diff --git a/lib/ai/mcp/rust/generics.v b/lib/ai/mcp/rust/generics.v new file mode 100644 index 00000000..32a769fc --- /dev/null +++ b/lib/ai/mcp/rust/generics.v @@ -0,0 +1,54 @@ +module rust + +import freeflowuniverse.herolib.ai.mcp {ToolContent} + +pub fn result_to_mcp_tool_contents[T](result T) []ToolContent { + return [result_to_mcp_tool_content[T](result)] +} + +pub fn result_to_mcp_tool_content[T](result T) ToolContent { + $if T is string { + return ToolContent{ + typ: 'text' + text: result.str() + } + } $else $if T is int { + return ToolContent{ + typ: 'number' + number: result.int() + } + } $else $if T is bool { + return ToolContent{ + typ: 'boolean' + boolean: result.bool() + } + } $else $if result is $array { + mut items := []ToolContent{} + for item in result { + items << result_to_mcp_tool_content(item) + } + return ToolContent{ + typ: 'array' + items: items + } + } $else $if T is $struct { + mut properties := map[string]ToolContent{} + $for field in T.fields { + properties[field.name] = result_to_mcp_tool_content(result.$(field.name)) + } + return ToolContent{ + typ: 'object' + properties: properties + } + } $else { + panic('Unsupported type: ${typeof(result)}') + } +} + +pub fn array_to_mcp_tool_contents[U](array []U) []ToolContent { + mut contents := []ToolContent{} + for item in array { + contents << result_to_mcp_tool_content(item) + } + return contents +} \ No newline at end of file diff --git a/lib/ai/mcp/rust/mcp.v b/lib/ai/mcp/rust/mcp.v new file mode 100644 index 00000000..e822d1f6 --- /dev/null +++ b/lib/ai/mcp/rust/mcp.v @@ -0,0 +1,52 @@ +module rust + +import freeflowuniverse.herolib.ai.mcp +import freeflowuniverse.herolib.schemas.jsonrpc +import log + +pub fn new_mcp_server() !&mcp.Server { + log.info('Creating new Rust MCP server') + + // Initialize the server with tools and prompts + mut server := mcp.new_server(mcp.MemoryBackend{ + tools: { + 'list_functions_in_file': list_functions_in_file_spec + 'list_structs_in_file': list_structs_in_file_spec + 'list_modules_in_dir': list_modules_in_dir_spec + 'get_import_statement': get_import_statement_spec + // 'get_module_dependency': get_module_dependency_spec + } + tool_handlers: { + 'list_functions_in_file': list_functions_in_file_handler + 'list_structs_in_file': list_structs_in_file_handler + 'list_modules_in_dir': list_modules_in_dir_handler + 'get_import_statement': get_import_statement_handler + // 'get_module_dependency': get_module_dependency_handler + } + prompts: { + 'rust_functions': rust_functions_prompt_spec + 'rust_structs': rust_structs_prompt_spec + 'rust_modules': rust_modules_prompt_spec + 'rust_imports': rust_imports_prompt_spec + 'rust_dependencies': rust_dependencies_prompt_spec + 'rust_tools_guide': rust_tools_guide_prompt_spec + } + prompt_handlers: { + 'rust_functions': rust_functions_prompt_handler + 'rust_structs': rust_structs_prompt_handler + 'rust_modules': rust_modules_prompt_handler + 'rust_imports': rust_imports_prompt_handler + 'rust_dependencies': rust_dependencies_prompt_handler + 'rust_tools_guide': rust_tools_guide_prompt_handler + } + }, mcp.ServerParams{ + config: mcp.ServerConfiguration{ + server_info: mcp.ServerInfo{ + name: 'rust' + version: '1.0.0' + } + } + })! + + return server +} diff --git a/lib/ai/mcp/rust/prompts.v b/lib/ai/mcp/rust/prompts.v new file mode 100644 index 00000000..c0f7f6bf --- /dev/null +++ b/lib/ai/mcp/rust/prompts.v @@ -0,0 +1,144 @@ +module rust + +import freeflowuniverse.herolib.ai.mcp +import os +import x.json2 as json { Any } + +// Prompt specification for Rust functions +const rust_functions_prompt_spec = mcp.Prompt{ + name: 'rust_functions' + description: 'Provides guidance on working with Rust functions and using the list_functions_in_file tool' + arguments: [] +} + +// Handler for rust_functions prompt +pub fn rust_functions_prompt_handler(arguments []string) ![]mcp.PromptMessage { + content := os.read_file('${os.dir(@FILE)}/prompts/functions.md')! + + return [mcp.PromptMessage{ + role: 'assistant' + content: mcp.PromptContent{ + typ: 'text' + text: content + } + }] +} + +// Prompt specification for Rust structs +const rust_structs_prompt_spec = mcp.Prompt{ + name: 'rust_structs' + description: 'Provides guidance on working with Rust structs and using the list_structs_in_file tool' + arguments: [] +} + +// Handler for rust_structs prompt +pub fn rust_structs_prompt_handler(arguments []string) ![]mcp.PromptMessage { + content := os.read_file('${os.dir(@FILE)}/prompts/structs.md')! + + return [mcp.PromptMessage{ + role: 'assistant' + content: mcp.PromptContent{ + typ: 'text' + text: content + } + }] +} + +// Prompt specification for Rust modules +const rust_modules_prompt_spec = mcp.Prompt{ + name: 'rust_modules' + description: 'Provides guidance on working with Rust modules and using the list_modules_in_dir tool' + arguments: [] +} + +// Handler for rust_modules prompt +pub fn rust_modules_prompt_handler(arguments []string) ![]mcp.PromptMessage { + content := os.read_file('${os.dir(@FILE)}/prompts/modules.md')! + + return [mcp.PromptMessage{ + role: 'assistant' + content: mcp.PromptContent{ + typ: 'text' + text: content + } + }] +} + +// Prompt specification for Rust imports +const rust_imports_prompt_spec = mcp.Prompt{ + name: 'rust_imports' + description: 'Provides guidance on working with Rust imports and using the get_import_statement tool' + arguments: [] +} + +// Handler for rust_imports prompt +pub fn rust_imports_prompt_handler(arguments []string) ![]mcp.PromptMessage { + content := os.read_file('${os.dir(@FILE)}/prompts/imports.md')! + + return [mcp.PromptMessage{ + role: 'assistant' + content: mcp.PromptContent{ + typ: 'text' + text: content + } + }] +} + +// Prompt specification for Rust dependencies +const rust_dependencies_prompt_spec = mcp.Prompt{ + name: 'rust_dependencies' + description: 'Provides guidance on working with Rust dependencies and using the get_module_dependency tool' + arguments: [] +} + +// Handler for rust_dependencies prompt +pub fn rust_dependencies_prompt_handler(arguments []string) ![]mcp.PromptMessage { + content := os.read_file('${os.dir(@FILE)}/prompts/dependencies.md')! + + return [mcp.PromptMessage{ + role: 'assistant' + content: mcp.PromptContent{ + typ: 'text' + text: content + } + }] +} + +// Prompt specification for general Rust tools guide +const rust_tools_guide_prompt_spec = mcp.Prompt{ + name: 'rust_tools_guide' + description: 'Provides a comprehensive guide on all available Rust tools and how to use them' + arguments: [] +} + +// Handler for rust_tools_guide prompt +pub fn rust_tools_guide_prompt_handler(arguments []string) ![]mcp.PromptMessage { + // Combine all prompt files into one comprehensive guide + functions_content := os.read_file('${os.dir(@FILE)}/prompts/functions.md')! + structs_content := os.read_file('${os.dir(@FILE)}/prompts/structs.md')! + modules_content := os.read_file('${os.dir(@FILE)}/prompts/modules.md')! + imports_content := os.read_file('${os.dir(@FILE)}/prompts/imports.md')! + dependencies_content := os.read_file('${os.dir(@FILE)}/prompts/dependencies.md')! + + combined_content := '# Rust Language Tools Guide\n\n' + + 'This guide provides comprehensive information on working with Rust code using the available tools.\n\n' + + '## Table of Contents\n\n' + + '1. [Functions](#functions)\n' + + '2. [Structs](#structs)\n' + + '3. [Modules](#modules)\n' + + '4. [Imports](#imports)\n' + + '5. [Dependencies](#dependencies)\n\n' + + '\n' + functions_content + '\n\n' + + '\n' + structs_content + '\n\n' + + '\n' + modules_content + '\n\n' + + '\n' + imports_content + '\n\n' + + '\n' + dependencies_content + + return [mcp.PromptMessage{ + role: 'assistant' + content: mcp.PromptContent{ + typ: 'text' + text: combined_content + } + }] +} diff --git a/lib/ai/mcp/rust/tools.v b/lib/ai/mcp/rust/tools.v new file mode 100644 index 00000000..680ae84b --- /dev/null +++ b/lib/ai/mcp/rust/tools.v @@ -0,0 +1,305 @@ +module rust + +import freeflowuniverse.herolib.ai.mcp {ToolContent} +import freeflowuniverse.herolib.lang.rust +import freeflowuniverse.herolib.schemas.jsonschema +import x.json2 as json { Any } + +// Tool specification for listing functions in a Rust file +const list_functions_in_file_spec = mcp.Tool{ + name: 'list_functions_in_file' + description: 'Lists all function definitions in a Rust file' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'file_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the Rust file' + }) + } + required: ['file_path'] + } +} + +// Handler for list_functions_in_file +pub fn list_functions_in_file_handler(arguments map[string]Any) !mcp.ToolCallResult { + file_path := arguments['file_path'].str() + result := rust.list_functions_in_file(file_path) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.array_to_mcp_tool_contents[string](result) + } +} + +// Tool specification for listing structs in a Rust file +const list_structs_in_file_spec = mcp.Tool{ + name: 'list_structs_in_file' + description: 'Lists all struct definitions in a Rust file' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'file_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the Rust file' + }) + } + required: ['file_path'] + } +} + +// Handler for list_structs_in_file +pub fn list_structs_in_file_handler(arguments map[string]Any) !mcp.ToolCallResult { + file_path := arguments['file_path'].str() + result := rust.list_structs_in_file(file_path) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.array_to_mcp_tool_contents[string](result) + } +} + +// Tool specification for listing modules in a directory +const list_modules_in_dir_spec = mcp.Tool{ + name: 'list_modules_in_dir' + description: 'Lists all Rust modules in a directory' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'dir_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the directory' + }) + } + required: ['dir_path'] + } +} + +// Handler for list_modules_in_dir +pub fn list_modules_in_dir_handler(arguments map[string]Any) !mcp.ToolCallResult { + dir_path := arguments['dir_path'].str() + result := rust.list_modules_in_directory(dir_path) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.array_to_mcp_tool_contents[string](result) + } +} + +// Tool specification for getting an import statement +const get_import_statement_spec = mcp.Tool{ + name: 'get_import_statement' + description: 'Generates appropriate Rust import statement for a module based on file paths' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'current_file': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the file where the import will be added' + }), + 'target_module': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the target module to be imported' + }) + } + required: ['current_file', 'target_module'] + } +} + +// Handler for get_import_statement +pub fn get_import_statement_handler(arguments map[string]Any) !mcp.ToolCallResult { + current_file := arguments['current_file'].str() + target_module := arguments['target_module'].str() + result := rust.generate_import_statement(current_file, target_module) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.result_to_mcp_tool_contents[string](result) + } +} + +// Tool specification for getting module dependency information +const get_module_dependency_spec = mcp.Tool{ + name: 'get_module_dependency' + description: 'Gets dependency information for adding a Rust module to a project' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'importer_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the file that will import the module' + }), + 'module_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the module that will be imported' + }) + } + required: ['importer_path', 'module_path'] + } +} + +struct Tester { + import_statement string + module_path string +} + +// Handler for get_module_dependency +pub fn get_module_dependency_handler(arguments map[string]Any) !mcp.ToolCallResult { + importer_path := arguments['importer_path'].str() + module_path := arguments['module_path'].str() + dependency := rust.get_module_dependency(importer_path, module_path) or { + return mcp.error_tool_call_result(err) + } + + return mcp.ToolCallResult{ + is_error: false + content: result_to_mcp_tool_contents[Tester](Tester{ + import_statement: dependency.import_statement + module_path: dependency.module_path + }) // Return JSON string + } +} + +// --- Get Function from File Tool --- + +// Specification for get_function_from_file tool +const get_function_from_file_spec = mcp.Tool{ + name: 'get_function_from_file' + description: 'Get the declaration of a Rust function from a specified file path.' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'file_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the Rust file.' + }), + 'function_name': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Name of the function to retrieve (e.g., \'my_function\' or \'MyStruct::my_method\').' + }) + } + required: ['file_path', 'function_name'] + } +} + +// Handler for get_function_from_file +pub fn get_function_from_file_handler(arguments map[string]Any) !mcp.ToolCallResult { + file_path := arguments['file_path'].str() + function_name := arguments['function_name'].str() + result := rust.get_function_from_file(file_path, function_name) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.result_to_mcp_tool_contents[string](result) + } +} + +// --- Get Function from Module Tool --- + +// Specification for get_function_from_module tool +const get_function_from_module_spec = mcp.Tool{ + name: 'get_function_from_module' + description: 'Get the declaration of a Rust function from a specified module path (directory or file).' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'module_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the Rust module directory or file.' + }), + 'function_name': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Name of the function to retrieve (e.g., \'my_function\' or \'MyStruct::my_method\').' + }) + } + required: ['module_path', 'function_name'] + } +} + +// Handler for get_function_from_module +pub fn get_function_from_module_handler(arguments map[string]Any) !mcp.ToolCallResult { + module_path := arguments['module_path'].str() + function_name := arguments['function_name'].str() + result := rust.get_function_from_module(module_path, function_name) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.result_to_mcp_tool_contents[string](result) + } +} + +// --- Get Struct from File Tool --- + +// Specification for get_struct_from_file tool +const get_struct_from_file_spec = mcp.Tool{ + name: 'get_struct_from_file' + description: 'Get the declaration of a Rust struct from a specified file path.' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'file_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the Rust file.' + }), + 'struct_name': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Name of the struct to retrieve (e.g., \'MyStruct\').' + }) + } + required: ['file_path', 'struct_name'] + } +} + +// Handler for get_struct_from_file +pub fn get_struct_from_file_handler(arguments map[string]Any) !mcp.ToolCallResult { + file_path := arguments['file_path'].str() + struct_name := arguments['struct_name'].str() + result := rust.get_struct_from_file(file_path, struct_name) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.result_to_mcp_tool_contents[string](result) + } +} + +// --- Get Struct from Module Tool --- + +// Specification for get_struct_from_module tool +const get_struct_from_module_spec = mcp.Tool{ + name: 'get_struct_from_module' + description: 'Get the declaration of a Rust struct from a specified module path (directory or file).' + input_schema: jsonschema.Schema{ + typ: 'object' + properties: { + 'module_path': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Path to the Rust module directory or file.' + }), + 'struct_name': jsonschema.SchemaRef(jsonschema.Schema{ + typ: 'string' + description: 'Name of the struct to retrieve (e.g., \'MyStruct\').' + }) + } + required: ['module_path', 'struct_name'] + } +} + +// Handler for get_struct_from_module +pub fn get_struct_from_module_handler(arguments map[string]Any) !mcp.ToolCallResult { + module_path := arguments['module_path'].str() + struct_name := arguments['struct_name'].str() + result := rust.get_struct_from_module(module_path, struct_name) or { + return mcp.error_tool_call_result(err) + } + return mcp.ToolCallResult{ + is_error: false + content: mcp.result_to_mcp_tool_contents[string](result) + } +} \ No newline at end of file diff --git a/lib/lang/rhai/prompts/generate_rhai_function_wrapper.md b/lib/lang/rhai/prompts/generate_rhai_function_wrapper.md new file mode 100644 index 00000000..f8956eff --- /dev/null +++ b/lib/lang/rhai/prompts/generate_rhai_function_wrapper.md @@ -0,0 +1,534 @@ +# Generate Single Rhai Wrapper Function + +You are tasked with creating a **single** Rhai wrapper function for the provided Rust function or method signature. + +## Input Rust Function + +```rust +@{gen.function} +``` + +## Input Rust Types + +Below are the struct declarations for types used in the function + +@for structure in gen.structs + ```rust + @{structure} + ``` +@end + +## Instructions + +1. **Analyze the Signature:** + * Identify the function/method name. + * Identify the input parameters and their types. + * Identify the return type. + * Determine if it's a method on a struct (e.g., `&self`, `&mut self`). + +2. **Define the Wrapper Signature:** + * The wrapper function name should generally match the original Rust function name (use snake_case). + * Input parameters should correspond to the Rust function's parameters. You might need to adjust types for Rhai compatibility (e.g., `&str` becomes `&str`, `String` becomes `String`, `Vec` might become `rhai::Array`, `HashMap` might become `rhai::Map`). + * If the original function is a method on a struct (e.g., `fn method(&self, ...) ` or `fn method_mut(&mut self, ...)`), the first parameter of the wrapper must be the receiver type (e.g., `mut? receiver: StructType`). Ensure the mutability matches. + * The return type **must** be `Result>`, where `T` is the Rhai-compatible equivalent of the original Rust function's return type. If the original function returns `Result`, `T` should be the Rhai-compatible version of `U`, and the error `E` should be mapped into `Box`. If the original returns `()`, use `Result<(), Box>`. If it returns a simple type `U`, use `Result>`. + +3. **Implement the Wrapper Body:** + * **Call the Original Function/Method:** Call the Rust function or method using the input parameters. Perform necessary type conversions if Rhai types (like `Array`, `Map`) were used in the wrapper signature. + * **Handle Struct Methods:** If it's a method, call it on the `receiver` parameter (e.g., `receiver.method(...)`). + * **Error Handling:** + * Use `.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error description: {}", e).into(), rhai::Position::NONE)))` to convert any potential errors from the original function call into a `Box`. Provide a descriptive error message. + * If the original function doesn't return `Result`, wrap the successful result using `Ok(...)`. + * **Return Value Conversion:** If the original function's success type needs conversion for Rhai (e.g., a complex struct to a `Map`, a `PathBuf` to `String`), perform the conversion before returning `Ok(...)`. Convert `PathBuf` or path references using `.to_string_lossy().to_string()` or `.to_string()` as appropriate. + +4. **Best Practices:** + * Use `rhai::{Engine, EvalAltResult, Dynamic, Map, Array}` imports as needed. + * **Prefer strongly typed return values** (`Result`, `Result`, `Result, ...>`) over `Result`. Only use `Dynamic` if the return type is truly variable or complex and cannot be easily represented otherwise. + * **Do NOT use the `#[rhai_fn]` attribute.** The function will be registered manually. + * Handle string type consistency (e.g., `String::from()` for literals if mixed with `format!`). + +### Error Handling + +Assume that the following function is available to use in the rhai wrapper body: + +```rust +// Helper functions for error conversion with improved context +fn [modulename]_error_to_rhai_error(result: Result) -> Result> {} +``` + +And feel free to use it like: +```rust +/// Create a new Container +pub fn container_new(name: &str) -> Result> { + nerdctl_error_to_rhai_error(Container::new(name)) +} +``` + +## Output Format + +Provide **only** the generated Rust code for the wrapper function, enclosed in triple backticks. + +```rust +// Your generated wrapper function here +pub fn wrapper_function_name(...) -> Result<..., Box> { + // Implementation +} +``` + +### Example + +Below is a bunch of input and outputted wrapped functions. + +Example Input Types: +```rust +pub struct Container { + /// Name of the container + pub name: String, + /// Container ID + pub container_id: Option, + /// Base image (if created from an image) + pub image: Option, + /// Configuration options + pub config: HashMap, + /// Port mappings + pub ports: Vec, + /// Volume mounts + pub volumes: Vec, + /// Environment variables + pub env_vars: HashMap, + /// Network to connect to + pub network: Option, + /// Network aliases + pub network_aliases: Vec, + /// CPU limit + pub cpu_limit: Option, + /// Memory limit + pub memory_limit: Option, + /// Memory swap limit + pub memory_swap_limit: Option, + /// CPU shares + pub cpu_shares: Option, + /// Restart policy + pub restart_policy: Option, + /// Health check + pub health_check: Option, + /// Whether to run in detached mode + pub detach: bool, + /// Snapshotter to use + pub snapshotter: Option, +} + +/// Health check configuration for a container +#[derive(Debug, Clone)] +pub struct HealthCheck { + /// Command to run for health check + pub cmd: String, + /// Time between running the check (default: 30s) + pub interval: Option, + /// Maximum time to wait for a check to complete (default: 30s) + pub timeout: Option, + /// Number of consecutive failures needed to consider unhealthy (default: 3) + pub retries: Option, + /// Start period for the container to initialize before counting retries (default: 0s) + pub start_period: Option, +} +``` + +Example Input Functions: +```rust + /// Set memory swap limit for the container + /// + /// # Arguments + /// + /// * `memory_swap` - Memory swap limit (e.g., "1g" for 1GB) + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_memory_swap_limit(mut self, memory_swap: &str) -> Self { + self.memory_swap_limit = Some(memory_swap.to_string()); + self + } + + /// Set CPU shares for the container (relative weight) + /// + /// # Arguments + /// + /// * `shares` - CPU shares (e.g., "1024" for default, "512" for half) + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_cpu_shares(mut self, shares: &str) -> Self { + self.cpu_shares = Some(shares.to_string()); + self + } + + /// Set restart policy for the container + /// + /// # Arguments + /// + /// * `policy` - Restart policy (e.g., "no", "always", "on-failure", "unless-stopped") + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_restart_policy(mut self, policy: &str) -> Self { + self.restart_policy = Some(policy.to_string()); + self + } + + /// Set a simple health check for the container + /// + /// # Arguments + /// + /// * `cmd` - Command to run for health check (e.g., "curl -f http://localhost/ || exit 1") + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_health_check(mut self, cmd: &str) -> Self { + // Use the health check script module to prepare the command + let prepared_cmd = prepare_health_check_command(cmd, &self.name); + + self.health_check = Some(HealthCheck { + cmd: prepared_cmd, + interval: None, + timeout: None, + retries: None, + start_period: None, + }); + self + } + + /// Set a health check with custom options for the container + /// + /// # Arguments + /// + /// * `cmd` - Command to run for health check + /// * `interval` - Optional time between running the check (e.g., "30s", "1m") + /// * `timeout` - Optional maximum time to wait for a check to complete (e.g., "30s", "1m") + /// * `retries` - Optional number of consecutive failures needed to consider unhealthy + /// * `start_period` - Optional start period for the container to initialize before counting retries (e.g., "30s", "1m") + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_health_check_options( + mut self, + cmd: &str, + interval: Option<&str>, + timeout: Option<&str>, + retries: Option, + start_period: Option<&str>, + ) -> Self { + // Use the health check script module to prepare the command + let prepared_cmd = prepare_health_check_command(cmd, &self.name); + + let mut health_check = HealthCheck { + cmd: prepared_cmd, + interval: None, + timeout: None, + retries: None, + start_period: None, + }; + + if let Some(interval_value) = interval { + health_check.interval = Some(interval_value.to_string()); + } + + if let Some(timeout_value) = timeout { + health_check.timeout = Some(timeout_value.to_string()); + } + + if let Some(retries_value) = retries { + health_check.retries = Some(retries_value); + } + + if let Some(start_period_value) = start_period { + health_check.start_period = Some(start_period_value.to_string()); + } + + self.health_check = Some(health_check); + self + } + + /// Set the snapshotter + /// + /// # Arguments + /// + /// * `snapshotter` - Snapshotter to use + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_snapshotter(mut self, snapshotter: &str) -> Self { + self.snapshotter = Some(snapshotter.to_string()); + self + } + + /// Set whether to run in detached mode + /// + /// # Arguments + /// + /// * `detach` - Whether to run in detached mode + /// + /// # Returns + /// + /// * `Self` - The container instance for method chaining + pub fn with_detach(mut self, detach: bool) -> Self { + self.detach = detach; + self + } + + /// Build the container + /// + /// # Returns + /// + /// * `Result` - Container instance or error + pub fn build(self) -> Result { + // If container already exists, return it + if self.container_id.is_some() { + return Ok(self); + } + + // If no image is specified, return an error + let image = match &self.image { + Some(img) => img, + None => return Err(NerdctlError::Other("No image specified for container creation".to_string())), + }; + + // Build the command arguments as strings + let mut args_strings = Vec::new(); + args_strings.push("run".to_string()); + + if self.detach { + args_strings.push("-d".to_string()); + } + + args_strings.push("--name".to_string()); + args_strings.push(self.name.clone()); + + // Add port mappings + for port in &self.ports { + args_strings.push("-p".to_string()); + args_strings.push(port.clone()); + } + + // Add volume mounts + for volume in &self.volumes { + args_strings.push("-v".to_string()); + args_strings.push(volume.clone()); + } + + // Add environment variables + for (key, value) in &self.env_vars { + args_strings.push("-e".to_string()); + args_strings.push(format!("{}={}", key, value)); + } + + // Add network configuration + if let Some(network) = &self.network { + args_strings.push("--network".to_string()); + args_strings.push(network.clone()); + } + + // Add network aliases + for alias in &self.network_aliases { + args_strings.push("--network-alias".to_string()); + args_strings.push(alias.clone()); + } + + // Add resource limits + if let Some(cpu_limit) = &self.cpu_limit { + args_strings.push("--cpus".to_string()); + args_strings.push(cpu_limit.clone()); + } + + if let Some(memory_limit) = &self.memory_limit { + args_strings.push("--memory".to_string()); + args_strings.push(memory_limit.clone()); + } + + if let Some(memory_swap_limit) = &self.memory_swap_limit { + args_strings.push("--memory-swap".to_string()); + args_strings.push(memory_swap_limit.clone()); + } + + if let Some(cpu_shares) = &self.cpu_shares { + args_strings.push("--cpu-shares".to_string()); + args_strings.push(cpu_shares.clone()); + } + + // Add restart policy + if let Some(restart_policy) = &self.restart_policy { + args_strings.push("--restart".to_string()); + args_strings.push(restart_policy.clone()); + } + + // Add health check + if let Some(health_check) = &self.health_check { + args_strings.push("--health-cmd".to_string()); + args_strings.push(health_check.cmd.clone()); + + if let Some(interval) = &health_check.interval { + args_strings.push("--health-interval".to_string()); + args_strings.push(interval.clone()); + } + + if let Some(timeout) = &health_check.timeout { + args_strings.push("--health-timeout".to_string()); + args_strings.push(timeout.clone()); + } + + if let Some(retries) = &health_check.retries { + args_strings.push("--health-retries".to_string()); + args_strings.push(retries.to_string()); + } + + if let Some(start_period) = &health_check.start_period { + args_strings.push("--health-start-period".to_string()); + args_strings.push(start_period.clone()); + } + } + + if let Some(snapshotter_value) = &self.snapshotter { + args_strings.push("--snapshotter".to_string()); + args_strings.push(snapshotter_value.clone()); + } + + // Add flags to avoid BPF issues + args_strings.push("--cgroup-manager=cgroupfs".to_string()); + + args_strings.push(image.clone()); + + // Convert to string slices for the command + let args: Vec<&str> = args_strings.iter().map(|s| s.as_str()).collect(); + + // Execute the command + let result = execute_nerdctl_command(&args)?; + + // Get the container ID from the output + let container_id = result.stdout.trim().to_string(); + + Ok(Self { + name: self.name, + container_id: Some(container_id), + image: self.image, + config: self.config, + ports: self.ports, + volumes: self.volumes, + env_vars: self.env_vars, + network: self.network, + network_aliases: self.network_aliases, + cpu_limit: self.cpu_limit, + memory_limit: self.memory_limit, + memory_swap_limit: self.memory_swap_limit, + cpu_shares: self.cpu_shares, + restart_policy: self.restart_policy, + health_check: self.health_check, + detach: self.detach, + snapshotter: self.snapshotter, + }) + } + +``` + +Example output functions: +```rust + +/// Set memory swap limit for a Container +pub fn container_with_memory_swap_limit(container: Container, memory_swap: &str) -> Container { + container.with_memory_swap_limit(memory_swap) +} + +/// Set CPU shares for a Container +pub fn container_with_cpu_shares(container: Container, shares: &str) -> Container { + container.with_cpu_shares(shares) +} + +/// Set health check with options for a Container +pub fn container_with_health_check_options( + container: Container, + cmd: &str, + interval: Option<&str>, + timeout: Option<&str>, + retries: Option, + start_period: Option<&str> +) -> Container { + // Convert i64 to u32 for retries + let retries_u32 = retries.map(|r| r as u32); + container.with_health_check_options(cmd, interval, timeout, retries_u32, start_period) +} + +/// Set snapshotter for a Container +pub fn container_with_snapshotter(container: Container, snapshotter: &str) -> Container { + container.with_snapshotter(snapshotter) +} + +/// Set detach mode for a Container +pub fn container_with_detach(container: Container, detach: bool) -> Container { + container.with_detach(detach) +} + +/// Build and run the Container +/// +/// This function builds and runs the container using the configured options. +/// It provides detailed error information if the build fails. +pub fn container_build(container: Container) -> Result> { + // Get container details for better error reporting + let container_name = container.name.clone(); + let image = container.image.clone().unwrap_or_else(|| "none".to_string()); + let ports = container.ports.clone(); + let volumes = container.volumes.clone(); + let env_vars = container.env_vars.clone(); + + // Try to build the container + let build_result = container.build(); + + // Handle the result with improved error context + match build_result { + Ok(built_container) => { + // Container built successfully + Ok(built_container) + }, + Err(err) => { + // Add more context to the error + let enhanced_error = match err { + NerdctlError::CommandFailed(msg) => { + // Provide more detailed error information + let mut enhanced_msg = format!("Failed to build container '{}' from image '{}': {}", + container_name, image, msg); + + // Add information about configured options that might be relevant + if !ports.is_empty() { + enhanced_msg.push_str(&format!("\nConfigured ports: {:?}", ports)); + } + + if !volumes.is_empty() { + enhanced_msg.push_str(&format!("\nConfigured volumes: {:?}", volumes)); + } + + if !env_vars.is_empty() { + enhanced_msg.push_str(&format!("\nConfigured environment variables: {:?}", env_vars)); + } + + // Add suggestions for common issues + if msg.contains("not found") || msg.contains("no such image") { + enhanced_msg.push_str("\nSuggestion: The specified image may not exist or may not be pulled yet. Try pulling the image first with nerdctl_image_pull()."); + } else if msg.contains("port is already allocated") { + enhanced_msg.push_str("\nSuggestion: One of the specified ports is already in use. Try using a different port or stopping the container using that port."); + } else if msg.contains("permission denied") { + enhanced_msg.push_str("\nSuggestion: Permission issues detected. Check if you have the necessary permissions to create containers or access the specified volumes."); + } + + NerdctlError::CommandFailed(enhanced_msg) + }, + _ => err + }; + + nerdctl_error_to_rhai_error(Err(enhanced_error)) + } + } +} +``` + diff --git a/lib/lang/rhai/rhai.v b/lib/lang/rhai/rhai.v new file mode 100644 index 00000000..4e1a67d5 --- /dev/null +++ b/lib/lang/rhai/rhai.v @@ -0,0 +1,58 @@ +module rhai + +import freeflowuniverse.herolib.ai.escalayer + +pub struct WrapperGenerator { +pub: + function string + structs []string +} + +// generate_rhai_function_wrapper generates a Rhai wrapper function for a given Rust function. +// +// Args: +// rust_function (string): The Rust function signature string. +// struct_declarations ([]string): Optional struct declarations used by the function. +// +// Returns: +// !string: The generated Rhai wrapper function code or an error. +pub fn generate_rhai_function_wrapper(rust_function string, struct_declarations []string) !string { + mut task := escalayer.new_task( + name: 'generate_rhai_function_wrapper' + description: 'Create a single Rhai wrapper for a Rust function' + ) + + mut gen := WrapperGenerator { + function: rust_function + structs: struct_declarations + } + + // Define a single unit task that handles everything + task.new_unit_task( + name: 'generate_rhai_function_wrapper' + prompt_function: gen.generate_rhai_function_wrapper_prompt + callback_function: gen.generate_rhai_function_wrapper_callback + base_model: escalayer.claude_3_sonnet // Use actual model identifier + retry_model: escalayer.gpt4 // Use actual model identifier + retry_count: 1 + ) + + return task.initiate('') +} + +pub fn (gen WrapperGenerator) generate_rhai_function_wrapper_prompt(input string) string { + return $tmpl('./prompts/generate_rhai_function_wrapper.md') +} + +// generate_rhai_function_wrapper_callback validates the generated Rhai wrapper. +// +// Args: +// rhai_wrapper (string): The generated wrapper code. +// +// Returns: +// !string: The validated wrapper code or an error. +pub fn (gen WrapperGenerator) generate_rhai_function_wrapper_callback(rhai_wrapper string) !string { + // TODO: Implement actual validation logic for the Rhai wrapper code. + // This could involve trying to parse it or other checks. + return rhai_wrapper // Return the input for now +} \ No newline at end of file diff --git a/lib/lang/rhai/rhai_test.v b/lib/lang/rhai/rhai_test.v new file mode 100644 index 00000000..83d46468 --- /dev/null +++ b/lib/lang/rhai/rhai_test.v @@ -0,0 +1,89 @@ +module rhai + +import freeflowuniverse.herolib.lang.rhai +// import os // Unused, remove later if not needed + +fn testsuite_begin() { + // Optional: Setup code before tests run +} + +fn testsuite_end() { + // Optional: Teardown code after tests run +} + +fn test_generate_wrapper_simple_function() { + rust_fn := 'pub fn add(a: i32, b: i32) -> i32' + expected_wrapper := 'pub fn add(a: i64, b: i64) -> Result> {\n // Assuming the function exists in the scope where the wrapper is defined\n Ok(add(a, b))\n}' + + actual_wrapper := rhai.generate_rhai_function_wrapper(rust_function: rust_fn, struct_declarations: []string{}) or { + assert false, 'Function returned error: ${err}' + return // Needed for compiler + } + // Normalize whitespace for comparison + assert actual_wrapper.trim_space() == expected_wrapper.trim_space() +} + +fn test_generate_wrapper_immutable_method() { + rust_fn := 'pub fn get_name(&self) -> String' + // receiver := 'MyStruct' // No longer passed directly + struct_decls := ['struct MyStruct { name: String }'] // Example declaration + expected_wrapper := 'pub fn get_name(receiver: &MyStruct) -> Result> {\n Ok(receiver.get_name())\n}' + + actual_wrapper := rhai.generate_rhai_function_wrapper(rust_function: rust_fn, struct_declarations: struct_decls) or { + assert false, 'Function returned error: ${err}' + return + } + assert actual_wrapper.trim_space() == expected_wrapper.trim_space() +} + +fn test_generate_wrapper_mutable_method() { + rust_fn := 'pub fn set_name(&mut self, new_name: String)' // Implicit () return + // receiver := 'MyStruct' // No longer passed directly + struct_decls := ['struct MyStruct { name: String }'] // Example declaration + expected_wrapper := 'pub fn set_name(receiver: &mut MyStruct, new_name: String) -> Result<(), Box> {\n Ok(receiver.set_name(new_name))\n}' + + actual_wrapper := rhai.generate_rhai_function_wrapper(rust_function: rust_fn, struct_declarations: struct_decls) or { + assert false, 'Function returned error: ${err}' + return + } + assert actual_wrapper.trim_space() == expected_wrapper.trim_space() +} + +fn test_generate_wrapper_function_returning_result() { + rust_fn := 'pub fn load_config(path: &str) -> Result' + // receiver := '' // No longer relevant + struct_decls := ['struct Config { ... }'] // Example placeholder declaration + expected_wrapper := 'pub fn load_config(path: &str) -> Result> {\n load_config(path)\n .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error in load_config: {}", e).into(), rhai::Position::NONE)))\n}' + + actual_wrapper := rhai.generate_rhai_function_wrapper(rust_function: rust_fn, struct_declarations: struct_decls) or { + assert false, 'Function returned error: ${err}' + return + } + assert actual_wrapper.trim_space() == expected_wrapper.trim_space() +} + +fn test_generate_wrapper_function_returning_pathbuf() { + rust_fn := 'pub fn get_home_dir() -> PathBuf' + // receiver := '' // No longer relevant + struct_decls := []string{} // No relevant structs + // Expecting conversion to String for Rhai + expected_wrapper := 'pub fn get_home_dir() -> Result> {\n Ok(get_home_dir().to_string_lossy().to_string())\n}' + actual_wrapper := rhai.generate_rhai_function_wrapper(rust_function: rust_fn, struct_declarations: struct_decls) or { + assert false, 'Function returned error: ${err}' + return + } + assert actual_wrapper.trim_space() == expected_wrapper.trim_space() +} + +fn test_generate_wrapper_function_with_vec() { + rust_fn := 'pub fn list_files(dir: &str) -> Vec' + // receiver := '' // No longer relevant + struct_decls := []string{} // No relevant structs + // Expecting Vec to map directly + expected_wrapper := 'pub fn list_files(dir: &str) -> Result, Box> {\n Ok(list_files(dir))\n}' + actual_wrapper := rhai.generate_rhai_function_wrapper(rust_function: rust_fn, struct_declarations: struct_decls) or { + assert false, 'Function returned error: ${err}' + return + } + assert actual_wrapper.trim_space() == expected_wrapper.trim_space() +} diff --git a/lib/lang/rust/rust.v b/lib/lang/rust/rust.v index 7511bfdb..00c1a580 100644 --- a/lib/lang/rust/rust.v +++ b/lib/lang/rust/rust.v @@ -1,6 +1,7 @@ module rust import os +import freeflowuniverse.herolib.core.pathlib // Reads and combines all Rust files in the given directory pub fn read_source_code(source_code_path string) !string { @@ -58,6 +59,94 @@ pub fn extract_module_name_from_path(path string) string { return dir_parts[dir_parts.len - 1] } +// Determines the source package information from a given source path +pub struct SourcePackageInfo { +pub: + name string // Package name + path string // Relative path to the package (for cargo.toml) + module string // Full module path (e.g., herodb::logic) +} + +// Detect source package and module information from a path +pub fn detect_source_package(source_path string) !SourcePackageInfo { + // Look for Cargo.toml in parent directories to find the crate root + mut current_path := source_path + mut package_name := '' + mut rel_path := '' + mut module_parts := []string{} + + // Extract module name from the directory path + mod_name := extract_module_name_from_path(source_path) + module_parts << mod_name + + // Look up parent directories until we find a Cargo.toml + for i := 0; i < 10; i++ { // limit depth to avoid infinite loops + parent_dir := os.dir(current_path) + cargo_path := os.join_path(parent_dir, 'Cargo.toml') + + if os.exists(cargo_path) { + // Found the root of the crate + cargo_content := os.read_file(cargo_path) or { + return error('Failed to read Cargo.toml at ${cargo_path}: ${err}') + } + + // Extract package name + for line in cargo_content.split('\n') { + if line.contains('name') && line.contains('=') { + parts := line.split('=') + if parts.len > 1 { + package_name = parts[1].trim_space().trim('"').trim("'") + break + } + } + } + + // Calculate relative path from current working directory to crate root + current_dir := os.getwd() + rel_path = pathlib.path_relative(parent_dir, current_dir) or { + return error('Failed to get relative path: ${err}') + } + if rel_path == '.' { + rel_path = './' + } + + break + } + + // Go up one directory + if parent_dir == current_path { + break // We've reached the root + } + + // Add directory name to module path parts (in reverse order) + parent_dir_name := os.base(parent_dir) + if parent_dir_name != '' && parent_dir_name != '.' { + module_parts.insert(0, parent_dir_name) + } + + current_path = parent_dir + } + + if package_name == '' { + // If no Cargo.toml found, use the last directory name as package name + package_name = os.base(os.dir(source_path)) + rel_path = '../' // default to parent directory + } + + // Construct the full module path + mut module_path := module_parts.join('::') + if module_parts.len >= 2 { + // Use only the last two components for the module path + module_path = module_parts[module_parts.len-2..].join('::') + } + + return SourcePackageInfo{ + name: package_name + path: rel_path + module: module_path + } +} + // Build and run a Rust project with an example pub fn run_example(project_dir string, example_name string) !(string, string) { // Change to the project directory @@ -77,7 +166,6 @@ pub fn run_example(project_dir string, example_name string) !(string, string) { return build_result.output, run_result.output } - // Extract function names from wrapper code fn extract_functions_from_code(code string) []string { mut functions := []string{} @@ -100,4 +188,918 @@ fn extract_functions_from_code(code string) []string { } return functions +} + +// Extract function names from Rust file +pub fn list_functions_in_file(file_path string) ![]string { + // Check if file exists + if !os.exists(file_path) { + return error('File not found: ${file_path}') + } + + // Read file content + content := os.read_file(file_path) or { + return error('Failed to read file: ${err}') + } + + return extract_functions_from_content(content) +} + +// Extract function names from content string +pub fn extract_functions_from_content(content string) []string { + mut functions := []string{} + lines := content.split('\n') + + mut in_comment_block := false + mut current_impl := '' // Track the current impl block + mut impl_level := 0 // Track nesting level of braces within impl + + for line in lines { + trimmed := line.trim_space() + + // Skip comment lines and empty lines + if trimmed.starts_with('//') || trimmed == '' { + continue + } + + // Handle block comments + if trimmed.starts_with('/*') { + in_comment_block = true + } + if in_comment_block { + if trimmed.contains('*/') { + in_comment_block = false + } + continue + } + + // Check for impl blocks + if trimmed.starts_with('impl ') { + // Extract the struct name from the impl declaration + mut struct_name := '' + + // Handle generic impls like "impl StructName" + if trimmed.contains('<') && trimmed.contains('>') { + // Complex case with generics + if trimmed.contains(' for ') { + // Format: impl Trait for StructName + parts := trimmed.split(' for ') + if parts.len > 1 { + struct_parts := parts[1].split('{') + if struct_parts.len > 0 { + struct_name = struct_parts[0].trim_space() + // Remove any generic parameters + if struct_name.contains('<') { + struct_name = struct_name.all_before('<') + } + } + } + } else { + // Format: impl StructName + after_impl := trimmed.all_after('impl') + after_generic := after_impl.all_after('>') + struct_parts := after_generic.split('{') + if struct_parts.len > 0 { + struct_name = struct_parts[0].trim_space() + // Remove any generic parameters + if struct_name.contains('<') { + struct_name = struct_name.all_before('<') + } + } + } + } else { + // Simple case without generics + if trimmed.contains(' for ') { + // Format: impl Trait for StructName + parts := trimmed.split(' for ') + if parts.len > 1 { + struct_parts := parts[1].split('{') + if struct_parts.len > 0 { + struct_name = struct_parts[0].trim_space() + } + } + } else { + // Format: impl StructName + parts := trimmed.split('impl ') + if parts.len > 1 { + struct_parts := parts[1].split('{') + if struct_parts.len > 0 { + struct_name = struct_parts[0].trim_space() + } + } + } + } + + current_impl = struct_name + if trimmed.contains('{') { + impl_level = 1 + } else { + impl_level = 0 + } + continue + } + + // Track brace levels to properly handle nested blocks + if current_impl != '' { + // Count opening braces + for c in trimmed { + if c == `{` { + impl_level++ + } else if c == `}` { + impl_level-- + // If we've closed the impl block, reset current_impl + if impl_level == 0 { + current_impl = '' + break + } + } + } + } + + // Look for function declarations + if (trimmed.starts_with('pub fn ') || trimmed.starts_with('fn ')) && !trimmed.contains(';') { + mut fn_name := '' + + // Extract function name + if trimmed.starts_with('pub fn ') { + fn_parts := trimmed.split('pub fn ') + if fn_parts.len > 1 { + name_parts := fn_parts[1].split('(') + if name_parts.len > 0 { + fn_name = name_parts[0].trim_space() + } + } + } else { + fn_parts := trimmed.split('fn ') + if fn_parts.len > 1 { + name_parts := fn_parts[1].split('(') + if name_parts.len > 0 { + fn_name = name_parts[0].trim_space() + } + } + } + + // Add function name to the list if it's not empty + if fn_name != '' { + if current_impl != '' { + // All functions in an impl block use :: notation + functions << '${current_impl}::${fn_name}' + } else { + // Regular function + functions << fn_name + } + } + } + } + + return functions +} + +// Extract struct names from Rust file +pub fn list_structs_in_file(file_path string) ![]string { + // Check if file exists + if !os.exists(file_path) { + return error('File not found: ${file_path}') + } + + // Read file content + content := os.read_file(file_path) or { + return error('Failed to read file: ${err}') + } + + return extract_structs_from_content(content) +} + +// Extract struct names from content string +pub fn extract_structs_from_content(content string) []string { + mut structs := []string{} + lines := content.split('\n') + + mut in_comment_block := false + + for line in lines { + trimmed := line.trim_space() + + // Skip comment lines and empty lines + if trimmed.starts_with('//') { + continue + } + + // Handle block comments + if trimmed.starts_with('/*') { + in_comment_block = true + } + if in_comment_block { + if trimmed.contains('*/') { + in_comment_block = false + } + continue + } + + // Look for struct declarations + if (trimmed.starts_with('pub struct ') || trimmed.starts_with('struct ')) && !trimmed.contains(';') { + mut struct_name := '' + + // Extract struct name + if trimmed.starts_with('pub struct ') { + struct_parts := trimmed.split('pub struct ') + if struct_parts.len > 1 { + name_parts := struct_parts[1].split('{') + if name_parts.len > 0 { + parts := name_parts[0].split('<') + struct_name = parts[0].trim_space() + } + } + } else { + struct_parts := trimmed.split('struct ') + if struct_parts.len > 1 { + name_parts := struct_parts[1].split('{') + if name_parts.len > 0 { + parts := name_parts[0].split('<') + struct_name = parts[0].trim_space() + } + } + } + + // Add struct name to the list if it's not empty + if struct_name != '' { + structs << struct_name + } + } + } + + return structs +} + +// Extract imports from a Rust file +pub fn extract_imports(file_path string) ![]string { + // Check if file exists + if !os.exists(file_path) { + return error('File not found: ${file_path}') + } + + // Read file content + content := os.read_file(file_path) or { + return error('Failed to read file: ${err}') + } + + return extract_imports_from_content(content) +} + +// Extract imports from content string +pub fn extract_imports_from_content(content string) []string { + mut imports := []string{} + lines := content.split('\n') + + mut in_comment_block := false + + for line in lines { + trimmed := line.trim_space() + + // Skip comment lines and empty lines + if trimmed.starts_with('//') { + continue + } + + // Handle block comments + if trimmed.starts_with('/*') { + in_comment_block = true + } + if in_comment_block { + if trimmed.contains('*/') { + in_comment_block = false + } + continue + } + + // Extract use statements + if trimmed.starts_with('use ') && trimmed.ends_with(';') { + import_part := trimmed[4..trimmed.len-1].trim_space() // Skip 'use ', remove trailing ';', trim spaces + imports << import_part + } + } + + return imports +} + +// Get module name from file path +pub fn get_module_name(file_path string) string { + // Extract filename from path + filename := os.base(file_path) + + // If it's mod.rs, use parent directory name + if filename == 'mod.rs' { + dir := os.dir(file_path) + return os.base(dir) + } + + // Otherwise use filename without extension + return filename.all_before('.rs') +} + +// List all modules in a directory +pub fn list_modules_in_directory(dir_path string) ![]string { + // Check if directory exists + if !os.exists(dir_path) || !os.is_dir(dir_path) { + return error('Directory not found: ${dir_path}') + } + + // Get all files in the directory + files := os.ls(dir_path) or { + return error('Failed to list files in directory: ${err}') + } + + mut modules := []string{} + + // Check for mod.rs + if files.contains('mod.rs') { + modules << os.base(dir_path) + } + + // Check for Rust files + for file in files { + if file.ends_with('.rs') && file != 'mod.rs' { + modules << file.all_before('.rs') + } + } + + // Check for directories that contain mod.rs + for file in files { + file_path := os.join_path(dir_path, file) + if os.is_dir(file_path) { + subfiles := os.ls(file_path) or { continue } + if subfiles.contains('mod.rs') { + modules << file + } + } + } + + return modules +} + +// Generate an import statement for a module based on current file and target module path +pub fn generate_import_statement(current_file_path string, target_module_path string) !string { + // Attempt to find the project root (directory containing Cargo.toml) + mut project_root := '' + mut current_path := os.dir(current_file_path) + + // Find the project root + for i := 0; i < 10; i++ { // Limit depth to avoid infinite loops + cargo_path := os.join_path(current_path, 'Cargo.toml') + if os.exists(cargo_path) { + project_root = current_path + break + } + + parent_dir := os.dir(current_path) + if parent_dir == current_path { + break // We've reached the root + } + current_path = parent_dir + } + + if project_root == '' { + return error('Could not find project root (Cargo.toml)') + } + + // Get package info + pkg_info := detect_source_package(current_file_path) or { + return error('Failed to detect package info: ${err}') + } + + // Check if target module is part of the same package + target_pkg_info := detect_source_package(target_module_path) or { + return error('Failed to detect target package info: ${err}') + } + + // If same package, generate a relative import + if pkg_info.name == target_pkg_info.name { + // Convert file paths to module paths + current_file_dir := os.dir(current_file_path) + target_file_dir := os.dir(target_module_path) + + // Get paths relative to src + current_rel_path := current_file_dir.replace('${project_root}/src/', '') + target_rel_path := target_file_dir.replace('${project_root}/src/', '') + + // Convert paths to module format + current_module := current_rel_path.replace('/', '::') + target_module := target_rel_path.replace('/', '::') + + // Generate import based on path relationship + if current_module == target_module { + // Same module, import target directly + target_name := get_module_name(target_module_path) + return 'use crate::${target_module}::${target_name};' + } else if current_module.contains(target_module) { + // Target is parent module + target_name := get_module_name(target_module_path) + return 'use super::${target_name};' + } else if target_module.contains(current_module) { + // Target is child module + target_name := get_module_name(target_module_path) + child_path := target_module.replace('${current_module}::', '') + return 'use self::${child_path}::${target_name};' + } else { + // Target is sibling or other module + target_name := get_module_name(target_module_path) + return 'use crate::${target_module}::${target_name};' + } + } else { + // External package + return 'use ${target_pkg_info.name}::${target_pkg_info.module};' + } +} + +// Extract dependencies from Cargo.toml +pub fn extract_dependencies(cargo_path string) !map[string]string { + // Check if file exists + if !os.exists(cargo_path) { + return error('Cargo.toml not found: ${cargo_path}') + } + + // Read file content + content := os.read_file(cargo_path) or { + return error('Failed to read Cargo.toml: ${err}') + } + + mut dependencies := map[string]string{} + mut in_dependencies_section := false + + lines := content.split('\n') + for line in lines { + trimmed := line.trim_space() + + // Check for dependencies section + if trimmed == '[dependencies]' { + in_dependencies_section = true + continue + } else if trimmed.starts_with('[') && in_dependencies_section { + // Left dependencies section + in_dependencies_section = false + continue + } + + // Extract dependency info + if in_dependencies_section && trimmed != '' { + if trimmed.contains('=') { + eq_pos := trimmed.index('=') or { continue } // Find the first '=' + name := trimmed[..eq_pos].trim_space() + mut value := trimmed[eq_pos+1..].trim_space() + + // Remove surrounding quotes if they exist (optional, but good practice for simple strings) + // Note: This won't remove braces for tables, which is desired. + if value.starts_with('"') && value.ends_with('"') { + value = value[1..value.len-1] + } else if value.starts_with("'") && value.ends_with("'") { + value = value[1..value.len-1] + } + dependencies[name] = value // Store the potentially complex value string + } + } + } + + return dependencies +} + +// Get a function declaration from a file by its name +pub fn get_function_from_file(file_path string, function_name string) !string { + // Check if file exists + if !os.exists(file_path) { + return error('File not found: ${file_path}') + } + + // Read file content + content := os.read_file(file_path) or { + return error('Failed to read file: ${err}') + } + + return get_function_from_content(content, function_name) +} + +// Get a function declaration from a module by its name +pub fn get_function_from_module(module_path string, function_name string) !string { + // Check if directory exists + if !os.exists(module_path) { + return error('Module path not found: ${module_path}') + } + + // If it's a directory, look for mod.rs or lib.rs + if os.is_dir(module_path) { + mod_rs_path := os.join_path(module_path, 'mod.rs') + lib_rs_path := os.join_path(module_path, 'lib.rs') + + if os.exists(mod_rs_path) { + result := get_function_from_file(mod_rs_path, function_name) or { + if err.msg().contains('Function ${function_name} not found') { + '' // Not found error, resolve or block to empty string + } else { + return err // Propagate other errors + } + } + if result != '' { return result } + } + + if os.exists(lib_rs_path) { // Changed else if to if + result := get_function_from_file(lib_rs_path, function_name) or { + if err.msg().contains('Function ${function_name} not found') { + '' // Not found error, resolve or block to empty string + } else { + return err // Propagate other errors + } + } + if result != '' { return result } + } + + // Try to find the function in any Rust file in the directory + files := os.ls(module_path) or { + return error('Failed to list files in module directory: ${err}') + } + + for file in files { + if file.ends_with('.rs') { + file_path := os.join_path(module_path, file) + result := get_function_from_file(file_path, function_name) or { + if err.msg().contains('Function ${function_name} not found') { + '' // Not found error, resolve or block to empty string + } else { + return err // Propagate other errors + } + } + if result != '' { return result } // Found it + } + } + + return error('Function ${function_name} not found in module ${module_path}') + } else { + // It's a file path, treat it as a direct file + return get_function_from_file(module_path, function_name) + } +} + +// Get a function declaration from content by its name +pub fn get_function_from_content(content string, function_name string) !string { + is_method := function_name.contains('::') + mut struct_name := '' + mut method_name := function_name + if is_method { + parts := function_name.split('::') + if parts.len == 2 { + struct_name = parts[0] + method_name = parts[1] + } else { + return error('Invalid method format: ${function_name}') + } + } + + lines := content.split('\n') + mut function_declaration := '' + mut brace_level := 0 + mut function_start_line_found := false + mut in_impl_block := false // Flag to track if we are inside the correct impl block + mut impl_brace_level := 0 // To know when the impl block ends + + for line in lines { + trimmed := line.trim_space() + if trimmed.starts_with('//') { continue } // Skip single-line comments + + // Handle finding the correct impl block if it's a method + if is_method && !in_impl_block { + if trimmed.contains('impl') && trimmed.contains(struct_name) { + in_impl_block = true + // Calculate the brace level *before* this impl line + // This is tricky, maybe just track entry/exit + for c in line { if c == `{` { impl_brace_level += 1 } } + continue // Don't process the impl line itself as the start + } + continue // Skip lines until the correct impl block is found + } + + // Handle exiting the impl block + if is_method && in_impl_block { + current_line_brace_change := line.count('{') - line.count('}') + if impl_brace_level + current_line_brace_change <= 0 { // Assuming impl starts at level 0 relative to its scope + in_impl_block = false // Exited the impl block + impl_brace_level = 0 + } + impl_brace_level += current_line_brace_change + } + + // Find the function/method start line + if !function_start_line_found { + mut is_target_line := false + if is_method && in_impl_block { + // Inside the correct impl, look for method + is_target_line = trimmed.contains('fn ${method_name}') || trimmed.contains('fn ${method_name}<') // Handle generics + } else if !is_method { + // Look for standalone function + is_target_line = trimmed.contains('fn ${function_name}') || trimmed.contains('fn ${function_name}<') + } + + if is_target_line { + function_start_line_found = true + function_declaration += line + '\n' + + // Count initial braces on the declaration line + for c in line { + if c == `{` { + brace_level++ + } else if c == `}` { + brace_level-- // Should ideally not happen on decl line + } + } + + // Handle single-line functions like `fn simple() -> i32 { 42 }` or trait methods ending with `;` + if brace_level == 0 && (line.contains('}') || line.contains(';')) { + break // Function definition is complete on this line + } + continue // Move to next line after finding the start + } + } + + // If function start found, append lines and track braces + if function_start_line_found { + function_declaration += line + '\n' + + // Count braces to determine when the function ends + for c in line { + if c == `{` { + brace_level++ + } else if c == `}` { + brace_level-- + } + } + + // Check if function ended + if brace_level <= 0 { // <= 0 to handle potential formatting issues + break + } + } + } + + if function_declaration == '' { + return error('Function ${function_name} not found in content') + } + + return function_declaration.trim_space() +} + +// Get a struct declaration from a file by its name +pub fn get_struct_from_file(file_path string, struct_name string) !string { + // Check if file exists + if !os.exists(file_path) { + return error('File not found: ${file_path}') + } + + // Read file content + content := os.read_file(file_path) or { + return error('Failed to read file: ${err}') + } + + return get_struct_from_content(content, struct_name) +} + +// Get a struct declaration from a module by its name +pub fn get_struct_from_module(module_path string, struct_name string) !string { + // Check if directory exists + if !os.exists(module_path) { + return error('Module path not found: ${module_path}') + } + + // If it's a directory, look for mod.rs or lib.rs + if os.is_dir(module_path) { + mod_rs_path := os.join_path(module_path, 'mod.rs') + lib_rs_path := os.join_path(module_path, 'lib.rs') + + if os.exists(mod_rs_path) { + result := get_struct_from_file(mod_rs_path, struct_name) or { + if err.msg().contains('Struct ${struct_name} not found') { + '' // Not found error, resolve or block to empty string + } else { + return err // Propagate other errors + } + } + if result != '' { return result } + } + + if os.exists(lib_rs_path) { // Changed else if to if + result := get_struct_from_file(lib_rs_path, struct_name) or { + if err.msg().contains('Struct ${struct_name} not found') { + '' // Not found error, resolve or block to empty string + } else { + return err // Propagate other errors + } + } + if result != '' { return result } + } + + // Try to find the struct in any Rust file in the directory + files := os.ls(module_path) or { + return error('Failed to list files in module directory: ${err}') + } + + for file in files { + if file.ends_with('.rs') { + file_path := os.join_path(module_path, file) + result := get_struct_from_file(file_path, struct_name) or { + if err.msg().contains('Struct ${struct_name} not found') { + '' // Not found error, resolve or block to empty string + } else { + return err // Propagate other errors + } + } + if result != '' { return result } // Found it + } + } + + return error('Struct ${struct_name} not found in module ${module_path}') + } else { + // It's a file path, treat it as a direct file + return get_struct_from_file(module_path, struct_name) + } +} + +// Get a struct declaration from content by its name +pub fn get_struct_from_content(content string, struct_name string) !string { + lines := content.split('\n') + + mut in_comment_block := false + mut brace_level := 0 // Tracks brace level *within* the target struct + mut struct_declaration := '' + mut struct_start_line_found := false + + for line in lines { + trimmed := line.trim_space() + if trimmed.starts_with('//') { continue } // Skip single-line comments + + // Handle block comments + if trimmed.starts_with('/*') { + in_comment_block = true + } + if in_comment_block { + if trimmed.contains('*/') { in_comment_block = false } + continue + } + + // Find the struct start line + if !struct_start_line_found { + // Check for `pub struct Name` or `struct Name` followed by { or ; + if (trimmed.starts_with('pub struct ${struct_name}') || trimmed.starts_with('struct ${struct_name}')) && + (trimmed.contains('{') || trimmed.ends_with(';') || trimmed.contains(' where ') || trimmed.contains('<')) { + + // Basic check to avoid matching struct names that are substrings of others + // Example: Don't match `MyStructExtended` when looking for `MyStruct` + // This is a simplified check, regex might be more robust + name_part := trimmed.all_after('struct ').trim_space() + if name_part.starts_with(struct_name) { + // Check if the character after the name is one that indicates end of name ('{', ';', '<', '(' or whitespace) + char_after := if name_part.len > struct_name.len { name_part[struct_name.len] } else { u8(` `) } + if char_after == u8(`{`) || char_after == u8(`;`) || char_after == u8(`<`) || char_after == u8(`(`) || char_after.is_space() { + struct_start_line_found = true + struct_declaration += line + '\n' + + // Count initial braces/check for semicolon on the declaration line + for c in line { + if c == `{` { brace_level++ } + else if c == `}` { brace_level-- } // Should not happen on decl line + } + + // Handle unit structs ending with semicolon + if trimmed.ends_with(';') { + break // Struct definition is complete on this line + } + + // Handle single-line structs like `struct Simple { field: i32 }` + if brace_level == 0 && line.contains('{') && line.contains('}') { + break // Struct definition is complete on this line + } + continue // Move to next line after finding the start + } + } + } + } + + // If struct start found, append lines and track braces + if struct_start_line_found { + struct_declaration += line + '\n' + + // Count braces to determine when the struct ends + for c in line { + if c == `{` { brace_level++ } + else if c == `}` { brace_level-- } + } + + // Check if struct ended + if brace_level <= 0 { // <= 0 handles potential formatting issues or initial non-zero level + break + } + } + } + + if struct_declaration == '' { + return error('Struct ${struct_name} not found in content') + } + + return struct_declaration.trim_space() +} + +// Find the project root directory (the one containing Cargo.toml) +fn find_project_root(path string) string { + mut current_path := path + + // If path is a file, get its directory + if !os.is_dir(current_path) { + current_path = os.dir(current_path) + } + + // Look up parent directories until we find a Cargo.toml + for i := 0; i < 10; i++ { // Limit depth to avoid infinite loops + cargo_path := os.join_path(current_path, 'Cargo.toml') + if os.exists(cargo_path) { + return current_path + } + + parent_dir := os.dir(current_path) + if parent_dir == current_path { + break // We've reached the filesystem root + } + current_path = parent_dir + } + + return '' // No project root found +} + +// Get module dependency information +pub fn get_module_dependency(importer_path string, module_path string) !ModuleDependency { + // Verify paths exist + if !os.exists(importer_path) { + return error('Importer path does not exist: ${importer_path}') + } + + if !os.exists(module_path) { + return error('Module path does not exist: ${module_path}') + } + + // Get import statement + import_statement := generate_import_statement(importer_path, module_path)! // Use local function + + // Try to find the project roots for both paths + importer_project_root := find_project_root(importer_path) + module_project_root := find_project_root(module_path) + + mut dependency := ModuleDependency{ + import_statement: import_statement + module_path: module_path + } + + // If they're in different projects, we need to extract dependency information + if importer_project_root != module_project_root && module_project_root != '' { + cargo_path := os.join_path(module_project_root, 'Cargo.toml') + if os.exists(cargo_path) { + // Get package info to determine name and version + pkg_info := detect_source_package(module_path) or { + return dependency // Return what we have if we can't get package info + } + dependency.package_name = pkg_info.name + + // Extract version from Cargo.toml if possible + dependencies := extract_dependencies(cargo_path) or { + return dependency // Return what we have if we can't extract dependencies + } + + // Check if the package is already a dependency + importer_cargo_path := os.join_path(importer_project_root, 'Cargo.toml') + if os.exists(importer_cargo_path) { + importer_dependencies := extract_dependencies(importer_cargo_path) or { + map[string]string{} // Empty map if we can't extract dependencies + } + + // Check if package is already a dependency + if pkg_info.name in importer_dependencies { + dependency.is_already_dependency = true + dependency.current_version = importer_dependencies[pkg_info.name] + } + } + + // Add cargo dependency line + dependency.cargo_dependency = '${pkg_info.name} = ""' // Placeholder for version + } + } else { + // Same project, no need for external dependency + dependency.is_in_same_project = true + } + + return dependency +} + +// Information about a module dependency +pub struct ModuleDependency { +pub mut: + import_statement string // The Rust import statement to use + module_path string // Path to the module + package_name string // Name of the package (crate) + cargo_dependency string // Line to add to Cargo.toml + current_version string // Current version if already a dependency + is_already_dependency bool // Whether the package is already a dependency + is_in_same_project bool // Whether the module is in the same project } \ No newline at end of file diff --git a/lib/lang/rust/rust_test.v b/lib/lang/rust/rust_test.v new file mode 100644 index 00000000..29da1562 --- /dev/null +++ b/lib/lang/rust/rust_test.v @@ -0,0 +1,416 @@ +module rust_test + +import freeflowuniverse.herolib.lang.rust +import os + +fn test_extract_functions_from_content() { + content := ' +// This is a comment +/* This is a block comment */ + +pub fn public_function() { + println("Hello, world!") +} + +fn private_function() { + println("Private function") +} + +// Another comment +pub fn another_function() -> i32 { + return 42 +} +' + functions := rust.extract_functions_from_content(content) + + assert functions.len == 3 + assert functions[0] == 'public_function' + assert functions[1] == 'private_function' + assert functions[2] == 'another_function' +} + +fn test_extract_structs_from_content() { + content := ' +// This is a comment +/* This is a block comment */ + +pub struct PublicStruct { + field: i32 +} + +struct PrivateStruct { + field: String +} + +pub struct GenericStruct { + field: T +} +' + structs := rust.extract_structs_from_content(content) + + assert structs.len == 3 + assert structs[0] == 'PublicStruct' + assert structs[1] == 'PrivateStruct' + assert structs[2] == 'GenericStruct' +} + +fn test_extract_imports_from_content() { + content := ' +// This is a comment +/* This is a block comment */ + +use std::io; +use std::fs::File; +use crate::module::function; + +// Some code here +fn main() { + println!("Hello, world!"); +} +' + imports := rust.extract_imports_from_content(content) + + assert imports.len == 3 + assert imports[0] == 'std::io' + assert imports[1] == 'std::fs::File' + assert imports[2] == 'crate::module::function' +} + +fn test_get_module_name() { + // Test regular file + assert rust.get_module_name('/path/to/file.rs') == 'file' + + // Test mod.rs file + assert rust.get_module_name('/path/to/module/mod.rs') == 'module' +} + +// Helper function to create temporary test files +fn setup_test_files() !string { + // Create temporary directory + tmp_dir := os.join_path(os.temp_dir(), 'rust_test_${os.getpid()}') + os.mkdir_all(tmp_dir) or { + return error('Failed to create temporary directory: ${err}') + } + + // Create test file + test_file_content := ' +// This is a test file +use std::io; +use std::fs::File; + +pub struct TestStruct { + field: i32 +} + +pub fn test_function() { + println!("Hello, world!"); +} + +fn private_function() { + println!("Private function"); +} +' + + test_file_path := os.join_path(tmp_dir, 'test_file.rs') + os.write_file(test_file_path, test_file_content) or { + os.rmdir_all(tmp_dir) or {} + return error('Failed to write test file: ${err}') + } + + // Create mod.rs file + mod_file_content := ' +// This is a mod file +pub mod test_file; + +pub fn mod_function() { + println!("Mod function"); +} +' + + mod_file_path := os.join_path(tmp_dir, 'mod.rs') + os.write_file(mod_file_path, mod_file_content) or { + os.rmdir_all(tmp_dir) or {} + return error('Failed to write mod file: ${err}') + } + + // Create submodule directory with mod.rs + submod_dir := os.join_path(tmp_dir, 'submodule') + os.mkdir_all(submod_dir) or { + os.rmdir_all(tmp_dir) or {} + return error('Failed to create submodule directory: ${err}') + } + + submod_file_content := ' +// This is a submodule mod file +pub fn submod_function() { + println!("Submodule function"); +} +' + + submod_file_path := os.join_path(submod_dir, 'mod.rs') + os.write_file(submod_file_path, submod_file_content) or { + os.rmdir_all(tmp_dir) or {} + return error('Failed to write submodule mod file: ${err}') + } + + // Create Cargo.toml + cargo_content := ' +[package] +name = "test_package" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = "1.0" +tokio = { version = "1.25", features = ["full"] } +' + + cargo_path := os.join_path(tmp_dir, 'Cargo.toml') + os.write_file(cargo_path, cargo_content) or { + os.rmdir_all(tmp_dir) or {} + return error('Failed to write Cargo.toml: ${err}') + } + + return tmp_dir +} + +fn teardown_test_files(tmp_dir string) { + os.rmdir_all(tmp_dir) or {} +} + +fn test_list_functions_in_file() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + test_file_path := os.join_path(tmp_dir, 'test_file.rs') + functions := rust.list_functions_in_file(test_file_path)! + + assert functions.len == 2 + assert functions.contains('test_function') + assert functions.contains('private_function') +} + +fn test_list_structs_in_file() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + test_file_path := os.join_path(tmp_dir, 'test_file.rs') + structs := rust.list_structs_in_file(test_file_path)! + + assert structs.len == 1 + assert structs[0] == 'TestStruct' +} + +fn test_extract_imports() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + test_file_path := os.join_path(tmp_dir, 'test_file.rs') + imports := rust.extract_imports(test_file_path)! + + assert imports.len == 2 + assert imports[0] == 'std::io' + assert imports[1] == 'std::fs::File' +} + +fn test_list_modules_in_directory() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + modules := rust.list_modules_in_directory(tmp_dir)! + + // Should contain the module itself (mod.rs), test_file.rs and submodule directory + assert modules.len == 3 + assert modules.contains(os.base(tmp_dir)) // Directory name (mod.rs) + assert modules.contains('test_file') + assert modules.contains('submodule') +} + +fn test_extract_dependencies() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + cargo_path := os.join_path(tmp_dir, 'Cargo.toml') + dependencies := rust.extract_dependencies(cargo_path)! + + assert dependencies.len == 2 + assert dependencies['serde'] == '1.0' + assert dependencies['tokio'] == '{ version = "1.25", features = ["full"] }' +} + +fn test_extract_impl_methods() { + test_impl_content := os.read_file('${os.dir(@FILE)}/test_impl.rs') or { + assert false, 'Failed to read test_impl.rs: ${err}' + return + } + + functions := rust.extract_functions_from_content(test_impl_content) + + assert functions.len == 3 + assert functions[0] == 'Currency::new' + assert functions[1] == 'Currency::to_usd' + assert functions[2] == 'Currency::to_currency' + + println('Extracted functions:') + for f in functions { + println(' "${f}"') + } +} + +fn test_get_function_from_content() { + mut content_lines := []string{} + content_lines << '// Some comment' + content_lines << '' + content_lines << 'fn standalone_function() -> i32 {' + content_lines << ' 42' + content_lines << '}' + content_lines << '' + content_lines << 'pub struct MyData {' + content_lines << ' value: String,' + content_lines << '}' + content_lines << '' + content_lines << 'impl MyData {' + content_lines << ' pub fn new(value: String) -> Self {' + content_lines << ' Self { value }' + content_lines << ' }' + content_lines << '' + content_lines << ' fn internal_method(&self) {' + content_lines << ' println!("Internal");' + content_lines << ' }' + content_lines << '}' + content_lines << '' + content_lines << '// Another comment' + content := content_lines.join('\n') + + // Test standalone function + decl1 := rust.get_function_from_content(content, 'standalone_function') or { + assert false, 'Failed: ${err}' + return + } + expected1 := 'fn standalone_function() -> i32 {\n 42\n}' + assert decl1.trim_space() == expected1 + + // Test struct method + decl2 := rust.get_function_from_content(content, 'MyData::new') or { + assert false, 'Failed: ${err}' + return + } + expected2 := 'pub fn new(value: String) -> Self {\n Self { value }\n }' + assert decl2.trim_space() == expected2 + + // Test private struct method + decl3 := rust.get_function_from_content(content, 'MyData::internal_method') or { + assert false, 'Failed: ${err}' + return + } + expected3 := 'fn internal_method(&self) {\n println!("Internal");\n }' + assert decl3.trim_space() == expected3 + + // Test function not found + _ := rust.get_function_from_content(content, 'non_existent_function') or { + assert err.msg() == 'Function non_existent_function not found in content' + return + } + assert false, 'Expected error for non-existent function' +} + +fn test_get_struct_from_content() { + mut content_lines := []string{} + content_lines << '// Comment' + content_lines << 'pub struct SimpleStruct {' + content_lines << ' field1: i32,' + content_lines << '}' + content_lines << '' + content_lines << 'struct GenericStruct {' + content_lines << ' data: T,' + content_lines << '}' + content_lines << '' + content_lines << '// Another struct' + content_lines << 'pub struct ComplexStruct where A: Clone {' + content_lines << ' a: A,' + content_lines << ' b: B,' + content_lines << ' c: Vec,' + content_lines << '}' + content_lines << '' + content_lines << 'struct EmptyStruct;' + content_lines << '' + content_lines << 'struct StructWithImpl {' + content_lines << ' val: bool,' + content_lines << '}' + content_lines << '' + content_lines << 'impl StructWithImpl {' + content_lines << ' fn method() {}' + content_lines << '}' + content := content_lines.join('\n') + + // Test simple struct + decl1 := rust.get_struct_from_content(content, 'SimpleStruct') or { + assert false, 'Failed: ${err}' + return + } + expected1 := 'pub struct SimpleStruct {\n field1: i32,\n}' + assert decl1.trim_space() == expected1 + + // Test generic struct + decl2 := rust.get_struct_from_content(content, 'GenericStruct') or { + assert false, 'Failed: ${err}' + return + } + expected2 := 'struct GenericStruct {\n data: T,\n}' + assert decl2.trim_space() == expected2 + + // Test complex struct + decl3 := rust.get_struct_from_content(content, 'ComplexStruct') or { + assert false, 'Failed: ${err}' + return + } + expected3 := 'pub struct ComplexStruct where A: Clone {\n a: A,\n b: B,\n c: Vec,\n}' + assert decl3.trim_space() == expected3 + + // Test empty struct + decl4 := rust.get_struct_from_content(content, 'EmptyStruct') or { + assert false, 'Failed: ${err}' + return + } + expected4 := 'struct EmptyStruct;' + assert decl4.trim_space() == expected4 + + // Test struct with impl + decl5 := rust.get_struct_from_content(content, 'StructWithImpl') or { + assert false, 'Failed: ${err}' + return + } + expected5 := 'struct StructWithImpl {\n val: bool,\n}' + assert decl5.trim_space() == expected5 + + // Test struct not found + _ := rust.get_struct_from_content(content, 'non_existent_struct') or { + assert err.msg() == 'Struct non_existent_struct not found in content' + return + } + assert false, 'Expected error for non-existent struct' +} + +fn test_get_struct_from_file() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + test_file_path := os.join_path(tmp_dir, 'test_file.rs') + structs := rust.list_structs_in_file(test_file_path)! + + assert structs.len == 1 + assert structs[0] == 'TestStruct' +} + +fn test_get_struct_from_module() ! { + tmp_dir := setup_test_files()! + defer { teardown_test_files(tmp_dir) } + + modules := rust.list_modules_in_directory(tmp_dir)! + + // Should contain the module itself (mod.rs), test_file.rs and submodule directory + assert modules.len == 3 + assert modules.contains(os.base(tmp_dir)) // Directory name (mod.rs) + assert modules.contains('test_file') + assert modules.contains('submodule') +} diff --git a/lib/lang/rust/test_impl.rs b/lib/lang/rust/test_impl.rs new file mode 100644 index 00000000..8077c695 --- /dev/null +++ b/lib/lang/rust/test_impl.rs @@ -0,0 +1,29 @@ +impl Currency { + /// Create a new currency with amount and code + pub fn new(amount: f64, currency_code: String) -> Self { + Self { + amount, + currency_code, + } + } + + /// Convert the currency to USD + pub fn to_usd(&self) -> Option { + if self.currency_code == "USD" { + return Some(self.clone()); + } + + EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, "USD") + .map(|amount| Currency::new(amount, "USD".to_string())) + } + + /// Convert the currency to another currency + pub fn to_currency(&self, target_currency: &str) -> Option { + if self.currency_code == target_currency { + return Some(self.clone()); + } + + EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, target_currency) + .map(|amount| Currency::new(amount, target_currency.to_string())) + } +} \ No newline at end of file