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