use anyhow::Error; use rhai::module_resolvers::StaticModuleResolver; use rhai::{Array, Dynamic, Engine, FnPtr, Scope}; use rhai::{EvalAltResult, ImmutableString, LexError, ParseError, ParseErrorType, Position}; use serde_json::{json, Value}; use smartstring::SmartString; use std::collections::HashMap; use crate::services::find::execute_find; use crate::services::state::AppState; pub struct ScriptService { engine: Engine, module_resolver: StaticModuleResolver, } fn json_value_to_dynamic(value: &Value) -> Dynamic { match value { Value::Null => Dynamic::UNIT, Value::Bool(b) => Dynamic::from(*b), Value::Number(n) => { if let Some(i) = n.as_i64() { Dynamic::from(i) } else if let Some(f) = n.as_f64() { Dynamic::from(f) } else { Dynamic::UNIT } } Value::String(s) => Dynamic::from(s.clone()), Value::Array(arr) => Dynamic::from( arr.iter() .map(json_value_to_dynamic) .collect::(), ), Value::Object(obj) => Dynamic::from( obj.iter() .map(|(k, v)| (SmartString::from(k), json_value_to_dynamic(v))) .collect::(), ), } } /// Converts any value to an array - single values become single-element arrays fn to_array(value: Dynamic) -> Array { if value.is_array() { // Already an array - return as-is value.cast::() } else if value.is_unit() || value.is::<()>() { // Handle empty/unit case Array::new() } else { // Convert single value to single-element array Array::from([value]) } } impl ScriptService { pub fn new(state: &AppState) -> Self { let mut engine = Engine::new(); let module_resolver = StaticModuleResolver::new(); // Configure engine for BASIC-like syntax engine.set_allow_anonymous_fn(true); engine.set_allow_looping(true); engine .register_custom_syntax( &[ "FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$", ], true, // We're modifying the scope by adding the loop variable |context, inputs| { // Get the iterator variable names let loop_var = inputs[0].get_string_value().unwrap(); let next_var = inputs[3].get_string_value().unwrap(); // Verify variable names match if loop_var != next_var { return Err(format!( "NEXT variable '{}' doesn't match FOR EACH variable '{}'", next_var, loop_var ) .into()); } // Evaluate the collection expression let collection = context.eval_expression_tree(&inputs[1])?; // Debug: Print the collection type println!("Collection type: {}", collection.type_name()); let ccc = collection.clone(); // Convert to array - with proper error handling let array = match collection.into_array() { Ok(arr) => arr, Err(err) => { return Err(format!( "foreach expected array, got {}: {}", ccc.type_name(), err ) .into()); } }; // Get the block as an expression tree let block = &inputs[2]; // Remember original scope length let orig_len = context.scope().len(); for item in array { // Push the loop variable into the scope context.scope_mut().push(loop_var.clone(), item); // Evaluate the block with the current scope match context.eval_expression_tree(block) { Ok(_) => (), Err(e) if e.to_string() == "EXIT FOR" => { context.scope_mut().rewind(orig_len); break; } Err(e) => { // Rewind the scope before returning error context.scope_mut().rewind(orig_len); return Err(e); } } // Remove the loop variable for next iteration context.scope_mut().rewind(orig_len); } Ok(Dynamic::UNIT) }, ) .unwrap(); // Register EXIT FOR engine .register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| { Err("EXIT FOR".into()) }) .unwrap(); // FIND command: FIND "table", "filter" // Clone the database reference outside the closure to avoid lifetime issues let db = state.db_custom.clone(); engine .register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, { let db = db.clone(); move |context, inputs| { let table_name = context.eval_expression_tree(&inputs[0])?; let filter = context.eval_expression_tree(&inputs[1])?; let binding = db.as_ref().unwrap(); // Use the current async context instead of creating a new runtime let binding2 = table_name.to_string(); let binding3 = filter.to_string(); let fut = execute_find( binding, &binding2, &binding3, ); // Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on let result = tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(fut) }) .map_err(|e| format!("DB error: {}", e))?; if let Some(results) = result.get("results") { let array = to_array(json_value_to_dynamic(results)); Ok(Dynamic::from(array)) } else { Err("No results".into()) } } }) .unwrap(); // SET command: SET "table", "key", "value" engine .register_custom_syntax( &["SET", "$expr$", ",", "$expr$", ",", "$expr$"], true, // Statement |context, inputs| { let table_name = context.eval_expression_tree(&inputs[0])?; let key_value = context.eval_expression_tree(&inputs[1])?; let value = context.eval_expression_tree(&inputs[2])?; let table_str = table_name.to_string(); let key_str = key_value.to_string(); let value_str = value.to_string(); let result = json!({ "command": "set", "status": "success", "table": table_str, "key": key_str, "value": value_str }); println!("SET executed: {}", result.to_string()); Ok(Dynamic::UNIT) }, ) .unwrap(); // GET command: GET "url" engine .register_custom_syntax( &["GET", "$expr$"], false, // Expression, not statement |context, inputs| { let url = context.eval_expression_tree(&inputs[0])?; let url_str = url.to_string(); println!("GET executed: {}", url_str.to_string()); Ok(format!("Content from {}", url_str).into()) }, ) .unwrap(); // CREATE SITE command: CREATE SITE "name", "company", "website", "template", "prompt" engine .register_custom_syntax( &[ "CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ], true, // Statement |context, inputs| { if inputs.len() < 5 { return Err("Not enough arguments for CREATE SITE".into()); } let name = context.eval_expression_tree(&inputs[0])?; let company = context.eval_expression_tree(&inputs[1])?; let website = context.eval_expression_tree(&inputs[2])?; let template = context.eval_expression_tree(&inputs[3])?; let prompt = context.eval_expression_tree(&inputs[4])?; let result = json!({ "command": "create_site", "name": name.to_string(), "company": company.to_string(), "website": website.to_string(), "template": template.to_string(), "prompt": prompt.to_string() }); println!("CREATE SITE executed: {}", result.to_string()); Ok(Dynamic::UNIT) }, ) .unwrap(); // CREATE DRAFT command: CREATE DRAFT "to", "subject", "body" engine .register_custom_syntax( &["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], true, // Statement |context, inputs| { if inputs.len() < 3 { return Err("Not enough arguments for CREATE DRAFT".into()); } let to = context.eval_expression_tree(&inputs[0])?; let subject = context.eval_expression_tree(&inputs[1])?; let body = context.eval_expression_tree(&inputs[2])?; let result = json!({ "command": "create_draft", "to": to.to_string(), "subject": subject.to_string(), "body": body.to_string() }); println!("CREATE DRAFT executed: {}", result.to_string()); Ok(Dynamic::UNIT) }, ) .unwrap(); // PRINT command engine .register_custom_syntax( &["PRINT", "$expr$"], true, // Statement |context, inputs| { let value = context.eval_expression_tree(&inputs[0])?; println!("{}", value); Ok(Dynamic::UNIT) }, ) .unwrap(); // Register web service functions engine.register_fn("web_get", |url: &str| format!("Response from {}", url)); ScriptService { engine, module_resolver, } } fn preprocess_basic_script(&self, script: &str) -> String { let mut result = String::new(); let mut for_stack: Vec = Vec::new(); let mut current_indent = 0; for line in script.lines() { let trimmed = line.trim(); // Skip empty lines and comments if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("REM") { result.push_str(line); result.push('\n'); continue; } // Handle FOR EACH start if trimmed.starts_with("FOR EACH") { for_stack.push(current_indent); result.push_str(&" ".repeat(current_indent)); result.push_str(trimmed); result.push_str("{\n"); current_indent += 4; result.push_str(&" ".repeat(current_indent)); result.push('\n'); continue; } // Handle NEXT if trimmed.starts_with("NEXT") { if let Some(expected_indent) = for_stack.pop() { if (current_indent - 4) != expected_indent { panic!("NEXT without matching FOR EACH"); } current_indent = current_indent - 4; result.push_str(&" ".repeat(current_indent)); result.push_str("}\n"); result.push_str(&" ".repeat(current_indent)); result.push_str(trimmed); result.push(';'); result.push('\n'); continue; } else { panic!("NEXT without matching FOR EACH"); } } // Handle EXIT FOR if trimmed == "EXIT FOR" { result.push_str(&" ".repeat(current_indent)); result.push_str(trimmed); result.push('\n'); continue; } // Handle regular lines - no semicolons added for BASIC-style commands result.push_str(&" ".repeat(current_indent)); let basic_commands = [ "SET", "CREATE", "PRINT", "FOR", "FIND", "GET", "EXIT", "IF", "THEN", "ELSE", "END IF", "WHILE", "WEND", "DO", "LOOP", ]; let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd)); let is_control_flow = trimmed.starts_with("IF") || trimmed.starts_with("ELSE") || trimmed.starts_with("END IF"); if is_basic_command || !for_stack.is_empty() || is_control_flow { // Don'ta add semicolons for BASIC-style commands or inside blocks result.push_str(trimmed); result.push(';'); } else { // Add semicolons only for non-BASIC statements result.push_str(trimmed); if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') { result.push(';'); } } result.push('\n'); } if !for_stack.is_empty() { panic!("Unclosed FOR EACH loop"); } result } /// Preprocesses BASIC-style script to handle semicolon-free syntax pub fn compile(&self, script: &str) -> Result> { let processed_script = self.preprocess_basic_script(script); match self.engine.compile(&processed_script) { Ok(ast) => Ok(ast), Err(parse_error) => Err(Box::new(EvalAltResult::from(parse_error))), } } pub fn run(&self, ast: &rhai::AST) -> Result> { self.engine.eval_ast(ast) } pub fn call_web_service( &self, endpoint: &str, data: HashMap, ) -> Result> { Ok(format!("Called {} with {:?}", endpoint, data)) } /// Execute a BASIC-style script without semicolons pub fn execute_basic_script(&self, script: &str) -> Result> { let processed = self.preprocess_basic_script(script); let ast = self.engine.compile(&processed)?; self.run(&ast) } }