rust and rhai mcp improvements

This commit is contained in:
Timur Gordon
2025-04-17 21:19:18 +02:00
parent 9870fcbc5d
commit 75f98bf349
25 changed files with 3232 additions and 47 deletions

View File

@@ -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)

3
lib/ai/mcp/README2.md Normal file
View File

@@ -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

View File

@@ -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) !

View File

@@ -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
}

View File

@@ -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')!
}
}
}

View File

@@ -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
}
})!

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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}'
}
}

View File

@@ -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
}
}

View File

@@ -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}'
// }

View File

@@ -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::<n>`
import package being wrapped as `use @{source_pkg_info.name}::@{source_pkg_info.module}`
your engine create function is called `create_rhai_engine`
```

View File

@@ -7,4 +7,4 @@ edition = "2021"
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}" }

View File

@@ -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{

21
lib/ai/mcp/rust/command.v Normal file
View File

@@ -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()!
}

View File

@@ -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
}

52
lib/ai/mcp/rust/mcp.v Normal file
View File

@@ -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
}

144
lib/ai/mcp/rust/prompts.v Normal file
View File

@@ -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' +
'<a name="functions"></a>\n' + functions_content + '\n\n' +
'<a name="structs"></a>\n' + structs_content + '\n\n' +
'<a name="modules"></a>\n' + modules_content + '\n\n' +
'<a name="imports"></a>\n' + imports_content + '\n\n' +
'<a name="dependencies"></a>\n' + dependencies_content
return [mcp.PromptMessage{
role: 'assistant'
content: mcp.PromptContent{
typ: 'text'
text: combined_content
}
}]
}

305
lib/ai/mcp/rust/tools.v Normal file
View File

@@ -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)
}
}

View File

@@ -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<T>` might become `rhai::Array`, `HashMap<K, V>` 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<T, Box<EvalAltResult>>`, where `T` is the Rhai-compatible equivalent of the original Rust function's return type. If the original function returns `Result<U, E>`, `T` should be the Rhai-compatible version of `U`, and the error `E` should be mapped into `Box<EvalAltResult>`. If the original returns `()`, use `Result<(), Box<EvalAltResult>>`. If it returns a simple type `U`, use `Result<U, Box<EvalAltResult>>`.
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<EvalAltResult>`. 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<String, ...>`, `Result<bool, ...>`, `Result<Vec<String>, ...>`) over `Result<Dynamic, ...>`. 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<T>(result: Result<T, [modulename]Error>) -> Result<T, Box<EvalAltResult>> {}
```
And feel free to use it like:
```rust
/// Create a new Container
pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> {
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<EvalAltResult>> {
// 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<String>,
/// Base image (if created from an image)
pub image: Option<String>,
/// Configuration options
pub config: HashMap<String, String>,
/// Port mappings
pub ports: Vec<String>,
/// Volume mounts
pub volumes: Vec<String>,
/// Environment variables
pub env_vars: HashMap<String, String>,
/// Network to connect to
pub network: Option<String>,
/// Network aliases
pub network_aliases: Vec<String>,
/// CPU limit
pub cpu_limit: Option<String>,
/// Memory limit
pub memory_limit: Option<String>,
/// Memory swap limit
pub memory_swap_limit: Option<String>,
/// CPU shares
pub cpu_shares: Option<String>,
/// Restart policy
pub restart_policy: Option<String>,
/// Health check
pub health_check: Option<HealthCheck>,
/// Whether to run in detached mode
pub detach: bool,
/// Snapshotter to use
pub snapshotter: Option<String>,
}
/// 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<String>,
/// Maximum time to wait for a check to complete (default: 30s)
pub timeout: Option<String>,
/// Number of consecutive failures needed to consider unhealthy (default: 3)
pub retries: Option<u32>,
/// Start period for the container to initialize before counting retries (default: 0s)
pub start_period: Option<String>,
}
```
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<u32>,
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<Self, NerdctlError>` - Container instance or error
pub fn build(self) -> Result<Self, NerdctlError> {
// 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<i64>,
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<Container, Box<EvalAltResult>> {
// 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))
}
}
}
```

58
lib/lang/rhai/rhai.v Normal file
View File

@@ -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
}

89
lib/lang/rhai/rhai_test.v Normal file
View File

@@ -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<i64, Box<EvalAltResult>> {\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<String, Box<EvalAltResult>> {\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<EvalAltResult>> {\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<Config, io::Error>'
// receiver := '' // No longer relevant
struct_decls := ['struct Config { ... }'] // Example placeholder declaration
expected_wrapper := 'pub fn load_config(path: &str) -> Result<Config, Box<EvalAltResult>> {\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<String, Box<EvalAltResult>> {\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<String>'
// receiver := '' // No longer relevant
struct_decls := []string{} // No relevant structs
// Expecting Vec<String> to map directly
expected_wrapper := 'pub fn list_files(dir: &str) -> Result<Vec<String>, Box<EvalAltResult>> {\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()
}

File diff suppressed because it is too large Load Diff

416
lib/lang/rust/rust_test.v Normal file
View File

@@ -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<T> {
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<T> {'
content_lines << ' data: T,'
content_lines << '}'
content_lines << ''
content_lines << '// Another struct'
content_lines << 'pub struct ComplexStruct<A, B> where A: Clone {'
content_lines << ' a: A,'
content_lines << ' b: B,'
content_lines << ' c: Vec<String>,'
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<T> {\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<A, B> where A: Clone {\n a: A,\n b: B,\n c: Vec<String>,\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')
}

View File

@@ -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<Currency> {
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<Currency> {
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()))
}
}