jsonforge 0.1.0

A Rust procedural macro for generating JSON schema validators from Rust types
Documentation
use proc_macro2::{Literal, TokenStream};
use quote::quote;
use serde_json::Value;
use syn::{Ident, Visibility};

use crate::error::{ForgeError, ForgeResult};
use crate::schema::types::{SchemaType, StructSchema, TopLevel};

use super::types::{schema_type_tokens, struct_def};
use super::util::{make_ident, to_screaming_snake};

/// Generate the embedded output (feature = "embedded"):
/// - Struct definition(s) with `&'static str` / `&'static [T]` fields.
/// - A `static <NAME>: ::phf::Map<…>` or a fixed-length array static.
pub fn generate(
    vis: &Visibility,
    data_vis: &Visibility,
    name: &Ident,
    top: &TopLevel,
    root: &Value,
) -> ForgeResult<TokenStream> {
    match top {
        TopLevel::Map { entry } => generate_phf_map(vis, data_vis, name, entry, root),
        TopLevel::Array { entry } => generate_static_array(vis, data_vis, name, entry, root),
        TopLevel::Struct(schema) => generate_single_struct(vis, data_vis, name, schema, root),
    }
}

// ---------------------------------------------------------------------------
// PHF map  (top-level JSON object)
// ---------------------------------------------------------------------------

fn generate_phf_map(
    vis: &Visibility,
    data_vis: &Visibility,
    name: &Ident,
    schema: &StructSchema,
    root: &Value,
) -> ForgeResult<TokenStream> {
    use phf_generator::generate_hash;

    let obj = root
        .as_object()
        .ok_or_else(|| ForgeError::call_site("expected a JSON object at top level"))?;

    let keys: Vec<&str> = obj.keys().map(String::as_str).collect();
    let values: Vec<&Value> = obj.values().collect();

    // Compute the perfect hash.
    let hash_state = generate_hash(&keys);

    let phf_key = Literal::u64_suffixed(hash_state.key);
    let disps: Vec<TokenStream> = hash_state
        .disps
        .iter()
        .map(|&(d1, d2)| {
            let d1 = Literal::u32_suffixed(d1);
            let d2 = Literal::u32_suffixed(d2);
            quote! { (#d1, #d2) }
        })
        .collect();

    // Reorder entries according to the hash map permutation.
    let entries: Vec<TokenStream> = hash_state
        .map
        .iter()
        .map(|&idx| {
            let k = keys[idx];
            let v = values[idx];
            let struct_lit = value_to_struct_literal(v, schema, name)?;
            Ok(quote! { (#k, #struct_lit) })
        })
        .collect::<ForgeResult<_>>()?;

    let struct_ts = struct_def(vis, name, schema, true);
    let static_name = make_ident(&to_screaming_snake(&name.to_string()));
    let entry_type = quote! { #name };

    Ok(quote! {
        #struct_ts

        #data_vis static #static_name: ::phf::Map<&'static str, #entry_type> = ::phf::Map {
            key: #phf_key,
            disps: &[#(#disps),*],
            entries: &[#(#entries),*],
        };
    })
}

// ---------------------------------------------------------------------------
// Static fixed-length array  (top-level JSON array)
// ---------------------------------------------------------------------------

fn generate_static_array(
    vis: &Visibility,
    data_vis: &Visibility,
    name: &Ident,
    entry_ty: &SchemaType,
    root: &Value,
) -> ForgeResult<TokenStream> {
    let arr = root
        .as_array()
        .ok_or_else(|| ForgeError::call_site("expected a JSON array at top level"))?;
    let len = arr.len();

    let struct_ts = match entry_ty {
        SchemaType::Struct(schema) => struct_def(vis, name, schema, true),
        _ => quote! {},
    };

    let item_tokens: Vec<TokenStream> = arr
        .iter()
        .map(|v| value_to_tokens(v, entry_ty, name))
        .collect::<ForgeResult<_>>()?;

    let static_name = make_ident(&format!("{}_DATA", to_screaming_snake(&name.to_string())));
    let rust_ty = if matches!(entry_ty, SchemaType::Struct(_)) {
        quote! { #name }
    } else {
        schema_type_tokens(entry_ty, true)
    };

    Ok(quote! {
        #struct_ts

        #data_vis static #static_name: [#rust_ty; #len] = [
            #(#item_tokens),*
        ];
    })
}

// ---------------------------------------------------------------------------
// Single top-level struct (not a keyed map)
// ---------------------------------------------------------------------------

fn generate_single_struct(
    vis: &Visibility,
    data_vis: &Visibility,
    name: &Ident,
    schema: &StructSchema,
    root: &Value,
) -> ForgeResult<TokenStream> {
    let struct_ts = struct_def(vis, name, schema, true);
    let static_name = make_ident(&to_screaming_snake(&name.to_string()));
    let instance = value_to_struct_literal(root, schema, name)?;

    Ok(quote! {
        #struct_ts

        #data_vis static #static_name: #name = #instance;
    })
}

// ---------------------------------------------------------------------------
// Value → token literal helpers
// ---------------------------------------------------------------------------

/// Render a JSON object value as `Name { field: val, … }`.
fn value_to_struct_literal(
    val: &Value,
    schema: &StructSchema,
    name: &Ident,
) -> ForgeResult<TokenStream> {
    let obj = val
        .as_object()
        .ok_or_else(|| ForgeError::call_site(format!("expected JSON object, got: {}", val)))?;

    let fields: Vec<TokenStream> = schema
        .fields
        .iter()
        .map(|f| {
            let field_ident = make_ident(&f.rust_name);
            let json_val = obj.get(&f.json_key).unwrap_or(&Value::Null);
            let val_ts = value_to_tokens(json_val, &f.ty, name)?;
            Ok(quote! { #field_ident: #val_ts })
        })
        .collect::<ForgeResult<_>>()?;

    Ok(quote! { #name { #(#fields,)* } })
}

/// Render any JSON value as a Rust literal / expression matching `ty`.
fn value_to_tokens(val: &Value, ty: &SchemaType, struct_name: &Ident) -> ForgeResult<TokenStream> {
    match (val, ty) {
        // Null → None
        (Value::Null, SchemaType::Optional(_)) => Ok(quote! { ::core::option::Option::None }),
        // Unwrap null to optional inner type
        (_, SchemaType::Optional(inner)) => {
            let inner_ts = value_to_tokens(val, inner, struct_name)?;
            Ok(quote! { ::core::option::Option::Some(#inner_ts) })
        },
        (Value::Bool(b), SchemaType::Bool) => Ok(quote! { #b }),
        (Value::Number(n), SchemaType::Integer) => {
            let v = n.as_i64().unwrap_or_default();
            Ok(quote! { #v })
        },
        (Value::Number(n), SchemaType::Float) => {
            let v = n.as_f64().unwrap_or_default();
            Ok(quote! { #v })
        },
        (Value::String(s), SchemaType::Str) => Ok(quote! { #s }),
        (Value::Array(arr), SchemaType::Array(inner)) => {
            let items: Vec<TokenStream> = arr
                .iter()
                .map(|v| value_to_tokens(v, inner, struct_name))
                .collect::<ForgeResult<_>>()?;
            Ok(quote! { &[#(#items),*] })
        },
        (Value::Object(_), SchemaType::Struct(schema)) => {
            value_to_struct_literal(val, schema, struct_name)
        },
        // Null for non-optional types: emit a default.
        (Value::Null, SchemaType::Str) => Ok(quote! { "" }),
        (Value::Null, SchemaType::Bool) => Ok(quote! { false }),
        (Value::Null, SchemaType::Integer) => Ok(quote! { 0i64 }),
        (Value::Null, SchemaType::Float) => Ok(quote! { 0.0f64 }),
        (Value::Null, SchemaType::Array(_)) => Ok(quote! { &[] }),
        _ => Err(ForgeError::call_site(format!(
            "cannot represent JSON value `{}` as type `{:?}`",
            val, ty
        ))),
    }
}