use syn::{Attribute, Result, Type};
use super::http_header::parse_http_header;
use crate::ir::{FieldDefinition, TransparentFieldDefinition};
pub(crate) fn parse_field(field: syn::Field) -> Result<FieldDefinition> {
let (is_extension, rename) = parse_extension(&field.attrs)?;
let is_from = has_attribute(&field.attrs, "from");
let is_source = has_attribute(&field.attrs, "source") || is_from;
let http_header = parse_http_header(&field.attrs)?;
let rust_name = field
.ident
.clone()
.ok_or_else(|| syn::Error::new_spanned(&field, "fields must be named"))?;
let output_name = rename.unwrap_or_else(|| rust_name.to_string());
let is_option = is_option_type(&field.ty);
Ok(FieldDefinition {
rust_name,
output_name,
ty: field.ty,
is_extension,
is_source,
is_from,
is_option,
http_header,
})
}
pub(crate) fn parse_transparent_field(field: syn::Field) -> Result<TransparentFieldDefinition> {
if field.ident.is_some() {
unreachable!("parse_transparent_field() can only be called on unnamed fields");
}
if let Some(attr) = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("source"))
{
return Err(syn::Error::new_spanned(
attr,
"#[source] attribute is not necessary on transparent variants",
));
}
Ok(TransparentFieldDefinition {
ty: field.ty,
is_from: has_attribute(&field.attrs, "from"),
})
}
fn has_attribute(attrs: &[Attribute], name: &str) -> bool {
attrs.iter().any(|attr| attr.path().is_ident(name))
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
let path = &type_path.path;
path.segments
.last()
.is_some_and(|seg| seg.ident == "Option")
&& (path.segments.len() == 1
|| (path.segments.len() == 3
&& (path.segments[0].ident == "std" || path.segments[0].ident == "core")
&& path.segments[1].ident == "option"))
} else {
false
}
}
fn parse_extension(attrs: &[Attribute]) -> Result<(bool, Option<String>)> {
let mut is_extension = false;
let mut rename: Option<String> = None;
for attr in attrs {
if !attr.path().is_ident("extension") {
continue;
}
if is_extension {
return Err(syn::Error::new_spanned(
attr,
"duplicate #[extension] attribute",
));
}
is_extension = true;
match &attr.meta {
syn::Meta::Path(_) => {}
syn::Meta::List(_) => {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
rename = Some(s.value());
Ok(())
} else {
Err(meta.error("unknown extension attribute, expected `rename = \"...\"`"))
}
})?;
}
syn::Meta::NameValue(_) => {
return Err(syn::Error::new_spanned(
attr,
"expected #[extension] or #[extension(rename = \"...\")]",
));
}
}
}
Ok((is_extension, rename))
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn bare_extension_is_detected() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[extension])];
let (is_extension, rename) = parse_extension(&attrs).unwrap();
assert!(is_extension);
assert_eq!(rename, None);
}
#[test]
fn extension_with_rename() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[extension(rename = "MY_FIELD")])];
let (is_extension, rename) = parse_extension(&attrs).unwrap();
assert!(is_extension);
assert_eq!(rename, Some("MY_FIELD".to_string()));
}
#[test]
fn no_extension_attribute() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[source])];
let (is_extension, rename) = parse_extension(&attrs).unwrap();
assert!(!is_extension);
assert_eq!(rename, None);
}
#[test]
fn reject_duplicate_extension() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[extension]), parse_quote!(#[extension])];
let err = parse_extension(&attrs).unwrap_err();
assert!(err.to_string().contains("duplicate #[extension] attribute"));
}
#[test]
fn reject_unknown_extension_key() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[extension(unknown = "value")])];
let err = parse_extension(&attrs).unwrap_err();
assert!(err.to_string().contains("unknown extension attribute"));
}
#[test]
fn reject_extension_with_no_parens() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[extension = "MyField"])];
let err = parse_extension(&attrs).unwrap_err();
assert!(
err.to_string()
.contains("expected #[extension] or #[extension(rename = \"...\")]")
);
}
}