Skip to main content

torn_api_codegen/model/
path.rs

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    ResolvedSchema, WarningReporter,
18};
19
20#[derive(Debug, Clone)]
21pub enum PathSegment {
22    Constant(String),
23    Parameter { name: String },
24}
25
26pub struct PrettySegments<'a>(pub &'a [PathSegment]);
27
28impl std::fmt::Display for PrettySegments<'_> {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        for segment in self.0 {
31            match segment {
32                PathSegment::Constant(c) => write!(f, "/{c}")?,
33                PathSegment::Parameter { name } => write!(f, "/{{{name}}}")?,
34            }
35        }
36
37        Ok(())
38    }
39}
40
41#[derive(Debug, Clone)]
42pub enum PathParameter {
43    Inline(Parameter),
44    Component(Parameter),
45}
46
47#[derive(Debug, Clone)]
48pub enum PathResponse {
49    Component { name: String },
50    // TODO: needs to be implemented
51    ArbitraryUnion(Union),
52    Text,
53}
54
55#[derive(Debug, Clone)]
56pub struct Path {
57    pub segments: Vec<PathSegment>,
58    pub name: String,
59    pub summary: Option<String>,
60    pub description: Option<String>,
61    pub parameters: Vec<PathParameter>,
62    pub response: PathResponse,
63}
64
65impl Path {
66    pub fn from_schema(
67        path: &str,
68        schema: &OpenApiPath,
69        parameters: &IndexMap<&str, OpenApiParameter>,
70        warnings: WarningReporter,
71    ) -> Option<Self> {
72        let mut segments = Vec::new();
73        for segment in path.strip_prefix('/')?.split('/') {
74            if segment.starts_with('{') && segment.ends_with('}') {
75                segments.push(PathSegment::Parameter {
76                    name: segment[1..(segment.len() - 1)].to_owned(),
77                });
78            } else {
79                segments.push(PathSegment::Constant(segment.to_owned()));
80            }
81        }
82
83        let summary = schema.get.summary.as_deref().map(ToOwned::to_owned);
84        let description = schema.get.description.as_deref().map(ToOwned::to_owned);
85
86        let mut params = Vec::with_capacity(schema.get.parameters.len());
87        for parameter in &schema.get.parameters {
88            match &parameter {
89                OpenApiPathParameter::Link { ref_path } => {
90                    let name = ref_path
91                        .strip_prefix("#/components/parameters/")?
92                        .to_owned();
93                    let param = parameters.get(&name.as_str())?;
94                    params.push(PathParameter::Component(Parameter::from_schema(
95                        &name,
96                        param,
97                        warnings.child(&name),
98                    )?));
99                }
100                OpenApiPathParameter::Inline(schema) => {
101                    let name = schema.name.to_upper_camel_case();
102                    let parameter = Parameter::from_schema(&name, schema, warnings.clone())?;
103                    params.push(PathParameter::Inline(parameter));
104                }
105            };
106        }
107
108        let mut suffixes = vec![];
109        let mut name = String::new();
110
111        for seg in &segments {
112            match seg {
113                PathSegment::Constant(val) => {
114                    name.push_str(&val.to_upper_camel_case());
115                }
116                PathSegment::Parameter { name } => {
117                    suffixes.push(format!("For{}", name.to_upper_camel_case()));
118                }
119            }
120        }
121
122        for suffix in suffixes {
123            name.push_str(&suffix);
124        }
125
126        let response = match &schema.get.response_content {
127            OpenApiResponseBody::Schema(link) => PathResponse::Component {
128                name: link
129                    .ref_path
130                    .strip_prefix("#/components/schemas/")?
131                    .to_owned(),
132            },
133            OpenApiResponseBody::Union { any_of: _ } => {
134                PathResponse::ArbitraryUnion(Union::from_schema(
135                    "Response",
136                    &schema.get.response_content,
137                    warnings.child("response"),
138                )?)
139            }
140            OpenApiResponseBody::Csv => PathResponse::Text,
141        };
142
143        Some(Self {
144            segments,
145            name,
146            summary,
147            description,
148            parameters: params,
149            response,
150        })
151    }
152
153    pub fn codegen_request(
154        &self,
155        resolved: &ResolvedSchema,
156        warnings: WarningReporter,
157    ) -> Option<TokenStream> {
158        let name = if self.segments.len() == 1 {
159            let Some(PathSegment::Constant(first)) = self.segments.first() else {
160                return None;
161            };
162            format_ident!("{}Request", first.to_upper_camel_case())
163        } else {
164            format_ident!("{}Request", self.name)
165        };
166
167        let mut ns = PathNamespace {
168            path: self,
169            ident: None,
170            elements: Vec::new(),
171        };
172
173        let mut fields = Vec::with_capacity(self.parameters.len());
174        let mut convert_field = Vec::with_capacity(self.parameters.len());
175        let mut start_fields = Vec::new();
176        let mut discriminant = Vec::new();
177        let mut discriminant_val = Vec::new();
178        let mut fmt_val = Vec::new();
179
180        for param in &self.parameters {
181            let (is_inline, param) = match &param {
182                PathParameter::Inline(param) => (true, param),
183                PathParameter::Component(param) => (false, param),
184            };
185
186            let (ty, builder_param) = match &param.r#type {
187                ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
188                    let ty_name = format_ident!("{}", param.name);
189
190                    if is_inline {
191                        ns.push_element(param.codegen(resolved)?);
192                        let path = ns.get_ident();
193
194                        (
195                            quote! {
196                                crate::request::models::#path::#ty_name
197                            },
198                            Some(quote! { #[cfg_attr(feature = "builder", builder(into))] }),
199                        )
200                    } else {
201                        (
202                            quote! {
203                                crate::parameters::#ty_name
204                            },
205                            Some(quote! { #[cfg_attr(feature = "builder", builder(into))]}),
206                        )
207                    }
208                }
209                ParameterType::String => (quote! { String }, None),
210                ParameterType::Boolean => (quote! { bool }, None),
211                ParameterType::Schema { type_name } => {
212                    let ty_name = format_ident!("{}", type_name);
213
214                    (
215                        quote! {
216                            crate::models::#ty_name
217                        },
218                        None,
219                    )
220                }
221                ParameterType::Array { .. } => {
222                    ns.push_element(param.codegen(resolved)?);
223                    let ty_name = param.r#type.codegen_type_name(&param.name);
224                    let path = ns.get_ident();
225                    (
226                        quote! {
227                            crate::request::models::#path::#ty_name
228                        },
229                        Some(quote! { #[cfg_attr(feature = "builder", builder(into))] }),
230                    )
231                }
232            };
233
234            let name = format_ident!("{}", param.name.to_snake_case());
235            let query_val = &param.value;
236
237            if param.location == ParameterLocation::Path {
238                if self.segments.iter().any(|s| {
239                    if let PathSegment::Parameter { name } = s {
240                        name == &param.value
241                    } else {
242                        false
243                    }
244                }) {
245                    discriminant.push(ty.clone());
246                    discriminant_val.push(quote! { self.#name });
247                    let path_name = format_ident!("{}", param.value);
248                    start_fields.push(quote! {
249                        #[cfg_attr(feature = "builder", builder(start_fn))]
250                        #builder_param
251                        pub #name: #ty
252                    });
253                    fmt_val.push(quote! {
254                        #path_name=self.#name
255                    });
256                } else {
257                    warnings.push(format!(
258                        "Provided path parameter is not present in the url: {}",
259                        param.value
260                    ));
261                }
262            } else {
263                let ty = if param.required {
264                    convert_field.push(quote! {
265                        parameters.push((#query_val, self.#name.to_string()));
266                    });
267                    ty
268                } else {
269                    convert_field.push(quote! {
270                        if let Some(value) = &self.#name {
271                            parameters.push((#query_val, value.to_string()));
272                        }
273                    });
274                    quote! { Option<#ty>}
275                };
276
277                fields.push(quote! {
278                    #builder_param
279                    pub #name: #ty
280                });
281            }
282        }
283
284        let response_ty = match &self.response {
285            PathResponse::Component { name } => {
286                let name = format_ident!("{name}");
287                quote! {
288                    crate::models::#name
289                }
290            }
291            PathResponse::ArbitraryUnion(union) => {
292                let path = ns.get_ident();
293                let ty_name = format_ident!("{}", union.name);
294
295                quote! {
296                    crate::request::models::#path::#ty_name
297                }
298            }
299            PathResponse::Text => quote! { String },
300        };
301
302        let mut path_fmt_str = String::new();
303        for seg in &self.segments {
304            match seg {
305                PathSegment::Constant(val) => _ = write!(path_fmt_str, "/{val}"),
306                PathSegment::Parameter { name } => _ = write!(path_fmt_str, "/{{{name}}}"),
307            }
308        }
309
310        if let PathResponse::ArbitraryUnion(union) = &self.response {
311            ns.push_element(union.codegen()?);
312        }
313
314        let ns = ns.codegen();
315
316        start_fields.extend(fields);
317
318        Some(quote! {
319            #ns
320
321            #[cfg_attr(feature = "builder", derive(bon::Builder))]
322            #[derive(Debug, Clone)]
323            #[cfg_attr(feature = "builder", builder(state_mod(vis = "pub(crate)"), on(String, into)))]
324            pub struct #name {
325                #(#start_fields),*
326            }
327
328            impl crate::request::IntoRequest for #name {
329                #[allow(unused_parens)]
330                type Discriminant = (#(#discriminant),*);
331                type Response = #response_ty;
332                fn into_request(self) -> (Self::Discriminant, crate::request::ApiRequest) {
333                    let path = format!(#path_fmt_str, #(#fmt_val),*);
334                    let mut parameters = Vec::new();
335                    #(#convert_field)*
336
337                    #[allow(unused_parens)]
338                    (
339                        (#(#discriminant_val),*),
340                        crate::request::ApiRequest {
341                            path,
342                            parameters,
343                        }
344                    )
345                }
346            }
347        })
348    }
349
350    pub fn codegen_scope_call(&self) -> Option<TokenStream> {
351        let mut extra_args = Vec::new();
352        let mut disc = Vec::new();
353
354        let snake_name = self.name.to_snake_case();
355
356        let request_name = format_ident!("{}Request", self.name);
357        let builder_name = format_ident!("{}RequestBuilder", self.name);
358        let builder_mod_name = format_ident!("{}_request_builder", snake_name);
359        let request_mod_name = format_ident!("{snake_name}");
360
361        let request_path = quote! { crate::request::models::#request_name };
362        let builder_path = quote! { crate::request::models::#builder_name };
363        let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
364
365        let tail = snake_name
366            .split_once('_')
367            .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
368
369        let fn_name = format_ident!("{tail}");
370
371        for param in &self.parameters {
372            let (param, is_inline) = match param {
373                PathParameter::Inline(param) => (param, true),
374                PathParameter::Component(param) => (param, false),
375            };
376
377            if param.location == ParameterLocation::Path
378                && self.segments.iter().any(|s| {
379                    if let PathSegment::Parameter { name } = s {
380                        name == &param.value
381                    } else {
382                        false
383                    }
384                })
385            {
386                let ty = match &param.r#type {
387                    ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
388                        let ty_name = format_ident!("{}", param.name);
389
390                        if is_inline {
391                            quote! {
392                                crate::request::models::#request_mod_name::#ty_name
393                            }
394                        } else {
395                            quote! {
396                                crate::parameters::#ty_name
397                            }
398                        }
399                    }
400                    ParameterType::String => quote! { String },
401                    ParameterType::Boolean => quote! { bool },
402                    ParameterType::Schema { type_name } => {
403                        let ty_name = format_ident!("{}", type_name);
404
405                        quote! {
406                            crate::models::#ty_name
407                        }
408                    }
409                    ParameterType::Array { .. } => {
410                        let ty_name = param.r#type.codegen_type_name(&param.name);
411
412                        quote! {
413                            crate::request::models::#request_mod_name::#ty_name
414                        }
415                    }
416                };
417
418                let arg_name = format_ident!("{}", param.value.to_snake_case());
419
420                extra_args.push(quote! { #arg_name: #ty, });
421                disc.push(arg_name);
422            }
423        }
424
425        let response_ty = match &self.response {
426            PathResponse::Component { name } => {
427                let name = format_ident!("{name}");
428                quote! {
429                    crate::models::#name
430                }
431            }
432            PathResponse::ArbitraryUnion(union) => {
433                let name = format_ident!("{}", union.name);
434                quote! {
435                    crate::request::models::#request_mod_name::#name
436                }
437            }
438            PathResponse::Text => quote! { String },
439        };
440
441        let doc = match (&self.summary, &self.description) {
442            (Some(summary), Some(description)) => {
443                Some(format!("{summary}\n\n# Description\n{description}"))
444            }
445            (Some(summary), None) => Some(summary.clone()),
446            (None, Some(description)) => Some(format!("# Description\n{description}")),
447            (None, None) => None,
448        };
449
450        let doc = doc.map(|d| {
451            quote! {
452                #[doc = #d]
453            }
454        });
455
456        Some(quote! {
457            #doc
458            pub async fn #fn_name<S>(
459                self,
460                #(#extra_args)*
461                builder: impl FnOnce(
462                    #builder_path<#builder_mod_path::Empty>
463                ) -> #builder_path<S>,
464            ) -> Result<#response_ty, E::Error>
465            where
466                S: #builder_mod_path::IsComplete,
467            {
468                let r = builder(#request_path::builder(#(#disc),*)).build();
469
470                self.0.fetch(r).await
471            }
472        })
473    }
474
475    pub fn codegen_bulk_scope_call(&self) -> Option<TokenStream> {
476        let mut disc = Vec::new();
477        let mut disc_ty = Vec::new();
478
479        let snake_name = self.name.to_snake_case();
480
481        let request_name = format_ident!("{}Request", self.name);
482        let builder_name = format_ident!("{}RequestBuilder", self.name);
483        let builder_mod_name = format_ident!("{}_request_builder", snake_name);
484        let request_mod_name = format_ident!("{snake_name}");
485
486        let request_path = quote! { crate::request::models::#request_name };
487        let builder_path = quote! { crate::request::models::#builder_name };
488        let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
489
490        let tail = snake_name
491            .split_once('_')
492            .map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
493
494        let fn_name = format_ident!("{tail}");
495
496        for param in &self.parameters {
497            let (param, is_inline) = match param {
498                PathParameter::Inline(param) => (param, true),
499                PathParameter::Component(param) => (param, false),
500            };
501            if param.location == ParameterLocation::Path
502                && self.segments.iter().any(|s| {
503                    if let PathSegment::Parameter { name } = s {
504                        name == &param.value
505                    } else {
506                        false
507                    }
508                })
509            {
510                let ty = match &param.r#type {
511                    ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
512                        let ty_name = format_ident!("{}", param.name);
513
514                        if is_inline {
515                            quote! {
516                                crate::request::models::#request_mod_name::#ty_name
517                            }
518                        } else {
519                            quote! {
520                                crate::parameters::#ty_name
521                            }
522                        }
523                    }
524                    ParameterType::String => quote! { String },
525                    ParameterType::Boolean => quote! { bool },
526                    ParameterType::Schema { type_name } => {
527                        let ty_name = format_ident!("{}", type_name);
528
529                        quote! {
530                            crate::models::#ty_name
531                        }
532                    }
533                    ParameterType::Array { .. } => {
534                        let name = param.r#type.codegen_type_name(&param.name);
535                        quote! {
536                            crate::request::models::#request_mod_name::#name
537                        }
538                    }
539                };
540
541                let arg_name = format_ident!("{}", param.value.to_snake_case());
542
543                disc_ty.push(ty);
544                disc.push(arg_name);
545            }
546        }
547
548        if disc.is_empty() {
549            return None;
550        }
551
552        let response_ty = match &self.response {
553            PathResponse::Component { name } => {
554                let name = format_ident!("{name}");
555                quote! {
556                    crate::models::#name
557                }
558            }
559            PathResponse::ArbitraryUnion(union) => {
560                let name = format_ident!("{}", union.name);
561                quote! {
562                    crate::request::models::#request_mod_name::#name
563                }
564            }
565            PathResponse::Text => quote! { String },
566        };
567
568        let disc = if disc.len() > 1 {
569            quote! { (#(#disc),*) }
570        } else {
571            quote! { #(#disc),* }
572        };
573
574        let disc_ty = if disc_ty.len() > 1 {
575            quote! { (#(#disc_ty),*) }
576        } else {
577            quote! { #(#disc_ty),* }
578        };
579
580        let doc = match (&self.summary, &self.description) {
581            (Some(summary), Some(description)) => {
582                Some(format!("{summary}\n\n# Description\n{description}"))
583            }
584            (Some(summary), None) => Some(summary.clone()),
585            (None, Some(description)) => Some(format!("# Description\n{description}")),
586            (None, None) => None,
587        };
588
589        let doc = doc.map(|d| {
590            quote! {
591                #[doc = #d]
592            }
593        });
594
595        Some(quote! {
596            #doc
597            pub fn #fn_name<S, I, B>(
598                self,
599                ids: I,
600                builder: B
601            ) -> impl futures::Stream<Item = (#disc_ty, Result<#response_ty, E::Error>)>
602            where
603                I: IntoIterator<Item = #disc_ty>,
604                S: #builder_mod_path::IsComplete,
605                B: Fn(
606                    #builder_path<#builder_mod_path::Empty>
607                ) -> #builder_path<S>,
608            {
609                let requests = ids.into_iter()
610                    .map(move |#disc| builder(#request_path::builder(#disc)).build());
611
612                let executor = self.executor;
613                executor.fetch_many(requests)
614            }
615        })
616    }
617}
618
619pub struct PathNamespace<'r> {
620    path: &'r Path,
621    ident: Option<Ident>,
622    elements: Vec<TokenStream>,
623}
624
625impl PathNamespace<'_> {
626    pub fn get_ident(&mut self) -> Ident {
627        self.ident
628            .get_or_insert_with(|| {
629                let name = self.path.name.to_snake_case();
630                format_ident!("{name}")
631            })
632            .clone()
633    }
634
635    pub fn push_element(&mut self, el: TokenStream) {
636        self.elements.push(el);
637    }
638
639    pub fn codegen(mut self) -> Option<TokenStream> {
640        if self.elements.is_empty() {
641            None
642        } else {
643            let ident = self.get_ident();
644            let elements = self.elements;
645            Some(quote! {
646                pub mod #ident {
647                    #(#elements)*
648                }
649            })
650        }
651    }
652}
653
654#[cfg(test)]
655mod test {
656    use super::*;
657
658    use crate::openapi::schema::test::get_schema;
659
660    #[test]
661    fn resolve_paths() {
662        let schema = get_schema();
663
664        let mut paths = 0;
665        let mut unresolved = vec![];
666
667        for (name, desc) in &schema.paths {
668            paths += 1;
669            if Path::from_schema(
670                name,
671                desc,
672                &schema.components.parameters,
673                WarningReporter::new(),
674            )
675            .is_none()
676            {
677                unresolved.push(name);
678            }
679        }
680
681        if !unresolved.is_empty() {
682            panic!(
683                "Failed to resolve {}/{} paths. Could not resolve [{}]",
684                unresolved.len(),
685                paths,
686                unresolved
687                    .into_iter()
688                    .map(|u| format!("`{u}`"))
689                    .collect::<Vec<_>>()
690                    .join(", ")
691            )
692        }
693    }
694
695    #[test]
696    fn codegen_paths() {
697        let schema = get_schema();
698        let resolved = ResolvedSchema::from_open_api(&schema);
699        let reporter = WarningReporter::new();
700
701        let mut paths = 0;
702        let mut unresolved = vec![];
703
704        for (name, desc) in &schema.paths {
705            paths += 1;
706            let Some(path) =
707                Path::from_schema(name, desc, &schema.components.parameters, reporter.clone())
708            else {
709                unresolved.push(name);
710                continue;
711            };
712
713            if path.codegen_scope_call().is_none()
714                || path.codegen_request(&resolved, reporter.clone()).is_none()
715            {
716                unresolved.push(name);
717            }
718        }
719
720        if !unresolved.is_empty() {
721            panic!(
722                "Failed to codegen {}/{} paths. Could not resolve [{}]",
723                unresolved.len(),
724                paths,
725                unresolved
726                    .into_iter()
727                    .map(|u| format!("`{u}`"))
728                    .collect::<Vec<_>>()
729                    .join(", ")
730            )
731        }
732    }
733}