1use heck::{ToPascalCase, ToSnakeCase};
2use openapiv3::{
3 MediaType, OpenAPI, Operation, Parameter, PathItem, ReferenceOr, RequestBody, Response,
4 Responses, Schema, StatusCode,
5};
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8
9use super::schemas::doc_attr;
10use super::security::{resolve_op_security, OpSecurity, SchemeInfo};
11use super::types::{is_string_enum, schema_to_rust_type, string_enum_values};
12
13#[derive(Debug)]
15pub struct OperationInfo {
16 pub operation_id: String,
17 pub method: String,
18 pub path: String,
19 pub summary: Option<String>,
20 pub description: Option<String>,
21 pub path_params: Vec<ParamInfo>,
22 pub query_params: Vec<ParamInfo>,
23 pub header_params: Vec<ParamInfo>,
24 pub body: Option<BodyInfo>,
25 pub responses: Vec<ResponseInfo>,
26 pub auth: OpSecurity,
27}
28
29#[derive(Debug)]
30pub struct ParamInfo {
31 pub name: String,
32 pub description: Option<String>,
33 pub required: bool,
34 pub rust_type: TokenStream,
36 pub is_enum: bool,
38 pub enum_ident: Option<syn::Ident>,
40 pub enum_values: Vec<String>,
42}
43
44#[derive(Debug)]
45pub struct BodyInfo {
46 pub description: Option<String>,
47 pub required: bool,
48 pub rust_type: TokenStream,
49}
50
51#[derive(Debug)]
52pub struct ResponseInfo {
53 pub status: ResponseStatus,
54 pub description: String,
55 pub rust_type: Option<TokenStream>,
56}
57
58#[derive(Debug)]
59pub enum ResponseStatus {
60 Code(u16),
61 Default,
62}
63
64#[must_use]
66pub fn collect_operations(openapi: &OpenAPI, schemes: &[SchemeInfo]) -> Vec<OperationInfo> {
67 let mut ops = Vec::new();
68 for (path, ref_or_item) in &openapi.paths.paths {
69 let item = match ref_or_item {
70 ReferenceOr::Item(i) => i,
71 ReferenceOr::Reference { .. } => continue,
72 };
73 for (method, operation) in path_item_operations(item) {
74 if let Some(info) =
75 build_operation_info(path, &method, operation, item, openapi, schemes)
76 {
77 ops.push(info);
78 }
79 }
80 }
81 ops
82}
83
84fn path_item_operations(item: &PathItem) -> Vec<(String, &Operation)> {
86 let mut out = Vec::new();
87 if let Some(op) = &item.get {
88 out.push(("get".into(), op));
89 }
90 if let Some(op) = &item.post {
91 out.push(("post".into(), op));
92 }
93 if let Some(op) = &item.put {
94 out.push(("put".into(), op));
95 }
96 if let Some(op) = &item.delete {
97 out.push(("delete".into(), op));
98 }
99 if let Some(op) = &item.patch {
100 out.push(("patch".into(), op));
101 }
102 if let Some(op) = &item.head {
103 out.push(("head".into(), op));
104 }
105 if let Some(op) = &item.options {
106 out.push(("options".into(), op));
107 }
108 if let Some(op) = &item.trace {
109 out.push(("trace".into(), op));
110 }
111 out
112}
113
114fn build_operation_info(
116 path: &str,
117 method: &str,
118 operation: &Operation,
119 path_item: &PathItem,
120 openapi: &OpenAPI,
121 schemes: &[SchemeInfo],
122) -> Option<OperationInfo> {
123 let operation_id = operation.operation_id.clone()?;
124
125 let mut all_params: Vec<&ReferenceOr<Parameter>> = Vec::new();
127 all_params.extend(path_item.parameters.iter());
128 all_params.extend(operation.parameters.iter());
129
130 let mut path_params = Vec::new();
131 let mut query_params = Vec::new();
132 let mut header_params = Vec::new();
133
134 for ref_or_param in &all_params {
135 let param = match ref_or_param {
136 ReferenceOr::Item(p) => p,
137 ReferenceOr::Reference { reference } => {
138 if let Some(resolved) = resolve_param_ref(reference, openapi) {
140 resolved
141 } else {
142 continue;
143 }
144 }
145 };
146
147 let data = param.parameter_data_ref();
148 let param_schema = param_schema(param, openapi);
149
150 let (is_enum, enum_ident, enum_values) = param_schema.as_ref().map_or_else(
151 || (false, None, vec![]),
152 |schema| {
153 if is_string_enum(schema) {
154 let ident = format_ident!(
155 "{}{}Query",
156 operation_id.to_pascal_case(),
157 data.name.to_pascal_case()
158 );
159 let vals = string_enum_values(schema);
160 (true, Some(ident), vals)
161 } else {
162 (false, None, vec![])
163 }
164 },
165 );
166
167 let rust_type = if is_enum {
168 let ei = enum_ident.as_ref().unwrap();
169 quote! { #ei }
170 } else if let Some(schema) = ¶m_schema {
171 let ref_or = ReferenceOr::Item(schema.clone());
172 schema_to_rust_type(&ref_or, true)
173 } else {
174 quote! { ::std::string::String }
175 };
176
177 let info = ParamInfo {
178 name: data.name.clone(),
179 description: data.description.clone(),
180 required: data.required,
181 rust_type,
182 is_enum,
183 enum_ident,
184 enum_values,
185 };
186
187 match param {
188 Parameter::Path { .. } => path_params.push(info),
189 Parameter::Query { .. } => query_params.push(info),
190 Parameter::Header { .. } => header_params.push(info),
191 Parameter::Cookie { .. } => {}
192 }
193 }
194
195 let body = operation
196 .request_body
197 .as_ref()
198 .and_then(|rb| build_body_info(rb, openapi));
199
200 let responses = build_responses(&operation.responses, openapi);
201 let auth = resolve_op_security(operation, openapi, schemes);
202
203 Some(OperationInfo {
204 operation_id,
205 method: method.to_owned(),
206 path: path.to_owned(),
207 summary: operation.summary.clone(),
208 description: operation.description.clone(),
209 path_params,
210 query_params,
211 header_params,
212 body,
213 responses,
214 auth,
215 })
216}
217
218fn resolve_param_ref<'a>(reference: &str, openapi: &'a OpenAPI) -> Option<&'a Parameter> {
220 let name = reference.strip_prefix("#/components/parameters/")?;
221 openapi.components.as_ref()?.parameters.get(name)?.as_item()
222}
223
224fn param_schema(param: &Parameter, openapi: &OpenAPI) -> Option<Schema> {
226 use openapiv3::ParameterSchemaOrContent;
227 let data = param.parameter_data_ref();
228 match &data.format {
229 ParameterSchemaOrContent::Schema(ref_or) => match ref_or {
230 ReferenceOr::Item(s) => Some(s.clone()),
231 ReferenceOr::Reference { reference } => {
232 let name = reference.strip_prefix("#/components/schemas/")?;
233 openapi
234 .components
235 .as_ref()?
236 .schemas
237 .get(name)?
238 .as_item()
239 .cloned()
240 }
241 },
242 ParameterSchemaOrContent::Content(_) => None,
243 }
244}
245
246fn build_body_info(ref_or_rb: &ReferenceOr<RequestBody>, openapi: &OpenAPI) -> Option<BodyInfo> {
248 let rb = match ref_or_rb {
249 ReferenceOr::Item(r) => r,
250 ReferenceOr::Reference { reference } => {
251 let name = reference.strip_prefix("#/components/requestBodies/")?;
252 openapi
253 .components
254 .as_ref()?
255 .request_bodies
256 .get(name)?
257 .as_item()?
258 }
259 };
260
261 let rust_type = json_media_type_to_rust(&rb.content, openapi)?;
262
263 Some(BodyInfo {
264 description: rb.description.clone(),
265 required: rb.required,
266 rust_type,
267 })
268}
269
270fn json_media_type_to_rust(
272 content: &indexmap::IndexMap<String, MediaType>,
273 _openapi: &OpenAPI,
274) -> Option<TokenStream> {
275 let media = content
276 .get("application/json")
277 .or_else(|| content.values().next())?;
278 let ref_or_schema = media.schema.as_ref()?;
279 Some(schema_to_rust_type(ref_or_schema, true))
280}
281
282fn build_responses(responses: &Responses, openapi: &OpenAPI) -> Vec<ResponseInfo> {
284 let mut out = Vec::new();
285
286 for (status_code, ref_or_resp) in &responses.responses {
287 let resp = match ref_or_resp {
288 ReferenceOr::Item(r) => r,
289 ReferenceOr::Reference { reference } => {
290 if let Some(r) = resolve_response_ref(reference, openapi) {
291 r
292 } else {
293 continue;
294 }
295 }
296 };
297
298 let rust_type = json_media_type_to_rust(&resp.content, openapi);
299
300 let status = match status_code {
301 StatusCode::Code(n) => ResponseStatus::Code(*n),
302 StatusCode::Range(_) => continue, };
304
305 out.push(ResponseInfo {
306 status,
307 description: resp.description.clone(),
308 rust_type,
309 });
310 }
311
312 if let Some(ref_or_default) = &responses.default {
314 let resp = match ref_or_default {
315 ReferenceOr::Item(r) => r,
316 ReferenceOr::Reference { reference } => {
317 if let Some(r) = resolve_response_ref(reference, openapi) {
318 r
319 } else {
320 return out;
321 }
322 }
323 };
324 out.push(ResponseInfo {
325 status: ResponseStatus::Default,
326 description: resp.description.clone(),
327 rust_type: None, });
329 }
330
331 out
332}
333
334fn resolve_response_ref<'a>(reference: &str, openapi: &'a OpenAPI) -> Option<&'a Response> {
336 let name = reference.strip_prefix("#/components/responses/")?;
337 openapi.components.as_ref()?.responses.get(name)?.as_item()
338}
339
340pub fn generate_operation_types(ops: &[OperationInfo]) -> TokenStream {
342 let items: Vec<TokenStream> = ops.iter().map(generate_single_operation_types).collect();
343 quote! { #(#items)* }
344}
345
346fn generate_single_operation_types(op: &OperationInfo) -> TokenStream {
348 let query_enums = generate_query_enums(op);
349 let request_struct = generate_request_struct(op);
350 let response_enum = generate_response_enum(op);
351 quote! {
352 #query_enums
353 #request_struct
354 #response_enum
355 }
356}
357
358fn generate_query_enums(op: &OperationInfo) -> TokenStream {
360 let enums: Vec<TokenStream> = op
361 .query_params
362 .iter()
363 .filter(|p| p.is_enum)
364 .map(|p| {
365 let ident = p.enum_ident.as_ref().unwrap();
366 let doc = doc_attr(&p.description);
367 let variants: Vec<TokenStream> = p
368 .enum_values
369 .iter()
370 .map(|v| {
371 let variant_ident = format_ident!("{}", v.to_pascal_case());
372 if variant_ident == v.as_str() {
373 quote! { #variant_ident }
374 } else {
375 quote! {
376 #[serde(rename = #v)]
377 #variant_ident
378 }
379 }
380 })
381 .collect();
382
383 quote! {
384 #doc
385 #[derive(
386 ::core::fmt::Debug,
387 ::core::clone::Clone,
388 ::serde::Serialize,
389 ::serde::Deserialize,
390 )]
391
392 pub enum #ident {
393 #(#variants,)*
394 }
395 }
396 })
397 .collect();
398
399 quote! { #(#enums)* }
400}
401
402fn generate_request_struct(op: &OperationInfo) -> TokenStream {
404 let ident = format_ident!("{}Request", op.operation_id.to_pascal_case());
405 let doc = combined_doc(op.summary.as_ref(), op.description.as_ref());
406
407 let mut fields: Vec<TokenStream> = Vec::new();
408
409 for p in &op.path_params {
410 let field_ident = format_ident!("{}", p.name.to_snake_case());
411 let ftype = &p.rust_type;
412 let fdoc = doc_attr(&p.description);
413 fields.push(quote! {
414 #fdoc
415 pub #field_ident: #ftype,
416 });
417 }
418
419 for p in &op.query_params {
420 let field_ident = format_ident!("{}", p.name.to_snake_case());
421 let inner = &p.rust_type;
422 let ftype = if p.required {
423 quote! { #inner }
424 } else {
425 quote! { ::core::option::Option<#inner> }
426 };
427 let fdoc = doc_attr(&p.description);
428 fields.push(quote! {
429 #fdoc
430 pub #field_ident: #ftype,
431 });
432 }
433
434 for p in &op.header_params {
435 let field_ident = format_ident!("{}", p.name.to_snake_case());
436 let fdoc = doc_attr(&p.description);
437 fields.push(quote! {
439 #fdoc
440 pub #field_ident: ::core::option::Option<::std::string::String>,
441 });
442 }
443
444 if let Some(body) = &op.body {
445 let inner = &body.rust_type;
446 let ftype = if body.required {
447 quote! { #inner }
448 } else {
449 quote! { ::core::option::Option<#inner> }
450 };
451 let bdoc = doc_attr(&body.description);
452 fields.push(quote! {
453 #bdoc
454 pub body: #ftype,
455 });
456 }
457
458 quote! {
459 #doc
460 #[derive(::core::fmt::Debug, ::core::clone::Clone)]
461 pub struct #ident {
462 #(#fields)*
463 }
464 }
465}
466
467fn generate_response_enum(op: &OperationInfo) -> TokenStream {
469 let ident = format_ident!("{}Response", op.operation_id.to_pascal_case());
470 let doc = combined_doc(op.summary.as_ref(), op.description.as_ref());
471
472 let variants: Vec<TokenStream> = op
473 .responses
474 .iter()
475 .map(|r| {
476 let vdoc = doc_attr(&Some(r.description.clone()));
477 match &r.status {
478 ResponseStatus::Code(n) => {
479 let variant_ident = format_ident!("Status{}", n);
480 r.rust_type.as_ref().map_or_else(
481 || {
482 quote! {
483 #vdoc
484 #variant_ident
485 }
486 },
487 |ty| {
488 quote! {
489 #vdoc
490 #variant_ident(#ty)
491 }
492 },
493 )
494 }
495 ResponseStatus::Default => {
496 quote! {
497 #vdoc
498 Default(::std::string::String)
499 }
500 }
501 }
502 })
503 .collect();
504
505 quote! {
506 #doc
507 #[derive(::core::fmt::Debug, ::core::clone::Clone)]
508 pub enum #ident {
509 #(#variants,)*
510 }
511 }
512}
513
514fn combined_doc(summary: Option<&String>, description: Option<&String>) -> TokenStream {
516 match (summary, description) {
517 (Some(s), Some(d)) if s != d => quote! { #[doc = #s] #[doc = ""] #[doc = #d] },
518 (Some(s), _) => quote! { #[doc = #s] },
519 (None, Some(d)) => quote! { #[doc = #d] },
520 (None, None) => quote! {},
521 }
522}