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