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 parameters.push((#query_val, self.#name.to_string()));
262 });
263 ty
264 } else {
265 convert_field.push(quote! {
266 if let Some(value) = &self.#name {
267 parameters.push((#query_val, value.to_string()));
268 }
269 });
270 quote! { Option<#ty>}
271 };
272
273 fields.push(quote! {
274 #builder_param
275 pub #name: #ty
276 });
277 }
278 }
279
280 let response_ty = match &self.response {
281 PathResponse::Component { name } => {
282 let name = format_ident!("{name}");
283 quote! {
284 crate::models::#name
285 }
286 }
287 PathResponse::ArbitraryUnion(union) => {
288 let path = ns.get_ident();
289 let ty_name = format_ident!("{}", union.name);
290
291 quote! {
292 crate::request::models::#path::#ty_name
293 }
294 }
295 };
296
297 let mut path_fmt_str = String::new();
298 for seg in &self.segments {
299 match seg {
300 PathSegment::Constant(val) => _ = write!(path_fmt_str, "/{val}"),
301 PathSegment::Parameter { name } => _ = write!(path_fmt_str, "/{{{name}}}"),
302 }
303 }
304
305 if let PathResponse::ArbitraryUnion(union) = &self.response {
306 ns.push_element(union.codegen()?);
307 }
308
309 let ns = ns.codegen();
310
311 start_fields.extend(fields);
312
313 Some(quote! {
314 #ns
315
316 #[cfg_attr(feature = "builder", derive(bon::Builder))]
317 #[derive(Debug, Clone)]
318 #[cfg_attr(feature = "builder", builder(state_mod(vis = "pub(crate)"), on(String, into)))]
319 pub struct #name {
320 #(#start_fields),*
321 }
322
323 impl crate::request::IntoRequest for #name {
324 #[allow(unused_parens)]
325 type Discriminant = (#(#discriminant),*);
326 type Response = #response_ty;
327 fn into_request(self) -> (Self::Discriminant, crate::request::ApiRequest) {
328 let path = format!(#path_fmt_str, #(#fmt_val),*);
329 let mut parameters = Vec::new();
330 #(#convert_field)*
331
332 #[allow(unused_parens)]
333 (
334 (#(#discriminant_val),*),
335 crate::request::ApiRequest {
336 path,
337 parameters,
338 }
339 )
340 }
341 }
342 })
343 }
344
345 pub fn codegen_scope_call(&self) -> Option<TokenStream> {
346 let mut extra_args = Vec::new();
347 let mut disc = Vec::new();
348
349 let snake_name = self.name.to_snake_case();
350
351 let request_name = format_ident!("{}Request", self.name);
352 let builder_name = format_ident!("{}RequestBuilder", self.name);
353 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
354 let request_mod_name = format_ident!("{snake_name}");
355
356 let request_path = quote! { crate::request::models::#request_name };
357 let builder_path = quote! { crate::request::models::#builder_name };
358 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
359
360 let tail = snake_name
361 .split_once('_')
362 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
363
364 let fn_name = format_ident!("{tail}");
365
366 for param in &self.parameters {
367 let (param, is_inline) = match param {
368 PathParameter::Inline(param) => (param, true),
369 PathParameter::Component(param) => (param, false),
370 };
371
372 if param.location == ParameterLocation::Path
373 && self.segments.iter().any(|s| {
374 if let PathSegment::Parameter { name } = s {
375 name == ¶m.value
376 } else {
377 false
378 }
379 })
380 {
381 let ty = match ¶m.r#type {
382 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
383 let ty_name = format_ident!("{}", param.name);
384
385 if is_inline {
386 quote! {
387 crate::request::models::#request_mod_name::#ty_name
388 }
389 } else {
390 quote! {
391 crate::parameters::#ty_name
392 }
393 }
394 }
395 ParameterType::String => quote! { String },
396 ParameterType::Boolean => quote! { bool },
397 ParameterType::Schema { type_name } => {
398 let ty_name = format_ident!("{}", type_name);
399
400 quote! {
401 crate::models::#ty_name
402 }
403 }
404 ParameterType::Array { .. } => {
405 let ty_name = param.r#type.codegen_type_name(¶m.name);
406
407 quote! {
408 crate::request::models::#request_mod_name::#ty_name
409 }
410 }
411 };
412
413 let arg_name = format_ident!("{}", param.value.to_snake_case());
414
415 extra_args.push(quote! { #arg_name: #ty, });
416 disc.push(arg_name);
417 }
418 }
419
420 let response_ty = match &self.response {
421 PathResponse::Component { name } => {
422 let name = format_ident!("{name}");
423 quote! {
424 crate::models::#name
425 }
426 }
427 PathResponse::ArbitraryUnion(union) => {
428 let name = format_ident!("{}", union.name);
429 quote! {
430 crate::request::models::#request_mod_name::#name
431 }
432 }
433 };
434
435 let doc = match (&self.summary, &self.description) {
436 (Some(summary), Some(description)) => {
437 Some(format!("{summary}\n\n# Description\n{description}"))
438 }
439 (Some(summary), None) => Some(summary.clone()),
440 (None, Some(description)) => Some(format!("# Description\n{description}")),
441 (None, None) => None,
442 };
443
444 let doc = doc.map(|d| {
445 quote! {
446 #[doc = #d]
447 }
448 });
449
450 Some(quote! {
451 #doc
452 pub async fn #fn_name<S>(
453 self,
454 #(#extra_args)*
455 builder: impl FnOnce(
456 #builder_path<#builder_mod_path::Empty>
457 ) -> #builder_path<S>,
458 ) -> Result<#response_ty, E::Error>
459 where
460 S: #builder_mod_path::IsComplete,
461 {
462 let r = builder(#request_path::builder(#(#disc),*)).build();
463
464 self.0.fetch(r).await
465 }
466 })
467 }
468
469 pub fn codegen_bulk_scope_call(&self) -> Option<TokenStream> {
470 let mut disc = Vec::new();
471 let mut disc_ty = Vec::new();
472
473 let snake_name = self.name.to_snake_case();
474
475 let request_name = format_ident!("{}Request", self.name);
476 let builder_name = format_ident!("{}RequestBuilder", self.name);
477 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
478 let request_mod_name = format_ident!("{snake_name}");
479
480 let request_path = quote! { crate::request::models::#request_name };
481 let builder_path = quote! { crate::request::models::#builder_name };
482 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
483
484 let tail = snake_name
485 .split_once('_')
486 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
487
488 let fn_name = format_ident!("{tail}");
489
490 for param in &self.parameters {
491 let (param, is_inline) = match param {
492 PathParameter::Inline(param) => (param, true),
493 PathParameter::Component(param) => (param, false),
494 };
495 if param.location == ParameterLocation::Path
496 && self.segments.iter().any(|s| {
497 if let PathSegment::Parameter { name } = s {
498 name == ¶m.value
499 } else {
500 false
501 }
502 })
503 {
504 let ty = match ¶m.r#type {
505 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
506 let ty_name = format_ident!("{}", param.name);
507
508 if is_inline {
509 quote! {
510 crate::request::models::#request_mod_name::#ty_name
511 }
512 } else {
513 quote! {
514 crate::parameters::#ty_name
515 }
516 }
517 }
518 ParameterType::String => quote! { String },
519 ParameterType::Boolean => quote! { bool },
520 ParameterType::Schema { type_name } => {
521 let ty_name = format_ident!("{}", type_name);
522
523 quote! {
524 crate::models::#ty_name
525 }
526 }
527 ParameterType::Array { .. } => {
528 let name = param.r#type.codegen_type_name(¶m.name);
529 quote! {
530 crate::request::models::#request_mod_name::#name
531 }
532 }
533 };
534
535 let arg_name = format_ident!("{}", param.value.to_snake_case());
536
537 disc_ty.push(ty);
538 disc.push(arg_name);
539 }
540 }
541
542 if disc.is_empty() {
543 return None;
544 }
545
546 let response_ty = match &self.response {
547 PathResponse::Component { name } => {
548 let name = format_ident!("{name}");
549 quote! {
550 crate::models::#name
551 }
552 }
553 PathResponse::ArbitraryUnion(union) => {
554 let name = format_ident!("{}", union.name);
555 quote! {
556 crate::request::models::#request_mod_name::#name
557 }
558 }
559 };
560
561 let disc = if disc.len() > 1 {
562 quote! { (#(#disc),*) }
563 } else {
564 quote! { #(#disc),* }
565 };
566
567 let disc_ty = if disc_ty.len() > 1 {
568 quote! { (#(#disc_ty),*) }
569 } else {
570 quote! { #(#disc_ty),* }
571 };
572
573 let doc = match (&self.summary, &self.description) {
574 (Some(summary), Some(description)) => {
575 Some(format!("{summary}\n\n# Description\n{description}"))
576 }
577 (Some(summary), None) => Some(summary.clone()),
578 (None, Some(description)) => Some(format!("# Description\n{description}")),
579 (None, None) => None,
580 };
581
582 let doc = doc.map(|d| {
583 quote! {
584 #[doc = #d]
585 }
586 });
587
588 Some(quote! {
589 #doc
590 pub fn #fn_name<S, I, B>(
591 self,
592 ids: I,
593 builder: B
594 ) -> impl futures::Stream<Item = (#disc_ty, Result<#response_ty, E::Error>)>
595 where
596 I: IntoIterator<Item = #disc_ty>,
597 S: #builder_mod_path::IsComplete,
598 B: Fn(
599 #builder_path<#builder_mod_path::Empty>
600 ) -> #builder_path<S>,
601 {
602 let requests = ids.into_iter()
603 .map(move |#disc| builder(#request_path::builder(#disc)).build());
604
605 let executor = self.executor;
606 executor.fetch_many(requests)
607 }
608 })
609 }
610}
611
612pub struct PathNamespace<'r> {
613 path: &'r Path,
614 ident: Option<Ident>,
615 elements: Vec<TokenStream>,
616}
617
618impl PathNamespace<'_> {
619 pub fn get_ident(&mut self) -> Ident {
620 self.ident
621 .get_or_insert_with(|| {
622 let name = self.path.name.to_snake_case();
623 format_ident!("{name}")
624 })
625 .clone()
626 }
627
628 pub fn push_element(&mut self, el: TokenStream) {
629 self.elements.push(el);
630 }
631
632 pub fn codegen(mut self) -> Option<TokenStream> {
633 if self.elements.is_empty() {
634 None
635 } else {
636 let ident = self.get_ident();
637 let elements = self.elements;
638 Some(quote! {
639 pub mod #ident {
640 #(#elements)*
641 }
642 })
643 }
644 }
645}
646
647#[cfg(test)]
648mod test {
649 use super::*;
650
651 use crate::openapi::schema::test::get_schema;
652
653 #[test]
654 fn resolve_paths() {
655 let schema = get_schema();
656
657 let mut paths = 0;
658 let mut unresolved = vec![];
659
660 for (name, desc) in &schema.paths {
661 paths += 1;
662 if Path::from_schema(
663 name,
664 desc,
665 &schema.components.parameters,
666 WarningReporter::new(),
667 )
668 .is_none()
669 {
670 unresolved.push(name);
671 }
672 }
673
674 if !unresolved.is_empty() {
675 panic!(
676 "Failed to resolve {}/{} paths. Could not resolve [{}]",
677 unresolved.len(),
678 paths,
679 unresolved
680 .into_iter()
681 .map(|u| format!("`{u}`"))
682 .collect::<Vec<_>>()
683 .join(", ")
684 )
685 }
686 }
687
688 #[test]
689 fn codegen_paths() {
690 let schema = get_schema();
691 let resolved = ResolvedSchema::from_open_api(&schema);
692 let reporter = WarningReporter::new();
693
694 let mut paths = 0;
695 let mut unresolved = vec![];
696
697 for (name, desc) in &schema.paths {
698 paths += 1;
699 let Some(path) =
700 Path::from_schema(name, desc, &schema.components.parameters, reporter.clone())
701 else {
702 unresolved.push(name);
703 continue;
704 };
705
706 if path.codegen_scope_call().is_none()
707 || path.codegen_request(&resolved, reporter.clone()).is_none()
708 {
709 unresolved.push(name);
710 }
711 }
712
713 if !unresolved.is_empty() {
714 panic!(
715 "Failed to codegen {}/{} paths. Could not resolve [{}]",
716 unresolved.len(),
717 paths,
718 unresolved
719 .into_iter()
720 .map(|u| format!("`{u}`"))
721 .collect::<Vec<_>>()
722 .join(", ")
723 )
724 }
725 }
726}