use toasty::schema::Model;
use topcoat::context::CxTestBuilder;
use super::*;
use crate::test_support::DummyUser;
#[test]
fn single_segment_lens_passes_traversal_is_refused() {
use toasty::schema::Model;
let single = toasty_core::stmt::Path::field(DummyUser::id(), 0);
assert_eq!(single_segment(&single), Ok(0));
let mut two = toasty_core::stmt::Path::field(DummyUser::id(), 0);
two.chain(&toasty_core::stmt::Path::field(DummyUser::id(), 1));
assert_eq!(
single_segment(&two),
Err(DeclarationErrorKind::TraversalLens { steps: 2 })
);
}
#[test]
fn lens_field_resolves_name_label_and_nullability() {
use toasty::schema::Model;
let model = DummyUser::schema();
let email = lens_field(DummyUser::fields().email(), &model).unwrap();
assert_eq!(email.name.app_unwrap(), "email");
assert_eq!(lens_label(&email), "Email");
assert!(!email.nullable());
assert_eq!(email.name.storage_name(), Some("email"));
let name = lens_field(DummyUser::fields().name(), &model).unwrap();
assert_eq!(name.name.app_unwrap(), "name");
assert_eq!(lens_label(&name), "Name");
}
#[test]
fn lens_field_unique_reads_the_model_index_list() {
use toasty::schema::Model;
let model = DummyUser::schema();
let root = model.as_root_unwrap();
let email = lens_field(DummyUser::fields().email(), &model).unwrap();
assert!(
lens_field_unique(&email, root),
"#[unique] on email must surface as a single-field unique index"
);
let name = lens_field(DummyUser::fields().name(), &model).unwrap();
assert!(
!lens_field_unique(&name, root),
"a field with no unique index must not report unique"
);
let id = lens_field(DummyUser::fields().id(), &model).unwrap();
assert!(
!lens_field_unique(&id, root),
"the primary key is unique by construction, not by declared constraint"
);
}
#[derive(Debug, Clone, toasty::Embed)]
struct Seo {
title: String,
description: String,
}
#[derive(Debug, Clone, toasty::Embed)]
struct Credit {
author: String,
}
#[derive(Debug, Clone, toasty::Embed)]
struct Poster {
url: String,
credit: Credit,
}
#[derive(Debug, Clone, toasty::Embed)]
enum Media {
#[column(variant = 1)]
Image { url: String, alt: String },
#[column(variant = 2)]
Video { video_url: String, poster: Poster },
}
#[derive(Debug, Clone, toasty::Embed)]
enum Publication {
#[column(variant = 1)]
Scheduled {
#[shared(timestamp)]
scheduled_at: String,
scheduled_for: String,
},
#[column(variant = 2)]
Published {
#[shared(timestamp)]
published_at: String,
canonical_url: String,
},
}
#[derive(Debug, Clone, toasty::Embed)]
struct Wrapper {
label: String,
inner: Media,
}
#[derive(Debug, Clone, toasty::Embed)]
struct Stats {
word_count: i64,
read_minutes: i64,
}
#[derive(Debug, Clone, toasty::Model)]
struct LensPost {
#[key]
#[auto]
id: uuid::Uuid,
title: String,
seo: Seo,
media: Media,
publication: Publication,
wrapper: Wrapper,
#[document]
stats: Stats,
}
#[derive(Debug, Clone, toasty::Model)]
struct Impostor {
#[key]
#[auto]
id: uuid::Uuid,
seo: Seo,
wrapper: Wrapper,
}
async fn cx_with(models: toasty::schema::ModelSet) -> Cx {
let db = toasty::Db::builder()
.models(models)
.connect("sqlite::memory:")
.await
.expect("connect");
CxTestBuilder::new().app_context(db).build()
}
async fn lens_cx() -> Cx {
cx_with(toasty::models!(LensPost)).await
}
#[tokio::test]
async fn a_plain_leaf_resolves_through_the_app_field() {
let cx = lens_cx().await;
let leaf = FieldResolver::new(schema_of(&cx))
.resolve(LensPost::fields().title())
.unwrap();
assert_eq!(leaf.name, "title");
assert_eq!(leaf.label, "Title");
assert!(
!leaf.nullable,
"a required top-level column is not storage-nullable"
);
}
#[tokio::test]
async fn a_leaf_in_an_embedded_struct_resolves_to_its_flattened_column() {
let cx = lens_cx().await;
let leaf = FieldResolver::new(schema_of(&cx))
.resolve(LensPost::fields().seo().title())
.unwrap();
assert_eq!(leaf.name, "seo_title");
assert_eq!(
leaf.label, "Seo title",
"a label reads the storage name as prose"
);
assert!(
leaf.nullable,
"an embedded leaf is never required by default"
);
}
#[tokio::test]
async fn a_leaf_in_a_struct_nested_in_an_enum_variant_resolves_to_one_column() {
let cx = lens_cx().await;
let resolver = FieldResolver::new(schema_of(&cx));
assert_eq!(
resolver
.resolve(LensPost::fields().media().image().url())
.unwrap()
.name,
"media_url"
);
assert_eq!(
resolver
.resolve(LensPost::fields().media().video().poster().url())
.unwrap()
.name,
"media_poster_url"
);
let leaf = resolver
.resolve(
LensPost::fields()
.media()
.video()
.poster()
.credit()
.author(),
)
.unwrap();
assert_eq!(leaf.name, "media_poster_credit_author");
assert_eq!(leaf.label, "Media poster credit author");
assert!(leaf.nullable);
}
#[tokio::test]
async fn a_variant_rooted_path_through_an_embedded_struct_resolves() {
let cx = lens_cx().await;
let resolver = FieldResolver::new(schema_of(&cx));
assert_eq!(
resolver
.resolve(LensPost::fields().wrapper().inner().image().url())
.unwrap()
.name,
"wrapper_inner_url"
);
assert_eq!(
resolver
.resolve(LensPost::fields().wrapper().inner().video().video_url())
.unwrap()
.name,
"wrapper_inner_video_url"
);
}
#[tokio::test]
async fn a_shared_column_resolves_for_every_variant_that_declares_it() {
let cx = lens_cx().await;
let resolver = FieldResolver::new(schema_of(&cx));
assert_eq!(
resolver
.resolve(LensPost::fields().publication().scheduled().scheduled_at())
.unwrap()
.name,
"publication_timestamp"
);
assert_eq!(
resolver
.resolve(LensPost::fields().publication().published().published_at())
.unwrap()
.name,
"publication_timestamp"
);
assert_eq!(
resolver
.resolve(LensPost::fields().publication().published().canonical_url())
.unwrap()
.name,
"publication_canonical_url"
);
}
#[tokio::test]
async fn a_document_leaf_resolves_to_the_document_column() {
let cx = lens_cx().await;
let leaf = FieldResolver::new(schema_of(&cx))
.resolve(LensPost::fields().stats().word_count())
.unwrap();
assert_eq!(leaf.name, "stats");
assert_eq!(leaf.label, "Stats");
assert!(
leaf.nullable,
"a document leaf is never required by default either"
);
}
#[tokio::test]
async fn a_root_model_the_schema_does_not_carry_resolves_to_nothing() {
let cx = cx_with(toasty::models!(Impostor)).await;
let resolver = FieldResolver::new(schema_of(&cx));
assert!(resolver.has_schema(), "the Db carries the app schema");
assert!(
schema_of(&cx)
.expect("the Db carries the app schema")
.app
.get_model(<LensPost as Model>::id())
.is_none(),
"this schema carries no model under the lens's id"
);
assert!(
resolver
.resolve_enum(LensPost::fields().publication().into())
.is_none(),
"an enum cannot resolve against a schema that has no LensPost"
);
}
#[tokio::test]
async fn a_root_model_the_schema_does_not_carry_refuses_a_leaf_lens() {
let cx = cx_with(toasty::models!(Impostor)).await;
let error = FieldResolver::new(schema_of(&cx))
.resolve(LensPost::fields().seo().title())
.expect_err("a lens the schema cannot bind is refused");
assert!(
matches!(error, DeclarationErrorKind::UnresolvedLens { .. }),
"{error}"
);
}
#[tokio::test]
async fn an_embedded_enum_resolves_its_discriminant_and_variants() {
let cx = lens_cx().await;
let shape = FieldResolver::new(schema_of(&cx))
.resolve_enum(LensPost::fields().publication().into())
.expect("an embedded enum resolves");
assert_eq!(shape.discriminant, "publication");
assert_eq!(
shape.variants,
[
("1".to_string(), "Scheduled".to_string()),
("2".to_string(), "Published".to_string()),
]
);
}
#[tokio::test]
async fn an_enum_inside_a_struct_resolves_through_its_path() {
let cx = lens_cx().await;
let shape = FieldResolver::new(schema_of(&cx))
.resolve_enum(LensPost::fields().wrapper().inner().into())
.expect("the nested enum resolves");
assert_eq!(shape.discriminant, "wrapper_inner");
}
#[tokio::test]
async fn anything_but_an_enum_resolves_to_nothing() {
let cx = lens_cx().await;
let resolver = FieldResolver::new(schema_of(&cx));
assert!(
resolver
.resolve_enum(LensPost::fields().seo().into())
.is_none(),
"a struct has no variant"
);
assert!(
resolver.resolve_enum(LensPost::fields().title()).is_none(),
"a primitive field is a leaf"
);
assert!(
resolver
.resolve_enum(LensPost::fields().stats().into())
.is_none(),
"a #[document] stores as one column"
);
assert!(
resolver
.resolve_enum(LensPost::fields().media().video().video_url())
.is_none(),
"a variant-rooted path names one variant, not the value"
);
}
#[test]
fn without_a_schema_there_is_no_walk() {
let cx = CxTestBuilder::new().build();
let resolver = FieldResolver::new(schema_of(&cx));
assert!(!resolver.has_schema(), "a bare Cx carries no Db");
assert!(
resolver
.resolve_enum(LensPost::fields().publication().into())
.is_none(),
"an enum needs the app schema"
);
}