Skip to main content

oxapi_impl/
types.rs

1//! Type generation using typify.
2
3use std::collections::HashMap;
4
5use heck::{AsSnakeCase, ToUpperCamelCase};
6use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
7use proc_macro2::TokenStream;
8use quote::{format_ident, quote, quote_spanned};
9use typify::{TypeId, TypeSpace, TypeSpaceSettings};
10
11/// Result of generating a type for a schema, including any inline struct definitions.
12pub struct GeneratedType {
13    /// The type expression (e.g., `MyStruct`, `Vec<String>`, `serde_json::Value`)
14    pub type_ref: TokenStream,
15    /// Any struct definitions that need to be emitted (for inline object schemas)
16    pub definitions: Vec<TokenStream>,
17}
18
19impl GeneratedType {
20    /// Create a GeneratedType with just a type reference (no inline definitions).
21    pub fn simple(type_ref: TokenStream) -> Self {
22        Self {
23            type_ref,
24            definitions: vec![],
25        }
26    }
27}
28
29use crate::openapi::{Operation, OperationParam, ParamLocation, ParsedSpec, RequestBody};
30use crate::{Error, GeneratedTypeKind, Result, TypeOverride, TypeOverrides};
31
32/// Type generator that wraps typify's TypeSpace.
33///
34/// Uses a two-pass approach:
35/// 1. Pass 1: Generate all types (component schemas + inline param schemas)
36/// 2. Pass 2: Generate code that references types using the registry built in Pass 1
37pub struct TypeGenerator {
38    type_space: TypeSpace,
39    /// Map from schema name (as in OpenAPI spec) to final type name (after typify processing)
40    schema_to_type: HashMap<String, String>,
41    /// Map from inline schema name hints to typify TypeIds for lookup during generation
42    inline_types: HashMap<String, TypeId>,
43}
44
45impl TypeGenerator {
46    /// Create a new type generator from a parsed spec with default settings.
47    pub fn new(spec: &ParsedSpec) -> Result<Self> {
48        Self::with_settings(spec, TypeSpaceSettings::default(), HashMap::new())
49    }
50
51    /// Create a new type generator from a parsed spec with custom settings and renames.
52    ///
53    /// The renames map should contain original schema name -> new name mappings.
54    /// These must match the patches configured in TypeSpaceSettings.
55    pub fn with_settings(
56        spec: &ParsedSpec,
57        settings: TypeSpaceSettings,
58        renames: HashMap<String, String>,
59    ) -> Result<Self> {
60        let mut type_space = TypeSpace::new(&settings);
61
62        // Collect schema names before adding to type space
63        let schema_names: Vec<String> = spec
64            .components
65            .as_ref()
66            .map(|c| c.schemas.keys().cloned().collect())
67            .unwrap_or_default();
68
69        // Add all component schemas to the type space
70        if let Some(components) = &spec.components {
71            let schemas = components
72                .schemas
73                .iter()
74                .map(|(name, schema)| {
75                    let schema = to_schemars(schema)?;
76                    Ok((name.clone(), schema))
77                })
78                .collect::<Result<Vec<_>>>()?;
79
80            type_space
81                .add_ref_types(schemas.into_iter())
82                .map_err(|e| Error::TypeGenError(e.to_string()))?;
83        }
84
85        // First validate that all renames reference schemas that exist in the spec
86        for original_name in renames.keys() {
87            if !schema_names.contains(original_name) {
88                return Err(Error::UnknownSchema {
89                    name: original_name.clone(),
90                    available: schema_names.join(", "),
91                });
92            }
93        }
94
95        // Build schema_to_type map by iterating TypeSpace to see what names typify generated
96        let generated_names: std::collections::HashSet<String> =
97            type_space.iter_types().map(|t| t.name()).collect();
98
99        let mut schema_to_type = HashMap::new();
100        for schema_name in &schema_names {
101            // Check if there's a rename for this schema
102            let expected_name = if let Some(renamed) = renames.get(schema_name) {
103                renamed.to_upper_camel_case()
104            } else {
105                schema_name.to_upper_camel_case()
106            };
107
108            // Verify the type exists in TypeSpace
109            if generated_names.contains(&expected_name) {
110                schema_to_type.insert(schema_name.clone(), expected_name);
111            } else {
112                // Type doesn't exist - this shouldn't happen if typify is working correctly
113                return Err(Error::TypeGenError(format!(
114                    "typify did not generate expected type '{}' for schema '{}'",
115                    expected_name, schema_name
116                )));
117            }
118        }
119
120        // Pre-populate inline schemas for all operation parameters
121        let mut inline_types = HashMap::new();
122        for op in spec.operations() {
123            let op_name = op.raw_name();
124
125            // Add path and query parameter schemas
126            for param in &op.parameters {
127                if let Some(ReferenceOr::Item(schema)) = &param.schema {
128                    let name_hint = param.name.to_upper_camel_case();
129                    register_inline_schema(&mut type_space, &mut inline_types, schema, name_hint);
130                }
131            }
132
133            // Add request body schemas
134            if let Some(body) = &op.request_body
135                && let Some(ReferenceOr::Item(schema)) = &body.schema
136            {
137                let name_hint = format!("{}Body", op_name.to_upper_camel_case());
138                register_inline_schema(&mut type_space, &mut inline_types, schema, name_hint);
139            }
140
141            // Add response body schemas
142            for resp in &op.responses {
143                if let Some(ReferenceOr::Item(schema)) = &resp.schema {
144                    let status_suffix = match resp.status_code {
145                        crate::openapi::ResponseStatus::Code(code) => code.to_string(),
146                        crate::openapi::ResponseStatus::Default => "Default".to_string(),
147                    };
148                    let name_hint =
149                        format!("{}Response{}", op_name.to_upper_camel_case(), status_suffix);
150                    register_inline_schema(&mut type_space, &mut inline_types, schema, name_hint);
151                }
152
153                // Add response header schemas
154                let status_suffix = match resp.status_code {
155                    crate::openapi::ResponseStatus::Code(code) => code.to_string(),
156                    crate::openapi::ResponseStatus::Default => "Default".to_string(),
157                };
158                for header in &resp.headers {
159                    if let Some(ReferenceOr::Item(schema)) = &header.schema {
160                        let name_hint = format!(
161                            "{}Response{}{}",
162                            op_name.to_upper_camel_case(),
163                            status_suffix,
164                            header.name.to_upper_camel_case()
165                        );
166                        register_inline_schema(
167                            &mut type_space,
168                            &mut inline_types,
169                            schema,
170                            name_hint,
171                        );
172                    }
173                }
174            }
175        }
176
177        Ok(Self {
178            type_space,
179            schema_to_type,
180            inline_types,
181        })
182    }
183
184    /// Generate all types as a TokenStream.
185    pub fn generate_all_types(&self) -> TokenStream {
186        self.type_space.to_stream()
187    }
188
189    /// Get the type name for a schema reference.
190    /// Returns the actual name from TypeSpace (after any renames applied by typify).
191    pub fn get_type_name(&self, reference: &str) -> Option<String> {
192        // Extract the schema name from the reference
193        let schema_name = reference.strip_prefix("#/components/schemas/")?;
194
195        // Look up in our map (built from TypeSpace)
196        self.schema_to_type.get(schema_name).cloned()
197    }
198
199    /// Resolve a schema reference to a type TokenStream.
200    /// Returns the type ident if found, or `serde_json::Value` as fallback.
201    fn resolve_reference(&self, reference: &str) -> TokenStream {
202        if let Some(type_name) = self.get_type_name(reference) {
203            let ident = format_ident!("{}", type_name);
204            quote! { #ident }
205        } else {
206            quote! { serde_json::Value }
207        }
208    }
209
210    /// Check if a schema is inline (not a reference).
211    pub fn is_inline_schema(schema: &ReferenceOr<Schema>) -> bool {
212        matches!(schema, ReferenceOr::Item(_))
213    }
214
215    /// Generate a type for an inline schema (returns only the type reference, discarding definitions).
216    /// For inline object types that need struct definitions, use `type_for_schema_with_definitions`.
217    pub fn type_for_schema(&self, schema: &ReferenceOr<Schema>, name_hint: &str) -> TokenStream {
218        self.type_for_schema_with_definitions(schema, name_hint)
219            .type_ref
220    }
221
222    /// Generate a type for a boxed schema reference.
223    pub fn type_for_boxed_schema(
224        &self,
225        schema: &ReferenceOr<Box<Schema>>,
226        name_hint: &str,
227    ) -> TokenStream {
228        match schema {
229            ReferenceOr::Reference { reference } => self.resolve_reference(reference),
230            ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint).type_ref,
231        }
232    }
233
234    /// Generate a type for a schema, returning both the type reference and any generated definitions.
235    /// Use this when you need to collect inline struct definitions.
236    pub fn type_for_schema_with_definitions(
237        &self,
238        schema: &ReferenceOr<Schema>,
239        name_hint: &str,
240    ) -> GeneratedType {
241        match schema {
242            ReferenceOr::Reference { reference } => {
243                GeneratedType::simple(self.resolve_reference(reference))
244            }
245            ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint),
246        }
247    }
248
249    /// Generate a type for an inline schema, returning both the type reference and any definitions.
250    ///
251    /// First checks if the schema was pre-registered with typify during initialization.
252    /// If found, uses typify's generated ident. Otherwise falls back to manual handling.
253    fn type_for_inline_schema(&self, schema: &Schema, name_hint: &str) -> GeneratedType {
254        // Check if this schema was pre-registered with typify
255        if let Some(type_id) = self.inline_types.get(name_hint)
256            && let Ok(typ) = self.type_space.get_type(type_id)
257        {
258            return GeneratedType::simple(typ.ident());
259        }
260
261        // Fall back to manual handling for schemas not pre-registered
262        // (e.g., nested schemas within arrays/objects that weren't traversed during init)
263        match &schema.schema_kind {
264            SchemaKind::Type(Type::String(_)) => GeneratedType::simple(quote! { String }),
265            SchemaKind::Type(Type::Integer(_)) => GeneratedType::simple(quote! { i64 }),
266            SchemaKind::Type(Type::Number(_)) => GeneratedType::simple(quote! { f64 }),
267            SchemaKind::Type(Type::Boolean(_)) => GeneratedType::simple(quote! { bool }),
268            SchemaKind::Type(Type::Array(arr)) => {
269                if let Some(items) = &arr.items {
270                    let inner = self.type_for_boxed_schema(items, &format!("{}Item", name_hint));
271                    GeneratedType::simple(quote! { Vec<#inner> })
272                } else {
273                    GeneratedType::simple(quote! { Vec<serde_json::Value> })
274                }
275            }
276            SchemaKind::Type(Type::Object(obj)) => self.generate_inline_struct(obj, name_hint),
277            _ => GeneratedType::simple(quote! { serde_json::Value }),
278        }
279    }
280
281    /// Generate an inline struct for an object schema.
282    fn generate_inline_struct(
283        &self,
284        obj: &openapiv3::ObjectType,
285        name_hint: &str,
286    ) -> GeneratedType {
287        let struct_name = format_ident!("{}", name_hint.to_upper_camel_case());
288        let mut definitions = Vec::new();
289
290        // Generate fields for each property
291        let fields: Vec<_> = obj
292            .properties
293            .iter()
294            .map(|(prop_name, prop_schema)| {
295                let field_name = format_ident!("{}", AsSnakeCase(prop_name).to_string());
296                let is_required = obj.required.contains(prop_name);
297
298                // Generate the inner type, collecting any nested definitions
299                let inner_hint = format!("{}{}", name_hint, prop_name.to_upper_camel_case());
300                let generated = match prop_schema {
301                    ReferenceOr::Reference { reference } => {
302                        GeneratedType::simple(self.resolve_reference(reference))
303                    }
304                    ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, &inner_hint),
305                };
306
307                // Collect nested definitions
308                definitions.extend(generated.definitions);
309
310                let field_type = if is_required {
311                    generated.type_ref
312                } else {
313                    let inner = generated.type_ref;
314                    quote! { Option<#inner> }
315                };
316
317                // Add serde rename if field name differs from property name
318                let snake_name = AsSnakeCase(prop_name).to_string();
319                if snake_name != *prop_name {
320                    quote! {
321                        #[serde(rename = #prop_name)]
322                        pub #field_name: #field_type
323                    }
324                } else {
325                    quote! { pub #field_name: #field_type }
326                }
327            })
328            .collect();
329
330        // Generate the struct definition
331        let struct_def = quote! {
332            #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
333            pub struct #struct_name {
334                #(#fields,)*
335            }
336        };
337
338        definitions.push(struct_def);
339
340        GeneratedType {
341            type_ref: quote! { #struct_name },
342            definitions,
343        }
344    }
345
346    /// Get the type for a path or query parameter.
347    pub fn param_type(&self, param: &OperationParam) -> TokenStream {
348        if let Some(schema) = &param.schema {
349            self.type_for_schema(schema, &param.name.to_upper_camel_case())
350        } else {
351            quote! { String }
352        }
353    }
354
355    /// Get the type for a request body.
356    pub fn request_body_type(&self, body: &RequestBody, op_name: &str) -> TokenStream {
357        if let Some(schema) = &body.schema {
358            self.type_for_schema(schema, &format!("{}Body", op_name.to_upper_camel_case()))
359        } else {
360            quote! { serde_json::Value }
361        }
362    }
363
364    /// Generate a query params struct for an operation.
365    ///
366    /// If `unknown_field` is provided, an additional `#[serde(flatten)]` field with that name
367    /// will be added to capture unknown query parameters as `HashMap<String, String>`.
368    pub fn generate_query_struct(
369        &self,
370        op: &Operation,
371        overrides: &TypeOverrides,
372        unknown_field: Option<&syn::Ident>,
373    ) -> Option<(syn::Ident, TokenStream)> {
374        // Check if replaced
375        if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Query) {
376            return None;
377        }
378
379        let query_params: Vec<_> = op
380            .parameters
381            .iter()
382            .filter(|p| p.location == ParamLocation::Query)
383            .collect();
384
385        // Allow struct generation with unknown_field even if no query params
386        if query_params.is_empty() && unknown_field.is_none() {
387            return None;
388        }
389
390        let default_name = format!(
391            "{}Query",
392            op.operation_id
393                .as_deref()
394                .unwrap_or(&op.path)
395                .to_upper_camel_case()
396        );
397
398        // Check for rename override
399        let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
400            overrides.get(op.method, &op.path, GeneratedTypeKind::Query)
401        {
402            name.clone()
403        } else {
404            format_ident!("{}", default_name)
405        };
406
407        let fields = query_params.iter().map(|param| {
408            let name = format_ident!("{}", heck::AsSnakeCase(&param.name).to_string());
409            let ty = self.param_type(param);
410
411            if param.required {
412                quote! { pub #name: #ty }
413            } else {
414                quote! { pub #name: Option<#ty> }
415            }
416        });
417
418        // Generate unknown field with serde(flatten) if requested
419        let unknown_field_def = unknown_field.map(|name| {
420            quote! {
421                #[serde(flatten)]
422                pub #name: ::std::collections::HashMap<String, String>
423            }
424        });
425
426        // Use struct name span so errors point to user's override if provided
427        let definition = quote_spanned! { struct_name.span() =>
428            #[derive(Debug, Clone, serde::Deserialize)]
429            pub struct #struct_name {
430                #(#fields,)*
431                #unknown_field_def
432            }
433        };
434
435        Some((struct_name, definition))
436    }
437
438    /// Generate a path params struct for an operation.
439    ///
440    /// Returns `Some((struct_name, struct_definition))` if the operation has path params,
441    /// or `None` if there are no path params or the struct is replaced by an override.
442    ///
443    /// Path parameters are sorted by their position in the URL path to ensure correct
444    /// serde deserialization order.
445    pub fn generate_path_struct(
446        &self,
447        op: &Operation,
448        overrides: &TypeOverrides,
449    ) -> Option<(syn::Ident, TokenStream)> {
450        // Check if replaced
451        if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Path) {
452            return None;
453        }
454
455        // Get path params and sort by position in URL
456        let mut path_params: Vec<_> = op
457            .parameters
458            .iter()
459            .filter(|p| p.location == ParamLocation::Path)
460            .collect();
461
462        if path_params.is_empty() {
463            return None;
464        }
465
466        // Sort by position in the path string
467        path_params.sort_by_key(|p| {
468            let placeholder = format!("{{{}}}", p.name);
469            op.path.find(&placeholder).unwrap_or(usize::MAX)
470        });
471
472        let default_name = format!(
473            "{}Path",
474            op.operation_id
475                .as_deref()
476                .unwrap_or(&op.path)
477                .to_upper_camel_case()
478        );
479
480        // Check for rename override
481        let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
482            overrides.get(op.method, &op.path, GeneratedTypeKind::Path)
483        {
484            name.clone()
485        } else {
486            format_ident!("{}", default_name)
487        };
488
489        let fields = path_params.iter().map(|param| {
490            let snake_name = heck::AsSnakeCase(&param.name).to_string();
491            let field_name = format_ident!("{}", snake_name);
492            let ty = self.param_type(param);
493            let param_name = &param.name;
494
495            // Add serde rename if field name differs from param name (for path deserialization)
496            if snake_name != param.name {
497                quote! {
498                    #[serde(rename = #param_name)]
499                    pub #field_name: #ty
500                }
501            } else {
502                quote! { pub #field_name: #ty }
503            }
504        });
505
506        // Use struct name span so errors point to user's override if provided
507        let definition = quote_spanned! { struct_name.span() =>
508            #[derive(Debug, Clone, serde::Deserialize)]
509            pub struct #struct_name {
510                #(#fields,)*
511            }
512        };
513
514        Some((struct_name, definition))
515    }
516}
517
518/// Convert any serializable schema type to a schemars schema for typify.
519fn to_schemars<T: serde::Serialize>(schema: &T) -> Result<schemars::schema::Schema> {
520    serde_value::to_value(schema)
521        .map_err(|e| Error::TypeGenError(format!("failed to serialize schema: {}", e)))?
522        .deserialize_into::<schemars::schema::Schema>()
523        .map_err(|e| Error::TypeGenError(format!("failed to deserialize schema: {}", e)))
524}
525
526/// Register an inline schema with typify, returning the TypeId if successful.
527fn register_inline_schema(
528    type_space: &mut TypeSpace,
529    inline_types: &mut HashMap<String, TypeId>,
530    schema: &Schema,
531    name_hint: String,
532) {
533    if let Ok(schemars_schema) = to_schemars(schema)
534        && let Ok(type_id) =
535            type_space.add_type_with_name(&schemars_schema, Some(name_hint.clone()))
536    {
537        inline_types.insert(name_hint, type_id);
538    }
539}