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