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) -> crate::request::ApiRequest<Self::Discriminant> {
288 #[allow(unused_parens)]
289 crate::request::ApiRequest {
290 path: format!(#path_fmt_str, #(#fmt_val),*),
291 parameters: std::iter::empty()
292 #(#convert_field)*
293 .collect(),
294 disriminant: (#(#discriminant_val),*),
295 }
296 }
297 }
298 })
299 }
300
301 pub fn codegen_scope_call(&self) -> Option<TokenStream> {
302 let mut extra_args = Vec::new();
303 let mut disc = Vec::new();
304
305 let snake_name = self.name.to_snake_case();
306
307 let request_name = format_ident!("{}Request", self.name);
308 let builder_name = format_ident!("{}RequestBuilder", self.name);
309 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
310 let request_mod_name = format_ident!("{snake_name}");
311
312 let request_path = quote! { crate::request::models::#request_name };
313 let builder_path = quote! { crate::request::models::#builder_name };
314 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
315
316 let tail = snake_name
317 .split_once('_')
318 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
319
320 let fn_name = format_ident!("{tail}");
321
322 for param in &self.parameters {
323 let (param, is_inline) = match param {
324 PathParameter::Inline(param) => (param, true),
325 PathParameter::Component(param) => (param, false),
326 };
327
328 if param.location == ParameterLocation::Path {
329 let ty = match ¶m.r#type {
330 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
331 let ty_name = format_ident!("{}", param.name);
332
333 if is_inline {
334 quote! {
335 crate::request::models::#request_mod_name::#ty_name
336 }
337 } else {
338 quote! {
339 crate::parameters::#ty_name
340 }
341 }
342 }
343 ParameterType::String => quote! { String },
344 ParameterType::Boolean => quote! { bool },
345 ParameterType::Schema { type_name } => {
346 let ty_name = format_ident!("{}", type_name);
347
348 quote! {
349 crate::models::#ty_name
350 }
351 }
352 ParameterType::Array { .. } => param.r#type.codegen_type_name(¶m.name),
353 };
354
355 let arg_name = format_ident!("{}", param.value.to_snake_case());
356
357 extra_args.push(quote! { #arg_name: #ty, });
358 disc.push(arg_name);
359 }
360 }
361
362 let response_ty = match &self.response {
363 PathResponse::Component { name } => {
364 let name = format_ident!("{name}");
365 quote! {
366 crate::models::#name
367 }
368 }
369 PathResponse::ArbitraryUnion(union) => {
370 let name = format_ident!("{}", union.name);
371 quote! {
372 crate::request::models::#request_mod_name::#name
373 }
374 }
375 };
376
377 Some(quote! {
378 pub async fn #fn_name<S>(
379 &self,
380 #(#extra_args)*
381 builder: impl FnOnce(
382 #builder_path<#builder_mod_path::Empty>
383 ) -> #builder_path<S>,
384 ) -> Result<#response_ty, E::Error>
385 where
386 S: #builder_mod_path::IsComplete,
387 {
388 let r = builder(#request_path::builder(#(#disc),*)).build();
389
390 self.0.fetch(r).await
391 }
392 })
393 }
394}
395
396pub struct PathNamespace<'r> {
397 path: &'r Path,
398 ident: Option<Ident>,
399 elements: Vec<TokenStream>,
400}
401
402impl PathNamespace<'_> {
403 pub fn get_ident(&mut self) -> Ident {
404 self.ident
405 .get_or_insert_with(|| {
406 let name = self.path.name.to_snake_case();
407 format_ident!("{name}")
408 })
409 .clone()
410 }
411
412 pub fn push_element(&mut self, el: TokenStream) {
413 self.elements.push(el);
414 }
415
416 pub fn codegen(mut self) -> Option<TokenStream> {
417 if self.elements.is_empty() {
418 None
419 } else {
420 let ident = self.get_ident();
421 let elements = self.elements;
422 Some(quote! {
423 pub mod #ident {
424 #(#elements)*
425 }
426 })
427 }
428 }
429}
430
431#[cfg(test)]
432mod test {
433 use super::*;
434
435 use crate::openapi::schema::OpenApiSchema;
436
437 #[test]
438 fn resolve_paths() {
439 let schema = OpenApiSchema::read().unwrap();
440
441 let mut paths = 0;
442 let mut unresolved = vec![];
443
444 for (name, desc) in &schema.paths {
445 paths += 1;
446 if Path::from_schema(name, desc, &schema.components.parameters).is_none() {
447 unresolved.push(name);
448 }
449 }
450
451 if !unresolved.is_empty() {
452 panic!(
453 "Failed to resolve {}/{} paths. Could not resolve [{}]",
454 unresolved.len(),
455 paths,
456 unresolved
457 .into_iter()
458 .map(|u| format!("`{u}`"))
459 .collect::<Vec<_>>()
460 .join(", ")
461 )
462 }
463 }
464
465 #[test]
466 fn codegen_paths() {
467 let schema = OpenApiSchema::read().unwrap();
468
469 let mut paths = 0;
470 let mut unresolved = vec![];
471
472 for (name, desc) in &schema.paths {
473 paths += 1;
474 let Some(path) = Path::from_schema(name, desc, &schema.components.parameters) else {
475 unresolved.push(name);
476 continue;
477 };
478
479 if path.codegen_scope_call().is_none() || path.codegen_request().is_none() {
480 unresolved.push(name);
481 }
482 }
483
484 if !unresolved.is_empty() {
485 panic!(
486 "Failed to codegen {}/{} paths. Could not resolve [{}]",
487 unresolved.len(),
488 paths,
489 unresolved
490 .into_iter()
491 .map(|u| format!("`{u}`"))
492 .collect::<Vec<_>>()
493 .join(", ")
494 )
495 }
496 }
497}