use proc_macro2::TokenStream;
use quote::quote;
use serde_json::Value;
use syn::{Fields, Ident, ItemStruct, Visibility};
use crate::error::{ForgeError, ForgeResult};
use crate::schema::user_type::{FieldType, classify, validate};
use crate::rename::RenameRule;
use super::util::{make_ident, to_screaming_snake};
#[cfg(feature = "embedded")]
use proc_macro2::Literal;
#[cfg_attr(not(feature = "embedded"), allow(dead_code))]
struct FieldInfo<'a> {
ident: &'a Ident,
json_key: String,
ft: FieldType,
}
fn named_fields(item: &ItemStruct, rename: Option<RenameRule>) -> ForgeResult<Vec<FieldInfo<'_>>> {
let named = match &item.fields {
Fields::Named(f) => f,
_ => {
return Err(ForgeError::call_site(
"`#[json_forge]` only supports structs with named fields",
));
},
};
named
.named
.iter()
.map(|f| {
let ident = f.ident.as_ref().unwrap();
let ft = classify(&f.ty);
if ft == FieldType::Opaque {
return Err(ForgeError::call_site(format!(
"field `{}` has a type that `json_forge` cannot map to a JSON value; \
supported types: `bool`, integer primitives, `f32`/`f64`, `String`, \
`&'static str`, `Vec<T>`, `&'static [T]`, `Option<T>`",
ident
)));
}
let json_key = match rename {
Some(rule) => rule.rust_name_to_json_key(&ident.to_string()),
None => ident.to_string(),
};
Ok(FieldInfo {
ident,
json_key,
ft,
})
})
.collect()
}
#[cfg(feature = "embedded")]
fn validate_and_build_literal(
name: &Ident,
fields: &[FieldInfo<'_>],
obj: &serde_json::Map<String, Value>,
entry_ctx: &str,
) -> ForgeResult<TokenStream> {
let mut field_tokens = Vec::with_capacity(fields.len());
for f in fields {
let json_val = obj.get(&f.json_key).unwrap_or(&Value::Null);
let ctx = format!("{entry_ctx}.{}", f.json_key);
if obj.get(&f.json_key).is_none() && !matches!(f.ft, FieldType::Optional(_)) {
return Err(ForgeError::call_site(format!(
"{ctx}: key `{}` is missing in the JSON and the field is not `Option<T>`",
f.json_key
)));
}
validate(json_val, &f.ft, &ctx)?;
let val_ts = value_to_tokens(json_val, &f.ft)?;
let ident = f.ident;
field_tokens.push(quote! { #ident: #val_ts });
}
Ok(quote! { #name { #(#field_tokens,)* } })
}
#[cfg(feature = "embedded")]
fn value_to_tokens(val: &Value, ft: &FieldType) -> ForgeResult<TokenStream> {
match (val, ft) {
(Value::Null, FieldType::Optional(_)) => Ok(quote! { ::core::option::Option::None }),
(_, FieldType::Optional(inner)) => {
let inner_ts = value_to_tokens(val, inner)?;
Ok(quote! { ::core::option::Option::Some(#inner_ts) })
},
(Value::String(s), FieldType::StaticStr) => Ok(quote! { #s }),
(Value::String(s), FieldType::OwnedString) => Ok(quote! { #s.to_owned() }),
(Value::Bool(b), FieldType::Bool) => Ok(quote! { #b }),
(Value::Number(n), FieldType::Int) => {
let v = n.as_i64().unwrap_or_default();
Ok(quote! { #v })
},
(Value::Number(n), FieldType::Float) => {
let v = n.as_f64().unwrap_or_default();
Ok(quote! { #v })
},
(Value::Array(arr), FieldType::StaticSlice(inner)) => {
let items: Vec<TokenStream> = arr
.iter()
.map(|v| value_to_tokens(v, inner))
.collect::<ForgeResult<_>>()?;
Ok(quote! { &[#(#items),*] })
},
(Value::Array(arr), FieldType::OwnedVec(inner)) => {
let items: Vec<TokenStream> = arr
.iter()
.map(|v| value_to_tokens(v, inner))
.collect::<ForgeResult<_>>()?;
Ok(quote! { vec![#(#items),*] })
},
_ => Err(ForgeError::call_site(format!(
"cannot convert JSON `{val}` to the declared type"
))),
}
}
#[cfg(feature = "embedded")]
fn emit_phf_map(
name: &Ident,
data_vis: &Visibility,
fields: &[FieldInfo<'_>],
root: &Value,
) -> ForgeResult<TokenStream> {
use phf_generator::generate_hash;
let obj = root.as_object().unwrap();
let keys: Vec<&str> = obj.keys().map(String::as_str).collect();
let values: Vec<&Value> = obj.values().collect();
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();
let entries: Vec<TokenStream> = hash_state
.map
.iter()
.map(|&idx| {
let k = keys[idx];
let v = values[idx];
let obj_map = v.as_object().ok_or_else(|| {
ForgeError::call_site(format!("entry `{k}` is not a JSON object"))
})?;
let lit = validate_and_build_literal(name, fields, obj_map, k)?;
Ok(quote! { (#k, #lit) })
})
.collect::<ForgeResult<_>>()?;
let static_name = make_ident(&to_screaming_snake(&name.to_string()));
Ok(quote! {
#data_vis static #static_name: ::phf::Map<&'static str, #name> = ::phf::Map {
key: #phf_key,
disps: &[#(#disps),*],
entries: &[#(#entries),*],
};
})
}
#[cfg(feature = "embedded")]
fn emit_static_array(
name: &Ident,
data_vis: &Visibility,
fields: &[FieldInfo<'_>],
root: &Value,
) -> ForgeResult<TokenStream> {
let arr = root.as_array().unwrap();
let len = arr.len();
let items: Vec<TokenStream> = arr
.iter()
.enumerate()
.map(|(i, v)| {
let obj = v
.as_object()
.ok_or_else(|| ForgeError::call_site(format!("array[{i}] is not a JSON object")))?;
validate_and_build_literal(name, fields, obj, &format!("[{i}]"))
})
.collect::<ForgeResult<_>>()?;
let static_name = make_ident(&format!("{}_DATA", to_screaming_snake(&name.to_string())));
Ok(quote! {
#data_vis static #static_name: [#name; #len] = [#(#items),*];
})
}
#[cfg(feature = "embedded")]
fn emit_single_static(
name: &Ident,
data_vis: &Visibility,
fields: &[FieldInfo<'_>],
root: &Value,
) -> ForgeResult<TokenStream> {
let obj = root.as_object().unwrap();
let instance = validate_and_build_literal(name, fields, obj, "<root>")?;
let static_name = make_ident(&to_screaming_snake(&name.to_string()));
Ok(quote! {
#data_vis static #static_name: #name = #instance;
})
}
fn inline_json_const(name: &Ident, data_vis: &Visibility, json_text: &str) -> TokenStream {
let const_name = make_ident(&format!("{}_JSON", to_screaming_snake(&name.to_string())));
quote! {
#data_vis const #const_name: &'static str = #json_text;
}
}
pub fn generate_with_text(
struct_item: &ItemStruct,
data_vis: &Visibility,
root: &Value,
json_text: &str,
use_embedded: bool,
rename_all: Option<RenameRule>,
) -> ForgeResult<TokenStream> {
let fields = named_fields(struct_item, rename_all)?;
let name = &struct_item.ident;
#[cfg(not(feature = "embedded"))]
let _ = use_embedded;
let data_ts: TokenStream = match root {
Value::Object(map) if map.values().all(|v| v.is_object()) && map.len() > 1 => {
for (k, v) in map {
let obj = v.as_object().unwrap();
for f in &fields {
let jv = obj.get(&f.json_key).unwrap_or(&Value::Null);
if obj.get(&f.json_key).is_none() && !matches!(f.ft, FieldType::Optional(_)) {
return Err(ForgeError::call_site(format!(
"entry `{k}`: key `{}` is missing and the field is not `Option<T>`",
f.json_key
)));
}
validate(jv, &f.ft, &format!("{k}.{}", f.json_key))?;
}
}
#[cfg(feature = "embedded")]
if use_embedded {
let embed_ts = emit_phf_map(name, data_vis, &fields, root)?;
return Ok(quote! { #struct_item #embed_ts });
}
inline_json_const(name, data_vis, json_text)
},
Value::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let obj = v.as_object().ok_or_else(|| {
ForgeError::call_site(format!("array[{i}] is not a JSON object"))
})?;
for f in &fields {
let jv = obj.get(&f.json_key).unwrap_or(&Value::Null);
validate(jv, &f.ft, &format!("[{i}].{}", f.json_key))?;
}
}
#[cfg(feature = "embedded")]
if use_embedded {
let embed_ts = emit_static_array(name, data_vis, &fields, root)?;
return Ok(quote! { #struct_item #embed_ts });
}
inline_json_const(name, data_vis, json_text)
},
Value::Object(map) => {
for f in &fields {
let jv = map.get(&f.json_key).unwrap_or(&Value::Null);
validate(jv, &f.ft, &format!("<root>.{}", f.json_key))?;
}
#[cfg(feature = "embedded")]
if use_embedded {
let embed_ts = emit_single_static(name, data_vis, &fields, root)?;
return Ok(quote! { #struct_item #embed_ts });
}
inline_json_const(name, data_vis, json_text)
},
_ => {
return Err(ForgeError::call_site(
"top-level JSON must be an object or array",
));
},
};
Ok(quote! { #struct_item #data_ts })
}