use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{
Attribute, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed, LitStr, spanned::Spanned,
};
use crate::derive::schema::property_names;
pub(crate) fn named_fields<'a>(
input: &'a DeriveInput,
derive: &str,
) -> syn::Result<&'a FieldsNamed> {
match &input.data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => Ok(fields),
Data::Struct(DataStruct { fields, .. }) => Err(syn::Error::new(
fields.span(),
format!(
"`{derive}` describes a group of named values, so it needs a struct with named fields"
),
)),
Data::Enum(data) => Err(syn::Error::new(
data.enum_token.span(),
format!("`{derive}` describes a group of named values, which an enum is not"),
)),
Data::Union(data) => Err(syn::Error::new(
data.union_token.span(),
format!("`{derive}` cannot describe a union"),
)),
}
}
pub(crate) fn unit_struct(input: &DeriveInput, derive: &str, purpose: &str) -> syn::Result<()> {
match &input.data {
Data::Struct(DataStruct {
fields: Fields::Unit,
..
}) => Ok(()),
Data::Struct(DataStruct { fields, .. }) if fields.is_empty() => Ok(()),
Data::Struct(DataStruct { fields, .. }) => Err(syn::Error::new(
fields.span(),
format!("`{derive}` marks a type that {purpose}, so it carries no fields"),
)),
Data::Enum(data) => Err(syn::Error::new(
data.enum_token.span(),
format!("`{derive}` marks a type that {purpose}, so it must be a unit struct"),
)),
Data::Union(data) => Err(syn::Error::new(
data.union_token.span(),
format!("`{derive}` marks a type that {purpose}, so it must be a unit struct"),
)),
}
}
pub(crate) fn is_deprecated(attrs: &[Attribute]) -> bool {
attrs
.iter()
.any(|attribute| attribute.path().is_ident("deprecated"))
}
pub(crate) fn skip_value(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
if meta.input.peek(syn::Token![=]) {
let _: syn::Expr = meta.value()?.parse()?;
} else if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _ = content.parse::<proc_macro2::TokenStream>()?;
}
Ok(())
}
pub(crate) fn wire_names(
input: &DeriveInput,
fields: &FieldsNamed,
attribute: &str,
) -> syn::Result<Vec<String>> {
reject_split_rename_all(input)?;
fields
.named
.iter()
.zip(property_names(input, fields))
.map(|(field, fallback)| {
reject_alias(field)?;
wire_name(field, attribute, fallback)
})
.collect()
}
fn wire_name(field: &Field, attribute: &str, fallback: String) -> syn::Result<String> {
if let Some(renamed) = kynos_rename(field, attribute)? {
return Ok(renamed);
}
if let Some(renamed) = serde_rename(field)? {
return Ok(renamed);
}
Ok(fallback)
}
fn reject_split_rename_all(input: &DeriveInput) -> syn::Result<()> {
for attr in &input.attrs {
if !attr.path().is_ident("serde") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") && meta.input.peek(syn::token::Paren) {
return Err(meta.error(
"a split `rename_all` gives every field two wire names, and a description \
can carry one. Say which with `rename_all = \"...\"`",
));
}
skip_value(&meta)
})?;
}
Ok(())
}
fn reject_alias(field: &Field) -> syn::Result<()> {
for attr in &field.attrs {
if !attr.path().is_ident("serde") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("alias") {
return Err(meta.error(
"an `alias` gives this field a second wire name, and a parameter has \
exactly one. Drop the `alias`, or make the name it carries the field's \
`rename`",
));
}
skip_value(&meta)
})?;
}
Ok(())
}
fn kynos_rename(field: &Field, attribute: &str) -> syn::Result<Option<String>> {
let mut found = None;
for attr in &field.attrs {
if !attr.path().is_ident(attribute) {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
found = Some(meta.value()?.parse::<LitStr>()?.value());
} else {
skip_value(&meta)?;
}
Ok(())
})?;
}
Ok(found)
}
fn serde_rename(field: &Field) -> syn::Result<Option<String>> {
let mut found = None;
for attr in &field.attrs {
if !attr.path().is_ident("serde") {
continue;
}
attr.parse_nested_meta(|meta| {
if !meta.path.is_ident("rename") {
return skip_value(&meta);
}
if meta.input.peek(syn::token::Paren) {
return Err(meta.error(
"a split `rename` gives this field two wire names, and a description can \
carry one. Say which with `rename = \"...\"`, or name it explicitly in the \
Kynos attribute",
));
}
found = Some(meta.value()?.parse::<LitStr>()?.value());
Ok(())
})?;
}
Ok(found)
}
pub(crate) fn doc_string(attrs: &[syn::Attribute]) -> Option<String> {
let text = attrs
.iter()
.filter_map(|attribute| match &attribute.meta {
syn::Meta::NameValue(pair) if pair.path.is_ident("doc") => match &pair.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(text),
..
}) => Some(text.value()),
_ => None,
},
_ => None,
})
.map(|line| line.trim().to_owned())
.collect::<Vec<_>>()
.join("\n");
let text = text.trim().to_owned();
(!text.is_empty()).then_some(text)
}
pub(crate) fn names_const(names: &[String]) -> TokenStream2 {
let literals = names
.iter()
.map(|name| LitStr::new(name, Span::call_site()));
quote! {
const NAMES: &'static [&'static str] = &[#(#literals),*];
}
}
pub(crate) fn reject_duplicate_names(
fields: &FieldsNamed,
names: &[String],
kind: &str,
) -> syn::Result<()> {
for (index, name) in names.iter().enumerate() {
if let Some(earlier) = names[..index].iter().position(|seen| seen == name) {
let field = fields
.named
.iter()
.nth(index)
.expect("index came from the same list");
return Err(syn::Error::new(
field.span(),
format!(
"two fields declare the {kind} `{name}`; the first is `{}`",
fields
.named
.iter()
.nth(earlier)
.and_then(|field| field.ident.as_ref())
.map_or_else(String::new, ToString::to_string)
),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests;