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