use syn::{
Ident, LitStr, Token, Type, bracketed, parenthesized,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
pub struct SchemaInput {
pub ty: Type,
pub omit: Option<Vec<String>>,
pub pick: Option<Vec<String>>,
}
impl Parse for SchemaInput {
#[allow(clippy::too_many_lines)]
fn parse(input: ParseStream) -> syn::Result<Self> {
let ty: Type = input.parse()?;
let mut omit = None;
let mut pick = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let ident_str = ident.to_string();
match ident_str.as_str() {
"omit" => {
if omit.is_some() {
return Err(syn::Error::new(
ident.span(),
"duplicate parameter `omit` in schema! invocation",
));
}
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> = content
.parse_terminated(syn::parse::ParseBuffer::parse::<LitStr>, Token![,])?;
omit = Some(fields.into_iter().map(|s| s.value()).collect());
}
"pick" => {
if pick.is_some() {
return Err(syn::Error::new(
ident.span(),
"duplicate parameter `pick` in schema! invocation",
));
}
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> = content
.parse_terminated(syn::parse::ParseBuffer::parse::<LitStr>, Token![,])?;
pick = Some(fields.into_iter().map(|s| s.value()).collect());
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!("unknown parameter: `{ident_str}`. Expected `omit` or `pick`"),
));
}
}
}
if omit.is_some() && pick.is_some() {
return Err(syn::Error::new(
input.span(),
"schema! macro: cannot use both `omit` and `pick` in the same invocation. Use one or the other to filter fields.",
));
}
Ok(Self { ty, omit, pick })
}
}
#[allow(clippy::struct_excessive_bools)]
pub struct SchemaTypeInput {
pub new_type: Ident,
pub source_type: Type,
pub omit: Option<Vec<String>>,
pub pick: Option<Vec<String>>,
pub rename: Option<Vec<(String, String)>>,
pub relation_adapters: Vec<(String, Ident)>,
pub add: Option<Vec<(String, Type)>>,
pub derive_clone: bool,
pub partial: Option<PartialMode>,
pub ignore_schema: bool,
pub schema_name: Option<String>,
pub rename_all: Option<String>,
pub multipart: bool,
pub omit_default: bool,
}
#[derive(Clone, Debug)]
pub enum PartialMode {
All,
Fields(Vec<String>),
}
struct AddField {
name: String,
ty: Type,
}
impl Parse for AddField {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
parenthesized!(content in input);
let name: LitStr = content.parse()?;
content.parse::<Token![:]>()?;
let ty: Type = content.parse()?;
Ok(Self {
name: name.value(),
ty,
})
}
}
struct RenamePair {
from: String,
to: String,
}
impl Parse for RenamePair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
parenthesized!(content in input);
let from: LitStr = content.parse()?;
content.parse::<Token![,]>()?;
let to: LitStr = content.parse()?;
Ok(Self {
from: from.value(),
to: to.value(),
})
}
}
struct RelationAdapterPair {
field_name: String,
adapter: Ident,
}
impl Parse for RelationAdapterPair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
parenthesized!(content in input);
let field_name: LitStr = content.parse()?;
content.parse::<Token![,]>()?;
let adapter: Ident = content.parse()?;
Ok(Self {
field_name: field_name.value(),
adapter,
})
}
}
impl Parse for SchemaTypeInput {
#[allow(clippy::too_many_lines)]
fn parse(input: ParseStream) -> syn::Result<Self> {
let new_type: Ident = input.parse()?;
let from_ident: Ident = input.parse()?;
if from_ident != "from" {
return Err(syn::Error::new(
from_ident.span(),
format!(
"schema_type! macro: expected `from` keyword, found `{from_ident}`. Use format: `schema_type!(NewType from SourceType, ...)`."
),
));
}
let source_type: Type = input.parse()?;
let mut omit = None;
let mut pick = None;
let mut rename = None;
let mut relation_adapters = Vec::new();
let mut add = None;
let mut derive_clone = true;
let mut partial = None;
let mut ignore_schema = false;
let mut schema_name = None;
let mut rename_all = None;
let mut multipart = false;
let mut omit_default = false;
let mut seen_params = std::collections::HashSet::<String>::new();
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let ident_str = ident.to_string();
if !seen_params.insert(ident_str.clone()) {
return Err(syn::Error::new(
ident.span(),
format!("duplicate parameter `{ident_str}` in schema_type! macro"),
));
}
match ident_str.as_str() {
"omit" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> = content
.parse_terminated(syn::parse::ParseBuffer::parse::<LitStr>, Token![,])?;
omit = Some(fields.into_iter().map(|s| s.value()).collect());
}
"pick" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> = content
.parse_terminated(syn::parse::ParseBuffer::parse::<LitStr>, Token![,])?;
pick = Some(fields.into_iter().map(|s| s.value()).collect());
}
"rename" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let pairs: Punctuated<RenamePair, Token![,]> =
content.parse_terminated(RenamePair::parse, Token![,])?;
rename = Some(pairs.into_iter().map(|p| (p.from, p.to)).collect());
}
"relation_adapters" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let pairs: Punctuated<RelationAdapterPair, Token![,]> =
content.parse_terminated(RelationAdapterPair::parse, Token![,])?;
relation_adapters = pairs
.into_iter()
.map(|p| (p.field_name, p.adapter))
.collect();
}
"add" => {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<AddField, Token![,]> =
content.parse_terminated(AddField::parse, Token![,])?;
add = Some(fields.into_iter().map(|f| (f.name, f.ty)).collect());
}
"clone" => {
input.parse::<Token![=]>()?;
let value: syn::LitBool = input.parse()?;
derive_clone = value.value();
}
"partial" => {
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let content;
let _ = bracketed!(content in input);
let fields: Punctuated<LitStr, Token![,]> = content.parse_terminated(
syn::parse::ParseBuffer::parse::<LitStr>,
Token![,],
)?;
partial = Some(PartialMode::Fields(
fields.into_iter().map(|s| s.value()).collect(),
));
} else {
partial = Some(PartialMode::All);
}
}
"ignore" => {
ignore_schema = true;
}
"name" => {
input.parse::<Token![=]>()?;
let name_lit: LitStr = input.parse()?;
schema_name = Some(name_lit.value());
}
"rename_all" => {
input.parse::<Token![=]>()?;
let rename_all_lit: LitStr = input.parse()?;
rename_all = Some(rename_all_lit.value());
}
"multipart" => {
multipart = true;
}
"omit_default" => {
omit_default = true;
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown parameter: `{ident_str}`. Expected `omit`, `pick`, `rename`, `relation_adapters`, `add`, `clone`, `partial`, `ignore`, `name`, `rename_all`, `multipart`, or `omit_default`"
),
));
}
}
}
if omit.is_some() && pick.is_some() {
return Err(syn::Error::new(
input.span(),
"schema_type! macro: cannot use both `omit` and `pick` in the same invocation. Use one or the other to filter fields.",
));
}
let parsed = Self {
new_type,
source_type,
omit,
pick,
rename,
relation_adapters,
add,
derive_clone,
partial,
ignore_schema,
schema_name,
rename_all,
multipart,
omit_default,
};
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_schema_input_simple() {
let tokens = quote::quote!(User);
let input: SchemaInput = syn::parse2(tokens).unwrap();
assert!(input.omit.is_none());
assert!(input.pick.is_none());
}
#[test]
fn test_parse_schema_input_with_omit() {
let tokens = quote::quote!(User, omit = ["password", "secret"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let omit = input.omit.unwrap();
assert_eq!(omit, vec!["password", "secret"]);
}
#[test]
fn test_parse_schema_input_with_pick() {
let tokens = quote::quote!(User, pick = ["id", "name"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let pick = input.pick.unwrap();
assert_eq!(pick, vec!["id", "name"]);
}
#[test]
fn test_parse_schema_input_omit_and_pick_error() {
let tokens = quote::quote!(User, omit = ["a"], pick = ["b"]);
let result: syn::Result<SchemaInput> = syn::parse2(tokens);
assert!(result.is_err());
}
#[test]
fn test_parse_schema_input_trailing_comma() {
let tokens = quote::quote!(User, omit = ["password"],);
let input: SchemaInput = syn::parse2(tokens).unwrap();
assert_eq!(input.omit.unwrap(), vec!["password"]);
}
#[test]
fn test_parse_schema_input_unknown_param() {
let tokens = quote::quote!(User, unknown = ["a"]);
let result: syn::Result<SchemaInput> = syn::parse2(tokens);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("unknown parameter"));
}
}
#[test]
fn test_parse_schema_type_input_simple() {
let tokens = quote::quote!(CreateUser from User);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "CreateUser");
assert!(input.omit.is_none());
assert!(input.pick.is_none());
assert!(input.rename.is_none());
assert!(input.relation_adapters.is_empty());
assert!(input.derive_clone);
}
#[test]
fn test_parse_schema_type_input_with_pick() {
let tokens = quote::quote!(CreateUser from User, pick = ["name", "email"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "CreateUser");
let pick = input.pick.unwrap();
assert_eq!(pick, vec!["name", "email"]);
}
#[test]
fn test_parse_schema_type_input_with_rename() {
let tokens =
quote::quote!(UserDTO from User, rename = [("id", "user_id"), ("name", "full_name")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "UserDTO");
let rename = input.rename.unwrap();
assert_eq!(rename.len(), 2);
assert_eq!(rename[0], ("id".to_string(), "user_id".to_string()));
assert_eq!(rename[1], ("name".to_string(), "full_name".to_string()));
}
#[test]
fn test_parse_schema_type_input_with_single_rename() {
let tokens = quote::quote!(UserDTO from User, rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let rename = input.rename.unwrap();
assert_eq!(rename.len(), 1);
assert_eq!(rename[0], ("id".to_string(), "user_id".to_string()));
}
#[test]
fn test_parse_schema_type_input_with_relation_adapters() {
let tokens = quote::quote!(MemoResponse from Memo, relation_adapters = [("user", UserInMemo), ("category", CategoryInMemo)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.relation_adapters.len(), 2);
assert_eq!(input.relation_adapters[0].0, "user");
assert_eq!(input.relation_adapters[0].1.to_string(), "UserInMemo");
assert_eq!(input.relation_adapters[1].0, "category");
assert_eq!(input.relation_adapters[1].1.to_string(), "CategoryInMemo");
}
#[test]
fn test_parse_schema_type_input_with_pick_and_rename() {
let tokens =
quote::quote!(UserDTO from User, pick = ["id", "name"], rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["id", "name"]);
assert_eq!(
input.rename.unwrap(),
vec![("id".to_string(), "user_id".to_string())]
);
}
#[test]
fn test_parse_schema_type_input_with_omit_and_rename() {
let tokens =
quote::quote!(UserPublic from User, omit = ["password"], rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.omit.unwrap(), vec!["password"]);
assert_eq!(
input.rename.unwrap(),
vec![("id".to_string(), "user_id".to_string())]
);
}
#[test]
fn test_parse_schema_type_input_with_clone_false() {
let tokens = quote::quote!(NonCloneUser from User, clone = false);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(!input.derive_clone);
}
#[test]
fn test_parse_schema_type_input_unknown_param_error() {
let tokens = quote::quote!(UserDTO from User, unknown = ["a"]);
let result: syn::Result<SchemaTypeInput> = syn::parse2(tokens);
assert!(result.is_err());
match result {
Err(e) => assert!(e.to_string().contains("unknown parameter")),
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn test_parse_schema_type_input_with_add_single() {
let tokens = quote::quote!(UserWithTimestamp from User, add = [("created_at": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "UserWithTimestamp");
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "created_at");
}
#[test]
fn test_parse_schema_type_input_with_add_multiple() {
let tokens = quote::quote!(UserWithMeta from User, add = [("created_at": String), ("updated_at": Option<String>)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let add = input.add.unwrap();
assert_eq!(add.len(), 2);
assert_eq!(add[0].0, "created_at");
assert_eq!(add[1].0, "updated_at");
}
#[test]
fn test_parse_schema_type_input_with_pick_and_add() {
let tokens = quote::quote!(CreateUserWithMeta from User, pick = ["name", "email"], add = [("request_id": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "request_id");
}
#[test]
fn test_parse_schema_type_input_with_omit_and_add() {
let tokens = quote::quote!(UserPublicWithMeta from User, omit = ["password"], add = [("display_name": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.omit.unwrap(), vec!["password"]);
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "display_name");
}
#[test]
fn test_parse_schema_type_input_with_add_complex_type() {
let tokens = quote::quote!(UserWithVec from User, add = [("tags": Vec<String>)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let add = input.add.unwrap();
assert_eq!(add.len(), 1);
assert_eq!(add[0].0, "tags");
}
#[test]
fn test_parse_schema_type_input_with_partial_all() {
let tokens = quote::quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(matches!(input.partial, Some(PartialMode::All)));
}
#[test]
fn test_parse_schema_type_input_with_partial_fields() {
let tokens = quote::quote!(UpdateUser from User, partial = ["name", "email"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
match input.partial {
Some(PartialMode::Fields(fields)) => {
assert_eq!(fields, vec!["name", "email"]);
}
_ => panic!("Expected PartialMode::Fields"),
}
}
#[test]
fn test_parse_schema_type_input_with_pick_and_partial() {
let tokens = quote::quote!(UpdateUser from User, pick = ["name", "email"], partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
assert!(matches!(input.partial, Some(PartialMode::All)));
}
#[test]
fn test_parse_schema_type_input_with_pick_and_partial_fields() {
let tokens =
quote::quote!(UpdateUser from User, pick = ["name", "email"], partial = ["name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
match input.partial {
Some(PartialMode::Fields(fields)) => {
assert_eq!(fields, vec!["name"]);
}
_ => panic!("Expected PartialMode::Fields"),
}
}
#[test]
fn test_parse_schema_type_input_with_ignore() {
let tokens = quote::quote!(NewType from User, ignore);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(input.ignore_schema);
}
#[test]
fn test_parse_schema_type_input_with_name() {
let tokens = quote::quote!(NewType from User, name = "CustomName");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.schema_name.as_deref(), Some("CustomName"));
}
#[test]
fn test_parse_schema_type_input_with_name_and_ignore() {
let tokens = quote::quote!(NewType from User, name = "CustomName", ignore);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.schema_name.as_deref(), Some("CustomName"));
assert!(input.ignore_schema);
}
#[test]
fn test_parse_schema_type_input_with_rename_all() {
let tokens = quote::quote!(NewType from User, rename_all = "snake_case");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.rename_all.as_deref(), Some("snake_case"));
}
#[test]
fn test_parse_schema_type_input_rename_all_with_other_params() {
let tokens =
quote::quote!(NewType from User, pick = ["id", "name"], rename_all = "snake_case");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["id", "name"]);
assert_eq!(input.rename_all.as_deref(), Some("snake_case"));
}
#[test]
fn test_parse_schema_type_multiple_commas_trailing() {
let tokens = quote::quote!(NewType from User, pick = ["id"],);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.pick.unwrap(), vec!["id"]);
}
#[test]
fn test_parse_schema_type_all_parameters() {
let tokens = quote::quote!(
NewType from crate::models::User,
pick = ["id", "name"],
rename = [("id", "user_id")],
relation_adapters = [("owner", OwnerSummary)],
add = [("request_id": String)],
clone = false,
partial,
ignore,
name = "CustomName",
rename_all = "snake_case",
multipart,
omit_default
);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let source_type = &input.source_type;
assert_eq!(input.new_type, "NewType");
assert_eq!(
quote::quote!(#source_type).to_string(),
"crate :: models :: User"
);
assert_eq!(
input.pick.as_deref(),
Some(["id".to_string(), "name".to_string()].as_slice())
);
assert_eq!(
input.rename.as_deref(),
Some([("id".to_string(), "user_id".to_string())].as_slice())
);
assert_eq!(input.relation_adapters[0].0, "owner");
assert_eq!(input.relation_adapters[0].1, "OwnerSummary");
assert_eq!(input.add.as_ref().unwrap()[0].0, "request_id");
assert!(!input.derive_clone);
assert!(matches!(input.partial, Some(PartialMode::All)));
assert!(input.ignore_schema);
assert_eq!(input.schema_name.as_deref(), Some("CustomName"));
assert_eq!(input.rename_all.as_deref(), Some("snake_case"));
assert!(input.multipart);
assert!(input.omit_default);
}
#[test]
fn test_parse_schema_type_input_wrong_from_keyword() {
let tokens = quote::quote!(NewType xyz User);
let result: syn::Result<SchemaTypeInput> = syn::parse2(tokens);
assert!(result.is_err());
match result {
Err(e) => assert!(e.to_string().contains("expected `from`"), "Error: {e}"),
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn test_parse_schema_type_input_misspelled_from() {
let tokens = quote::quote!(NewType fron User);
let result: syn::Result<SchemaTypeInput> = syn::parse2(tokens);
assert!(result.is_err());
match result {
Err(e) => assert!(
e.to_string()
.contains("expected `from` keyword, found `fron`"),
"Error: {e}"
),
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn test_parse_schema_type_input_omit_and_pick_error_schema_type() {
let tokens = quote::quote!(NewType from User, omit = ["a"], pick = ["b"]);
let result: syn::Result<SchemaTypeInput> = syn::parse2(tokens);
assert!(result.is_err());
match result {
Err(e) => assert!(
e.to_string().contains("cannot use both `omit` and `pick`"),
"Error: {e}"
),
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn test_parse_schema_type_input_pick_then_omit_error() {
let tokens = quote::quote!(NewType from User, pick = ["a"], omit = ["b"]);
let result: syn::Result<SchemaTypeInput> = syn::parse2(tokens);
assert!(result.is_err());
match result {
Err(e) => assert!(
e.to_string().contains("cannot use both `omit` and `pick`"),
"Error: {e}"
),
Ok(_) => panic!("Expected error"),
}
}
#[test]
fn test_parse_schema_type_input_with_multipart() {
let tokens = quote::quote!(UploadReq from CreateUploadRequest, multipart);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert_eq!(input.new_type.to_string(), "UploadReq");
assert!(input.multipart);
}
#[test]
fn test_parse_schema_type_input_with_multipart_and_pick() {
let tokens =
quote::quote!(UploadReq from CreateUploadRequest, multipart, pick = ["name", "file"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(input.multipart);
assert_eq!(input.pick.unwrap(), vec!["name", "file"]);
}
#[test]
fn test_parse_schema_type_input_with_multipart_and_partial() {
let tokens = quote::quote!(PatchUpload from CreateUploadRequest, multipart, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(input.multipart);
assert!(matches!(input.partial, Some(PartialMode::All)));
}
#[test]
fn test_parse_schema_type_input_with_omit_default() {
let tokens = quote::quote!(CreateUser from Model, omit_default);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(input.omit_default);
}
#[test]
fn test_parse_schema_type_input_with_omit_default_and_omit() {
let tokens = quote::quote!(CreateUser from Model, omit_default, omit = ["password"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(input.omit_default);
assert_eq!(input.omit.unwrap(), vec!["password"]);
}
#[test]
fn test_parse_schema_type_input_with_omit_default_and_pick() {
let tokens = quote::quote!(CreateUser from Model, omit_default, pick = ["name", "email"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(input.omit_default);
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
}
#[test]
fn test_parse_schema_type_input_omit_default_defaults_to_false() {
let tokens = quote::quote!(CreateUser from User);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
assert!(!input.omit_default);
}
#[rstest::rstest]
#[case::omit(
quote::quote!(User, omit = ["a"], omit = ["b"]),
"duplicate parameter `omit` in schema! invocation"
)]
#[case::pick(
quote::quote!(User, pick = ["a"], pick = ["b"]),
"duplicate parameter `pick` in schema! invocation"
)]
fn schema_input_rejects_duplicate_filter(
#[case] tokens: proc_macro2::TokenStream,
#[case] expected: &str,
) {
let Err(error) = syn::parse2::<SchemaInput>(tokens) else {
panic!("duplicate must fail");
};
assert_eq!(error.to_string(), expected);
}
}