use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DataStruct, DeriveInput, Fields, FieldsNamed};
use super::{named_fields, reject_duplicate_names, unit_struct, wire_name};
fn only_field(declaration: &TokenStream2) -> syn::Field {
named(quote!(struct Holder { #declaration }))
.named
.into_iter()
.next()
.expect("one field")
}
fn item(declaration: TokenStream2) -> DeriveInput {
syn::parse2(declaration).expect("an item")
}
fn named(declaration: TokenStream2) -> FieldsNamed {
match item(declaration).data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => fields,
_ => panic!("a struct with named fields"),
}
}
#[test]
fn a_wire_name_prefers_the_kynos_rename_then_serdes_then_the_identifier() {
for kynos in [None, Some("from_kynos")] {
for serde in [None, Some("from_serde")] {
let kynos_attribute = kynos.map(|name| quote!(#[param(rename = #name)]));
let serde_attribute = serde.map(|name| quote!(#[serde(rename = #name)]));
let field = only_field("e! {
#kynos_attribute
#serde_attribute
user_id: u64
});
let expected = kynos.or(serde).unwrap_or("user_id");
assert_eq!(
wire_name(&field, "param").expect("a wire name"),
expected,
"kynos: {kynos:?}, serde: {serde:?}"
);
}
}
}
#[test]
fn an_unrecognized_key_does_not_swallow_the_keys_after_it() {
for (shape, skipped) in [
("a named value", quote!(unknown = 1)),
("a parenthesized group", quote!(unknown(a, b))),
("a bare path", quote!(unknown)),
] {
let field = only_field("e! {
#[param(#skipped, rename = "chosen")]
user_id: u64
});
assert_eq!(
wire_name(&field, "param").expect("a wire name"),
"chosen",
"{shape} must be stepped over, not consumed"
);
}
}
#[test]
fn an_unrecognized_key_alone_is_not_an_error() {
let field = only_field("e! {
#[param(unknown = 1)]
user_id: u64
});
assert_eq!(wire_name(&field, "param").expect("a wire name"), "user_id");
}
#[test]
fn only_the_named_attribute_is_consulted() {
let field = only_field("e! {
#[header(rename = "X-Other")]
user_id: u64
});
assert_eq!(wire_name(&field, "param").expect("a wire name"), "user_id");
}
#[test]
fn named_fields_accepts_a_struct_with_named_fields() {
let input = item(quote!(
struct Query {
page: u32,
}
));
assert!(named_fields(&input, "QueryParams").is_ok());
}
#[test]
fn a_unit_struct_is_accepted_however_it_is_spelled() {
for declaration in [
quote!(
struct Users;
),
quote!(
struct Users {}
),
] {
let input = item(declaration);
assert!(unit_struct(&input, "Tag", "names a group of operations").is_ok());
}
}
#[test]
fn distinct_names_are_not_duplicates() {
let fields = named(quote!(
struct Query {
page: u32,
size: u32,
}
));
let names = ["page".to_owned(), "size".to_owned()];
assert!(reject_duplicate_names(&fields, &names, "parameter").is_ok());
}
#[test]
fn a_duplicate_names_the_field_that_claimed_it_first() {
let fields = named(quote!(
struct Query {
page: u32,
offset: u32,
}
));
let names = ["cursor".to_owned(), "cursor".to_owned()];
let error = reject_duplicate_names(&fields, &names, "parameter")
.expect_err("two fields on one wire name must be refused");
let reported = error.to_string();
assert!(reported.contains("two fields declare the parameter `cursor`"));
assert!(reported.contains("the first is `page`"));
}
fn cases() -> Vec<(&'static str, syn::Result<()>, &'static str)> {
fn shape(input: &DeriveInput) -> syn::Result<()> {
named_fields(input, "QueryParams").map(|_| ())
}
fn unit(input: &DeriveInput) -> syn::Result<()> {
unit_struct(input, "Tag", "names a group of operations")
}
let duplicate = named(quote!(
struct Query {
page: u32,
offset: u32,
}
));
vec![
(
"named fields asked of a tuple struct",
shape(&item(quote!(
struct Query(u32);
))),
"needs a struct with named fields",
),
(
"named fields asked of an enum",
shape(&item(quote!(
enum Query {
A,
}
))),
"which an enum is not",
),
(
"named fields asked of a union",
shape(&item(quote!(
union Query {
a: u32,
}
))),
"cannot describe a union",
),
(
"a unit struct asked of a struct with fields",
unit(&item(quote!(
struct Users {
name: String,
}
))),
"carries no fields",
),
(
"a unit struct asked of an enum",
unit(&item(quote!(
enum Users {
A,
}
))),
"must be a unit struct",
),
(
"a unit struct asked of a union",
unit(&item(quote!(
union Users {
a: u32,
}
))),
"must be a unit struct",
),
(
"serde's split rename, which gives one field two wire names",
wire_name(
&only_field("e! {
#[serde(rename(serialize = "a", deserialize = "b"))]
user_id: u64
}),
"param",
)
.map(|_| ()),
"two wire names",
),
(
"two fields on one wire name",
reject_duplicate_names(
&duplicate,
&["cursor".to_owned(), "cursor".to_owned()],
"parameter",
),
"two fields declare the",
),
]
}
#[test]
fn each_case_raises_the_diagnostic_it_names() {
for (description, outcome, expected) in cases() {
let Err(error) = outcome else {
panic!("{description} must be rejected");
};
let reported = error.to_string();
assert!(
reported.contains(expected),
"{description}: expected a diagnostic containing {expected:?}, got {reported:?}"
);
}
}
#[test]
fn every_shared_diagnostic_has_a_case() {
const SOURCE: &str = include_str!("../common.rs");
let implementation = SOURCE
.split_once("\n#[cfg(test)]")
.map_or(SOURCE, |(before, _)| before);
let sites = implementation.matches("syn::Error::new(").count()
+ implementation.matches("meta.error(").count();
assert_eq!(
cases().len(),
sites,
"`common.rs` raises {sites} diagnostic(s) and {} have a case",
cases().len()
);
}