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