2025-07-17 20:26:26 -03:00
|
|
|
use anyhow::Error;
|
2025-07-14 16:34:09 -03:00
|
|
|
use rhai::module_resolvers::StaticModuleResolver;
|
2025-07-16 01:38:09 -03:00
|
|
|
use rhai::{Array, Dynamic, Engine, FnPtr, Scope};
|
|
|
|
|
use rhai::{EvalAltResult, ImmutableString, LexError, ParseError, ParseErrorType, Position};
|
2025-07-17 20:26:26 -03:00
|
|
|
use serde_json::{json, Value};
|
2025-07-19 00:45:40 -03:00
|
|
|
use smartstring::SmartString;
|
2025-07-14 16:34:09 -03:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
2025-07-19 00:45:40 -03:00
|
|
|
use crate::services::find::execute_find;
|
|
|
|
|
use crate::services::state::AppState;
|
|
|
|
|
|
2025-07-14 16:34:09 -03:00
|
|
|
pub struct ScriptService {
|
|
|
|
|
engine: Engine,
|
|
|
|
|
module_resolver: StaticModuleResolver,
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-17 20:26:26 -03:00
|
|
|
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::<rhai::Array>(),
|
|
|
|
|
),
|
|
|
|
|
Value::Object(obj) => Dynamic::from(
|
|
|
|
|
obj.iter()
|
|
|
|
|
.map(|(k, v)| (SmartString::from(k), json_value_to_dynamic(v)))
|
|
|
|
|
.collect::<rhai::Map>(),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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::<Array>()
|
|
|
|
|
} else if value.is_unit() || value.is::<()>() {
|
|
|
|
|
// Handle empty/unit case
|
|
|
|
|
Array::new()
|
|
|
|
|
} else {
|
|
|
|
|
// Convert single value to single-element array
|
|
|
|
|
Array::from([value])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-14 16:34:09 -03:00
|
|
|
impl ScriptService {
|
2025-07-19 00:45:40 -03:00
|
|
|
pub fn new(state: &AppState) -> Self {
|
2025-07-14 16:34:09 -03:00
|
|
|
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);
|
|
|
|
|
|
2025-07-17 20:26:26 -03:00
|
|
|
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());
|
|
|
|
|
}
|
2025-07-16 01:56:22 -03:00
|
|
|
|
2025-07-17 20:26:26 -03:00
|
|
|
// 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
|
2025-07-16 01:56:22 -03:00
|
|
|
context.scope_mut().rewind(orig_len);
|
2025-07-16 01:38:09 -03:00
|
|
|
}
|
|
|
|
|
|
2025-07-17 20:26:26 -03:00
|
|
|
Ok(Dynamic::UNIT)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
2025-07-16 01:38:09 -03:00
|
|
|
// Register EXIT FOR
|
|
|
|
|
engine
|
|
|
|
|
.register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| {
|
|
|
|
|
Err("EXIT FOR".into())
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
2025-07-14 16:34:09 -03:00
|
|
|
// FIND command: FIND "table", "filter"
|
2025-07-19 00:45:40 -03:00
|
|
|
// Clone the database reference outside the closure to avoid lifetime issues
|
|
|
|
|
let db = state.db_custom.clone();
|
|
|
|
|
|
2025-07-14 16:40:21 -03:00
|
|
|
engine
|
2025-07-19 00:45:40 -03:00
|
|
|
.register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, {
|
|
|
|
|
let db = db.clone();
|
|
|
|
|
|
|
|
|
|
move |context, inputs| {
|
2025-07-14 16:40:21 -03:00
|
|
|
let table_name = context.eval_expression_tree(&inputs[0])?;
|
|
|
|
|
let filter = context.eval_expression_tree(&inputs[1])?;
|
2025-07-19 00:45:40 -03:00
|
|
|
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))
|
2025-07-17 20:26:26 -03:00
|
|
|
} else {
|
2025-07-19 00:45:40 -03:00
|
|
|
Err("No results".into())
|
2025-07-17 20:26:26 -03:00
|
|
|
}
|
2025-07-19 00:45:40 -03:00
|
|
|
}
|
|
|
|
|
})
|
2025-07-14 16:40:21 -03:00
|
|
|
.unwrap();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
// SET command: SET "table", "key", "value"
|
2025-07-14 16:40:21 -03:00
|
|
|
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();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
// GET command: GET "url"
|
2025-07-14 16:40:21 -03:00
|
|
|
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();
|
2025-07-16 01:38:09 -03:00
|
|
|
|
2025-07-14 21:05:55 -03:00
|
|
|
println!("GET executed: {}", url_str.to_string());
|
2025-07-14 16:40:21 -03:00
|
|
|
Ok(format!("Content from {}", url_str).into())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
// CREATE SITE command: CREATE SITE "name", "company", "website", "template", "prompt"
|
2025-07-14 16:40:21 -03:00
|
|
|
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();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
// CREATE DRAFT command: CREATE DRAFT "to", "subject", "body"
|
2025-07-14 16:40:21 -03:00
|
|
|
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();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
// PRINT command
|
2025-07-14 16:40:21 -03:00
|
|
|
engine
|
|
|
|
|
.register_custom_syntax(
|
|
|
|
|
&["PRINT", "$expr$"],
|
|
|
|
|
true, // Statement
|
|
|
|
|
|context, inputs| {
|
|
|
|
|
let value = context.eval_expression_tree(&inputs[0])?;
|
|
|
|
|
println!("{}", value);
|
|
|
|
|
Ok(Dynamic::UNIT)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
// Register web service functions
|
2025-07-14 16:40:21 -03:00
|
|
|
engine.register_fn("web_get", |url: &str| format!("Response from {}", url));
|
2025-07-14 16:34:09 -03:00
|
|
|
|
|
|
|
|
ScriptService {
|
|
|
|
|
engine,
|
|
|
|
|
module_resolver,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn preprocess_basic_script(&self, script: &str) -> String {
|
|
|
|
|
let mut result = String::new();
|
2025-07-16 01:38:09 -03:00
|
|
|
let mut for_stack: Vec<usize> = Vec::new();
|
|
|
|
|
let mut current_indent = 0;
|
2025-07-14 16:40:21 -03:00
|
|
|
|
2025-07-14 16:34:09 -03:00
|
|
|
for line in script.lines() {
|
|
|
|
|
let trimmed = line.trim();
|
2025-07-14 16:40:21 -03:00
|
|
|
|
2025-07-14 16:34:09 -03:00
|
|
|
// Skip empty lines and comments
|
|
|
|
|
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("REM") {
|
|
|
|
|
result.push_str(line);
|
|
|
|
|
result.push('\n');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-07-14 16:40:21 -03:00
|
|
|
|
2025-07-16 01:38:09 -03:00
|
|
|
// 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;
|
2025-07-14 16:34:09 -03:00
|
|
|
}
|
2025-07-16 01:38:09 -03:00
|
|
|
|
|
|
|
|
// 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);
|
2025-07-17 20:26:26 -03:00
|
|
|
result.push(';');
|
|
|
|
|
result.push('\n');
|
2025-07-16 01:38:09 -03:00
|
|
|
continue;
|
|
|
|
|
} else {
|
|
|
|
|
panic!("NEXT without matching FOR EACH");
|
|
|
|
|
}
|
2025-07-14 16:34:09 -03:00
|
|
|
}
|
2025-07-14 16:40:21 -03:00
|
|
|
|
2025-07-16 01:38:09 -03:00
|
|
|
// Handle EXIT FOR
|
|
|
|
|
if trimmed == "EXIT FOR" {
|
|
|
|
|
result.push_str(&" ".repeat(current_indent));
|
|
|
|
|
result.push_str(trimmed);
|
|
|
|
|
result.push('\n');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-07-14 16:40:21 -03:00
|
|
|
|
2025-07-16 01:38:09 -03:00
|
|
|
// 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",
|
2025-07-16 01:56:22 -03:00
|
|
|
"END IF", "WHILE", "WEND", "DO", "LOOP",
|
2025-07-16 01:38:09 -03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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);
|
2025-07-17 20:26:26 -03:00
|
|
|
result.push(';');
|
2025-07-14 16:34:09 -03:00
|
|
|
} else {
|
2025-07-16 01:38:09 -03:00
|
|
|
// Add semicolons only for non-BASIC statements
|
|
|
|
|
result.push_str(trimmed);
|
2025-07-14 16:34:09 -03:00
|
|
|
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
|
|
|
|
|
result.push(';');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.push('\n');
|
|
|
|
|
}
|
2025-07-14 16:40:21 -03:00
|
|
|
|
2025-07-16 01:38:09 -03:00
|
|
|
if !for_stack.is_empty() {
|
|
|
|
|
panic!("Unclosed FOR EACH loop");
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-14 16:34:09 -03:00
|
|
|
result
|
|
|
|
|
}
|
2025-07-17 20:26:26 -03:00
|
|
|
|
2025-07-16 01:38:09 -03:00
|
|
|
/// Preprocesses BASIC-style script to handle semicolon-free syntax
|
2025-07-14 16:40:21 -03:00
|
|
|
pub fn compile(&self, script: &str) -> Result<rhai::AST, Box<EvalAltResult>> {
|
|
|
|
|
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))),
|
|
|
|
|
}
|
2025-07-14 16:34:09 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn run(&self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
|
|
|
self.engine.eval_ast(ast)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn call_web_service(
|
|
|
|
|
&self,
|
|
|
|
|
endpoint: &str,
|
|
|
|
|
data: HashMap<String, String>,
|
|
|
|
|
) -> Result<String, Box<EvalAltResult>> {
|
|
|
|
|
Ok(format!("Called {} with {:?}", endpoint, data))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Execute a BASIC-style script without semicolons
|
|
|
|
|
pub fn execute_basic_script(&self, script: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
|
|
|
let processed = self.preprocess_basic_script(script);
|
|
|
|
|
let ast = self.engine.compile(&processed)?;
|
|
|
|
|
self.run(&ast)
|
|
|
|
|
}
|
|
|
|
|
}
|