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"), _) => PrimitiveType::Float,
211 (Some("string"), None) => PrimitiveType::String,
212 (Some("boolean"), None) => PrimitiveType::Bool,
213 _ => return None,
214 };
215
216 Some(Self {
217 name,
218 description,
219 required,
220 nullable: false,
221 deprecated: schema.deprecated,
222 r#type: PropertyType::Primitive(prim),
223 })
224 }
225 _ => None,
226 }
227 }
228
229 pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
230 let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
231
232 let name = &self.name;
233 let (name, serde_attr) = match name.as_str() {
234 "type" => (format_ident!("r#type"), None),
235 name if name != name.to_snake_case() => (
236 format_ident!("{}", name.to_snake_case()),
237 Some(quote! { #[serde(rename = #name)]}),
238 ),
239 _ => (format_ident!("{name}"), None),
240 };
241
242 let ty_inner = self.r#type.codegen(namespace)?;
243
244 let ty = if !self.required || self.nullable {
245 quote! { Option<#ty_inner> }
246 } else {
247 ty_inner
248 };
249
250 let deprecated = self.deprecated.then(|| {
251 let note = self.description.as_ref().map(|d| quote! { note = #d });
252
253 quote! {
254 #[deprecated(#note)]
255 }
256 });
257
258 Some(quote! {
259 #desc
260 #deprecated
261 #serde_attr
262 pub #name: #ty
263 })
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Default)]
268pub struct Object {
269 pub name: String,
270 pub description: Option<String>,
271 pub properties: Vec<Property>,
272}
273
274impl Object {
275 pub fn from_schema_object(
276 name: &str,
277 schema: &OpenApiType,
278 schemas: &IndexMap<&str, OpenApiType>,
279 ) -> Option<Self> {
280 let mut result = Object {
281 name: name.to_owned(),
282 description: schema.description.as_deref().map(ToOwned::to_owned),
283 ..Default::default()
284 };
285
286 let Some(props) = &schema.properties else {
287 return None;
288 };
289
290 let required = schema.required.clone().unwrap_or_default();
291
292 for (prop_name, prop) in props {
293 if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
295 continue;
296 }
297
298 if *prop_name == "value" && name == "TornHof" {
300 continue;
301 }
302
303 result.properties.push(Property::from_schema(
304 prop_name,
305 required.contains(prop_name),
306 prop,
307 schemas,
308 )?);
309 }
310
311 Some(result)
312 }
313
314 pub fn from_all_of(
315 name: &str,
316 types: &[OpenApiType],
317 schemas: &IndexMap<&str, OpenApiType>,
318 ) -> Option<Self> {
319 let mut result = Self {
320 name: name.to_owned(),
321 ..Default::default()
322 };
323
324 for r#type in types {
325 let r#type = if let OpenApiType {
326 ref_path: Some(path),
327 ..
328 } = r#type
329 {
330 let name = path.strip_prefix("#/components/schemas/")?;
331 schemas.get(name)?
332 } else {
333 r#type
334 };
335 let obj = Self::from_schema_object(name, r#type, schemas)?;
336
337 result.description = result.description.or(obj.description);
338 result.properties.extend(obj.properties);
339 }
340
341 Some(result)
342 }
343
344 pub fn codegen(&self) -> Option<TokenStream> {
345 let doc = self.description.as_ref().map(|d| {
346 quote! {
347 #[doc = #d]
348 }
349 });
350
351 let mut namespace = ObjectNamespace {
352 object: self,
353 ident: None,
354 elements: Vec::default(),
355 };
356
357 let mut props = Vec::with_capacity(self.properties.len());
358 for prop in &self.properties {
359 props.push(prop.codegen(&mut namespace)?);
360 }
361
362 let name = format_ident!("{}", self.name);
363 let ns = namespace.codegen();
364
365 Some(quote! {
366 #ns
367
368 #doc
369 #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
370 pub struct #name {
371 #(#props),*
372 }
373 })
374 }
375}
376
377pub struct ObjectNamespace<'o> {
378 object: &'o Object,
379 ident: Option<Ident>,
380 elements: Vec<TokenStream>,
381}
382
383impl ObjectNamespace<'_> {
384 pub fn get_ident(&mut self) -> Ident {
385 self.ident
386 .get_or_insert_with(|| {
387 let name = self.object.name.to_snake_case();
388 format_ident!("{name}")
389 })
390 .clone()
391 }
392
393 pub fn push_element(&mut self, el: TokenStream) {
394 self.elements.push(el);
395 }
396
397 pub fn codegen(mut self) -> Option<TokenStream> {
398 if self.elements.is_empty() {
399 None
400 } else {
401 let ident = self.get_ident();
402 let elements = self.elements;
403 Some(quote! {
404 pub mod #ident {
405 #(#elements)*
406 }
407 })
408 }
409 }
410}
411
412#[cfg(test)]
413mod test {
414 use super::*;
415
416 use crate::openapi::schema::OpenApiSchema;
417
418 #[test]
419 fn resolve_object() {
420 let schema = OpenApiSchema::read().unwrap();
421
422 let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
423
424 let resolved =
425 Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
426 .unwrap();
427 let _code = resolved.codegen().unwrap();
428 }
429
430 #[test]
431 fn resolve_objects() {
432 let schema = OpenApiSchema::read().unwrap();
433
434 let mut objects = 0;
435 let mut unresolved = vec![];
436
437 for (name, desc) in &schema.components.schemas {
438 if desc.r#type == Some("object") {
439 objects += 1;
440 if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
441 unresolved.push(name);
442 }
443 }
444 }
445
446 if !unresolved.is_empty() {
447 panic!(
448 "Failed to resolve {}/{} objects. Could not resolve [{}]",
449 unresolved.len(),
450 objects,
451 unresolved
452 .into_iter()
453 .map(|u| format!("`{u}`"))
454 .collect::<Vec<_>>()
455 .join(", ")
456 )
457 }
458 }
459}