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 = 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 quote! {
166 crate::request::models::#path::#ty_name
167 }
168 } else {
169 quote! {
170 crate::parameters::#ty_name
171 }
172 }
173 }
174 ParameterType::String => quote! { String },
175 ParameterType::Boolean => quote! { bool },
176 ParameterType::Schema { type_name } => {
177 let ty_name = format_ident!("{}", type_name);
178
179 quote! {
180 crate::models::#ty_name
181 }
182 }
183 ParameterType::Array { .. } => {
184 ns.push_element(param.codegen()?);
185 let ty_name = param.r#type.codegen_type_name(¶m.name);
186 let path = ns.get_ident();
187 quote! {
188 crate::request::models::#path::#ty_name
189 }
190 }
191 };
192
193 let name = format_ident!("{}", param.name.to_snake_case());
194 let query_val = ¶m.value;
195
196 if param.location == ParameterLocation::Path {
197 discriminant.push(ty.clone());
198 discriminant_val.push(quote! { self.#name });
199 let path_name = format_ident!("{}", param.value);
200 start_fields.push(quote! {
201 #[builder(start_fn)]
202 pub #name: #ty
203 });
204 fmt_val.push(quote! {
205 #path_name=self.#name
206 });
207 } else {
208 let ty = if param.required {
209 convert_field.push(quote! {
210 .chain(std::iter::once(&self.#name).map(|v| (#query_val, v.to_string())))
211 });
212 ty
213 } else {
214 convert_field.push(quote! {
215 .chain(self.#name.as_ref().into_iter().map(|v| (#query_val, v.to_string())))
216 });
217 quote! { Option<#ty>}
218 };
219
220 fields.push(quote! {
221 pub #name: #ty
222 });
223 }
224 }
225
226 let response_ty = match &self.response {
227 PathResponse::Component { name } => {
228 let name = format_ident!("{name}");
229 quote! {
230 crate::models::#name
231 }
232 }
233 PathResponse::ArbitraryUnion(union) => {
234 let path = ns.get_ident();
235 let ty_name = format_ident!("{}", union.name);
236
237 quote! {
238 crate::request::models::#path::#ty_name
239 }
240 }
241 };
242
243 let mut path_fmt_str = String::new();
244 for seg in &self.segments {
245 match seg {
246 PathSegment::Constant(val) => _ = write!(path_fmt_str, "/{}", val),
247 PathSegment::Parameter { name } => _ = write!(path_fmt_str, "/{{{}}}", name),
248 }
249 }
250
251 if let PathResponse::ArbitraryUnion(union) = &self.response {
252 ns.push_element(union.codegen()?);
253 }
254
255 let ns = ns.codegen();
256
257 start_fields.extend(fields);
258
259 Some(quote! {
260 #ns
261
262 #[derive(Debug, Clone, bon::Builder)]
263 #[builder(state_mod(vis = "pub(crate)"))]
264 pub struct #name {
265 #(#start_fields),*
266 }
267
268 impl crate::request::IntoRequest for #name {
269 #[allow(unused_parens)]
270 type Discriminant = (#(#discriminant),*);
271 type Response = #response_ty;
272 fn into_request(self) -> crate::request::ApiRequest<Self::Discriminant> {
273 #[allow(unused_parens)]
274 crate::request::ApiRequest {
275 path: format!(#path_fmt_str, #(#fmt_val),*),
276 parameters: std::iter::empty()
277 #(#convert_field)*
278 .collect(),
279 disriminant: (#(#discriminant_val),*),
280 }
281 }
282 }
283 })
284 }
285
286 pub fn codegen_scope_call(&self) -> Option<TokenStream> {
287 let mut extra_args = Vec::new();
288 let mut disc = Vec::new();
289
290 let snake_name = self.name.to_snake_case();
291
292 let request_name = format_ident!("{}Request", self.name);
293 let builder_name = format_ident!("{}RequestBuilder", self.name);
294 let builder_mod_name = format_ident!("{}_request_builder", snake_name);
295 let request_mod_name = format_ident!("{snake_name}");
296
297 let request_path = quote! { crate::request::models::#request_name };
298 let builder_path = quote! { crate::request::models::#builder_name };
299 let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
300
301 let tail = snake_name
302 .split_once('_')
303 .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
304
305 let fn_name = format_ident!("{tail}");
306
307 for param in &self.parameters {
308 let (param, is_inline) = match param {
309 PathParameter::Inline(param) => (param, true),
310 PathParameter::Component(param) => (param, false),
311 };
312
313 if param.location == ParameterLocation::Path {
314 let ty = match ¶m.r#type {
315 ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
316 let ty_name = format_ident!("{}", param.name);
317
318 if is_inline {
319 quote! {
320 crate::request::models::#request_mod_name::#ty_name
321 }
322 } else {
323 quote! {
324 crate::parameters::#ty_name
325 }
326 }
327 }
328 ParameterType::String => quote! { String },
329 ParameterType::Boolean => quote! { bool },
330 ParameterType::Schema { type_name } => {
331 let ty_name = format_ident!("{}", type_name);
332
333 quote! {
334 crate::models::#ty_name
335 }
336 }
337 ParameterType::Array { .. } => param.r#type.codegen_type_name(¶m.name),
338 };
339
340 let arg_name = format_ident!("{}", param.value.to_snake_case());
341
342 extra_args.push(quote! { #arg_name: #ty, });
343 disc.push(arg_name);
344 }
345 }
346
347 let response_ty = match &self.response {
348 PathResponse::Component { name } => {
349 let name = format_ident!("{name}");
350 quote! {
351 crate::models::#name
352 }
353 }
354 PathResponse::ArbitraryUnion(union) => {
355 let name = format_ident!("{}", union.name);
356 quote! {
357 crate::request::models::#request_mod_name::#name
358 }
359 }
360 };
361
362 Some(quote! {
363 pub async fn #fn_name<S>(
364 &self,
365 #(#extra_args)*
366 builder: impl FnOnce(
367 #builder_path<#builder_mod_path::Empty>
368 ) -> #builder_path<S>,
369 ) -> Result<#response_ty, E::Error>
370 where
371 S: #builder_mod_path::IsComplete,
372 {
373 let r = builder(#request_path::builder(#(#disc),*)).build();
374
375 self.0.fetch(r).await
376 }
377 })
378 }
379}
380
381pub struct PathNamespace<'r> {
382 path: &'r Path,
383 ident: Option<Ident>,
384 elements: Vec<TokenStream>,
385}
386
387impl PathNamespace<'_> {
388 pub fn get_ident(&mut self) -> Ident {
389 self.ident
390 .get_or_insert_with(|| {
391 let name = self.path.name.to_snake_case();
392 format_ident!("{name}")
393 })
394 .clone()
395 }
396
397 pub fn push_element(&mut self, el: TokenStream) {
398 self.elements.push(el);
399 }
400
401 pub fn codegen(mut self) -> Option<TokenStream> {
402 if self.elements.is_empty() {
403 None
404 } else {
405 let ident = self.get_ident();
406 let elements = self.elements;
407 Some(quote! {
408 pub mod #ident {
409 #(#elements)*
410 }
411 })
412 }
413 }
414}
415
416#[cfg(test)]
417mod test {
418 use super::*;
419
420 use crate::openapi::schema::OpenApiSchema;
421
422 #[test]
423 fn resolve_paths() {
424 let schema = OpenApiSchema::read().unwrap();
425
426 let mut paths = 0;
427 let mut unresolved = vec![];
428
429 for (name, desc) in &schema.paths {
430 paths += 1;
431 if Path::from_schema(name, desc, &schema.components.parameters).is_none() {
432 unresolved.push(name);
433 }
434 }
435
436 if !unresolved.is_empty() {
437 panic!(
438 "Failed to resolve {}/{} paths. Could not resolve [{}]",
439 unresolved.len(),
440 paths,
441 unresolved
442 .into_iter()
443 .map(|u| format!("`{u}`"))
444 .collect::<Vec<_>>()
445 .join(", ")
446 )
447 }
448 }
449
450 #[test]
451 fn codegen_paths() {
452 let schema = OpenApiSchema::read().unwrap();
453
454 let mut paths = 0;
455 let mut unresolved = vec![];
456
457 for (name, desc) in &schema.paths {
458 paths += 1;
459 let Some(path) = Path::from_schema(name, desc, &schema.components.parameters) else {
460 unresolved.push(name);
461 continue;
462 };
463
464 if path.codegen_scope_call().is_none() || path.codegen_request().is_none() {
465 unresolved.push(name);
466 }
467 }
468
469 if !unresolved.is_empty() {
470 panic!(
471 "Failed to codegen {}/{} paths. Could not resolve [{}]",
472 unresolved.len(),
473 paths,
474 unresolved
475 .into_iter()
476 .map(|u| format!("`{u}`"))
477 .collect::<Vec<_>>()
478 .join(", ")
479 )
480 }
481 }
482}