use crate::{aggregate_root, entity, value_object};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Result};
type Expander = fn(DeriveInput) -> Result<TokenStream>;
fn rejects(expand: Expander, cases: &[(&str, &str)]) {
for (source, expected) in cases {
let input = syn::parse_str(source).unwrap();
let error = expand(input).unwrap_err().to_string();
assert!(error.contains(expected), "{source}: {error}");
}
}
#[test]
fn value_object_expands_all_shapes_with_validation_and_serde() {
let shapes = [
quote!(
struct Value<T>(T);
),
quote!(
struct Value<T> {
value: T,
}
),
quote!(
struct Value<T>(T, u32);
),
quote!(
struct Value<T> {
value: T,
count: u32,
}
),
quote!(
enum Value<T> {
Unit,
Newtype(T),
Tuple(T, u32),
Named { value: T },
}
),
];
for shape in shapes {
for validated in [false, true] {
for serde in [false, true] {
let attributes = match (validated, serde) {
(false, false) => quote!(),
(true, false) => quote!(#[value_object(validate = check, error = Error)]),
(false, true) => quote!(#[value_object(serde)]),
(true, true) => quote!(#[value_object(validate = check, error = Error, serde)]),
};
let input = syn::parse2(quote!(#attributes #shape)).unwrap();
let output = value_object::expand(input).unwrap().to_string();
for implementation in [
"ValueObject for Value",
"Debug for Value",
"Clone for Value",
"Hash for Value",
"as_inner",
"into_inner",
] {
assert!(
output.contains(implementation),
"{implementation}: {output}"
);
}
assert_eq!(output.contains("try_new"), validated);
assert_eq!(output.contains("Serialize for Value"), serde);
assert_eq!(output.contains("Deserialize <"), serde);
if validated {
assert!(output.contains("check (&"));
if serde {
assert!(output.contains("value object validation failed"));
}
}
}
}
}
}
#[test]
fn trait_only_generates_exactly_one_trait_impl() {
for (expand, source, expected_inner) in [
(
value_object::expand as Expander,
"#[value_object(trait_only)] struct V<T>(T);",
Some("T"),
),
(
value_object::expand,
"#[value_object(trait_only)] struct V<T> { value: T }",
Some("T"),
),
(
value_object::expand,
"#[value_object(trait_only)] struct V<T>(T, u32);",
Some("Self"),
),
(
value_object::expand,
"#[value_object(trait_only)] struct V<T> { value: T, count: u32 }",
Some("Self"),
),
(
value_object::expand,
"#[value_object(trait_only)] enum V<T> { AB, Ab, Value(T) }",
Some("Self"),
),
(
entity::expand,
"#[entity(trait_only)] struct V<T> where T: Eq { #[entity(id)] key: T, state: u32 }",
None,
),
] {
let output = expand(syn::parse_str(source).unwrap()).unwrap();
let impl_count = output
.clone()
.into_iter()
.filter(
|token| matches!(token, proc_macro2::TokenTree::Ident(ident) if ident == "impl"),
)
.count();
assert_eq!(impl_count, 1, "{source}");
let output = output.to_string();
assert!(output.contains("for V"));
if let Some(inner) = expected_inner {
assert!(output.contains(&format!("type Inner = {inner}")));
} else {
assert!(output.contains("type Id = T"));
assert!(output.contains("where T : Eq"));
}
}
}
#[test]
fn trait_only_rejects_invalid_options_and_shapes() {
rejects(
value_object::expand,
&[
(
"#[value_object(trait_only, trait_only)] struct V(u32);",
"duplicate trait_only",
),
(
"#[value_object(trait_only = true)] struct V(u32);",
"trait_only is a flag",
),
(
"#[value_object(trait_only, validate = check, error = Error)] struct V(u32);",
"cannot be combined",
),
(
"#[value_object(validate = check, error = Error, trait_only)] struct V(u32);",
"cannot be combined",
),
(
"#[value_object(trait_only, serde)] struct V(u32);",
"cannot be combined",
),
(
"#[value_object(trait_only, error = Error)] struct V(u32);",
"cannot be combined",
),
(
"#[value_object(trait_only)] struct V;",
"at least one field",
),
(
"#[value_object(trait_only)] enum V {}",
"at least one variant",
),
(
"#[value_object(trait_only)] union V { a: u32 }",
"struct or enum",
),
],
);
rejects(
entity::expand,
&[
(
"#[entity(trait_only, trait_only)] struct V { #[entity(id)] id: u32 }",
"duplicate trait_only",
),
(
"#[entity(trait_only)] #[entity(trait_only)] struct V { #[entity(id)] id: u32 }",
"duplicate trait_only",
),
(
"#[entity(trait_only = true)] struct V { #[entity(id)] id: u32 }",
"trait_only is a flag",
),
(
"#[entity()] struct V { #[entity(id)] id: u32 }",
"requires the trait_only flag",
),
("#[entity(trait_only)] struct V { id: u32 }", "exactly one"),
(
"#[entity(trait_only)] struct V { #[entity(id)] a: u32, #[entity(id)] b: u32 }",
"exactly one",
),
("#[entity(trait_only)] struct V(u32);", "named-field struct"),
(
"struct V { #[entity(trait_only)] id: u32 }",
"unknown entity attribute",
),
],
);
}
#[test]
fn serde_avoids_existing_deserialization_lifetimes() {
let input = syn::parse2(quote! {
#[value_object(serde)]
struct Borrowed<'__ddd_de, '__ddd_dex, T>(&'__ddd_de T, &'__ddd_dex T);
})
.unwrap();
let output = value_object::expand(input).unwrap().to_string();
assert!(output.contains("'__ddd_dexx"));
assert!(output.contains("'__ddd_de"));
}
#[test]
fn value_object_reports_invalid_shapes_and_options() {
rejects(
value_object::expand,
&[
("struct Empty;", "at least one field"),
("struct Empty {}", "at least one field"),
("struct Empty();", "at least one field"),
("enum Empty {}", "at least one variant"),
("union Value { a: u32 }", "struct or enum"),
("enum Value { AB, Ab }", "unique after lower-casing"),
("#[value_object()] struct V(u32);", "at least one option"),
(
"#[value_object(other)] struct V(u32);",
"unknown value_object attribute",
),
(
"#[value_object(serde = true)] struct V(u32);",
"serde is a flag",
),
(
"#[value_object(serde)] #[value_object(serde)] struct V(u32);",
"only once",
),
(
"#[value_object(validate = check)] struct V(u32);",
"specified together",
),
(
"#[value_object(error = Error)] struct V(u32);",
"specified together",
),
(
"#[value_object(validate = a, validate = b)] struct V(u32);",
"duplicate validate",
),
(
"#[value_object(error = A, error = B)] struct V(u32);",
"duplicate error",
),
(
"#[value_object(validate = check, error = Error)] struct V(pub u32);",
"inner fields must not be public",
),
(
"#[value_object(validate = check, error = Error)] struct V { pub a: u32, b: u32 }",
"fields must not be public",
),
],
);
}
#[test]
fn entity_expansion_uses_only_marked_identity() {
let input = syn::parse2(quote! {
struct Item<T> { #[allow(dead_code)] payload: T, #[entity(id)] key: u64 }
})
.unwrap();
let output = entity::expand(input).unwrap().to_string();
assert!(output.contains("type Id = u64"));
assert!(output.contains("self . key == other . key"));
assert!(output.contains("Hash :: hash (& self . key"));
assert!(!output.contains("self . payload"));
}
#[test]
fn entity_reports_attribute_and_shape_errors() {
rejects(
entity::expand,
&[
(
"#[entity(id)] struct V { id: u64 }",
"placed on a named field",
),
("struct V(u64);", "requires a named-field struct"),
("enum V { A }", "only be derived for a named-field struct"),
("struct V { id: u64 }", "exactly one"),
(
"struct V { #[entity(id)] a: u64, #[entity(id)] b: u64 }",
"exactly one",
),
("struct V { #[entity()] a: u64 }", "requires the id flag"),
(
"struct V { #[entity(other)] a: u64 }",
"unknown entity attribute",
),
("struct V { #[entity(id = true)] a: u64 }", "id is a flag"),
(
"struct V { #[entity(id)] #[entity(id)] a: u64 }",
"duplicate id",
),
],
);
}
#[test]
fn aggregate_expansion_preserves_event_type_and_drains_queue() {
for queue in [quote!(Vec<Event>), quote!(::std::vec::Vec<Event>)] {
let input = syn::parse2(quote! {
struct Root<Event> { #[allow(dead_code)] id: u64, #[aggregate(events)] pending: #queue }
})
.unwrap();
let output = aggregate_root::expand(input).unwrap().to_string();
assert!(output.contains("type Event = Event"));
assert!(output.contains("self . pending . push (event)"));
assert!(output.contains("mem :: take (& mut self . pending)"));
assert!(output.contains("Event : :: ddd_toolkit_core :: DomainEvent"));
}
}
#[test]
fn aggregate_reports_attribute_and_queue_errors() {
rejects(
aggregate_root::expand,
&[
(
"#[aggregate(events)] struct V { q: Vec<E> }",
"placed on an event queue field",
),
("struct V(Vec<E>);", "requires a named-field struct"),
("enum V { A }", "only be derived for a named-field struct"),
("struct V { q: Vec<E> }", "exactly one"),
(
"struct V { #[aggregate(events)] a: Vec<E>, #[aggregate(events)] b: Vec<E> }",
"exactly one",
),
(
"struct V { #[aggregate()] a: Vec<E> }",
"requires the events flag",
),
(
"struct V { #[aggregate(other)] a: Vec<E> }",
"only the events flag",
),
(
"struct V { #[aggregate(events = true)] a: Vec<E> }",
"events is a flag",
),
],
);
for queue in [
"&[E]",
"fake::Vec<E>",
"Vec",
"std::vec::Vec",
"other::vec::Vec<E>",
"Vec<E, E>",
"Vec<'a>",
"<T as Trait>::Vec<E>",
] {
let source = format!("struct V {{ #[aggregate(events)] q: {queue} }}");
let expected = if queue == "Vec<E, E>" {
"exactly one event type"
} else if queue == "Vec<'a>" {
"type event argument"
} else {
"must have type Vec<Event>"
};
rejects(aggregate_root::expand, &[(source.as_str(), expected)]);
}
}