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                };
222
223                if let Some(default) = options.default {
224                    _ = write!(desc, "The default value is {default}.");
225                }
226
227                let doc = quote! {
228                    #[doc = #desc]
229                };
230
231                Some(quote! {
232                    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
233                    #doc
234                    pub struct #name(i32);
235
236                    #constructor
237
238                    impl From<#name> for i32 {
239                        fn from(value: #name) -> Self {
240                            value.0
241                        }
242                    }
243
244                    impl #name {
245                        pub fn into_inner(self) -> i32 {
246                            self.0
247                        }
248                    }
249
250                    impl std::fmt::Display for #name {
251                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
252                            write!(f, "{}", self.0)
253                        }
254                    }
255                })
256            }
257            ParameterType::Enum { options, r#type } => {
258                let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
259                if let Some(default) = &options.default {
260                    let default = default.to_upper_camel_case();
261                    _ = write!(
262                        desc,
263                        r#"
264# Notes
265The default value [Self::{}](self::{}#variant.{})"#,
266                        default, self.name, default
267                    );
268                }
269
270                let doc = quote! { #[doc = #desc]};
271                let inner = r#type.codegen()?;
272
273                Some(quote! {
274                    #doc
275                    #inner
276                })
277            }
278            ParameterType::Array { items } => {
279                let (inner_name, outer_name) = match items.as_ref() {
280                    ParameterType::I32 { .. }
281                    | ParameterType::String
282                    | ParameterType::Array { .. }
283                    | ParameterType::Enum { .. } => self.name.strip_suffix('s').map_or_else(
284                        || (self.name.to_owned(), format!("{}s", self.name)),
285                        |s| (s.to_owned(), self.name.to_owned()),
286                    ),
287                    ParameterType::Boolean => ("bool".to_owned(), self.name.clone()),
288                    ParameterType::Schema { type_name } => (type_name.clone(), self.name.clone()),
289                };
290
291                let inner = Self {
292                    r#type: *items.clone(),
293                    name: inner_name.clone(),
294                    ..self.clone()
295                };
296
297                let mut code = inner.codegen().unwrap_or_default();
298
299                let name = format_ident!("{}", outer_name);
300                let inner_ty = items.codegen_type_name(&inner_name);
301
302                code.extend(quote! {
303                    #[derive(Debug, Clone)]
304                    pub struct #name(pub Vec<#inner_ty>);
305
306                    impl std::fmt::Display for #name {
307                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
308                            let mut first = true;
309                            for el in &self.0 {
310                                if first {
311                                    first = false;
312                                    write!(f, "{el}")?;
313                                } else {
314                                    write!(f, ",{el}")?;
315                                }
316                            }
317                            Ok(())
318                        }
319                    }
320                });
321
322                Some(code)
323            }
324            _ => None,
325        }
326    }
327}
328
329#[cfg(test)]
330mod test {
331    use crate::openapi::{path::OpenApiPathParameter, schema::OpenApiSchema};
332
333    use super::*;
334
335    #[test]
336    fn resolve_components() {
337        let schema = OpenApiSchema::read().unwrap();
338
339        let mut parameters = 0;
340        let mut unresolved = vec![];
341
342        for (name, desc) in &schema.components.parameters {
343            parameters += 1;
344            if Parameter::from_schema(name, desc).is_none() {
345                unresolved.push(name);
346            }
347        }
348
349        if !unresolved.is_empty() {
350            panic!(
351                "Failed to resolve {}/{} params. Could not resolve [{}]",
352                unresolved.len(),
353                parameters,
354                unresolved
355                    .into_iter()
356                    .map(|u| format!("`{u}`"))
357                    .collect::<Vec<_>>()
358                    .join(", ")
359            )
360        }
361    }
362
363    #[test]
364    fn resolve_inline() {
365        let schema = OpenApiSchema::read().unwrap();
366
367        let mut params = 0;
368        let mut unresolved = Vec::new();
369
370        for (path, body) in &schema.paths {
371            for param in &body.get.parameters {
372                if let OpenApiPathParameter::Inline(inline) = param {
373                    params += 1;
374                    if Parameter::from_schema(inline.name, inline).is_none() {
375                        unresolved.push(format!("`{}.{}`", path, inline.name));
376                    }
377                }
378            }
379        }
380
381        if !unresolved.is_empty() {
382            panic!(
383                "Failed to resolve {}/{} inline params. Could not resolve [{}]",
384                unresolved.len(),
385                params,
386                unresolved.join(", ")
387            )
388        }
389    }
390
391    #[test]
392    fn codegen_inline() {
393        let schema = OpenApiSchema::read().unwrap();
394
395        let mut params = 0;
396        let mut unresolved = Vec::new();
397
398        for (path, body) in &schema.paths {
399            for param in &body.get.parameters {
400                if let OpenApiPathParameter::Inline(inline) = param {
401                    if inline.r#in == SchemaLocation::Query {
402                        let Some(param) = Parameter::from_schema(inline.name, inline) else {
403                            continue;
404                        };
405                        if matches!(
406                            param.r#type,
407                            ParameterType::Schema { .. }
408                                | ParameterType::Boolean
409                                | ParameterType::String
410                        ) {
411                            continue;
412                        }
413                        params += 1;
414                        if param.codegen().is_none() {
415                            unresolved.push(format!("`{}.{}`", path, inline.name));
416                        }
417                    }
418                }
419            }
420        }
421
422        if !unresolved.is_empty() {
423            panic!(
424                "Failed to codegen {}/{} inline params. Could not codegen [{}]",
425                unresolved.len(),
426                params,
427                unresolved.join(", ")
428            )
429        }
430    }
431}