jsonx-derive 0.1.0

Derive macro for jsonx::JsonxConstructor.
Documentation
//! Derive macro for [`jsonx::JsonxConstructor`].
//!
//! This crate is an implementation detail of [`jsonx`]; use it through the
//! re-exported `#[derive(JsonxConstructor)]` and do not depend on it directly.
//!
//! [`jsonx`]: https://docs.rs/jsonx
//! [`jsonx::JsonxConstructor`]: https://docs.rs/jsonx

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, LitStr};

/// Derives [`JsonxConstructor`](https://docs.rs/jsonx) — and the `serde`
/// `Serialize`/`Deserialize` impls that wire it in — for a type that implements
/// [`Display`](std::fmt::Display) and [`FromStr`](std::str::FromStr).
///
/// The constructor name defaults to the type name, lowercased. Override it with
/// `#[jsonx(name = "...")]`:
///
/// ```ignore
/// #[derive(jsonx::JsonxConstructor)]
/// #[jsonx(name = "semver")]
/// struct Version { major: u16, minor: u16 }
/// // (Version must also implement Display + FromStr)
/// ```
#[proc_macro_derive(JsonxConstructor, attributes(jsonx))]
pub fn derive_jsonx_constructor(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand(input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let ident = &input.ident;

    // Constructor name: `#[jsonx(name = "...")]`, else the type name lowercased.
    let mut name: Option<LitStr> = None;
    for attr in &input.attrs {
        if !attr.path().is_ident("jsonx") {
            continue;
        }
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("name") {
                name = Some(meta.value()?.parse()?);
                Ok(())
            } else {
                Err(meta.error("unknown `jsonx` option; expected `name = \"...\"`"))
            }
        })?;
    }
    let name = name.unwrap_or_else(|| LitStr::new(&ident.to_string().to_lowercase(), ident.span()));

    if !is_valid_ctor_name(&name.value()) {
        return Err(syn::Error::new_spanned(
            &name,
            "constructor name must match `[A-Za-z_][0-9A-Za-z_]*`",
        ));
    }

    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    // The `Deserialize` impl needs an extra `'de` lifetime.
    let mut de_generics = input.generics.clone();
    de_generics
        .params
        .insert(0, syn::parse_quote!('de));
    let (de_impl_generics, _, _) = de_generics.split_for_impl();

    Ok(quote! {
        impl #impl_generics ::jsonx::JsonxConstructor for #ident #ty_generics #where_clause {
            const TOKEN: &'static str = ::jsonx::ctor!(#name);

            fn to_jsonx_arg(&self) -> ::std::string::String {
                ::std::string::ToString::to_string(self)
            }

            fn from_jsonx_arg(arg: &str) -> ::std::result::Result<Self, ::std::string::String> {
                <Self as ::std::str::FromStr>::from_str(arg)
                    .map_err(|e| ::std::string::ToString::to_string(&e))
            }
        }

        impl #impl_generics ::jsonx::__derive::serde::Serialize for #ident #ty_generics #where_clause {
            fn serialize<__S>(&self, serializer: __S) -> ::std::result::Result<__S::Ok, __S::Error>
            where
                __S: ::jsonx::__derive::serde::Serializer,
            {
                ::jsonx::constructor::serialize(self, serializer)
            }
        }

        impl #de_impl_generics ::jsonx::__derive::serde::Deserialize<'de> for #ident #ty_generics #where_clause {
            fn deserialize<__D>(deserializer: __D) -> ::std::result::Result<Self, __D::Error>
            where
                __D: ::jsonx::__derive::serde::Deserializer<'de>,
            {
                ::jsonx::constructor::deserialize(deserializer)
            }
        }
    })
}

/// Mirrors the JSONX identifier rule (`^[A-Za-z_][0-9A-Za-z_]*$`). Duplicated
/// here because a proc-macro crate cannot depend on `jsonx` (which depends on
/// it).
fn is_valid_ctor_name(name: &str) -> bool {
    let mut bytes = name.bytes();
    match bytes.next() {
        Some(b) if b.is_ascii_alphabetic() || b == b'_' => {}
        _ => return false,
    }
    bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}