Skip to main content

jsonx_derive/
lib.rs

1//! Derive macro for [`jsonx::JsonxConstructor`].
2//!
3//! This crate is an implementation detail of [`jsonx`]; use it through the
4//! re-exported `#[derive(JsonxConstructor)]` and do not depend on it directly.
5//!
6//! [`jsonx`]: https://docs.rs/jsonx
7//! [`jsonx::JsonxConstructor`]: https://docs.rs/jsonx
8
9use proc_macro::TokenStream;
10use quote::quote;
11use syn::{parse_macro_input, DeriveInput, LitStr};
12
13/// Derives [`JsonxConstructor`](https://docs.rs/jsonx) — and the `serde`
14/// `Serialize`/`Deserialize` impls that wire it in — for a type that implements
15/// [`Display`](std::fmt::Display) and [`FromStr`](std::str::FromStr).
16///
17/// The constructor name defaults to the type name, lowercased. Override it with
18/// `#[jsonx(name = "...")]`:
19///
20/// ```ignore
21/// #[derive(jsonx::JsonxConstructor)]
22/// #[jsonx(name = "semver")]
23/// struct Version { major: u16, minor: u16 }
24/// // (Version must also implement Display + FromStr)
25/// ```
26#[proc_macro_derive(JsonxConstructor, attributes(jsonx))]
27pub fn derive_jsonx_constructor(input: TokenStream) -> TokenStream {
28    let input = parse_macro_input!(input as DeriveInput);
29    expand(input)
30        .unwrap_or_else(syn::Error::into_compile_error)
31        .into()
32}
33
34fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
35    let ident = &input.ident;
36
37    // Constructor name: `#[jsonx(name = "...")]`, else the type name lowercased.
38    let mut name: Option<LitStr> = None;
39    for attr in &input.attrs {
40        if !attr.path().is_ident("jsonx") {
41            continue;
42        }
43        attr.parse_nested_meta(|meta| {
44            if meta.path.is_ident("name") {
45                name = Some(meta.value()?.parse()?);
46                Ok(())
47            } else {
48                Err(meta.error("unknown `jsonx` option; expected `name = \"...\"`"))
49            }
50        })?;
51    }
52    let name = name.unwrap_or_else(|| LitStr::new(&ident.to_string().to_lowercase(), ident.span()));
53
54    if !is_valid_ctor_name(&name.value()) {
55        return Err(syn::Error::new_spanned(
56            &name,
57            "constructor name must match `[A-Za-z_][0-9A-Za-z_]*`",
58        ));
59    }
60
61    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
62
63    // The `Deserialize` impl needs an extra `'de` lifetime.
64    let mut de_generics = input.generics.clone();
65    de_generics
66        .params
67        .insert(0, syn::parse_quote!('de));
68    let (de_impl_generics, _, _) = de_generics.split_for_impl();
69
70    Ok(quote! {
71        impl #impl_generics ::jsonx::JsonxConstructor for #ident #ty_generics #where_clause {
72            const TOKEN: &'static str = ::jsonx::ctor!(#name);
73
74            fn to_jsonx_arg(&self) -> ::std::string::String {
75                ::std::string::ToString::to_string(self)
76            }
77
78            fn from_jsonx_arg(arg: &str) -> ::std::result::Result<Self, ::std::string::String> {
79                <Self as ::std::str::FromStr>::from_str(arg)
80                    .map_err(|e| ::std::string::ToString::to_string(&e))
81            }
82        }
83
84        impl #impl_generics ::jsonx::__derive::serde::Serialize for #ident #ty_generics #where_clause {
85            fn serialize<__S>(&self, serializer: __S) -> ::std::result::Result<__S::Ok, __S::Error>
86            where
87                __S: ::jsonx::__derive::serde::Serializer,
88            {
89                ::jsonx::constructor::serialize(self, serializer)
90            }
91        }
92
93        impl #de_impl_generics ::jsonx::__derive::serde::Deserialize<'de> for #ident #ty_generics #where_clause {
94            fn deserialize<__D>(deserializer: __D) -> ::std::result::Result<Self, __D::Error>
95            where
96                __D: ::jsonx::__derive::serde::Deserializer<'de>,
97            {
98                ::jsonx::constructor::deserialize(deserializer)
99            }
100        }
101    })
102}
103
104/// Mirrors the JSONX identifier rule (`^[A-Za-z_][0-9A-Za-z_]*$`). Duplicated
105/// here because a proc-macro crate cannot depend on `jsonx` (which depends on
106/// it).
107fn is_valid_ctor_name(name: &str) -> bool {
108    let mut bytes = name.bytes();
109    match bytes.next() {
110        Some(b) if b.is_ascii_alphabetic() || b == b'_' => {}
111        _ => return false,
112    }
113    bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
114}