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 ..
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}