schema2struct/generator.rs
1use inflections::Inflect;
2use quote::{format_ident, quote, ToTokens};
3use serde_json::{Map, Value};
4use syn::Ident;
5
6pub struct JsonMacroInput {
7 pub struct_name: Ident,
8 pub content: Value,
9}
10/// Generates Rust structs from a JSON-like structure with flexible configuration.
11///
12/// # Parameters
13/// - `json_struct`: The input JSON macro structure
14/// - `base_name`: The base name for the primary struct
15///
16/// # Returns
17/// A tuple containing:
18/// 1. The main generated struct as a token stream
19/// 2. A vector of additional nested structs
20pub fn generate_structs(
21 json_struct: &JsonMacroInput,
22 base_name: &Ident,
23) -> (proc_macro2::TokenStream, Vec<proc_macro2::TokenStream>) {
24 // Collect all generated structs
25 let mut all_structs = Vec::new();
26 let mut fields = Vec::new();
27
28 let content = match json_struct.content.as_object() {
29 Some(obj) => obj,
30 None => &Map::new(),
31 };
32
33 for (key, value) in content {
34 if key.eq("struct_name") {
35 continue;
36 }
37
38 let key = key.to_snake_case();
39 // Just in case the identifier is not a valid struct name
40 let field_name = format_ident!("{}", key);
41
42 // Infer field type and handle nested structures
43 let field_type = match value {
44 Value::String(_) => quote!(String),
45 Value::Number(_) => quote!(f64),
46 Value::Bool(_) => quote!(bool),
47
48 Value::Array(arr) => {
49 let (elem_type, _) = infer_array_type(arr);
50 quote!(Vec<#elem_type>)
51 }
52
53 Value::Object(obj) => {
54 // Generate nested struct for object and concat the key with the struct name
55 //
56 // `Example`
57 //
58 //```rust
59 //
60 // struct User {
61 // age: UserAge
62 // }
63 //
64 // struct UserAge;
65 //
66 //````
67 let nested_name = {
68 if let Some(struct_name_value) = obj.get("struct_name") {
69 if let Value::String(struct_name) = struct_name_value {
70 if struct_name.eq("key") {
71 format_ident!("{}", key.to_pascal_case())
72 } else {
73 format_ident!("{}", struct_name.to_pascal_case())
74 }
75 } else {
76 unreachable!()
77 }
78 } else {
79 format_ident!("{}{}", base_name, key.to_pascal_case())
80 }
81 };
82
83 let nested_macro_input = JsonMacroInput {
84 struct_name: json_struct.struct_name.clone(),
85 content: Value::Object(obj.clone()),
86 };
87
88 // Recursively generate nested structs
89 let (nested_struct, nested_structs) =
90 generate_structs(&nested_macro_input, &nested_name);
91
92 all_structs.extend(nested_structs);
93 all_structs.push(nested_struct.clone());
94
95 format_ident!("{}", nested_name).into_token_stream()
96 }
97 Value::Null => quote!(Option<::serde_json::Value>),
98 };
99
100 // Handle Serde alias configuration
101 //
102 // this is usefull when serializing, and when also specifing the @camel|pascal|snake flags
103 //
104 // if you have a json that's formatted like so
105 //
106 // ```json
107 // {
108 // "name": "Abdullah",
109 // "jobs_list": ["Cybersecurity"]
110 // }
111 // ```
112 //
113 // the keys are written in snake_case,
114 // which means if you have a sruct that you want to deserialize to which has an attribte that looks like this
115 //
116 // ```rust
117 // #[derive(Deserialize, Serialize)]
118 // #[serde(rename_all = "camelCase")]
119 // struct User {
120 // name: String,
121 // jobs_list: Vec<String>
122 // }
123 // ```
124 //
125 // this will only deserialize if you give it a camelCase keys, not snake_case
126 //
127 // this is where the `#[serde(alias = "jobs_list")]` comes in, it allows you to have both,
128 // so you can deserialize with camelCase and snake_case
129 let field = quote! {
130 #[serde(alias = #key)]
131 pub #field_name: #field_type
132 };
133
134 fields.push(field);
135 }
136
137 // Generate the main struct with optional rename strategy
138 let main_struct = quote! {
139 #[derive(::serde::Deserialize, ::serde::Serialize, ::std::clone::Clone, ::std::fmt::Debug, ::std::default::Default)]
140 #[serde(rename_all = "camelCase")]
141 pub struct #base_name {
142 #(#fields),*
143 }
144 };
145
146 (main_struct, all_structs)
147}
148
149/// Infers the element type for an array of JSON values.
150///
151/// # Parameters
152/// - `arr`: A slice of JSON values
153///
154/// # Returns
155/// A tuple containing:
156/// 1. The inferred element type as a token stream
157/// 2. Any additional generated structs (currently unused)
158fn infer_array_type(arr: &[Value]) -> (proc_macro2::TokenStream, Vec<proc_macro2::TokenStream>) {
159 // Handle empty array
160 if arr.is_empty() {
161 return (quote!(::serde_json::Value), Vec::new());
162 }
163
164 // Infer type based on first element
165 match &arr[0] {
166 Value::String(_) => (quote!(String), Vec::new()),
167 Value::Number(_) => (quote!(f64), Vec::new()),
168 Value::Bool(_) => (quote!(bool), Vec::new()),
169 _ => (quote!(::serde_json::Value), Vec::new()),
170 }
171}