#![deny(clippy::pedantic, clippy::nursery)]
use std::convert::identity;
use attr::{document::DocAttr, parse_attrs};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse2, Error, Fields, FieldsUnnamed, ItemStruct, Result, parse_quote};
use crate::error::syn_error;
mod attr;
mod error;
pub fn derive_document2(input: TokenStream) -> TokenStream {
expand_derive_document(input).map_or_else(Error::into_compile_error, identity)
}
fn expand_derive_document(input: TokenStream) -> Result<TokenStream> {
let item = parse2::<ItemStruct>(input)?;
let name = &item.ident;
let Some(document_attr) = parse_attrs::<DocAttr>(&item.attrs, "document")?
else {
syn_error!(r#"The #[document(...)] attribute is required"#);
};
let DocAttr {
validator: Some(validator),
crate_path,
formatter,
} = document_attr
else {
syn_error!(r#"The #[document(validator = "...")] attribute is required"#);
};
let crate_path = crate_path.unwrap_or_else(|| parse_quote!(document_validator));
let Fields::Unnamed(FieldsUnnamed {
unnamed: ref fields,
..
}) = item.fields
else {
syn_error!("This trait may only be derived by a newtype struct");
};
if fields.len() != 1 {
syn_error!("This trait may only be derived by a newtype struct");
}
let formatted_document = formatter.map_or(quote![document], |fmt| quote![#fmt(document)]);
Ok(quote! {
impl #crate_path::Document for #name {
fn new(document: &str) -> Option<Self> where Self: Sized {
Self::validate(document).then(|| Self(#formatted_document.into()))
}
fn validate(document: &str) -> bool where Self: Sized {
#validator(document)
}
fn as_str(&self) -> &str {
&self.0
}
}
})
}