torn_api_codegen/model/
parameter.rs

1use std::fmt::Write;
2
3use heck::ToUpperCamelCase;
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote, ToTokens};
6
7use crate::openapi::parameter::{
8    OpenApiParameter, OpenApiParameterDefault, OpenApiParameterSchema,
9    ParameterLocation as SchemaLocation,
10};
11
12use super::r#enum::Enum;
13
14#[derive(Debug, Clone)]
15pub struct ParameterOptions<P> {
16    pub default: Option<P>,
17    pub minimum: Option<P>,
18    pub maximum: Option<P>,
19}
20
21#[derive(Debug, Clone)]
22pub enum ParameterType {
23    I32 {
24        options: ParameterOptions<i32>,
25    },
26    String,
27    Boolean,
28    Enum {
29        options: ParameterOptions<String>,
30        r#type: Enum,
31    },
32    Schema {
33        type_name: String,
34    },
35    Array {
36        items: Box<ParameterType>,
37    },
38}
39
40impl ParameterType {
41    pub fn from_schema(name: &str, schema: &OpenApiParameterSchema) -> Option<Self> {
42        match schema {
43            OpenApiParameterSchema {
44                r#type: Some("integer"),
45                // BUG: missing for some types in the spec
46
47                // format: Some("int32"),
48                ..
49            } => {
50                let default = match schema.default {
51                    Some(OpenApiParameterDefault::Int(d)) => Some(d),
52                    None => None,
53                    _ => return None,
54                };
55
56                Some(Self::I32 {
57                    options: ParameterOptions {
58                        default,
59                        minimum: schema.minimum,
60                        maximum: schema.maximum,
61                    },
62                })
63            }
64            OpenApiParameterSchema {
65                r#type: Some("string"),
66                r#enum: Some(variants),
67                ..
68            } if variants.as_slice() == ["true", "false"]
69                || variants.as_slice() == ["false", "true "] =>
70            {
71                Some(ParameterType::Boolean)
72            }
73            OpenApiParameterSchema {
74                r#type: Some("string"),
75                r#enum: Some(_),
76                ..
77            } => {
78                let default = match schema.default {
79                    Some(OpenApiParameterDefault::Str(d)) => Some(d.to_owned()),
80                    None => None,
81                    _ => return None,
82                };
83
84                Some(ParameterType::Enum {
85                    options: ParameterOptions {
86                        default,
87                        minimum: None,
88                        maximum: None,
89                    },
90                    r#type: Enum::from_parameter_schema(name, schema)?,
91                })
92            }
93            OpenApiParameterSchema {
94                r#type: Some("string"),
95                ..
96            } => Some(ParameterType::String),
97            OpenApiParameterSchema {
98                ref_path: Some(path),
99                ..
100            } => {
101                let type_name = path.strip_prefix("#/components/schemas/")?.to_owned();
102
103                Some(ParameterType::Schema { type_name })
104            }
105            OpenApiParameterSchema {
106                r#type: Some("array"),
107                items: Some(items),
108                ..
109            } => Some(Self::Array {
110                items: Box::new(Self::from_schema(name, items)?),
111            }),
112            _ => None,
113        }
114    }
115
116    pub fn codegen_type_name(&self, name: &str) -> TokenStream {
117        match self {
118            Self::I32 { .. } | Self::String | Self::Enum { .. } | Self::Array { .. } => {
119                format_ident!("{name}").into_token_stream()
120            }
121            Self::Boolean => quote! { bool },
122            Self::Schema { type_name } => {
123                let type_name = format_ident!("{type_name}",);
124                quote! { crate::models::#type_name }
125            }
126        }
127    }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ParameterLocation {
132    Query,
133    Path,
134}
135
136#[derive(Debug, Clone)]
137pub struct Parameter {
138    pub name: String,
139    pub value: String,
140    pub description: Option<String>,
141    pub r#type: ParameterType,
142    pub required: bool,
143    pub location: ParameterLocation,
144}
145
146impl Parameter {
147    pub fn from_schema(name: &str, schema: &OpenApiParameter) -> Option<Self> {
148        let name = match name {
149            "From" => "FromTimestamp".to_owned(),
150            "To" => "ToTimestamp".to_owned(),
151            name => name.to_owned(),
152        };
153        let value = schema.name.to_owned();
154        let description = schema.description.as_deref().map(ToOwned::to_owned);
155
156        let location = match &schema.r#in {
157            SchemaLocation::Query => ParameterLocation::Query,
158            SchemaLocation::Path => ParameterLocation::Path,
159        };
160
161        let r#type = ParameterType::from_schema(&name, &schema.schema)?;
162
163        Some(Self {
164            name,
165            value,
166            description,
167            r#type,
168            required: schema.required,
169            location,
170        })
171    }
172
173    pub fn codegen(&self) -> Option<TokenStream> {
174        match &self.r#type {
175            ParameterType::I32 { options } => {
176                let name = format_ident!("{}", self.name);
177
178                let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
179
180                if options.default.is_some()
181                    || options.minimum.is_some()
182                    || options.maximum.is_some()
183                {
184                    _ = writeln!(desc, "\n # Notes");
185                }
186
187                let constructor = if let (Some(min), Some(max)) = (options.minimum, options.maximum)
188                {
189                    _ = write!(desc, "Values have to lie between {min} and {max}. ");
190                    let name_raw = &self.name;
191                    quote! {
192                        impl #name {
193                            pub fn new(inner: i32) -> Result<Self, crate::ParameterError> {
194                                if inner > #max || inner < #min {
195                                    Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
196                                } else {
197                                    Ok(Self(inner))
198                                }
199                            }
200                        }
201
202                        impl TryFrom<i32> for #name {
203                            type Error = crate::ParameterError;
204                            fn try_from(inner: i32) -> Result<Self, Self::Error> {
205                                if inner > #max || inner < #min {
206                                    Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
207                                } else {
208                                    Ok(Self(inner))
209                                }
210                            }
211                        }
212                    }
213                } else {
214                    quote! {
215                        impl #name {
216                            pub fn new(inner: i32) -> Self {
217                                Self(inner)
218                            }
219                        }
220
221                        impl From<i32> for #name {
222                            fn from(inner: i32) -> Self {
223                                Self(inner)
224                            }
225                        }
226                    }
227                };
228
229                if let Some(default) = options.default {
230                    _ = write!(desc, "The default value is {default}.");
231                }
232
233                let doc = quote! {
234                    #[doc = #desc]
235                };
236
237                Some(quote! {
238                    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
239                    #doc
240                    pub struct #name(i32);
241
242                    #constructor
243
244                    impl From<#name> for i32 {
245                        fn from(value: #name) -> Self {
246                            value.0
247                        }
248                    }
249
250                    impl #name {
251                        pub fn into_inner(self) -> i32 {
252                            self.0
253                        }
254                    }
255
256                    impl std::fmt::Display for #name {
257                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
258                            write!(f, "{}", self.0)
259                        }
260                    }
261                })
262            }
263            ParameterType::Enum { options, r#type } => {
264                let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
265                if let Some(default) = &options.default {
266                    let default = default.to_upper_camel_case();
267                    _ = write!(
268                        desc,
269                        r#"
270# Notes
271The default value [Self::{}](self::{}#variant.{})"#,
272                        default, self.name, default
273                    );
274                }
275
276                let doc = quote! { #[doc = #desc]};
277                let inner = r#type.codegen()?;
278
279                Some(quote! {
280                    #doc
281                    #inner
282                })
283            }
284            ParameterType::Array { items } => {
285                let (inner_name, outer_name) = match items.as_ref() {
286                    ParameterType::I32 { .. }
287                    | ParameterType::String
288                    | ParameterType::Array { .. }
289                    | ParameterType::Enum { .. } => self.name.strip_suffix('s').map_or_else(
290                        || (self.name.to_owned(), format!("{}s", self.name)),
291                        |s| (s.to_owned(), self.name.to_owned()),
292                    ),
293                    ParameterType::Boolean => ("bool".to_owned(), self.name.clone()),
294                    ParameterType::Schema { type_name } => (type_name.clone(), self.name.clone()),
295                };
296
297                let inner = Self {
298                    r#type: *items.clone(),
299                    name: inner_name.clone(),
300                    ..self.clone()
301                };
302
303                let mut code = inner.codegen().unwrap_or_default();
304
305                let name = format_ident!("{}", outer_name);
306                let inner_ty = if matches!(items.as_ref(), ParameterType::Schema { type_name: _ }) {
307                    let inner_name = format_ident!("{}", inner_name);
308                    quote! { crate::models::#inner_name }
309                } else {
310                    items.codegen_type_name(&inner_name).to_token_stream()
311                };
312
313                code.extend(quote! {
314                    #[derive(Debug, Clone)]
315                    pub struct #name(pub Vec<#inner_ty>);
316
317                    impl std::fmt::Display for #name {
318                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
319                            let mut first = true;
320                            for el in &self.0 {
321                                if first {
322                                    first = false;
323                                    write!(f, "{el}")?;
324                                } else {
325                                    write!(f, ",{el}")?;
326                                }
327                            }
328                            Ok(())
329                        }
330                    }
331
332                    impl<T> From<T> for #name where T: IntoIterator<Item = #inner_ty> {
333                        fn from(value: T) -> #name {
334                            let items = value.into_iter().collect();
335
336                            Self(items)
337                        }
338                    }
339                });
340
341                Some(code)
342            }
343            _ => None,
344        }
345    }
346}
347
348#[cfg(test)]
349mod test {
350    use crate::openapi::{path::OpenApiPathParameter, schema::OpenApiSchema};
351
352    use super::*;
353
354    #[test]
355    fn resolve_components() {
356        let schema = OpenApiSchema::read().unwrap();
357
358        let mut parameters = 0;
359        let mut unresolved = vec![];
360
361        for (name, desc) in &schema.components.parameters {
362            parameters += 1;
363            if Parameter::from_schema(name, desc).is_none() {
364                unresolved.push(name);
365            }
366        }
367
368        if !unresolved.is_empty() {
369            panic!(
370                "Failed to resolve {}/{} params. Could not resolve [{}]",
371                unresolved.len(),
372                parameters,
373                unresolved
374                    .into_iter()
375                    .map(|u| format!("`{u}`"))
376                    .collect::<Vec<_>>()
377                    .join(", ")
378            )
379        }
380    }
381
382    #[test]
383    fn resolve_inline() {
384        let schema = OpenApiSchema::read().unwrap();
385
386        let mut params = 0;
387        let mut unresolved = Vec::new();
388
389        for (path, body) in &schema.paths {
390            for param in &body.get.parameters {
391                if let OpenApiPathParameter::Inline(inline) = param {
392                    params += 1;
393                    if Parameter::from_schema(inline.name, inline).is_none() {
394                        unresolved.push(format!("`{}.{}`", path, inline.name));
395                    }
396                }
397            }
398        }
399
400        if !unresolved.is_empty() {
401            panic!(
402                "Failed to resolve {}/{} inline params. Could not resolve [{}]",
403                unresolved.len(),
404                params,
405                unresolved.join(", ")
406            )
407        }
408    }
409
410    #[test]
411    fn codegen_inline() {
412        let schema = OpenApiSchema::read().unwrap();
413
414        let mut params = 0;
415        let mut unresolved = Vec::new();
416
417        for (path, body) in &schema.paths {
418            for param in &body.get.parameters {
419                if let OpenApiPathParameter::Inline(inline) = param {
420                    if inline.r#in == SchemaLocation::Query {
421                        let Some(param) = Parameter::from_schema(inline.name, inline) else {
422                            continue;
423                        };
424                        if matches!(
425                            param.r#type,
426                            ParameterType::Schema { .. }
427                                | ParameterType::Boolean
428                                | ParameterType::String
429                        ) {
430                            continue;
431                        }
432                        params += 1;
433                        if param.codegen().is_none() {
434                            unresolved.push(format!("`{}.{}`", path, inline.name));
435                        }
436                    }
437                }
438            }
439        }
440
441        if !unresolved.is_empty() {
442            panic!(
443                "Failed to codegen {}/{} inline params. Could not codegen [{}]",
444                unresolved.len(),
445                params,
446                unresolved.join(", ")
447            )
448        }
449    }
450}