use super::{
COUNTS, Container, Field, Fields, Lit, LitFloat, LitInt, NUMERIC, Span, Spanned, TokenStream2,
Type, Variant, quote, skip_value, string_value,
};
pub(super) fn field_name(field: &Field, container: &Container) -> String {
if let Some(renamed) = serde_rename(&field.attrs) {
return renamed;
}
let ident = field
.ident
.as_ref()
.map(ToString::to_string)
.unwrap_or_default();
container
.rename_all
.as_deref()
.map_or(ident.clone(), |style| rename(&ident, style))
}
pub(super) fn variant_name(variant: &Variant, container: &Container) -> String {
if let Some(renamed) = serde_rename(&variant.attrs) {
return renamed;
}
let ident = variant.ident.to_string();
container
.rename_all
.as_deref()
.map_or(ident.clone(), |style| rename(&ident, style))
}
pub(super) fn serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
let mut found = None;
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
found = string_value(&meta)?;
} else {
skip_value(&meta)?;
}
Ok(())
});
}
found
}
pub(super) fn rename(ident: &str, style: &str) -> String {
let words = || {
let mut words: Vec<String> = Vec::new();
let mut current = String::new();
for character in ident.chars() {
if character == '_' {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
continue;
}
if character.is_uppercase() && !current.is_empty() {
words.push(std::mem::take(&mut current));
}
current.extend(character.to_lowercase());
}
if !current.is_empty() {
words.push(current);
}
words
};
let capitalize = |word: &str| {
let mut characters = word.chars();
characters.next().map_or_else(String::new, |first| {
first.to_uppercase().collect::<String>() + characters.as_str()
})
};
match style {
"lowercase" => ident.to_lowercase(),
"UPPERCASE" => ident.to_uppercase(),
"snake_case" => words().join("_"),
"SCREAMING_SNAKE_CASE" => words().join("_").to_uppercase(),
"kebab-case" => words().join("-"),
"SCREAMING-KEBAB-CASE" => words().join("-").to_uppercase(),
"PascalCase" => words().iter().map(|word| capitalize(word)).collect(),
"camelCase" => {
let words = words();
let mut renamed = words.first().cloned().unwrap_or_default();
for word in words.iter().skip(1) {
renamed.push_str(&capitalize(word));
}
renamed
}
_ => ident.to_owned(),
}
}
pub(super) fn is_described(field: &Field) -> bool {
!(serde_flag(&field.attrs, &["skip", "skip_deserializing"])
|| (is_phantom(&field.ty) && is_flattened(field)))
}
pub(super) fn described_members(fields: &Fields) -> Vec<&Field> {
fields.iter().filter(|field| is_described(field)).collect()
}
pub(super) fn transparent_picks(fields: &Fields) -> (Option<&Field>, Option<&Field>) {
let pick = |excluded: &[&str]| {
let mut candidates = fields
.iter()
.filter(|field| !is_phantom(&field.ty) && !serde_flag(&field.attrs, excluded));
match (candidates.next(), candidates.next()) {
(Some(only), None) => Some(only),
_ => None,
}
};
(
pick(&["skip", "skip_serializing"]),
pick(&["skip", "skip_deserializing", "default"]),
)
}
pub(super) fn transparent_member(fields: &Fields) -> Option<&Field> {
match transparent_picks(fields) {
(Some(written), Some(read)) => std::ptr::eq(written, read).then_some(written),
(Some(only), None) | (None, Some(only)) => Some(only),
(None, None) => None,
}
}
pub(super) fn is_phantom(ty: &Type) -> bool {
let mut ty = ty;
while let Type::Group(group) = ty {
ty = &group.elem;
}
let Type::Path(path) = ty else {
return false;
};
path.path
.segments
.last()
.is_some_and(|segment| segment.ident == "PhantomData")
}
pub(super) fn is_skipped_both_ways(attrs: &[syn::Attribute]) -> bool {
serde_flag(attrs, &["skip"])
|| (serde_flag(attrs, &["skip_serializing"]) && serde_flag(attrs, &["skip_deserializing"]))
}
pub(super) fn is_unit_like(fields: &Fields) -> bool {
match fields {
Fields::Unit => true,
Fields::Unnamed(unnamed) => {
unnamed.unnamed.len() == 1 && is_skipped_both_ways(&unnamed.unnamed[0].attrs)
}
Fields::Named(_) => false,
}
}
pub(super) fn is_flattened(field: &Field) -> bool {
serde_flag(&field.attrs, &["flatten"])
}
pub(super) fn open_span(field: &Field) -> Option<Span> {
let mut found = None;
for attr in &field.attrs {
if !attr.path().is_ident("schema") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("open") {
found = Some(meta.path.span());
return Ok(());
}
skip_value(&meta)
});
}
found
}
pub(super) fn serde_key_span(attrs: &[syn::Attribute], keys: &[&str]) -> Option<(String, Span)> {
let mut found = None;
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
let named = meta.path.get_ident().map(ToString::to_string);
if let Some(key) = named.filter(|key| found.is_none() && keys.contains(&key.as_str())) {
found = Some((key, meta.path.span()));
}
skip_value(&meta)
});
}
found
}
pub(super) fn is_open(field: &Field) -> bool {
open_span(field).is_some()
}
pub(super) fn is_required(field: &Field, container: &Container) -> bool {
!is_option(&field.ty) && !container.default && !serde_flag(&field.attrs, &["default"])
}
pub(super) fn is_option(ty: &Type) -> bool {
let Type::Path(path) = ty else {
return false;
};
path.qself.is_none()
&& path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Option")
}
pub(super) fn serde_flag(attrs: &[syn::Attribute], keys: &[&str]) -> bool {
let mut found = false;
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta
.path
.get_ident()
.is_some_and(|key| keys.contains(&key.to_string().as_str()))
{
found = true;
}
skip_value(&meta)
});
}
found
}
pub(super) fn constraints(field: &Field) -> Option<TokenStream2> {
let mut assignments: Vec<TokenStream2> = Vec::new();
for attr in &field.attrs {
if !attr.path().is_ident("schema") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
let Some(key) = meta.path.get_ident() else {
return skip_value(&meta);
};
let name = key.to_string();
if name == "open" {
return Ok(());
}
if name == "unique_items" {
assignments.push(quote! {
constraints.unique_items = ::core::option::Option::Some(true);
});
return Ok(());
}
let field = syn::Ident::new(&name, key.span());
let literal: Lit = meta.value()?.parse()?;
if NUMERIC.contains(&name.as_str()) {
if let Some(number) = as_float(&literal) {
assignments.push(quote! {
constraints.#field = ::core::option::Option::Some(#number);
});
}
} else if COUNTS.contains(&name.as_str()) {
if let Lit::Int(count) = &literal {
let count = LitInt::new(&format!("{}u64", count.base10_digits()), count.span());
assignments.push(quote! {
constraints.#field = ::core::option::Option::Some(#count);
});
}
} else if name == "pattern" {
if let Lit::Str(pattern) = &literal {
assignments.push(quote! {
constraints.pattern = ::core::option::Option::Some(
::std::string::String::from(#pattern),
);
});
}
}
Ok(())
});
}
if assignments.is_empty() {
return None;
}
Some(quote! {
{
let mut constraints = ::kynos::schema::constraints::Constraints::default();
#(#assignments)*
constraints
}
})
}
pub(super) fn as_float(literal: &Lit) -> Option<LitFloat> {
let (digits, span) = match literal {
Lit::Int(value) => (value.token().to_string(), value.span()),
Lit::Float(value) => (value.token().to_string(), value.span()),
_ => return None,
};
let digits = digits.trim_end_matches(|character: char| character.is_alphabetic());
Some(LitFloat::new(&format!("{digits}f64"), span))
}