use serde::{Deserialize, Serialize};
use vantage_core::{Result, error};
use vantage_vista::{NoExtras, VistaSpec};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MongoTableExtras {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mongo: Option<MongoBlock>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MongoBlock {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collection: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MongoColumnExtras {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mongo: Option<MongoColumnBlock>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MongoColumnBlock {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nested_path: Option<String>,
}
impl MongoColumnBlock {
pub fn resolved_path(&self, column_name: &str) -> Result<Option<Vec<String>>> {
if let Some(p) = &self.nested_path {
let segments: Vec<String> = p.split('.').map(str::to_string).collect();
if segments.iter().any(String::is_empty) {
return Err(error!(
"nested_path must not be empty or contain empty segments",
column = column_name.to_string(),
nested_path = p.clone()
));
}
return Ok(Some(segments));
}
if let Some(f) = &self.field {
if f.is_empty() {
return Err(error!(
"field must not be empty",
column = column_name.to_string()
));
}
return Ok(Some(vec![f.clone()]));
}
Ok(None)
}
}
pub type MongoVistaSpec = VistaSpec<MongoTableExtras, MongoColumnExtras, NoExtras>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn yaml_parses_into_mongo_vista_spec() {
let yaml = r#"
name: client
columns:
_id:
type: object_id
flags: [id]
name:
type: string
flags: [title, searchable]
is_paying_client:
type: bool
mongo:
collection: clients
"#;
let spec: MongoVistaSpec = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(spec.name, "client");
assert_eq!(spec.columns.len(), 3);
assert_eq!(
spec.driver
.mongo
.as_ref()
.and_then(|m| m.collection.as_deref()),
Some("clients")
);
assert_eq!(spec.columns["_id"].col_type.as_deref(), Some("object_id"));
assert!(spec.columns["name"].flags.contains(&"title".to_string()));
}
#[test]
fn yaml_rejects_unknown_mongo_block_field() {
let yaml = r#"
name: client
columns:
_id: { type: object_id, flags: [id] }
mongo:
collection: clients
bogus: 1
"#;
let err = serde_yaml_ng::from_str::<MongoVistaSpec>(yaml).unwrap_err();
assert!(err.to_string().contains("bogus") || err.to_string().contains("unknown"));
}
#[test]
fn yaml_collection_defaults_to_spec_name_when_block_omitted() {
let yaml = r#"
name: clients
columns:
_id: { type: object_id, flags: [id] }
"#;
let spec: MongoVistaSpec = serde_yaml_ng::from_str(yaml).unwrap();
assert!(spec.driver.mongo.is_none());
}
#[test]
fn resolved_path_rejects_empty_nested_path() {
let block = MongoColumnBlock {
field: None,
nested_path: Some(String::new()),
};
let err = block
.resolved_path("city")
.expect_err("empty nested_path must error");
let msg = err.to_string();
assert!(msg.contains("nested_path"), "error: {msg}");
assert!(msg.contains("city"), "error: {msg}");
}
#[test]
fn resolved_path_rejects_nested_path_with_empty_segments() {
for bad in ["a..b", ".a", "a.", "."] {
let block = MongoColumnBlock {
field: None,
nested_path: Some(bad.to_string()),
};
assert!(
block.resolved_path("city").is_err(),
"path `{bad}` should have errored"
);
}
}
#[test]
fn resolved_path_rejects_empty_field() {
let block = MongoColumnBlock {
field: Some(String::new()),
nested_path: None,
};
let err = block
.resolved_path("name")
.expect_err("empty field must error");
let msg = err.to_string();
assert!(msg.contains("field"), "error: {msg}");
assert!(msg.contains("name"), "error: {msg}");
}
#[test]
fn resolved_path_accepts_valid_nested_and_field() {
let block = MongoColumnBlock {
field: None,
nested_path: Some("address.city".into()),
};
assert_eq!(
block.resolved_path("city").unwrap(),
Some(vec!["address".into(), "city".into()])
);
let block = MongoColumnBlock {
field: Some("fullName".into()),
nested_path: None,
};
assert_eq!(
block.resolved_path("full_name").unwrap(),
Some(vec!["fullName".into()])
);
let block = MongoColumnBlock::default();
assert_eq!(block.resolved_path("anything").unwrap(), None);
}
}