openapi_model_generator/
generator.rs

1use crate::{
2    models::{
3        CompositionModel, Model, ModelType, RequestModel, ResponseModel, UnionModel, UnionType,
4    },
5    Result,
6};
7
8const RUST_RESERVED_KEYWORDS: &[&str] = &[
9    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
10    "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
11    "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
12    "while", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "try",
13    "typeof", "unsized", "virtual", "yield",
14];
15
16const EMPTY_RESPONSE_NAME: &str = "UnknownResponse";
17const EMPTY_REQUEST_NAME: &str = "UnknownRequest";
18
19fn is_reserved_word(string_to_check: &str) -> bool {
20    RUST_RESERVED_KEYWORDS.contains(&string_to_check)
21}
22
23pub fn generate_models(
24    models: &[ModelType],
25    requests: &[RequestModel],
26    responses: &[ResponseModel],
27) -> Result<String> {
28    let mut output = String::new();
29
30    output.push_str("use serde::{Serialize, Deserialize};\n");
31    output.push_str("use uuid::Uuid;\n");
32    output.push_str("use chrono::{DateTime, NaiveDate, Utc};\n\n");
33
34    for model_type in models {
35        match model_type {
36            ModelType::Struct(model) => {
37                output.push_str(&generate_model(model)?);
38                output.push('\n');
39            }
40            ModelType::Union(union) => {
41                output.push_str(&generate_union(union)?);
42                output.push('\n');
43            }
44            ModelType::Composition(comp) => {
45                output.push_str(&generate_composition(comp)?);
46                output.push('\n');
47            }
48        }
49    }
50
51    for request in requests {
52        output.push_str(&generate_request_model(request)?);
53        output.push('\n');
54    }
55
56    for response in responses {
57        output.push_str(&generate_response_model(response)?);
58        output.push('\n');
59    }
60
61    Ok(output)
62}
63
64fn generate_model(model: &Model) -> Result<String> {
65    let mut output = String::new();
66
67    if !model.name.is_empty() {
68        output.push_str(&format!("/// {}\n", model.name));
69    }
70
71    output.push_str("#[derive(Debug, Serialize, Deserialize)]\n");
72    output.push_str(&format!("pub struct {} {{\n", model.name));
73
74    for field in &model.fields {
75        let field_type = match field.field_type.as_str() {
76            "String" => "String",
77            "f64" => "f64",
78            "i64" => "i64",
79            "bool" => "bool",
80            "DateTime" => "DateTime<Utc>",
81            "Date" => "NaiveDate",
82            "Uuid" => "Uuid",
83            _ => &field.field_type,
84        };
85
86        let mut lowercased_name = field.name.to_lowercase();
87        if is_reserved_word(&lowercased_name) {
88            lowercased_name = format!("r#{lowercased_name}")
89        }
90
91        output.push_str(&format!(
92            "    #[serde(rename = \"{}\")]\n",
93            field.name.to_lowercase()
94        ));
95
96        if field.is_required && !field.is_nullable {
97            output.push_str(&format!("    pub {lowercased_name}: {field_type},\n",));
98        } else {
99            output.push_str(&format!(
100                "    pub {lowercased_name}: Option<{field_type}>,\n",
101            ));
102        }
103    }
104
105    output.push_str("}\n\n");
106    Ok(output)
107}
108
109fn generate_request_model(request: &RequestModel) -> Result<String> {
110    let mut output = String::new();
111    tracing::info!("Generating request model");
112    tracing::info!("{:#?}", request);
113
114    if request.name.is_empty() || request.name == EMPTY_REQUEST_NAME {
115        return Ok(String::new());
116    }
117
118    output.push_str(&format!("/// {}\n", request.name));
119    output.push_str("#[derive(Debug, Serialize)]\n");
120    output.push_str(&format!("pub struct {} {{\n", request.name));
121    output.push_str("    pub content_type: String,\n");
122    output.push_str(&format!("    pub body: {},\n", request.schema));
123    output.push_str("}\n");
124    Ok(output)
125}
126
127fn generate_response_model(response: &ResponseModel) -> Result<String> {
128    let mut output = String::new();
129
130    // Return if name is empty
131    if response.name.is_empty() || response.name == EMPTY_RESPONSE_NAME {
132        return Ok(String::new());
133    }
134
135    output.push_str(&format!("/// {}\n", response.name));
136    output.push_str("#[derive(Debug, Deserialize)]\n");
137    output.push_str(&format!("pub struct {} {{\n", response.name));
138    output.push_str("    pub status_code: String,\n");
139    output.push_str("    pub content_type: String,\n");
140    output.push_str(&format!("    pub body: {},\n", response.schema));
141    if let Some(desc) = &response.description {
142        output.push_str(&format!("    /// {desc}\n"));
143    }
144    output.push_str("}\n");
145    Ok(output)
146}
147
148fn generate_union(union: &UnionModel) -> Result<String> {
149    let mut output = String::new();
150
151    output.push_str(&format!(
152        "/// {} ({})\n",
153        union.name,
154        match union.union_type {
155            UnionType::OneOf => "oneOf",
156            UnionType::AnyOf => "anyOf",
157        }
158    ));
159    output.push_str("#[derive(Debug, Serialize, Deserialize)]\n");
160    output.push_str("#[serde(tag = \"type\")]\n");
161    output.push_str(&format!("pub enum {} {{\n", union.name));
162
163    for variant in &union.variants {
164        output.push_str(&format!("    {} {{\n", variant.name));
165
166        for field in &variant.fields {
167            let field_type = match field.field_type.as_str() {
168                "String" => "String",
169                "f64" => "f64",
170                "i64" => "i64",
171                "bool" => "bool",
172                "DateTime" => "DateTime<Utc>",
173                "Date" => "NaiveDate",
174                "Uuid" => "Uuid",
175                _ => &field.field_type,
176            };
177
178            let mut lowercased_name = field.name.to_lowercase();
179            if is_reserved_word(&lowercased_name) {
180                lowercased_name = format!("r#{lowercased_name}");
181            }
182
183            output.push_str(&format!(
184                "        #[serde(rename = \"{}\")]\n",
185                field.name.to_lowercase()
186            ));
187
188            if field.is_required && !field.is_nullable {
189                output.push_str(&format!("        {lowercased_name}: {field_type},\n"));
190            } else {
191                output.push_str(&format!(
192                    "        {lowercased_name}: Option<{field_type}>,\n"
193                ));
194            }
195        }
196
197        output.push_str("    },\n");
198    }
199
200    output.push_str("}\n");
201    Ok(output)
202}
203
204fn generate_composition(comp: &CompositionModel) -> Result<String> {
205    let mut output = String::new();
206
207    output.push_str(&format!("/// {} (allOf composition)\n", comp.name));
208    output.push_str("#[derive(Debug, Serialize, Deserialize)]\n");
209    output.push_str(&format!("pub struct {} {{\n", comp.name));
210
211    for field in &comp.all_fields {
212        let field_type = match field.field_type.as_str() {
213            "String" => "String",
214            "f64" => "f64",
215            "i64" => "i64",
216            "bool" => "bool",
217            "DateTime" => "DateTime<Utc>",
218            "Date" => "NaiveDate",
219            "Uuid" => "Uuid",
220            _ => &field.field_type,
221        };
222
223        let mut lowercased_name = field.name.to_lowercase();
224        if is_reserved_word(&lowercased_name) {
225            lowercased_name = format!("r#{lowercased_name}");
226        }
227
228        output.push_str(&format!(
229            "    #[serde(rename = \"{}\")]\n",
230            field.name.to_lowercase()
231        ));
232
233        if field.is_required && !field.is_nullable {
234            output.push_str(&format!("    pub {lowercased_name}: {field_type},\n"));
235        } else {
236            output.push_str(&format!(
237                "    pub {lowercased_name}: Option<{field_type}>,\n"
238            ));
239        }
240    }
241
242    output.push_str("}\n");
243    Ok(output)
244}
245
246pub fn generate_rust_code(models: &[Model]) -> Result<String> {
247    let mut code = String::new();
248
249    code.push_str("use serde::{Serialize, Deserialize};\n");
250    code.push_str("use uuid::Uuid;\n");
251    code.push_str("use chrono::{DateTime, NaiveDate, Utc};\n\n");
252
253    for model in models {
254        code.push_str(&format!("/// {}\n", model.name));
255        code.push_str("#[derive(Debug, Serialize, Deserialize)]\n");
256        code.push_str(&format!("pub struct {} {{\n", model.name));
257
258        for field in &model.fields {
259            let field_type = match field.field_type.as_str() {
260                "String" => "String",
261                "f64" => "f64",
262                "i64" => "i64",
263                "bool" => "bool",
264                "DateTime" => "DateTime<Utc>",
265                "Date" => "NaiveDate",
266                "Uuid" => "Uuid",
267                _ => &field.field_type,
268            };
269
270            let mut lowercased_name = field.name.to_lowercase();
271            if is_reserved_word(&lowercased_name) {
272                lowercased_name = format!("r#{lowercased_name}")
273            }
274
275            code.push_str(&format!(
276                "    #[serde(rename = \"{}\")]\n",
277                field.name.to_lowercase()
278            ));
279
280            if field.is_required {
281                code.push_str(&format!("    pub {lowercased_name}: {field_type},\n",));
282            } else {
283                code.push_str(&format!(
284                    "    pub {lowercased_name}: Option<{field_type}>,\n",
285                ));
286            }
287        }
288
289        code.push_str("}\n\n");
290    }
291
292    Ok(code)
293}
294
295pub fn generate_lib() -> Result<String> {
296    let mut code = String::new();
297    code.push_str("pub mod models;\n");
298
299    Ok(code)
300}