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 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}