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