use std::sync::{Arc, LazyLock};
use bson::{Bson, Document};
use vantage_core::{Context, Result, error};
use vantage_rhai::rhai::{Array, Dynamic, Map};
use vantage_rhai::{Block, Env, Host, Limits};
use vantage_vista::{DataVocab, TableShell, TargetResolver, Vista, VistaFactory};
use crate::rhai_engine::{MongoVocab, dynamic_to_bson};
use crate::vista::factory::MongoVistaFactory;
use crate::vista::spec::pipeline::{check_stages, not_a_stage};
fn pipeline_host() -> &'static Host {
static HOST: LazyLock<Host> = LazyLock::new(|| {
Host::builder(Limits::background())
.vocab(MongoVocab)
.build()
});
&HOST
}
pub(crate) fn eval_pipeline(code: &str, args: &[(String, String)]) -> Result<Vec<Document>> {
let mut map = Map::new();
for (k, v) in args {
map.insert(k.as_str().into(), v.clone().into());
}
let env = Env::new().var("args", Dynamic::from_map(map));
let value = pipeline_host()
.compile(&Block::from(code))
.and_then(|script| script.eval(&env))
.context(error!("rhai pipeline script failed"))?;
let type_name = value.type_name();
let items = value.try_cast::<Array>().ok_or_else(|| {
error!(
"rhai pipeline script must evaluate to an array of stages",
got = type_name
)
})?;
let stages = items
.into_iter()
.enumerate()
.map(|(index, stage)| {
match dynamic_to_bson(stage)
.with_context(|| error!("pipeline stage is not BSON", stage = index))?
{
Bson::Document(doc) => Ok(doc),
_ => Err(not_a_stage(index)),
}
})
.collect::<Result<Vec<_>>>()?;
check_stages(&stages)?;
Ok(stages)
}
pub(crate) fn apply_modify(factory: &MongoVistaFactory, vista: Vista, code: &str) -> Result<Vista> {
let host = script_host(&*vista.source, factory.clone());
vantage_vista::eval_modify_script(&host, code, vista)
}
pub(crate) fn script_host(shell: &dyn TableShell, factory: MongoVistaFactory) -> Host {
Host::builder(Limits::background())
.vocab_fn(|engine| shell.register_rhai_extensions(engine))
.vocab(DataVocab::describe(Some(target_resolver(factory))))
.build()
}
fn target_resolver(factory: MongoVistaFactory) -> TargetResolver {
Arc::new(move |name| factory.build_from_spec(factory.script_spec(name)?))
}
#[cfg(test)]
mod tests {
use super::*;
use bson::doc;
#[test]
fn pipeline_script_sees_args() {
let code = r#"
let stages = [];
if args.cat != "" { stages.push(#{"$match": #{category: args.cat}}); }
stages.push(#{"$sort": doc([["n", -1], ["_id", 1]])});
stages
"#;
let stages = eval_pipeline(code, &[("cat".into(), "cake".into())]).unwrap();
assert_eq!(
stages,
vec![
doc! { "$match": { "category": "cake" } },
doc! { "$sort": { "n": -1_i64, "_id": 1_i64 } },
]
);
let stages = eval_pipeline(code, &[("cat".into(), String::new())]).unwrap();
assert_eq!(stages.len(), 1);
}
#[test]
fn pipeline_script_must_return_stages() {
let err = eval_pipeline(r#"#{"$match": #{}}"#, &[]).unwrap_err();
assert!(err.to_string().contains("array of stages"), "{err}");
let err = eval_pipeline("[]", &[]).unwrap_err();
assert!(err.to_string().contains("at least one stage"), "{err}");
let err = eval_pipeline(r#"[#{"$match": #{}}, 1]"#, &[]).unwrap_err();
assert!(err.to_string().contains("stage: 1"), "{err}");
}
#[test]
fn pipeline_script_is_bounded() {
let err = eval_pipeline("loop {}", &[]).unwrap_err();
assert!(format!("{err:?}").contains("limit"), "{err:?}");
}
}