torn_api_codegen/model/
parameter.rs

1use std::fmt::Write;
2
3use heck::ToUpperCamelCase;
4use proc_macro2::TokenStream;
5use quote::{ToTokens, format_ident, quote};
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 = items.codegen_type_name(&inner_name);
307
308                code.extend(quote! {
309                    #[derive(Debug, Clone)]
310                    pub struct #name(pub Vec<#inner_ty>);
311
312                    impl std::fmt::Display for #name {
313                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
314                            let mut first = true;
315                            for el in &self.0 {
316                                if first {
317                                    first = false;
318                                    write!(f, "{el}")?;
319                                } else {
320                                    write!(f, ",{el}")?;
321                                }
322                            }
323                            Ok(())
324                        }
325                    }
326
327                    impl<T> From<T> for #name where T: IntoIterator<Item = #inner_ty> {
328                        fn from(value: T) -> #name {
329                            let items = value.into_iter().collect();
330
331                            Self(items)
332                        }
333                    }
334                });
335
336                Some(code)
337            }
338            _ => None,
339        }
340    }
341}
342
343#[cfg(test)]
344mod test {
345    use crate::openapi::{path::OpenApiPathParameter, schema::OpenApiSchema};
346
347    use super::*;
348
349    #[test]
350    fn resolve_components() {
351        let schema = OpenApiSchema::read().unwrap();
352
353        let mut parameters = 0;
354        let mut unresolved = vec![];
355
356        for (name, desc) in &schema.components.parameters {
357            parameters += 1;
358            if Parameter::from_schema(name, desc).is_none() {
359                unresolved.push(name);
360            }
361        }
362
363        if !unresolved.is_empty() {
364            panic!(
365                "Failed to resolve {}/{} params. Could not resolve [{}]",
366                unresolved.len(),
367                parameters,
368                unresolved
369                    .into_iter()
370                    .map(|u| format!("`{u}`"))
371                    .collect::<Vec<_>>()
372                    .join(", ")
373            )
374        }
375    }
376
377    #[test]
378    fn resolve_inline() {
379        let schema = OpenApiSchema::read().unwrap();
380
381        let mut params = 0;
382        let mut unresolved = Vec::new();
383
384        for (path, body) in &schema.paths {
385            for param in &body.get.parameters {
386                if let OpenApiPathParameter::Inline(inline) = param {
387                    params += 1;
388                    if Parameter::from_schema(inline.name, inline).is_none() {
389                        unresolved.push(format!("`{}.{}`", path, inline.name));
390                    }
391                }
392            }
393        }
394
395        if !unresolved.is_empty() {
396            panic!(
397                "Failed to resolve {}/{} inline params. Could not resolve [{}]",
398                unresolved.len(),
399                params,
400                unresolved.join(", ")
401            )
402        }
403    }
404
405    #[test]
406    fn codegen_inline() {
407        let schema = OpenApiSchema::read().unwrap();
408
409        let mut params = 0;
410        let mut unresolved = Vec::new();
411
412        for (path, body) in &schema.paths {
413            for param in &body.get.parameters {
414                if let OpenApiPathParameter::Inline(inline) = param {
415                    if inline.r#in == SchemaLocation::Query {
416                        let Some(param) = Parameter::from_schema(inline.name, inline) else {
417                            continue;
418                        };
419                        if matches!(
420                            param.r#type,
421                            ParameterType::Schema { .. }
422                                | ParameterType::Boolean
423                                | ParameterType::String
424                        ) {
425                            continue;
426                        }
427                        params += 1;
428                        if param.codegen().is_none() {
429                            unresolved.push(format!("`{}.{}`", path, inline.name));
430                        }
431                    }
432                }
433            }
434        }
435
436        if !unresolved.is_empty() {
437            panic!(
438                "Failed to codegen {}/{} inline params. Could not codegen [{}]",
439                unresolved.len(),
440                params,
441                unresolved.join(", ")
442            )
443        }
444    }
445}