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        }
154
155        Ok(Self {
156            type_space,
157            schema_to_type,
158            inline_types,
159        })
160    }
161
162    /// Generate all types as a TokenStream.
163    pub fn generate_all_types(&self) -> TokenStream {
164        self.type_space.to_stream()
165    }
166
167    /// Get the type name for a schema reference.
168    /// Returns the actual name from TypeSpace (after any renames applied by typify).
169    pub fn get_type_name(&self, reference: &str) -> Option<String> {
170        // Extract the schema name from the reference
171        let schema_name = reference.strip_prefix("#/components/schemas/")?;
172
173        // Look up in our map (built from TypeSpace)
174        self.schema_to_type.get(schema_name).cloned()
175    }
176
177    /// Resolve a schema reference to a type TokenStream.
178    /// Returns the type ident if found, or `serde_json::Value` as fallback.
179    fn resolve_reference(&self, reference: &str) -> TokenStream {
180        if let Some(type_name) = self.get_type_name(reference) {
181            let ident = format_ident!("{}", type_name);
182            quote! { #ident }
183        } else {
184            quote! { serde_json::Value }
185        }
186    }
187
188    /// Check if a schema is inline (not a reference).
189    pub fn is_inline_schema(schema: &ReferenceOr<Schema>) -> bool {
190        matches!(schema, ReferenceOr::Item(_))
191    }
192
193    /// Generate a type for an inline schema (returns only the type reference, discarding definitions).
194    /// For inline object types that need struct definitions, use `type_for_schema_with_definitions`.
195    pub fn type_for_schema(&self, schema: &ReferenceOr<Schema>, name_hint: &str) -> TokenStream {
196        self.type_for_schema_with_definitions(schema, name_hint)
197            .type_ref
198    }
199
200    /// Generate a type for a boxed schema reference.
201    pub fn type_for_boxed_schema(
202        &self,
203        schema: &ReferenceOr<Box<Schema>>,
204        name_hint: &str,
205    ) -> TokenStream {
206        match schema {
207            ReferenceOr::Reference { reference } => self.resolve_reference(reference),
208            ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint).type_ref,
209        }
210    }
211
212    /// Generate a type for a schema, returning both the type reference and any generated definitions.
213    /// Use this when you need to collect inline struct definitions.
214    pub fn type_for_schema_with_definitions(
215        &self,
216        schema: &ReferenceOr<Schema>,
217        name_hint: &str,
218    ) -> GeneratedType {
219        match schema {
220            ReferenceOr::Reference { reference } => {
221                GeneratedType::simple(self.resolve_reference(reference))
222            }
223            ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint),
224        }
225    }
226
227    /// Generate a type for an inline schema, returning both the type reference and any definitions.
228    ///
229    /// First checks if the schema was pre-registered with typify during initialization.
230    /// If found, uses typify's generated ident. Otherwise falls back to manual handling.
231    fn type_for_inline_schema(&self, schema: &Schema, name_hint: &str) -> GeneratedType {
232        // Check if this schema was pre-registered with typify
233        if let Some(type_id) = self.inline_types.get(name_hint)
234            && let Ok(typ) = self.type_space.get_type(type_id)
235        {
236            return GeneratedType::simple(typ.ident());
237        }
238
239        // Fall back to manual handling for schemas not pre-registered
240        // (e.g., nested schemas within arrays/objects that weren't traversed during init)
241        match &schema.schema_kind {
242            SchemaKind::Type(Type::String(_)) => GeneratedType::simple(quote! { String }),
243            SchemaKind::Type(Type::Integer(_)) => GeneratedType::simple(quote! { i64 }),
244            SchemaKind::Type(Type::Number(_)) => GeneratedType::simple(quote! { f64 }),
245            SchemaKind::Type(Type::Boolean(_)) => GeneratedType::simple(quote! { bool }),
246            SchemaKind::Type(Type::Array(arr)) => {
247                if let Some(items) = &arr.items {
248                    let inner = self.type_for_boxed_schema(items, &format!("{}Item", name_hint));
249                    GeneratedType::simple(quote! { Vec<#inner> })
250                } else {
251                    GeneratedType::simple(quote! { Vec<serde_json::Value> })
252                }
253            }
254            SchemaKind::Type(Type::Object(obj)) => self.generate_inline_struct(obj, name_hint),
255            _ => GeneratedType::simple(quote! { serde_json::Value }),
256        }
257    }
258
259    /// Generate an inline struct for an object schema.
260    fn generate_inline_struct(
261        &self,
262        obj: &openapiv3::ObjectType,
263        name_hint: &str,
264    ) -> GeneratedType {
265        let struct_name = format_ident!("{}", name_hint.to_upper_camel_case());
266        let mut definitions = Vec::new();
267
268        // Generate fields for each property
269        let fields: Vec<_> = obj
270            .properties
271            .iter()
272            .map(|(prop_name, prop_schema)| {
273                let field_name = format_ident!("{}", AsSnakeCase(prop_name).to_string());
274                let is_required = obj.required.contains(prop_name);
275
276                // Generate the inner type, collecting any nested definitions
277                let inner_hint = format!("{}{}", name_hint, prop_name.to_upper_camel_case());
278                let generated = match prop_schema {
279                    ReferenceOr::Reference { reference } => {
280                        GeneratedType::simple(self.resolve_reference(reference))
281                    }
282                    ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, &inner_hint),
283                };
284
285                // Collect nested definitions
286                definitions.extend(generated.definitions);
287
288                let field_type = if is_required {
289                    generated.type_ref
290                } else {
291                    let inner = generated.type_ref;
292                    quote! { Option<#inner> }
293                };
294
295                // Add serde rename if field name differs from property name
296                let snake_name = AsSnakeCase(prop_name).to_string();
297                if snake_name != *prop_name {
298                    quote! {
299                        #[serde(rename = #prop_name)]
300                        pub #field_name: #field_type
301                    }
302                } else {
303                    quote! { pub #field_name: #field_type }
304                }
305            })
306            .collect();
307
308        // Generate the struct definition
309        let struct_def = quote! {
310            #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
311            pub struct #struct_name {
312                #(#fields,)*
313            }
314        };
315
316        definitions.push(struct_def);
317
318        GeneratedType {
319            type_ref: quote! { #struct_name },
320            definitions,
321        }
322    }
323
324    /// Get the type for a path or query parameter.
325    pub fn param_type(&self, param: &OperationParam) -> TokenStream {
326        if let Some(schema) = &param.schema {
327            self.type_for_schema(schema, &param.name.to_upper_camel_case())
328        } else {
329            quote! { String }
330        }
331    }
332
333    /// Get the type for a request body.
334    pub fn request_body_type(&self, body: &RequestBody, op_name: &str) -> TokenStream {
335        if let Some(schema) = &body.schema {
336            self.type_for_schema(schema, &format!("{}Body", op_name.to_upper_camel_case()))
337        } else {
338            quote! { serde_json::Value }
339        }
340    }
341
342    /// Generate a query params struct for an operation.
343    ///
344    /// If `unknown_field` is provided, an additional `#[serde(flatten)]` field with that name
345    /// will be added to capture unknown query parameters as `HashMap<String, String>`.
346    pub fn generate_query_struct(
347        &self,
348        op: &Operation,
349        overrides: &TypeOverrides,
350        unknown_field: Option<&syn::Ident>,
351    ) -> Option<(syn::Ident, TokenStream)> {
352        // Check if replaced
353        if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Query) {
354            return None;
355        }
356
357        let query_params: Vec<_> = op
358            .parameters
359            .iter()
360            .filter(|p| p.location == ParamLocation::Query)
361            .collect();
362
363        // Allow struct generation with unknown_field even if no query params
364        if query_params.is_empty() && unknown_field.is_none() {
365            return None;
366        }
367
368        let default_name = format!(
369            "{}Query",
370            op.operation_id
371                .as_deref()
372                .unwrap_or(&op.path)
373                .to_upper_camel_case()
374        );
375
376        // Check for rename override
377        let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
378            overrides.get(op.method, &op.path, GeneratedTypeKind::Query)
379        {
380            name.clone()
381        } else {
382            format_ident!("{}", default_name)
383        };
384
385        let fields = query_params.iter().map(|param| {
386            let name = format_ident!("{}", heck::AsSnakeCase(&param.name).to_string());
387            let ty = self.param_type(param);
388
389            if param.required {
390                quote! { pub #name: #ty }
391            } else {
392                quote! { pub #name: Option<#ty> }
393            }
394        });
395
396        // Generate unknown field with serde(flatten) if requested
397        let unknown_field_def = unknown_field.map(|name| {
398            quote! {
399                #[serde(flatten)]
400                pub #name: ::std::collections::HashMap<String, String>
401            }
402        });
403
404        // Use struct name span so errors point to user's override if provided
405        let definition = quote_spanned! { struct_name.span() =>
406            #[derive(Debug, Clone, serde::Deserialize)]
407            pub struct #struct_name {
408                #(#fields,)*
409                #unknown_field_def
410            }
411        };
412
413        Some((struct_name, definition))
414    }
415
416    /// Generate a path params struct for an operation.
417    ///
418    /// Returns `Some((struct_name, struct_definition))` if the operation has path params,
419    /// or `None` if there are no path params or the struct is replaced by an override.
420    ///
421    /// Path parameters are sorted by their position in the URL path to ensure correct
422    /// serde deserialization order.
423    pub fn generate_path_struct(
424        &self,
425        op: &Operation,
426        overrides: &TypeOverrides,
427    ) -> Option<(syn::Ident, TokenStream)> {
428        // Check if replaced
429        if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Path) {
430            return None;
431        }
432
433        // Get path params and sort by position in URL
434        let mut path_params: Vec<_> = op
435            .parameters
436            .iter()
437            .filter(|p| p.location == ParamLocation::Path)
438            .collect();
439
440        if path_params.is_empty() {
441            return None;
442        }
443
444        // Sort by position in the path string
445        path_params.sort_by_key(|p| {
446            let placeholder = format!("{{{}}}", p.name);
447            op.path.find(&placeholder).unwrap_or(usize::MAX)
448        });
449
450        let default_name = format!(
451            "{}Path",
452            op.operation_id
453                .as_deref()
454                .unwrap_or(&op.path)
455                .to_upper_camel_case()
456        );
457
458        // Check for rename override
459        let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
460            overrides.get(op.method, &op.path, GeneratedTypeKind::Path)
461        {
462            name.clone()
463        } else {
464            format_ident!("{}", default_name)
465        };
466
467        let fields = path_params.iter().map(|param| {
468            let snake_name = heck::AsSnakeCase(&param.name).to_string();
469            let field_name = format_ident!("{}", snake_name);
470            let ty = self.param_type(param);
471            let param_name = &param.name;
472
473            // Add serde rename if field name differs from param name (for path deserialization)
474            if snake_name != param.name {
475                quote! {
476                    #[serde(rename = #param_name)]
477                    pub #field_name: #ty
478                }
479            } else {
480                quote! { pub #field_name: #ty }
481            }
482        });
483
484        // Use struct name span so errors point to user's override if provided
485        let definition = quote_spanned! { struct_name.span() =>
486            #[derive(Debug, Clone, serde::Deserialize)]
487            pub struct #struct_name {
488                #(#fields,)*
489            }
490        };
491
492        Some((struct_name, definition))
493    }
494}
495
496/// Convert any serializable schema type to a schemars schema for typify.
497fn to_schemars<T: serde::Serialize>(schema: &T) -> Result<schemars::schema::Schema> {
498    serde_value::to_value(schema)
499        .map_err(|e| Error::TypeGenError(format!("failed to serialize schema: {}", e)))?
500        .deserialize_into::<schemars::schema::Schema>()
501        .map_err(|e| Error::TypeGenError(format!("failed to deserialize schema: {}", e)))
502}
503
504/// Register an inline schema with typify, returning the TypeId if successful.
505fn register_inline_schema(
506    type_space: &mut TypeSpace,
507    inline_types: &mut HashMap<String, TypeId>,
508    schema: &Schema,
509    name_hint: String,
510) {
511    if let Ok(schemars_schema) = to_schemars(schema)
512        && let Ok(type_id) =
513            type_space.add_type_with_name(&schemars_schema, Some(name_hint.clone()))
514    {
515        inline_types.insert(name_hint, type_id);
516    }
517}