use bson::Document;
use vantage_core::{Result, VantageError, error};
use vantage_vista::VistaSpec;
use super::{MongoBlock, MongoTableExtras};
impl MongoBlock {
pub fn pipeline_stages(&self) -> Result<Option<Vec<Document>>> {
let Some(stages) = &self.pipeline else {
return Ok(None);
};
let stages = stages
.iter()
.enumerate()
.map(|(index, stage)| {
if !stage.is_object() {
return Err(not_a_stage(index));
}
bson::to_document(stage).map_err(|e| {
error!(
"pipeline stage is not a BSON document",
stage = index,
details = e.to_string()
)
})
})
.collect::<Result<Vec<_>>>()?;
check_stages(&stages)?;
Ok(Some(stages))
}
}
const WRITING_STAGES: [&str; 2] = ["$out", "$merge"];
pub(crate) fn check_stages(stages: &[Document]) -> Result<()> {
if stages.is_empty() {
return Err(error!("pipeline must have at least one stage"));
}
for (index, stage) in stages.iter().enumerate() {
if stage.len() != 1 || !stage.keys().all(|k| k.starts_with('$')) {
return Err(not_a_stage(index));
}
if let Some(op) = stage.keys().find(|k| WRITING_STAGES.contains(&k.as_str())) {
return Err(error!(
"a pipeline view is read-only and cannot write with `$out` or `$merge`",
stage = index,
operator = op.as_str()
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use bson::doc;
use super::*;
#[test]
fn writing_stages_are_refused() {
for writer in [
doc! { "$out": "copy" },
doc! { "$merge": { "into": "copy" } },
] {
let stages = vec![doc! { "$match": {} }, writer.clone()];
let msg = check_stages(&stages).unwrap_err().to_string();
assert!(msg.contains("read-only"), "{writer}: {msg}");
assert!(msg.contains("stage: 1"), "{writer}: {msg}");
}
assert!(check_stages(&[doc! { "$match": {} }]).is_ok());
}
}
pub(crate) fn not_a_stage(index: usize) -> VantageError {
error!(
"pipeline stage must be a map with one `$` operator key",
stage = index
)
}
pub trait DriverBlockArgs {
fn driver_block_args(&self) -> Vec<(String, String)>;
}
impl<C, R> DriverBlockArgs for VistaSpec<MongoTableExtras, C, R> {
fn driver_block_args(&self) -> Vec<(String, String)> {
self.driver
.mongo
.as_ref()
.map(|b| b.args.clone())
.unwrap_or_default()
}
}