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