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}