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 = 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::test::get_schema};
346
347 use super::*;
348
349 #[test]
350 fn resolve_components() {
351 let schema = get_schema();
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 = get_schema();
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 = get_schema();
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}