jsonforge 0.1.0

A Rust procedural macro for generating JSON schema validators from Rust types
Documentation
use proc_macro2::Span;
use syn::{
    Ident, LitBool, LitStr, Result, Token, Visibility,
    parse::{Parse, ParseStream},
};

use crate::rename::RenameRule;

/// Parsed arguments for the `json_forge!` macro.
///
/// Syntax:
/// ```text
/// json_forge! {
///     path     = "relative/path/to/file.json",
///     name     = "MyEntry",
///     vis      = pub,         // struct visibility (default: pub)
///     data_vis = pub(crate),  // data visibility  (default: same as vis)
///     embedded = false,       // override embedded mode (default: follows feature flag)
/// }
/// ```
///
/// Accepted visibility tokens: `pub`, `pub(crate)`, `pub(super)`, `pub(self)`, `private`.
pub struct MacroInput {
    pub path: LitStr,
    pub name: Ident,
    /// Visibility applied to the generated struct type.
    pub vis: Visibility,
    /// Visibility applied to the generated static / const data item.
    /// When `None` the same visibility as `vis` is used.
    pub data_vis: Option<Visibility>,
    /// Explicit embedded-mode override.
    /// `Some(true)` requires the `embedded` feature; `Some(false)` forces runtime;
    /// `None` follows the Cargo `embedded` feature flag.
    pub embedded: Option<bool>,
    /// How JSON keys map to Rust field names (default: `SnakeCase` = no conversion).
    pub rename_all: Option<RenameRule>,
}

/// A single `key = value` pair in the macro input.
enum KvPair {
    Path(LitStr),
    Name(Ident),
    Vis(Visibility),
    DataVis(Visibility),
    Embedded(bool),
    RenameAll(RenameRule),
}

/// Parse a visibility token.
///
/// Accepts `pub`, `pub(crate)`, `pub(super)`, `pub(self)`, or the special
/// identifier `private` which maps to `Visibility::Inherited` (no modifier).
fn parse_vis(input: ParseStream<'_>) -> Result<Visibility> {
    if input.peek(Token![pub]) {
        input.parse::<Visibility>()
    } else {
        let ident: Ident = input.parse()?;
        if ident == "private" {
            Ok(Visibility::Inherited)
        } else {
            Err(syn::Error::new(
                ident.span(),
                format!(
                    "expected `pub`, `pub(crate)`, `pub(super)`, `pub(self)`, or `private`; \
                     found `{ident}`"
                ),
            ))
        }
    }
}

impl Parse for KvPair {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let key: Ident = input.parse()?;
        input.parse::<Token![=]>()?;

        match key.to_string().as_str() {
            "path" => Ok(KvPair::Path(input.parse()?)),
            "name" => {
                // Accept either a string literal or a bare identifier.
                if input.peek(LitStr) {
                    let lit: LitStr = input.parse()?;
                    Ok(KvPair::Name(Ident::new(&lit.value(), lit.span())))
                } else {
                    Ok(KvPair::Name(input.parse()?))
                }
            },
            "vis" => Ok(KvPair::Vis(parse_vis(input)?)),
            "data_vis" => Ok(KvPair::DataVis(parse_vis(input)?)),
            "embedded" => {
                let b: LitBool = input.parse()?;
                Ok(KvPair::Embedded(b.value()))
            },
            "rename_all" => {
                let lit: LitStr = input.parse()?;
                Ok(KvPair::RenameAll(RenameRule::from_lit(&lit)?))
            },
            other => Err(syn::Error::new(
                key.span(),
                format!(
                    "unknown key `{other}`; \
                     expected `path`, `name`, `vis`, `data_vis`, `embedded`, or `rename_all`"
                ),
            )),
        }
    }
}

impl Parse for MacroInput {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let mut path: Option<LitStr> = None;
        let mut name: Option<Ident> = None;
        let mut vis: Option<Visibility> = None;
        let mut data_vis: Option<Visibility> = None;
        let mut embedded: Option<bool> = None;
        let mut rename_all: Option<RenameRule> = None;

        while !input.is_empty() {
            let kv: KvPair = input.parse()?;
            // Optional trailing comma.
            let _ = input.parse::<Token![,]>();

            match kv {
                KvPair::Path(v) => {
                    if path.replace(v).is_some() {
                        return Err(syn::Error::new(Span::call_site(), "duplicate `path` key"));
                    }
                },
                KvPair::Name(v) => {
                    if name.replace(v).is_some() {
                        return Err(syn::Error::new(Span::call_site(), "duplicate `name` key"));
                    }
                },
                KvPair::Vis(v) => {
                    if vis.replace(v).is_some() {
                        return Err(syn::Error::new(Span::call_site(), "duplicate `vis` key"));
                    }
                },
                KvPair::DataVis(v) => {
                    if data_vis.replace(v).is_some() {
                        return Err(syn::Error::new(
                            Span::call_site(),
                            "duplicate `data_vis` key",
                        ));
                    }
                },
                KvPair::Embedded(v) => {
                    if embedded.replace(v).is_some() {
                        return Err(syn::Error::new(
                            Span::call_site(),
                            "duplicate `embedded` key",
                        ));
                    }
                },
                KvPair::RenameAll(v) => {
                    if rename_all.replace(v).is_some() {
                        return Err(syn::Error::new(
                            Span::call_site(),
                            "duplicate `rename_all` key",
                        ));
                    }
                },
            }
        }

        let path =
            path.ok_or_else(|| syn::Error::new(Span::call_site(), "missing required key `path`"))?;
        let name =
            name.ok_or_else(|| syn::Error::new(Span::call_site(), "missing required key `name`"))?;
        let vis = vis.unwrap_or(Visibility::Public(Token![pub](Span::call_site())));

        Ok(MacroInput {
            path,
            name,
            vis,
            data_vis,
            embedded,
            rename_all,
        })
    }
}

// ---------------------------------------------------------------------------
// Attribute macro input  (#[json_forge(path = "...", ...)])
// ---------------------------------------------------------------------------

/// Arguments for the `#[json_forge(...)]` attribute macro.
///
/// Only `path` is required; `data_vis` and `embedded` are optional.
/// The struct name and struct visibility are taken directly from the annotated item.
pub struct AttrInput {
    pub path: LitStr,
    /// Overrides the visibility of the emitted data item.
    /// `None` means inherit the struct's own visibility.
    pub data_vis: Option<Visibility>,
    /// `Some(true)` forces embedded, `Some(false)` forces runtime,
    /// `None` follows the `embedded` Cargo feature flag.
    pub embedded: Option<bool>,
    /// How snake_case Rust field names map to JSON keys (default: `SnakeCase` = no conversion).
    pub rename_all: Option<RenameRule>,
}

impl Parse for AttrInput {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let mut path: Option<LitStr> = None;
        let mut data_vis: Option<Visibility> = None;
        let mut embedded: Option<bool> = None;
        let mut rename_all: Option<RenameRule> = None;

        while !input.is_empty() {
            let key: Ident = input.parse()?;
            input.parse::<Token![=]>()?;

            match key.to_string().as_str() {
                "path" => {
                    let v: LitStr = input.parse()?;
                    if path.replace(v).is_some() {
                        return Err(syn::Error::new(Span::call_site(), "duplicate `path` key"));
                    }
                },
                "data_vis" => {
                    let v = parse_vis(input)?;
                    if data_vis.replace(v).is_some() {
                        return Err(syn::Error::new(
                            Span::call_site(),
                            "duplicate `data_vis` key",
                        ));
                    }
                },
                "embedded" => {
                    let b: LitBool = input.parse()?;
                    if embedded.replace(b.value()).is_some() {
                        return Err(syn::Error::new(
                            Span::call_site(),
                            "duplicate `embedded` key",
                        ));
                    }
                },
                "rename_all" => {
                    let lit: LitStr = input.parse()?;
                    if rename_all.replace(RenameRule::from_lit(&lit)?).is_some() {
                        return Err(syn::Error::new(
                            Span::call_site(),
                            "duplicate `rename_all` key",
                        ));
                    }
                },
                other => {
                    return Err(syn::Error::new(
                        key.span(),
                        format!(
                            "unknown key `{other}`; \
                             expected `path`, `data_vis`, `embedded`, or `rename_all`"
                        ),
                    ));
                },
            }

            let _ = input.parse::<Token![,]>();
        }

        let path =
            path.ok_or_else(|| syn::Error::new(Span::call_site(), "missing required key `path`"))?;

        Ok(AttrInput {
            path,
            data_vis,
            embedded,
            rename_all,
        })
    }
}