use super::{Field, FieldGeneration, FieldWriteManagement, Value};
use crate::{
authoring_types::Primitive,
node::{Arg, Item},
};
use darling::{FromMeta, ast::NestedMeta};
use quote::{format_ident, quote};
use syn::parse_quote;
fn field(ident: &str, primitive: Primitive) -> Field {
Field {
name: format_ident!("{ident}"),
value: Value {
item: Item {
primitive: Some(primitive),
unbounded: matches!(primitive, Primitive::Blob | Primitive::Text),
..Item::default()
},
..Value::default()
},
default: None,
generated: None,
write_management: None,
}
}
#[test]
fn relation_fields_require_canonical_identity_suffixes() {
let mut relation = field("user", Primitive::Ulid);
relation.value.item.relation = Some(parse_quote!(User));
relation
.validate()
.expect_err("one relation without the identity suffix must reject");
relation.name = format_ident!("user_id");
relation
.validate()
.expect("canonical relation identity suffix should validate");
}
#[test]
fn rust_default_matching_is_application_only() {
let mut text = field("name", Primitive::Text);
text.default = Some(Arg::FuncPath(parse_quote!(String::new)));
assert!(text.default_matches_implicit_default());
assert_eq!(
text.rust_default_expr()
.expect("default should exist")
.to_string(),
quote!(String::new().into()).to_string(),
);
}
#[test]
fn authored_enum_defaults_preserve_type_and_variant_names() {
let mut status = field("status", Primitive::Unit);
status.value.item.primitive = None;
status.value.item.is = Some(parse_quote!(Status));
status.default = Some(Arg::ConstPath(parse_quote!(Status::Active)));
status
.validate()
.expect("matching unit-enum default should validate");
status.default = Some(Arg::ConstPath(parse_quote!(OtherStatus::Active)));
status
.validate()
.expect_err("a default from another enum must reject");
}
#[test]
fn generated_ulid_is_schema_owned_and_shape_checked() {
let mut id = field("id", Primitive::Ulid);
id.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
Ulid::generate
))));
id.validate()
.expect("the admitted Ulid generator should validate");
id.value.item.primitive = Some(Primitive::Nat64);
id.value.item.unbounded = false;
id.validate()
.expect_err("a generator on the wrong primitive must reject");
}
#[test]
fn generated_identity_accepts_every_exact_unsigned_width() {
for primitive in [
Primitive::Nat8,
Primitive::Nat16,
Primitive::Nat32,
Primitive::Nat64,
Primitive::Nat128,
] {
let mut id = field("id", primitive);
id.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
Identity::next
))));
id.validate()
.expect("exact unsigned identity generator should validate");
assert!(id.is_identity_generated());
assert_eq!(
id.rust_default_expr()
.expect("identity should have a Rust placeholder")
.to_string(),
quote!(Default::default()).to_string(),
);
}
}
#[test]
fn generated_identity_rejects_other_primitive_kinds() {
let mut id = field("id", Primitive::Int64);
id.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
Identity::next
))));
id.validate()
.expect_err("signed identity generation must reject");
id.value.item.primitive = Some(Primitive::Nat64);
id.validate()
.expect("the same generator should admit an unsigned identity");
}
#[test]
fn generated_clause_requires_a_quoted_function_path() {
let generated = FieldGeneration::from_list(&[NestedMeta::Meta(syn::Meta::NameValue(
parse_quote!(insert = "Timestamp::now"),
))])
.expect("a quoted generator path should parse");
let FieldGeneration::Insert(Arg::FuncPath(path)) = generated else {
panic!("generated insert must retain the authored function path");
};
assert_eq!(
quote!(#path).to_string(),
quote!(Timestamp::now).to_string()
);
}
#[test]
fn identity_like_defaults_require_explicit_persisted_literals() {
let mut id = field("id", Primitive::Ulid);
id.default = Some(Arg::FuncPath(parse_quote!(Ulid::default)));
id.validate()
.expect_err("identity-like implicit defaults must reject");
id.default = None;
id.validate()
.expect("the same identity field should admit without an implicit default");
}
#[test]
fn managed_fields_do_not_accept_insert_generators() {
let mut created_at = field("created_at", Primitive::Timestamp);
created_at.write_management = Some(FieldWriteManagement::CreatedAt);
created_at.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
Timestamp::now
))));
created_at
.validate()
.expect_err("managed and generated ownership must not overlap");
created_at.generated = None;
created_at
.validate()
.expect("a managed field without a competing generator should validate");
}
#[test]
fn identity_defaults_reject_invalid_text_during_field_validation() {
for primitive in [
Primitive::Account,
Primitive::Principal,
Primitive::Subaccount,
Primitive::Ulid,
] {
let mut field = field("identity", primitive);
field.default = Some(Arg::String(parse_quote!("zz")));
field
.validate()
.expect_err("malformed identity text must reject before Rust Default generation");
}
}
#[test]
fn subaccount_default_lowering_is_fallible_for_unicode_hex() {
let mut field = field("sub", Primitive::Subaccount);
let text = format!("aƩ{}", "0".repeat(61));
field.default = Some(Arg::String(syn::LitStr::new(
&text,
proc_macro2::Span::call_site(),
)));
assert!(field.rust_default_expr().is_none());
field.validate().expect_err("Unicode is not Subaccount hex");
}
#[test]
fn identity_defaults_share_validation_and_panic_free_construction() {
for (primitive, text) in [
(Primitive::Account, "2vxsx-fae".to_string()),
(Primitive::Principal, "aaaaa-aa".to_string()),
(Primitive::Principal, "2vxsx-fae".to_string()),
(Primitive::Subaccount, "ab".repeat(32)),
(Primitive::Subaccount, format!("0x{}", "AB".repeat(32))),
(Primitive::Ulid, "00000000000000000000000000".to_string()),
(Primitive::Ulid, "7ZZZZZZZZZZZZZZZZZZZZZZZZZ".to_string()),
] {
let mut field = field("identity", primitive);
field.default = Some(Arg::String(syn::LitStr::new(
&text,
proc_macro2::Span::call_site(),
)));
field
.validate()
.expect("valid identity literals must validate");
let expression = field
.rust_default_expr()
.expect("valid literals construct defaults");
syn::parse2::<syn::Expr>(expression).expect("construction must be valid Rust");
}
for text in [
"",
"0x",
"zz",
&"g".repeat(64),
&"0".repeat(63),
&"0".repeat(65),
] {
let mut field = field("sub", Primitive::Subaccount);
field.default = Some(Arg::String(syn::LitStr::new(
text,
proc_macro2::Span::call_site(),
)));
field
.validate()
.expect_err("invalid Subaccount literals must reject");
assert!(field.rust_default_expr().is_none());
}
}