1use std::fmt::Write;
2
3use heck::{ToSnakeCase, ToUpperCamelCase};
4use indexmap::IndexMap;
5use proc_macro2::TokenStream;
6use quote::{format_ident, quote};
7use syn::Ident;
8
9use crate::openapi::{
10 parameter::OpenApiParameter,
11 path::{OpenApiPath, OpenApiPathParameter, OpenApiResponseBody},
12};
13
14use super::{
15 parameter::{Parameter, ParameterLocation, ParameterType},
16 union::Union,
17 ResolvedSchema, WarningReporter,
18};
19
20#[derive(Debug, Clone)]
21pub enum PathSegment {
22 Constant(String),
23 Parameter { name: String },
24}
25
26pub struct PrettySegments<'a>(pub &'a [PathSegment]);
27
28impl std::fmt::Display for PrettySegments<'_> {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 for segment in self.0 {
31 match segment {
32 PathSegment::Constant(c) => write!(f, "/{c}")?,
33 PathSegment::Parameter { name } => write!(f, "/{{{name}}}")?,
34 }
35 }
36
37 Ok(())
38 }
39}
40
41#[derive(Debug, Clone)]
42pub enum PathParameter {
43 Inline(Parameter),
44 Component(Parameter),
45}
46
47#[derive(Debug, Clone)]
48pub enum PathResponse {
49 Component { name: String },
50 ArbitraryUnion(Union),
52}
53
54#[derive(Debug, Clone)]
55pub struct Path {
56 pub segments: Vec<PathSegment>,
57 pub name: String,
58 pub summary: Option<String>,
59 pub description: Option<String>,
60 pub parameters: Vec<PathParameter>,
61 pub response: PathResponse,
62}
63
64impl Path {
65 pub fn from_schema(
66 path: &str,
67 schema: &OpenApiPath,
68 parameters: &IndexMap<&str, OpenApiParameter>,
69 warnings: WarningReporter,
70 ) -> Option<Self> {
71 let mut segments = Vec::new();
72 for segment in path.strip_prefix('/')?.split('/') {
73 if segment.starts_with('{') && segment.ends_with('}') {
74 segments.push(PathSegment::Parameter {
75 name: segment[1..(segment.len() - 1)].to_owned(),
76 });
77 } else {
78 segments.push(PathSegment::Constant(segment.to_owned()));
79 }
80 }
81
82 let summary = schema.get.summary.as_deref().map(ToOwned::to_owned);
83 let description = schema.get.description.as_deref().map(ToOwned::to_owned);
84
85 let mut params = Vec::with_capacity(schema.get.parameters.len());
86 for parameter in &schema.get.parameters {
87 match ¶meter {
88 OpenApiPathParameter::Link { ref_path } => {
89 let name = ref_path
90 .strip_prefix("#/components/parameters/")?
91 .to_owned();
92 let param = parameters.get(&name.as_str())?;
93 params.push(PathParameter::Component(Parameter::from_schema(
94 &name, param,
95 )?));
96 }
97 OpenApiPathParameter::Inline(schema) => {
98 let name = schema.name.to_upper_camel_case();
99 let parameter = Parameter::from_schema(&name, schema)?;
100 params.push(PathParameter::Inline(parameter));
101 }
102 };
103 }
104
105 let mut suffixes = vec![];
106 let mut name = String::new();
107
108 for seg in &segments {
109 match seg {
110 PathSegment::Constant(val) => {
111 name.push_str(&val.to_upper_camel_case());
112 }
113 PathSegment::Parameter { name } => {
114 suffixes.push(format!("For{}", name.to_upper_camel_case()));
115 }
116 }
117 }
118
119 for suffix in suffixes {
120 name.push_str(&suffix);
121 }
122
123 let response = match &schema.get.response_content {
124 OpenApiResponseBody::Schema(link) => PathResponse::Component {
125 name: link
126 .ref_path
127 .strip_prefix("#/components/schemas/")?
128 .to_owned(),
129 },
130 OpenApiResponseBody::Union { any_of: _ } => {
131 PathResponse::ArbitraryUnion(Union::from_schema(
132 "Response",
133 &schema.get.response_content,
134 warnings.child("response"),
135 )?)
136 }
137 };
138
139 Some(Self {
140 segments,
141 name,
142 summary,
143 description,
144 parameters: params,
145 response,
146 })
147 }
148
149 pub fn codegen_request(
150 &self,
151 resolved: &ResolvedSchema,
152 warnings: WarningReporter,
153 ) -> Option<TokenStream> {
154 let name = if self.segments.len() == 1 {
155 let Some(PathSegment::Constant(first)) = self.segments.first() else {
156 return None;
157 };
158 format_ident!("{}Request", first.to_upper_camel_case())
159 } else {
160 format_ident!("{}Request", self.name)
161 };
162
163 let mut ns = PathNamespace {
164 path: self,
165 ident: None,
166 elements: Vec::new(),
167 };
168
169 let mut fields = Vec::with_capacity(self.parameters.len());
170 let mut convert_field = Vec::with_capacity(self.parameters.len());
171 let mut start_fields = Vec::new();
172 let mut discriminant = Vec::new();
173 let mut discriminant_val = Vec::new();
174 let mut fmt_val = Vec::new();
175
176 for param in &self.parameters {
177 let (is_inline, param) = match ¶m {
178 PathParameter::Inline(param) => (true, param),
179 PathParameter::Component(param) => (false, param),
180 };
181
182 let (ty, builder_param) = match ¶m.r#type {
183 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
184 let ty_name = format_ident!("{}", param.name);
185
186 if is_inline {
187 ns.push_element(param.codegen(resolved)?);
188 let path = ns.get_ident();
189
190 (
191 quote! {
192 crate::request::models::#path::#ty_name
193 },
194 Some(quote! { #[cfg_attr(feature = "builder", builder(into))] }),
195 )
196 } else {
197 (
198 quote! {
199 crate::parameters::#ty_name
200 },
201 Some(quote! { #[cfg_attr(feature = "builder", builder(into))]}),
202 )
203 }
204 }
205 ParameterType::String => (quote! { String }, None),
206 ParameterType::Boolean => (quote! { bool }, None),
207 ParameterType::Schema { type_name } => {
208 let ty_name = format_ident!("{}", type_name);
209
210 (
211 quote! {
212 crate::models::#ty_name
213 },
214 None,
215 )
216 }
217 ParameterType::Array { .. } => {
218 ns.push_element(param.codegen(resolved)?);
219 let ty_name = param.r#type.codegen_type_name(¶m.name);
220 let path = ns.get_ident();
221 (
222 quote! {
223 crate::request::models::#path::#ty_name
224 },
225 Some(quote! { #[cfg_attr(feature = "builder", builder(into))] }),
226 )
227 }
228 };
229
230 let name = format_ident!("{}", param.name.to_snake_case());
231 let query_val = ¶m.value;
232
233 if param.location == ParameterLocation::Path {
234 if self.segments.iter().any(|s| {
235 if let PathSegment::Parameter { name } = s {
236 name == ¶m.value
237 } else {
238 false
239 }
240 }) {
241 discriminant.push(ty.clone());
242 discriminant_val.push(quote! { self.#name });
243 let path_name = format_ident!("{}", param.value);
244 start_fields.push(quote! {
245 #[cfg_attr(feature = "builder", builder(start_fn))]
246 #builder_param
247 pub #name: #ty
248 });
249 fmt_val.push(quote! {
250 #path_name=self.#name
251 });
252 } else {
253 warnings.push(format!(
254 "Provided path parameter is not present in the url: {}",
255 param.value
256 ));
257 }
258 } else {
259 let ty = if param.required {
260 convert_field.push(quote! {
261 .chain(std::iter::once(&self.#name).map(|v| (#query_val, v.to_string())))
262 });
263 ty
264 } else {
265 convert_field.push(quote! {
266 .chain(self.#name.as_ref().into_iter().map(|v| (#query_val, v.to_string())))
267 });
268 quote! { Option<#ty>}
269 };
270
271 fields.push(quote! {
272 #builder_param
273 pub #name: #ty
274 });
275 }
276 }
277
278 let response_ty = match &self.response {
279 PathResponse::Component { name } => {
280 let name = format_ident!("{name}");
281 quote! {
282 crate::models::#name
283 }
284 }
285 PathResponse::ArbitraryUnion(union) => {
286 let path = ns.get_ident();
287 let ty_name = format_ident!("{}", union.name);
288
289 quote! {
290 crate::request::models::#path::#ty_name
291 }
292 }
293 };
294
295 let mut path_fmt_str = String::new();
296 for seg in &self.segments {
297 match seg {
298 PathSegment::Constant(val) => _ = write!(path_fmt_str, "/{}", val),
299 PathSegment::Parameter { name } => _ = write!(path_fmt_str, "/{{{}}}", name),
300 }
301 }
302
303 if let PathResponse::ArbitraryUnion(union) = &self.response {
304 ns.push_element(union.codegen()?);
305 }
306
307 let ns = ns.codegen();
308
309 start_fields.extend(fields);
310
311 Some(quote! {
312 #ns
313
314 #[cfg_attr(feature = "builder", derive(bon::Builder))]
315 #[derive(Debug, Clone)]
316 #[cfg_attr(feature = "builder", builder(state_mod(vis = "pub(crate)"), on(String, into)))]
317 pub struct #name {
318 #(#start_fields),*
319 }
320
321 impl crate::request::IntoRequest for #name {
322 #[allow(unused_parens)]
323 type Discriminant = (#(#discriminant),*);
324 type Response = #response_ty;
325 fn into_request(self) -> (Self::Discriminant, crate::request::ApiRequest) {
326 let path = format!(#path_fmt_str, #(#fmt_val),*);
327 #[allow(unused_parens)]
328 (
329 (#(#discriminant_val),*),
330 crate::request::ApiRequest {
331 path,
332 parameters: std::iter::empty()
333 #(#convert_field)*
334 .collect(),
335 }
336 )
337 }
338 }
339 })
340 }
341
342 pub fn codegen_scope_call(&self) -> Option<TokenStream> {
343 let mut extra_args = Vec::new();
344 let mut disc = Vec::new();
345
346 let snake_name = self.name.to_snake_case();
347
348 let request_name = format_ident!("{}Request", self.name);
349 let builder_name = format_ident!("{}RequestBuilder", self.name);
350 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
351 let request_mod_name = format_ident!("{snake_name}");
352
353 let request_path = quote! { crate::request::models::#request_name };
354 let builder_path = quote! { crate::request::models::#builder_name };
355 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
356
357 let tail = snake_name
358 .split_once('_')
359 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
360
361 let fn_name = format_ident!("{tail}");
362
363 for param in &self.parameters {
364 let (param, is_inline) = match param {
365 PathParameter::Inline(param) => (param, true),
366 PathParameter::Component(param) => (param, false),
367 };
368
369 if param.location == ParameterLocation::Path
370 && self.segments.iter().any(|s| {
371 if let PathSegment::Parameter { name } = s {
372 name == ¶m.value
373 } else {
374 false
375 }
376 })
377 {
378 let ty = match ¶m.r#type {
379 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
380 let ty_name = format_ident!("{}", param.name);
381
382 if is_inline {
383 quote! {
384 crate::request::models::#request_mod_name::#ty_name
385 }
386 } else {
387 quote! {
388 crate::parameters::#ty_name
389 }
390 }
391 }
392 ParameterType::String => quote! { String },
393 ParameterType::Boolean => quote! { bool },
394 ParameterType::Schema { type_name } => {
395 let ty_name = format_ident!("{}", type_name);
396
397 quote! {
398 crate::models::#ty_name
399 }
400 }
401 ParameterType::Array { .. } => {
402 let ty_name = param.r#type.codegen_type_name(¶m.name);
403
404 quote! {
405 crate::request::models::#request_mod_name::#ty_name
406 }
407 }
408 };
409
410 let arg_name = format_ident!("{}", param.value.to_snake_case());
411
412 extra_args.push(quote! { #arg_name: #ty, });
413 disc.push(arg_name);
414 }
415 }
416
417 let response_ty = match &self.response {
418 PathResponse::Component { name } => {
419 let name = format_ident!("{name}");
420 quote! {
421 crate::models::#name
422 }
423 }
424 PathResponse::ArbitraryUnion(union) => {
425 let name = format_ident!("{}", union.name);
426 quote! {
427 crate::request::models::#request_mod_name::#name
428 }
429 }
430 };
431
432 let doc = match (&self.summary, &self.description) {
433 (Some(summary), Some(description)) => {
434 Some(format!("{summary}\n\n# Description\n{description}"))
435 }
436 (Some(summary), None) => Some(summary.clone()),
437 (None, Some(description)) => Some(format!("# Description\n{description}")),
438 (None, None) => None,
439 };
440
441 let doc = doc.map(|d| {
442 quote! {
443 #[doc = #d]
444 }
445 });
446
447 Some(quote! {
448 #doc
449 pub async fn #fn_name<S>(
450 self,
451 #(#extra_args)*
452 builder: impl FnOnce(
453 #builder_path<#builder_mod_path::Empty>
454 ) -> #builder_path<S>,
455 ) -> Result<#response_ty, E::Error>
456 where
457 S: #builder_mod_path::IsComplete,
458 {
459 let r = builder(#request_path::builder(#(#disc),*)).build();
460
461 self.0.fetch(r).await
462 }
463 })
464 }
465
466 pub fn codegen_bulk_scope_call(&self) -> Option<TokenStream> {
467 let mut disc = Vec::new();
468 let mut disc_ty = Vec::new();
469
470 let snake_name = self.name.to_snake_case();
471
472 let request_name = format_ident!("{}Request", self.name);
473 let builder_name = format_ident!("{}RequestBuilder", self.name);
474 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
475 let request_mod_name = format_ident!("{snake_name}");
476
477 let request_path = quote! { crate::request::models::#request_name };
478 let builder_path = quote! { crate::request::models::#builder_name };
479 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
480
481 let tail = snake_name
482 .split_once('_')
483 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
484
485 let fn_name = format_ident!("{tail}");
486
487 for param in &self.parameters {
488 let (param, is_inline) = match param {
489 PathParameter::Inline(param) => (param, true),
490 PathParameter::Component(param) => (param, false),
491 };
492 if param.location == ParameterLocation::Path
493 && self.segments.iter().any(|s| {
494 if let PathSegment::Parameter { name } = s {
495 name == ¶m.value
496 } else {
497 false
498 }
499 })
500 {
501 let ty = match ¶m.r#type {
502 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
503 let ty_name = format_ident!("{}", param.name);
504
505 if is_inline {
506 quote! {
507 crate::request::models::#request_mod_name::#ty_name
508 }
509 } else {
510 quote! {
511 crate::parameters::#ty_name
512 }
513 }
514 }
515 ParameterType::String => quote! { String },
516 ParameterType::Boolean => quote! { bool },
517 ParameterType::Schema { type_name } => {
518 let ty_name = format_ident!("{}", type_name);
519
520 quote! {
521 crate::models::#ty_name
522 }
523 }
524 ParameterType::Array { .. } => {
525 let name = param.r#type.codegen_type_name(¶m.name);
526 quote! {
527 crate::request::models::#request_mod_name::#name
528 }
529 }
530 };
531
532 let arg_name = format_ident!("{}", param.value.to_snake_case());
533
534 disc_ty.push(ty);
535 disc.push(arg_name);
536 }
537 }
538
539 if disc.is_empty() {
540 return None;
541 }
542
543 let response_ty = match &self.response {
544 PathResponse::Component { name } => {
545 let name = format_ident!("{name}");
546 quote! {
547 crate::models::#name
548 }
549 }
550 PathResponse::ArbitraryUnion(union) => {
551 let name = format_ident!("{}", union.name);
552 quote! {
553 crate::request::models::#request_mod_name::#name
554 }
555 }
556 };
557
558 let disc = if disc.len() > 1 {
559 quote! { (#(#disc),*) }
560 } else {
561 quote! { #(#disc),* }
562 };
563
564 let disc_ty = if disc_ty.len() > 1 {
565 quote! { (#(#disc_ty),*) }
566 } else {
567 quote! { #(#disc_ty),* }
568 };
569
570 let doc = match (&self.summary, &self.description) {
571 (Some(summary), Some(description)) => {
572 Some(format!("{summary}\n\n# Description\n{description}"))
573 }
574 (Some(summary), None) => Some(summary.clone()),
575 (None, Some(description)) => Some(format!("# Description\n{description}")),
576 (None, None) => None,
577 };
578
579 let doc = doc.map(|d| {
580 quote! {
581 #[doc = #d]
582 }
583 });
584
585 Some(quote! {
586 #doc
587 pub fn #fn_name<S, I, B>(
588 self,
589 ids: I,
590 builder: B
591 ) -> impl futures::Stream<Item = (#disc_ty, Result<#response_ty, E::Error>)>
592 where
593 I: IntoIterator<Item = #disc_ty>,
594 S: #builder_mod_path::IsComplete,
595 B: Fn(
596 #builder_path<#builder_mod_path::Empty>
597 ) -> #builder_path<S>,
598 {
599 let requests = ids.into_iter()
600 .map(move |#disc| builder(#request_path::builder(#disc)).build());
601
602 let executor = self.executor;
603 executor.fetch_many(requests)
604 }
605 })
606 }
607}
608
609pub struct PathNamespace<'r> {
610 path: &'r Path,
611 ident: Option<Ident>,
612 elements: Vec<TokenStream>,
613}
614
615impl PathNamespace<'_> {
616 pub fn get_ident(&mut self) -> Ident {
617 self.ident
618 .get_or_insert_with(|| {
619 let name = self.path.name.to_snake_case();
620 format_ident!("{name}")
621 })
622 .clone()
623 }
624
625 pub fn push_element(&mut self, el: TokenStream) {
626 self.elements.push(el);
627 }
628
629 pub fn codegen(mut self) -> Option<TokenStream> {
630 if self.elements.is_empty() {
631 None
632 } else {
633 let ident = self.get_ident();
634 let elements = self.elements;
635 Some(quote! {
636 pub mod #ident {
637 #(#elements)*
638 }
639 })
640 }
641 }
642}
643
644#[cfg(test)]
645mod test {
646 use super::*;
647
648 use crate::openapi::schema::test::get_schema;
649
650 #[test]
651 fn resolve_paths() {
652 let schema = get_schema();
653
654 let mut paths = 0;
655 let mut unresolved = vec![];
656
657 for (name, desc) in &schema.paths {
658 paths += 1;
659 if Path::from_schema(
660 name,
661 desc,
662 &schema.components.parameters,
663 WarningReporter::new(),
664 )
665 .is_none()
666 {
667 unresolved.push(name);
668 }
669 }
670
671 if !unresolved.is_empty() {
672 panic!(
673 "Failed to resolve {}/{} paths. Could not resolve [{}]",
674 unresolved.len(),
675 paths,
676 unresolved
677 .into_iter()
678 .map(|u| format!("`{u}`"))
679 .collect::<Vec<_>>()
680 .join(", ")
681 )
682 }
683 }
684
685 #[test]
686 fn codegen_paths() {
687 let schema = get_schema();
688 let resolved = ResolvedSchema::from_open_api(&schema);
689 let reporter = WarningReporter::new();
690
691 let mut paths = 0;
692 let mut unresolved = vec![];
693
694 for (name, desc) in &schema.paths {
695 paths += 1;
696 let Some(path) =
697 Path::from_schema(name, desc, &schema.components.parameters, reporter.clone())
698 else {
699 unresolved.push(name);
700 continue;
701 };
702
703 if path.codegen_scope_call().is_none()
704 || path.codegen_request(&resolved, reporter.clone()).is_none()
705 {
706 unresolved.push(name);
707 }
708 }
709
710 if !unresolved.is_empty() {
711 panic!(
712 "Failed to codegen {}/{} paths. Could not resolve [{}]",
713 unresolved.len(),
714 paths,
715 unresolved
716 .into_iter()
717 .map(|u| format!("`{u}`"))
718 .collect::<Vec<_>>()
719 .join(", ")
720 )
721 }
722 }
723}