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