use std::path::Path;
use std::sync::Arc;
use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use vantage_core::{Result, error};
use vantage_rhai::rhai;
use vantage_rhai::rhai::{Dynamic, Engine, EvalAltResult, Map, Position};
use vantage_rhai::{Block, Compiled, Env, Host, Limits, Vocab};
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 CmdVocab {
pub command: String,
pub env: IndexMap<String, String>,
pub pass_path: bool,
pub base_dir: Option<Arc<Path>>,
}
pub(crate) struct CompiledScript {
script: Compiled<Block>,
}
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 host = Host::builder(Limits::background())
.vocab(CmdVocab {
command,
env,
pass_path,
base_dir,
})
.build();
let script = host.compile_uncached(&Block::from(script)).map_err(|e| {
error!(
"command rhai script failed to compile",
detail = e.to_string()
)
})?;
Ok(Self { script })
}
}
impl Vocab for CmdVocab {
fn register(&self, engine: &mut Engine) {
let command = self.command.clone();
let env = self.env.clone();
let pass_path = self.pass_path;
let base_dir = self.base_dir.clone();
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)
},
);
}
}
impl CompiledScript {
pub(crate) fn eval(&self, ctx: QueryContext) -> Result<Vec<JsonValue>> {
let env = Env::new()
.var("conditions", conditions_dynamic(&ctx.conditions)?)
.var("columns", to_dynamic(&ctx.columns)?)
.var("limit", opt_int(ctx.limit))
.var("offset", opt_int(ctx.offset))
.var("id_column", opt_string(ctx.id_column))
.var("id", opt_string(ctx.id))
.var("row", to_dynamic(&ctx.row)?);
let result: Dynamic = self
.script
.eval(&env)
.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)
}