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