use ciborium::Value as CborValue;
use vantage_types::Record;
use crate::{Column, JoinKey, Reference, Vista, VistaMetadata, mocks::MockShell};
fn client_vista() -> Vista {
client_vista_over(MockShell::new(), MockShell::new())
}
fn client_vista_over(bakeries: MockShell, orders: MockShell) -> Vista {
let metadata = VistaMetadata::new()
.with_column(Column::new("id", "String").with_flag("id"))
.with_column(Column::new("name", "String"))
.with_id_column("id")
.with_reference(Reference::has_one(
"bakery",
"bakery",
JoinKey::new("bakery_id", "id"),
))
.with_reference(Reference::has_many(
"orders",
"order",
JoinKey::new("id", "client_id"),
));
let shell = MockShell::new()
.with_metadata(metadata)
.with_ref_target("bakery", bakeries)
.with_ref_target("orders", orders);
Vista::new("client", shell)
}
fn text(s: &str) -> CborValue {
CborValue::Text(s.into())
}
fn map(pairs: &[(&str, CborValue)]) -> CborValue {
CborValue::Map(
pairs
.iter()
.map(|(k, v)| (CborValue::Text((*k).into()), v.clone()))
.collect(),
)
}
fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
pairs
.iter()
.map(|(k, v)| ((*k).into(), v.clone()))
.collect()
}
#[test]
fn flat_record_has_no_relations() {
let (main, has_one, has_many) = client_vista()
.classify_insert(&record(&[
("name", text("John")),
("bakery_id", text("b1")),
]))
.unwrap();
assert_eq!(main.get("name"), Some(&text("John")));
assert_eq!(main.get("bakery_id"), Some(&text("b1")));
assert!(has_one.is_empty());
assert!(has_many.is_empty());
}
#[test]
fn dotted_keys_group_into_one_has_one_child() {
let (main, has_one, has_many) = client_vista()
.classify_insert(&record(&[
("name", text("John")),
("bakery.name", text("New Bakery")),
("bakery.profit_margin", CborValue::Integer(10.into())),
]))
.unwrap();
assert_eq!(main.get("name"), Some(&text("John")));
assert!(main.get("bakery.name").is_none());
assert!(has_many.is_empty());
assert_eq!(has_one.len(), 1);
let (relation, fk, child) = &has_one[0];
assert_eq!(relation, "bakery");
assert_eq!(fk, "bakery_id");
assert_eq!(child.get("name"), Some(&text("New Bakery")));
assert_eq!(
child.get("profit_margin"),
Some(&CborValue::Integer(10.into()))
);
}
#[test]
fn bare_map_is_a_has_one_child() {
let (_main, has_one, _has_many) = client_vista()
.classify_insert(&record(&[("bakery", map(&[("name", text("New Bakery"))]))]))
.unwrap();
assert_eq!(has_one.len(), 1);
assert_eq!(has_one[0].2.get("name"), Some(&text("New Bakery")));
}
#[test]
fn bare_list_is_a_has_many_group() {
let (_main, has_one, has_many) = client_vista()
.classify_insert(&record(&[(
"orders",
CborValue::Array(vec![
map(&[("total", CborValue::Integer(1.into()))]),
map(&[("total", CborValue::Integer(2.into()))]),
]),
)]))
.unwrap();
assert!(has_one.is_empty());
assert_eq!(has_many.len(), 1);
let (relation, fk, children) = &has_many[0];
assert_eq!(relation, "orders");
assert_eq!(fk, "client_id");
assert_eq!(children.len(), 2);
}
#[test]
fn bare_scalar_links_an_existing_record_via_the_foreign_key() {
let (main, has_one, has_many) = client_vista()
.classify_insert(&record(&[
("name", text("John")),
("bakery", text("bakery:hill_valley")),
]))
.unwrap();
assert!(has_one.is_empty());
assert!(has_many.is_empty());
assert!(main.get("bakery").is_none());
assert_eq!(main.get("bakery_id"), Some(&text("bakery:hill_valley")));
}
#[test]
fn a_link_and_child_data_for_one_relation_is_an_error() {
let scalar_and_dotted = client_vista().classify_insert(&record(&[
("bakery", text("bakery:hill_valley")),
("bakery.name", text("New Bakery")),
]));
let err = scalar_and_dotted.unwrap_err().to_string();
assert!(err.contains("both a record link and child data"), "{err}");
let fk_and_map = client_vista().classify_insert(&record(&[
("bakery_id", text("b1")),
("bakery", map(&[("name", text("New Bakery"))])),
]));
assert!(fk_and_map.is_err());
assert!(
client_vista()
.classify_insert(&record(&[("bakery", text("bakery:hill_valley"))]))
.is_ok()
);
assert!(
client_vista()
.classify_insert(&record(&[("bakery", map(&[("name", text("New"))]))]))
.is_ok()
);
}
#[test]
fn has_many_given_a_scalar_is_an_error() {
let err = client_vista().classify_insert(&record(&[("orders", text("order:1"))]));
assert!(err.is_err());
}
#[test]
fn has_one_given_a_list_is_an_error() {
let err =
client_vista().classify_insert(&record(&[("bakery", CborValue::Array(vec![map(&[])]))]));
assert!(err.is_err());
}
#[test]
fn has_many_given_a_map_is_an_error() {
let err = client_vista().classify_insert(&record(&[("orders", map(&[("total", text("x"))]))]));
assert!(err.is_err());
}
#[tokio::test]
async fn nested_insert_writes_and_links_children() {
use vantage_dataset::traits::{InsertableValueSet, ReadableValueSet};
let (bakeries, orders) = (MockShell::new(), MockShell::new());
let clients = client_vista_over(bakeries.clone(), orders.clone());
let id = clients
.insert_return_id_value(&record(&[
("name", text("John")),
("bakery.name", text("Hill Valley")),
(
"orders",
CborValue::Array(vec![map(&[("total", text("9"))])]),
),
]))
.await
.unwrap();
let client = clients.get_value(&id).await.unwrap().unwrap();
let bakery_id = client
.get("bakery_id")
.cloned()
.expect("has-one link stamped");
assert_eq!(bakeries.len(), 1);
assert_eq!(
Some(bakery_id),
bakeries.record_ids().first().cloned().map(CborValue::Text)
);
let order = orders.get_record(&orders.record_ids()[0]).unwrap();
assert_eq!(order.get("client_id"), Some(&text(&id)));
}
#[test]
fn a_contained_relation_is_a_plain_field() {
let metadata = VistaMetadata::new().with_reference(Reference::contains_many("lines", "lines"));
let vista = Vista::new("order", MockShell::new().with_metadata(metadata));
let lines = CborValue::Array(vec![map(&[("qty", CborValue::Integer(1.into()))])]);
let (main, has_one, has_many) = vista
.classify_insert(&record(&[("lines", lines.clone())]))
.unwrap();
assert_eq!(main.get("lines"), Some(&lines));
assert!(has_one.is_empty() && has_many.is_empty());
}