Update dependencies and enhance script functionality
All checks were successful
GBCI / build (push) Successful in 16m37s

- Added `smartstring` dependency to Cargo.toml
- Refactored script logic in `main.rs` for improved item handling
- Enhanced `dns.sh` to log standard output and error
- Introduced JSON handling functions in `script.rs` for dynamic value conversion
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-07-17 20:26:26 -03:00
parent 39ae9cb977
commit 0f0ea3e137
5 changed files with 157 additions and 79 deletions

1
Cargo.lock generated
View file

@ -1655,6 +1655,7 @@ dependencies = [
"rhai",
"serde",
"serde_json",
"smartstring",
"sqlx",
"tempfile",
"tokio",

View file

@ -12,30 +12,29 @@ actix-cors = "0.6"
actix-multipart = "0.6"
actix-web = "4"
actix-ws="0.3.0"
anyhow = "1.0"
async-stream = "0.3"
bytes = "1.1"
futures-util = "0.3"
reqwest = { version = "0.11", features = ["json", "stream"] }
rhai = "1.22.2"
chrono = { version = "0.4", features = ["serde"] }
dotenv = "0.15"
env_logger = "0.10"
futures = "0.3"
futures-util = "0.3"
imap = "2.0"
langchain-rust = "4.4.3"
lettre = { version = "0.10", features = ["smtp-transport", "builder", "tokio1", "tokio1-native-tls"] }
log = "0.4"
mailparse = "0.13"
minio = { git = "https://github.com/minio/minio-rs", branch = "master" }
native-tls = "0.2"
reqwest = { version = "0.11", features = ["json", "stream"] }
rhai = "1.22.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
smartstring = "1.0" # Use the latest version from crates.io
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }
tempfile = "3"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.17"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }
env_logger = "0.10"
anyhow = "1.0"
futures = "0.3"
langchain-rust = "4.4.3"
async-stream = "0.3"

View file

@ -32,12 +32,11 @@ async fn main() -> std::io::Result<()> {
let script_service = ScriptService::new();
let script = r#"
let items = [ 1, 2, 3, 4]
let items = FIND "rob", "ACTION=EMUL1"
FOR EACH item IN items
PRINT item
NEXT item
let list = FIND "rob", "ACTION=EMUL1"
let text = GET "example.com" "#;
let text = GET "example.com"
PRINT item.name
NEXT item "#;
match script_service.compile(script) {
Ok(ast) => {

View file

@ -57,6 +57,9 @@ After=network.target
User=gbuser
ExecStart=/opt/gbo/bin/coredns -conf /opt/gbo/conf/Corefile
Restart=always
StandardOutput=append:/opt/gbo/logs/stdout.log
StandardError=append:/opt/gbo/logs/stderr.log
[Install]
WantedBy=multi-user.target
EOF2

View file

@ -1,7 +1,9 @@
use smartstring::SmartString;
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;
use serde_json::{json, Value};
use std::collections::HashMap;
pub struct ScriptService {
@ -9,6 +11,47 @@ pub struct ScriptService {
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::<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])
}
}
impl ScriptService {
pub fn new() -> Self {
let mut engine = Engine::new();
@ -18,67 +61,76 @@ impl ScriptService {
engine.set_allow_anonymous_fn(true);
engine.set_allow_looping(true);
use rhai::{Array, Dynamic, Engine, Scope};
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();
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])?;
// Get the block as an expression tree
let block = &inputs[2];
// Convert to array
let array: Array = if collection.is_array() {
collection.cast()
} else {
return Err("FOR EACH can only iterate over arrays".into());
};
// 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.to_string(), item);
// Evaluate the block with the current scope
match context.eval_expression_tree(block) {
Ok(_) => (),
Err(e) if e.to_string() == "EXIT FOR" => break,
Err(e) => {
// Rewind the scope before returning error
context.scope_mut().rewind(orig_len);
return Err(e);
// 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());
}
}
// Remove the loop variable for next iteration
context.scope_mut().rewind(orig_len);
}
// Evaluate the collection expression
let collection = context.eval_expression_tree(&inputs[1])?;
Ok(Dynamic::UNIT)
},
)
.unwrap();
// 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
@ -99,14 +151,37 @@ engine
let table_str = table_name.to_string();
let filter_str = filter.to_string();
use serde_json::json;
let result = json!({
"command": "find",
"table": table_str,
"filter": filter_str,
"results": []
"results": [
{
"id": 1,
"name": "dummy1"
},
{
"id": 2,
"name": "dummy2"
}
]
});
println!("SET executed: {}", result.to_string());
Ok(Dynamic::from(result.to_string()))
if let serde_json::Value::Object(ref obj) = result {
if let Some(results_value) = obj.get("results") {
let dynamic_results = json_value_to_dynamic(results_value);
// Now you can work with it as Dynamic
let array = to_array(dynamic_results);
Ok(Dynamic::from(array))
} else {
Err("No results found".into())
}
} else {
Err("Invalid result format".into())
}
},
)
.unwrap();
@ -272,8 +347,8 @@ engine
result.push_str("}\n");
result.push_str(&" ".repeat(current_indent));
result.push_str(trimmed);
result.push(';');
result.push('\n');
result.push(';');
result.push('\n');
continue;
} else {
panic!("NEXT without matching FOR EACH");
@ -304,6 +379,7 @@ engine
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);
@ -320,7 +396,7 @@ engine
result
}
/// Preprocesses BASIC-style script to handle semicolon-free syntax
pub fn compile(&self, script: &str) -> Result<rhai::AST, Box<EvalAltResult>> {
let processed_script = self.preprocess_basic_script(script);