1use heck::{ToSnakeCase, ToUpperCamelCase};
2use indexmap::IndexMap;
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote, ToTokens};
5use syn::Ident;
6
7use crate::openapi::r#type::OpenApiType;
8
9use super::r#enum::Enum;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum PrimitiveType {
13 Bool,
14 I32,
15 I64,
16 String,
17 Float,
18 DateTime,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum PropertyType {
23 Primitive(PrimitiveType),
24 Ref(String),
25 Enum(Enum),
26 Nested(Box<Object>),
27 Array(Box<PropertyType>),
28}
29
30impl PropertyType {
31 pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
32 match self {
33 Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()),
34 Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()),
35 Self::Primitive(PrimitiveType::I64) => Some(format_ident!("i64").into_token_stream()),
36 Self::Primitive(PrimitiveType::String) => {
37 Some(format_ident!("String").into_token_stream())
38 }
39 Self::Primitive(PrimitiveType::DateTime) => {
40 Some(quote! { chrono::DateTime<chrono::Utc> })
41 }
42 Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()),
43 Self::Ref(path) => {
44 let name = path.strip_prefix("#/components/schemas/")?;
45 let name = format_ident!("{name}");
46
47 Some(quote! { crate::models::#name })
48 }
49 Self::Enum(r#enum) => {
50 let code = r#enum.codegen()?;
51 namespace.push_element(code);
52
53 let ns = namespace.get_ident();
54 let name = format_ident!("{}", r#enum.name);
55
56 Some(quote! {
57 #ns::#name
58 })
59 }
60 Self::Array(array) => {
61 let inner_ty = array.codegen(namespace)?;
62
63 Some(quote! {
64 Vec<#inner_ty>
65 })
66 }
67 Self::Nested(nested) => {
68 let code = nested.codegen()?;
69 namespace.push_element(code);
70
71 let ns = namespace.get_ident();
72 let name = format_ident!("{}", nested.name);
73
74 Some(quote! {
75 #ns::#name
76 })
77 }
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Property {
84 pub name: String,
85 pub description: Option<String>,
86 pub required: bool,
87 pub nullable: bool,
88 pub r#type: PropertyType,
89 pub deprecated: bool,
90}
91
92impl Property {
93 pub fn from_schema(
94 name: &str,
95 required: bool,
96 schema: &OpenApiType,
97 schemas: &IndexMap<&str, OpenApiType>,
98 ) -> Option<Self> {
99 let name = name.to_owned();
100 let description = schema.description.as_deref().map(ToOwned::to_owned);
101
102 match schema {
103 OpenApiType {
104 r#enum: Some(_), ..
105 } => Some(Self {
106 r#type: PropertyType::Enum(Enum::from_schema(
107 &name.clone().to_upper_camel_case(),
108 schema,
109 )?),
110 name,
111 description,
112 required,
113 deprecated: schema.deprecated,
114 nullable: false,
115 }),
116 OpenApiType {
117 one_of: Some(types),
118 ..
119 } => match types.as_slice() {
120 [left, OpenApiType {
121 r#type: Some("null"),
122 ..
123 }] => {
124 let mut inner = Self::from_schema(&name, required, left, schemas)?;
125 inner.nullable = true;
126 Some(inner)
127 }
128 [left @ .., OpenApiType {
129 r#type: Some("null"),
130 ..
131 }] => {
132 let rest = OpenApiType {
133 one_of: Some(left.to_owned()),
134 ..schema.clone()
135 };
136 let mut inner = Self::from_schema(&name, required, &rest, schemas)?;
137 inner.nullable = true;
138 Some(inner)
139 }
140 cases => {
141 let r#enum = Enum::from_one_of(&name.to_upper_camel_case(), cases)?;
142 Some(Self {
143 name,
144 description,
145 required,
146 nullable: false,
147 deprecated: schema.deprecated,
148 r#type: PropertyType::Enum(r#enum),
149 })
150 }
151 },
152 OpenApiType {
153 all_of: Some(types),
154 ..
155 } => {
156 let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
157 Some(Self {
158 name,
159 description,
160 required,
161 nullable: false,
162 deprecated: schema.deprecated,
163 r#type: PropertyType::Nested(Box::new(composite)),
164 })
165 }
166 OpenApiType {
167 r#type: Some("object"),
168 ..
169 } => Some(Self {
170 r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
171 &name.clone().to_upper_camel_case(),
172 schema,
173 schemas,
174 )?)),
175 name,
176 description,
177 required,
178 deprecated: schema.deprecated,
179 nullable: false,
180 }),
181 OpenApiType {
182 ref_path: Some(path),
183 ..
184 } => Some(Self {
185 name,
186 description,
187 r#type: PropertyType::Ref((*path).to_owned()),
188 required,
189 deprecated: schema.deprecated,
190 nullable: false,
191 }),
192 OpenApiType {
193 r#type: Some("array"),
194 items: Some(items),
195 ..
196 } => {
197 let inner = Self::from_schema(&name, required, items, schemas)?;
198
199 Some(Self {
200 name,
201 description,
202 required,
203 nullable: false,
204 deprecated: schema.deprecated,
205 r#type: PropertyType::Array(Box::new(inner.r#type)),
206 })
207 }
208 OpenApiType {
209 r#type: Some(_), ..
210 } => {
211 let prim = match (schema.r#type, schema.format) {
212 (Some("integer"), Some("int32")) => PrimitiveType::I32,
213 (Some("integer"), Some("int64")) => PrimitiveType::I64,
214 (Some("number"), _) | (_, Some("float")) => {
215 PrimitiveType::Float
216 }
217 (Some("string"), None) => PrimitiveType::String,
218 (Some("string"), Some("date")) => PrimitiveType::DateTime,
219 (Some("boolean"), None) => PrimitiveType::Bool,
220 _ => return None,
221 };
222
223 Some(Self {
224 name,
225 description,
226 required,
227 nullable: false,
228 deprecated: schema.deprecated,
229 r#type: PropertyType::Primitive(prim),
230 })
231 }
232 _ => None,
233 }
234 }
235
236 pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
237 let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
238
239 let name = &self.name;
240 let (name, serde_attr) = match name.as_str() {
241 "type" => (format_ident!("r#type"), None),
242 name if name != name.to_snake_case() => (
243 format_ident!("{}", name.to_snake_case()),
244 Some(quote! { #[serde(rename = #name)]}),
245 ),
246 _ => (format_ident!("{name}"), None),
247 };
248
249 let ty_inner = self.r#type.codegen(namespace)?;
250
251 let ty = if !self.required || self.nullable {
252 quote! { Option<#ty_inner> }
253 } else {
254 ty_inner
255 };
256
257 let deprecated = self.deprecated.then(|| {
258 let note = self.description.as_ref().map(|d| quote! { note = #d });
259
260 quote! {
261 #[deprecated(#note)]
262 }
263 });
264
265 Some(quote! {
266 #desc
267 #deprecated
268 #serde_attr
269 pub #name: #ty
270 })
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Default)]
275pub struct Object {
276 pub name: String,
277 pub description: Option<String>,
278 pub properties: Vec<Property>,
279}
280
281impl Object {
282 pub fn from_schema_object(
283 name: &str,
284 schema: &OpenApiType,
285 schemas: &IndexMap<&str, OpenApiType>,
286 ) -> Option<Self> {
287 let mut result = Object {
288 name: name.to_owned(),
289 description: schema.description.as_deref().map(ToOwned::to_owned),
290 ..Default::default()
291 };
292
293 let Some(props) = &schema.properties else {
294 return None;
295 };
296
297 let required = schema.required.clone().unwrap_or_default();
298
299 for (prop_name, prop) in props {
300 if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
302 continue;
303 }
304
305 if *prop_name == "value" && name == "TornHof" {
307 continue;
308 }
309
310 result.properties.push(Property::from_schema(
311 prop_name,
312 required.contains(prop_name),
313 prop,
314 schemas,
315 )?);
316 }
317
318 Some(result)
319 }
320
321 pub fn from_all_of(
322 name: &str,
323 types: &[OpenApiType],
324 schemas: &IndexMap<&str, OpenApiType>,
325 ) -> Option<Self> {
326 let mut result = Self {
327 name: name.to_owned(),
328 ..Default::default()
329 };
330
331 for r#type in types {
332 let r#type = if let OpenApiType {
333 ref_path: Some(path),
334 ..
335 } = r#type
336 {
337 let name = path.strip_prefix("#/components/schemas/")?;
338 schemas.get(name)?
339 } else {
340 r#type
341 };
342 let obj = Self::from_schema_object(name, r#type, schemas)?;
343
344 result.description = result.description.or(obj.description);
345 result.properties.extend(obj.properties);
346 }
347
348 Some(result)
349 }
350
351 pub fn codegen(&self) -> Option<TokenStream> {
352 let doc = self.description.as_ref().map(|d| {
353 quote! {
354 #[doc = #d]
355 }
356 });
357
358 let mut namespace = ObjectNamespace {
359 object: self,
360 ident: None,
361 elements: Vec::default(),
362 };
363
364 let mut props = Vec::with_capacity(self.properties.len());
365 for prop in &self.properties {
366 props.push(prop.codegen(&mut namespace)?);
367 }
368
369 let name = format_ident!("{}", self.name);
370 let ns = namespace.codegen();
371
372 Some(quote! {
373 #ns
374
375 #doc
376 #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
377 pub struct #name {
378 #(#props),*
379 }
380 })
381 }
382}
383
384pub struct ObjectNamespace<'o> {
385 object: &'o Object,
386 ident: Option<Ident>,
387 elements: Vec<TokenStream>,
388}
389
390impl ObjectNamespace<'_> {
391 pub fn get_ident(&mut self) -> Ident {
392 self.ident
393 .get_or_insert_with(|| {
394 let name = self.object.name.to_snake_case();
395 format_ident!("{name}")
396 })
397 .clone()
398 }
399
400 pub fn push_element(&mut self, el: TokenStream) {
401 self.elements.push(el);
402 }
403
404 pub fn codegen(mut self) -> Option<TokenStream> {
405 if self.elements.is_empty() {
406 None
407 } else {
408 let ident = self.get_ident();
409 let elements = self.elements;
410 Some(quote! {
411 pub mod #ident {
412 #(#elements)*
413 }
414 })
415 }
416 }
417}
418
419#[cfg(test)]
420mod test {
421 use super::*;
422
423 use crate::openapi::schema::test::get_schema;
424
425 #[test]
426 fn resolve_object() {
427 let schema = get_schema();
428
429 let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
430
431 let resolved =
432 Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
433 .unwrap();
434 let _code = resolved.codegen().unwrap();
435 }
436
437 #[test]
438 fn resolve_objects() {
439 let schema = get_schema();
440
441 let mut objects = 0;
442 let mut unresolved = vec![];
443
444 for (name, desc) in &schema.components.schemas {
445 if desc.r#type == Some("object") {
446 objects += 1;
447 if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
448 unresolved.push(name);
449 }
450 }
451 }
452
453 if !unresolved.is_empty() {
454 panic!(
455 "Failed to resolve {}/{} objects. Could not resolve [{}]",
456 unresolved.len(),
457 objects,
458 unresolved
459 .into_iter()
460 .map(|u| format!("`{u}`"))
461 .collect::<Vec<_>>()
462 .join(", ")
463 )
464 }
465 }
466}