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}