use std::path::Path;
use std::sync::Arc;
use indexmap::IndexMap;
use rhai::{Dynamic, Engine, EvalAltResult, Map, Position, Scope};
use serde_json::Value as JsonValue;
use vantage_core::{Result, error};
use crate::condition::CmdCondition;
use crate::exec::run_command;
pub(crate) struct QueryContext {
pub conditions: Vec<CmdCondition>,
pub columns: Vec<String>,
pub limit: Option<i64>,
pub offset: Option<i64>,
pub id_column: Option<String>,
pub id: Option<String>,
pub row: ciborium::value::Value,
}
fn runtime_err(msg: impl Into<String>) -> Box<EvalAltResult> {
Box::new(EvalAltResult::ErrorRuntime(
Dynamic::from(msg.into()),
Position::NONE,
))
}
fn dynamic_to_arg(d: Dynamic) -> String {
if d.is_string() {
d.into_string().unwrap_or_default()
} else {
d.to_string()
}
}
pub(crate) struct CompiledScript {
engine: Engine,
ast: rhai::AST,
}
impl CompiledScript {
pub(crate) fn compile(
command: String,
env: IndexMap<String, String>,
pass_path: bool,
base_dir: Option<Arc<Path>>,
script: &str,
) -> Result<Self> {
let mut engine = Engine::new();
engine.set_max_expr_depths(256, 256);
engine.register_fn(
"parse_json",
|s: &str| -> std::result::Result<Dynamic, Box<EvalAltResult>> {
let v: JsonValue =
serde_json::from_str(s).map_err(|e| runtime_err(format!("parse_json: {e}")))?;
rhai::serde::to_dynamic(v)
},
);
engine.register_fn(
"parse_jsonl",
|s: &str| -> std::result::Result<Dynamic, Box<EvalAltResult>> {
let mut out = rhai::Array::new();
for line in s.lines().filter(|l| !l.trim().is_empty()) {
let v: JsonValue = serde_json::from_str(line)
.map_err(|e| runtime_err(format!("parse_jsonl: {e}")))?;
out.push(rhai::serde::to_dynamic(v)?);
}
Ok(out.into())
},
);
engine.register_fn(
"run",
move |args: rhai::Array| -> std::result::Result<Map, Box<EvalAltResult>> {
let argv: Vec<String> = args.into_iter().map(dynamic_to_arg).collect();
let out = run_command(&command, &argv, &env, pass_path, base_dir.as_deref())
.map_err(|e| runtime_err(e.to_string()))?;
let err = out.stderr.trim();
if !err.is_empty() {
if out.exit_code != 0 {
tracing::warn!(
target: "vantage_cmd",
command = %command,
args = ?argv,
exit_code = out.exit_code,
"command stderr: {err}",
);
} else {
tracing::debug!(
target: "vantage_cmd",
command = %command,
args = ?argv,
"command stderr: {err}",
);
}
}
let mut map = Map::new();
map.insert("stdout".into(), out.stdout.into());
map.insert("stderr".into(), out.stderr.into());
map.insert("exit_code".into(), (out.exit_code as i64).into());
Ok(map)
},
);
let ast = engine.compile(script).map_err(|e| {
error!(
"command rhai script failed to compile",
detail = e.to_string()
)
})?;
Ok(Self { engine, ast })
}
pub(crate) fn eval(&self, ctx: QueryContext) -> Result<Vec<JsonValue>> {
let mut scope = Scope::new();
scope.push_dynamic("conditions", conditions_dynamic(&ctx.conditions)?);
scope.push_dynamic("columns", to_dynamic(&ctx.columns)?);
scope.push_dynamic("limit", opt_int(ctx.limit));
scope.push_dynamic("offset", opt_int(ctx.offset));
scope.push_dynamic("id_column", opt_string(ctx.id_column));
scope.push_dynamic("id", opt_string(ctx.id));
scope.push_dynamic("row", to_dynamic(&ctx.row)?);
let result: Dynamic = self
.engine
.eval_ast_with_scope(&mut scope, &self.ast)
.map_err(|e| error!("command rhai script failed", detail = e.to_string()))?;
if !result.is_array() {
return Err(error!(
"command rhai script must return an array of rows",
got = result.type_name().to_string()
));
}
let arr = result
.into_array()
.map_err(|t| error!("expected array result", got = t.to_string()))?;
arr.into_iter()
.map(|row| {
serde_json::to_value(&row)
.map_err(|e| error!("failed to convert row to JSON", detail = e.to_string()))
})
.collect()
}
}
fn opt_int(v: Option<i64>) -> Dynamic {
v.map(Dynamic::from).unwrap_or(Dynamic::UNIT)
}
fn opt_string(v: Option<String>) -> Dynamic {
v.map(Dynamic::from).unwrap_or(Dynamic::UNIT)
}
fn to_dynamic<T: serde::Serialize>(v: &T) -> Result<Dynamic> {
rhai::serde::to_dynamic(v)
.map_err(|e| error!("failed to seed rhai scope", detail = e.to_string()))
}
fn conditions_dynamic(conditions: &[CmdCondition]) -> Result<Dynamic> {
let arr: Vec<JsonValue> = conditions
.iter()
.map(|c| {
serde_json::json!({
"field": c.field(),
"op": c.op(),
"value": c.json_value(),
})
})
.collect();
to_dynamic(&arr)
}