1use heck::{ToSnakeCase, ToUpperCamelCase};
2use indexmap::IndexMap;
3use proc_macro2::TokenStream;
4use quote::{ToTokens, format_ident, quote};
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}
86
87impl Property {
88 pub fn from_schema(
89 name: &str,
90 required: bool,
91 schema: &OpenApiType,
92 schemas: &IndexMap<&str, OpenApiType>,
93 ) -> Option<Self> {
94 let name = name.to_owned();
95 let description = schema.description.as_deref().map(ToOwned::to_owned);
96
97 match schema {
98 OpenApiType {
99 r#enum: Some(_), ..
100 } => Some(Self {
101 r#type: PropertyType::Enum(Enum::from_schema(
102 &name.clone().to_upper_camel_case(),
103 schema,
104 )?),
105 name,
106 description,
107 required,
108 nullable: false,
109 }),
110 OpenApiType {
111 one_of: Some(types),
112 ..
113 } => match types.as_slice() {
114 [
115 left,
116 OpenApiType {
117 r#type: Some("null"),
118 ..
119 },
120 ] => {
121 let mut inner = Self::from_schema(&name, required, left, schemas)?;
122 inner.nullable = true;
123 Some(inner)
124 }
125 [
126 left @ ..,
127 OpenApiType {
128 r#type: Some("null"),
129 ..
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: None,
145 required,
146 nullable: false,
147 r#type: PropertyType::Enum(r#enum),
148 })
149 }
150 },
151 OpenApiType {
152 all_of: Some(types),
153 ..
154 } => {
155 let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
156 Some(Self {
157 name,
158 description: None,
159 required,
160 nullable: false,
161 r#type: PropertyType::Nested(Box::new(composite)),
162 })
163 }
164 OpenApiType {
165 r#type: Some("object"),
166 ..
167 } => Some(Self {
168 r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
169 &name.clone().to_upper_camel_case(),
170 schema,
171 schemas,
172 )?)),
173 name,
174 description,
175 required,
176 nullable: false,
177 }),
178 OpenApiType {
179 ref_path: Some(path),
180 ..
181 } => Some(Self {
182 name,
183 description,
184 r#type: PropertyType::Ref((*path).to_owned()),
185 required,
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 r#type: PropertyType::Array(Box::new(inner.r#type)),
201 })
202 }
203 OpenApiType {
204 r#type: Some(_), ..
205 } => {
206 let prim = match (schema.r#type, schema.format) {
207 (Some("integer"), Some("int32")) => PrimitiveType::I32,
208 (Some("integer"), Some("int64")) => PrimitiveType::I64,
209 (Some("number"), Some("float")) => PrimitiveType::Float,
210 (Some("string"), None) => PrimitiveType::String,
211 (Some("boolean"), None) => PrimitiveType::Bool,
212 _ => return None,
213 };
214
215 Some(Self {
216 name,
217 description,
218 required,
219 nullable: false,
220 r#type: PropertyType::Primitive(prim),
221 })
222 }
223 _ => None,
224 }
225 }
226
227 pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
228 let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
229
230 let name = &self.name;
231 let (name, serde_attr) = match name.as_str() {
232 "type" => (format_ident!("r#type"), None),
233 name if name != name.to_snake_case() => (
234 format_ident!("{}", name.to_snake_case()),
235 Some(quote! { #[serde(rename = #name)]}),
236 ),
237 _ => (format_ident!("{name}"), None),
238 };
239
240 let ty_inner = self.r#type.codegen(namespace)?;
241
242 let ty = if !self.required || self.nullable {
243 quote! { Option<#ty_inner> }
244 } else {
245 ty_inner
246 };
247
248 Some(quote! {
249 #desc
250 #serde_attr
251 pub #name: #ty
252 })
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Default)]
257pub struct Object {
258 pub name: String,
259 pub description: Option<String>,
260 pub properties: Vec<Property>,
261}
262
263impl Object {
264 pub fn from_schema_object(
265 name: &str,
266 schema: &OpenApiType,
267 schemas: &IndexMap<&str, OpenApiType>,
268 ) -> Option<Self> {
269 let mut result = Object {
270 name: name.to_owned(),
271 description: schema.description.as_deref().map(ToOwned::to_owned),
272 ..Default::default()
273 };
274
275 let Some(props) = &schema.properties else {
276 return None;
277 };
278
279 let required = schema.required.clone().unwrap_or_default();
280
281 for (prop_name, prop) in props {
282 if *prop_name == "itemDetails" {
284 continue;
285 }
286
287 if *prop_name == "value" && name == "TornHof" {
289 continue;
290 }
291
292 result.properties.push(Property::from_schema(
293 prop_name,
294 required.contains(prop_name),
295 prop,
296 schemas,
297 )?);
298 }
299
300 Some(result)
301 }
302
303 pub fn from_all_of(
304 name: &str,
305 types: &[OpenApiType],
306 schemas: &IndexMap<&str, OpenApiType>,
307 ) -> Option<Self> {
308 let mut result = Self {
309 name: name.to_owned(),
310 ..Default::default()
311 };
312
313 for r#type in types {
314 let r#type = if let OpenApiType {
315 ref_path: Some(path),
316 ..
317 } = r#type
318 {
319 let name = path.strip_prefix("#/components/schemas/")?;
320 schemas.get(name)?
321 } else {
322 r#type
323 };
324 let obj = Self::from_schema_object(name, r#type, schemas)?;
325
326 result.description = result.description.or(obj.description);
327 result.properties.extend(obj.properties);
328 }
329
330 Some(result)
331 }
332
333 pub fn codegen(&self) -> Option<TokenStream> {
334 let doc = self.description.as_ref().map(|d| {
335 quote! {
336 #[doc = #d]
337 }
338 });
339
340 let mut namespace = ObjectNamespace {
341 object: self,
342 ident: None,
343 elements: Vec::default(),
344 };
345
346 let mut props = Vec::with_capacity(self.properties.len());
347 for prop in &self.properties {
348 props.push(prop.codegen(&mut namespace)?);
349 }
350
351 let name = format_ident!("{}", self.name);
352 let ns = namespace.codegen();
353
354 Some(quote! {
355 #ns
356
357 #doc
358 #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
359 pub struct #name {
360 #(#props),*
361 }
362 })
363 }
364}
365
366pub struct ObjectNamespace<'o> {
367 object: &'o Object,
368 ident: Option<Ident>,
369 elements: Vec<TokenStream>,
370}
371
372impl ObjectNamespace<'_> {
373 pub fn get_ident(&mut self) -> Ident {
374 self.ident
375 .get_or_insert_with(|| {
376 let name = self.object.name.to_snake_case();
377 format_ident!("{name}")
378 })
379 .clone()
380 }
381
382 pub fn push_element(&mut self, el: TokenStream) {
383 self.elements.push(el);
384 }
385
386 pub fn codegen(mut self) -> Option<TokenStream> {
387 if self.elements.is_empty() {
388 None
389 } else {
390 let ident = self.get_ident();
391 let elements = self.elements;
392 Some(quote! {
393 pub mod #ident {
394 #(#elements)*
395 }
396 })
397 }
398 }
399}
400
401#[cfg(test)]
402mod test {
403 use super::*;
404
405 use crate::openapi::schema::OpenApiSchema;
406
407 #[test]
408 fn resolve_object() {
409 let schema = OpenApiSchema::read().unwrap();
410
411 let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
412
413 let resolved =
414 Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
415 .unwrap();
416 let _code = resolved.codegen().unwrap();
417 }
418
419 #[test]
420 fn resolve_objects() {
421 let schema = OpenApiSchema::read().unwrap();
422
423 let mut objects = 0;
424 let mut unresolved = vec![];
425
426 for (name, desc) in &schema.components.schemas {
427 if desc.r#type == Some("object") {
428 objects += 1;
429 if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
430 unresolved.push(name);
431 }
432 }
433 }
434
435 if !unresolved.is_empty() {
436 panic!(
437 "Failed to resolve {}/{} objects. Could not resolve [{}]",
438 unresolved.len(),
439 objects,
440 unresolved
441 .into_iter()
442 .map(|u| format!("`{u}`"))
443 .collect::<Vec<_>>()
444 .join(", ")
445 )
446 }
447 }
448}