use toasty::stmt::{List, Query};
use topcoat::context::Cx;
use crate::{
DeclarationErrorKind,
schema::{ChoiceField, Field, OptionSource, Schema},
test_support::memory_db,
values::FormValue,
};
#[derive(Debug, toasty::Model, Clone)]
struct Country {
#[key]
#[auto]
id: uuid::Uuid,
name: String,
}
#[derive(Debug, toasty::Model, Clone)]
struct City {
#[key]
#[auto]
id: uuid::Uuid,
country_id: uuid::Uuid,
name: String,
}
#[derive(Debug, toasty::Model)]
struct Address {
#[key]
#[auto]
id: uuid::Uuid,
country_id: uuid::Uuid,
city_id: uuid::Uuid,
}
macro_rules! source {
($source:ident, $model:ident $(, $creates:literal)?) => {
struct $source;
impl OptionSource for $source {
type Model = $model;
$(fn creates() -> bool {
$creates
})?
fn label(_cx: &Cx, record: &$model) -> String {
record.name.clone()
}
fn scoped_query(_cx: &Cx) -> topcoat::Result<Query<List<$model>>> {
Ok(Query::all())
}
fn lists(_cx: &Cx) -> bool {
true
}
}
};
}
source!(Countries, Country);
source!(Cities, City);
source!(CreatingCountries, Country, true);
source!(CreatingCities, City, true);
fn country() -> ChoiceField {
Field::choice(Address::fields().country_id()).relationship::<Countries>()
}
fn city(country: &ChoiceField) -> ChoiceField {
Field::choice(Address::fields().city_id())
.relationship::<Cities>()
.depends_on(country, City::fields().country_id())
}
struct World {
cx: Cx,
france: String,
paris: String,
berlin: String,
}
async fn world() -> World {
let mut db = memory_db(toasty::models!(Country, City, Address)).await;
let mut country_with = async |country: &str, city: &str| {
let country = toasty::create!(Country {
name: country.to_string()
})
.exec(&mut db)
.await
.unwrap();
let city = toasty::create!(City {
country_id: country.id,
name: city.to_string(),
})
.exec(&mut db)
.await
.unwrap();
(country.id.to_string(), city.id.to_string())
};
let (france, paris) = country_with("France", "Paris").await;
let (_, berlin) = country_with("Germany", "Berlin").await;
World {
cx: Cx::default().with(db),
france,
paris,
berlin,
}
}
#[tokio::test]
async fn a_dependent_choice_refuses_a_row_of_another_parent_value() {
let world = world().await;
let country = country();
let city = city(&country);
let check = async |value: &str, parent: Option<&str>| {
city.validate_exists(&world.cx, &value.into(), parent, None)
.await
};
assert!(check(&world.paris, Some(&world.france)).await.is_empty());
for parent in [
Some(world.france.as_str()),
Some(""),
None,
Some("not-a-key"),
] {
assert_eq!(
check(&world.berlin, parent).await,
["City id is invalid"],
"parent {parent:?}"
);
}
let mut handle = crate::db::db(&world.cx).unwrap();
let mut tx = handle.transaction().await.unwrap();
assert!(
city.recheck(
&world.cx,
&(&world.paris).into(),
Some(&world.france),
None,
&mut tx
)
.await
.is_empty()
);
assert_eq!(
city.recheck(
&world.cx,
&(&world.berlin).into(),
Some(&world.france),
None,
&mut tx
)
.await,
["City id is invalid"]
);
}
#[test]
fn a_dependent_choice_names_a_column_of_its_source_and_a_placed_parent() {
let misdeclared = |field: ChoiceField| {
let country = country();
let field = field.depends_on(&country, City::fields().country_id());
Schema::new((country, field)).declaration_errors()
};
let error = [DeclarationErrorKind::MisdeclaredDependentChoice {
field: "city_id".to_string(),
}];
assert_eq!(
misdeclared(Field::choice(Address::fields().city_id()).relationship::<Countries>()),
error,
"a column of another model than the source"
);
assert_eq!(
misdeclared(Field::choice(Address::fields().city_id()).options(["a"])),
error,
"no relationship to narrow"
);
assert_eq!(
Schema::new(
Field::choice(Address::fields().city_id())
.options(["a"])
.options_where(|_cx| City::fields().name().eq("Paris"))
)
.declaration_errors(),
[DeclarationErrorKind::UnfilterableChoice {
field: "city_id".to_string(),
}],
"a filter with no relationship to narrow"
);
assert_eq!(
Schema::new(city(&country())).declaration_errors(),
[DeclarationErrorKind::UnplacedParentField {
field: "city_id".to_string(),
parent: "country_id".to_string(),
}]
);
for listed in [city(&country()).radio(), city(&country()).buttons()] {
assert_eq!(
Schema::new((country(), listed)).declaration_errors(),
[DeclarationErrorKind::ListedDependentChoice {
field: "city_id".to_string(),
}]
);
}
let radio = country().radio();
let dependent = city(&radio);
assert!(
Schema::new((radio, dependent))
.declaration_errors()
.is_empty()
);
let country = country();
let city = city(&country);
assert!(Schema::new((country, city)).declaration_errors().is_empty());
}
#[tokio::test]
async fn a_choice_past_the_cap_keeps_its_stored_key() {
let world = world().await;
let france: uuid::Uuid = world.france.parse().unwrap();
let mut db = crate::db::db(&world.cx).unwrap();
for i in 0..crate::schema::MAX_RELATIONSHIP_OPTIONS {
toasty::create!(City {
country_id: france,
name: format!("Town {i}"),
})
.exec(&mut db)
.await
.unwrap();
}
let country = country();
let city = city(&country);
let paris = FormValue::from(world.paris.as_str());
let check = async |stored: Option<&FormValue>| {
city.validate_exists(&world.cx, &paris, Some(&world.france), stored)
.await
};
assert!(check(Some(&paris)).await.is_empty(), "the stored key");
assert_eq!(
check(None).await,
["City id could not load options, retry"],
"a key the record does not store"
);
}
#[tokio::test]
async fn a_searchable_dependent_choice_past_the_cap_checks_among_its_parent_rows() {
let world = world().await;
let france: uuid::Uuid = world.france.parse().unwrap();
let mut db = crate::db::db(&world.cx).unwrap();
for i in 0..crate::schema::MAX_RELATIONSHIP_OPTIONS {
toasty::create!(City {
country_id: france,
name: format!("Town {i}"),
})
.exec(&mut db)
.await
.unwrap();
}
let country = country();
let city = city(&country).searchable();
assert!(
city.validate_exists(&world.cx, &(&world.paris).into(), Some(&world.france), None)
.await
.is_empty()
);
assert_eq!(
city.validate_exists(
&world.cx,
&(&world.berlin).into(),
Some(&world.france),
None
)
.await,
["City id is invalid"]
);
}
#[test]
fn a_creatable_choice_offers_a_resource_s_records() {
let error = |field: &str| {
[DeclarationErrorKind::UncreatableChoice {
field: field.to_string(),
}]
};
assert_eq!(
Schema::new(country().creatable()).declaration_errors(),
error("country_id"),
"a source that is not a resource has no create page"
);
assert_eq!(
Schema::new(
Field::choice(Address::fields().country_id())
.options(["a"])
.creatable()
)
.declaration_errors(),
error("country_id"),
"static options have no related record"
);
}
#[test]
fn only_a_single_independent_enabled_select_is_creatable() {
let creating =
|| Field::choice(Address::fields().country_id()).relationship::<CreatingCountries>();
let refused = [DeclarationErrorKind::UncreatableChoice {
field: "country_id".to_string(),
}];
let errors = |schema: Schema| {
schema
.declaration_errors()
.into_iter()
.filter(|error| matches!(error, DeclarationErrorKind::UncreatableChoice { .. }))
.collect::<Vec<_>>()
};
assert_eq!(errors(Schema::new(creating().creatable())), []);
assert_eq!(
errors(Schema::new(creating().multiple().creatable())),
refused,
"a multiple choice"
);
assert_eq!(
errors(Schema::new(creating().buttons().creatable())),
refused,
"buttons"
);
let shown = Field::choice(Address::fields().city_id()).options(["a", "b"]);
assert_eq!(
errors(Schema::new((
creating().disabled_when(&shown, ["a"]).creatable(),
shown,
))),
refused,
"a choice a condition disables posts nothing"
);
let country = country();
let city = Field::choice(Address::fields().city_id())
.relationship::<CreatingCities>()
.depends_on(&country, City::fields().country_id())
.creatable();
assert_eq!(
errors(Schema::new((country, city))),
[DeclarationErrorKind::UncreatableChoice {
field: "city_id".to_string(),
}],
"a dependent choice"
);
}