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