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 { .. } => param.r#type.codegen_type_name(¶m.name),
356 };
357
358 let arg_name = format_ident!("{}", param.value.to_snake_case());
359
360 extra_args.push(quote! { #arg_name: #ty, });
361 disc.push(arg_name);
362 }
363 }
364
365 let response_ty = match &self.response {
366 PathResponse::Component { name } => {
367 let name = format_ident!("{name}");
368 quote! {
369 crate::models::#name
370 }
371 }
372 PathResponse::ArbitraryUnion(union) => {
373 let name = format_ident!("{}", union.name);
374 quote! {
375 crate::request::models::#request_mod_name::#name
376 }
377 }
378 };
379
380 let doc = match (&self.summary, &self.description) {
381 (Some(summary), Some(description)) => {
382 Some(format!("{summary}\n\n# Description\n{description}"))
383 }
384 (Some(summary), None) => Some(summary.clone()),
385 (None, Some(description)) => Some(format!("# Description\n{description}")),
386 (None, None) => None,
387 };
388
389 let doc = doc.map(|d| {
390 quote! {
391 #[doc = #d]
392 }
393 });
394
395 Some(quote! {
396 #doc
397 pub async fn #fn_name<S>(
398 self,
399 #(#extra_args)*
400 builder: impl FnOnce(
401 #builder_path<#builder_mod_path::Empty>
402 ) -> #builder_path<S>,
403 ) -> Result<#response_ty, E::Error>
404 where
405 S: #builder_mod_path::IsComplete,
406 {
407 let r = builder(#request_path::builder(#(#disc),*)).build();
408
409 self.0.fetch(r).await
410 }
411 })
412 }
413
414 pub fn codegen_bulk_scope_call(&self) -> Option<TokenStream> {
415 let mut disc = Vec::new();
416 let mut disc_ty = Vec::new();
417
418 let snake_name = self.name.to_snake_case();
419
420 let request_name = format_ident!("{}Request", self.name);
421 let builder_name = format_ident!("{}RequestBuilder", self.name);
422 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
423 let request_mod_name = format_ident!("{snake_name}");
424
425 let request_path = quote! { crate::request::models::#request_name };
426 let builder_path = quote! { crate::request::models::#builder_name };
427 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
428
429 let tail = snake_name
430 .split_once('_')
431 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
432
433 let fn_name = format_ident!("{tail}");
434
435 for param in &self.parameters {
436 let (param, is_inline) = match param {
437 PathParameter::Inline(param) => (param, true),
438 PathParameter::Component(param) => (param, false),
439 };
440
441 if param.location == ParameterLocation::Path {
442 let ty = match ¶m.r#type {
443 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
444 let ty_name = format_ident!("{}", param.name);
445
446 if is_inline {
447 quote! {
448 crate::request::models::#request_mod_name::#ty_name
449 }
450 } else {
451 quote! {
452 crate::parameters::#ty_name
453 }
454 }
455 }
456 ParameterType::String => quote! { String },
457 ParameterType::Boolean => quote! { bool },
458 ParameterType::Schema { type_name } => {
459 let ty_name = format_ident!("{}", type_name);
460
461 quote! {
462 crate::models::#ty_name
463 }
464 }
465 ParameterType::Array { .. } => param.r#type.codegen_type_name(¶m.name),
466 };
467
468 let arg_name = format_ident!("{}", param.value.to_snake_case());
469
470 disc_ty.push(ty);
471 disc.push(arg_name);
472 }
473 }
474
475 if disc.is_empty() {
476 return None;
477 }
478
479 let response_ty = match &self.response {
480 PathResponse::Component { name } => {
481 let name = format_ident!("{name}");
482 quote! {
483 crate::models::#name
484 }
485 }
486 PathResponse::ArbitraryUnion(union) => {
487 let name = format_ident!("{}", union.name);
488 quote! {
489 crate::request::models::#request_mod_name::#name
490 }
491 }
492 };
493
494 let disc = if disc.len() > 1 {
495 quote! { (#(#disc),*) }
496 } else {
497 quote! { #(#disc),* }
498 };
499
500 let disc_ty = if disc_ty.len() > 1 {
501 quote! { (#(#disc_ty),*) }
502 } else {
503 quote! { #(#disc_ty),* }
504 };
505
506 let doc = match (&self.summary, &self.description) {
507 (Some(summary), Some(description)) => {
508 Some(format!("{summary}\n\n# Description\n{description}"))
509 }
510 (Some(summary), None) => Some(summary.clone()),
511 (None, Some(description)) => Some(format!("# Description\n{description}")),
512 (None, None) => None,
513 };
514
515 let doc = doc.map(|d| {
516 quote! {
517 #[doc = #d]
518 }
519 });
520
521 Some(quote! {
522 #doc
523 pub fn #fn_name<S, I, B>(
524 self,
525 ids: I,
526 builder: B
527 ) -> impl futures::Stream<Item = (#disc_ty, Result<#response_ty, E::Error>)>
528 where
529 I: IntoIterator<Item = #disc_ty>,
530 S: #builder_mod_path::IsComplete,
531 B: Fn(
532 #builder_path<#builder_mod_path::Empty>
533 ) -> #builder_path<S>,
534 {
535 let requests = ids.into_iter()
536 .map(move |#disc| builder(#request_path::builder(#disc)).build());
537
538 let executor = self.executor;
539 executor.fetch_many(requests)
540 }
541 })
542 }
543}
544
545pub struct PathNamespace<'r> {
546 path: &'r Path,
547 ident: Option<Ident>,
548 elements: Vec<TokenStream>,
549}
550
551impl PathNamespace<'_> {
552 pub fn get_ident(&mut self) -> Ident {
553 self.ident
554 .get_or_insert_with(|| {
555 let name = self.path.name.to_snake_case();
556 format_ident!("{name}")
557 })
558 .clone()
559 }
560
561 pub fn push_element(&mut self, el: TokenStream) {
562 self.elements.push(el);
563 }
564
565 pub fn codegen(mut self) -> Option<TokenStream> {
566 if self.elements.is_empty() {
567 None
568 } else {
569 let ident = self.get_ident();
570 let elements = self.elements;
571 Some(quote! {
572 pub mod #ident {
573 #(#elements)*
574 }
575 })
576 }
577 }
578}
579
580#[cfg(test)]
581mod test {
582 use super::*;
583
584 use crate::openapi::schema::OpenApiSchema;
585
586 #[test]
587 fn resolve_paths() {
588 let schema = OpenApiSchema::read().unwrap();
589
590 let mut paths = 0;
591 let mut unresolved = vec![];
592
593 for (name, desc) in &schema.paths {
594 paths += 1;
595 if Path::from_schema(name, desc, &schema.components.parameters).is_none() {
596 unresolved.push(name);
597 }
598 }
599
600 if !unresolved.is_empty() {
601 panic!(
602 "Failed to resolve {}/{} paths. Could not resolve [{}]",
603 unresolved.len(),
604 paths,
605 unresolved
606 .into_iter()
607 .map(|u| format!("`{u}`"))
608 .collect::<Vec<_>>()
609 .join(", ")
610 )
611 }
612 }
613
614 #[test]
615 fn codegen_paths() {
616 let schema = OpenApiSchema::read().unwrap();
617
618 let mut paths = 0;
619 let mut unresolved = vec![];
620
621 for (name, desc) in &schema.paths {
622 paths += 1;
623 let Some(path) = Path::from_schema(name, desc, &schema.components.parameters) else {
624 unresolved.push(name);
625 continue;
626 };
627
628 if path.codegen_scope_call().is_none() || path.codegen_request().is_none() {
629 unresolved.push(name);
630 }
631 }
632
633 if !unresolved.is_empty() {
634 panic!(
635 "Failed to codegen {}/{} paths. Could not resolve [{}]",
636 unresolved.len(),
637 paths,
638 unresolved
639 .into_iter()
640 .map(|u| format!("`{u}`"))
641 .collect::<Vec<_>>()
642 .join(", ")
643 )
644 }
645 }
646}