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