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::{
7    IntegerFormat, NumberFormat, ReferenceOr, Schema, SchemaKind, Type, VariantOrUnknownOrEmpty,
8};
9use proc_macro2::TokenStream;
10use quote::{format_ident, quote, quote_spanned};
11use typify::{TypeSpace, TypeSpaceSettings};
12
13/// Result of generating a type for a schema, including any inline struct definitions.
14pub struct GeneratedType {
15    /// The type expression (e.g., `MyStruct`, `Vec<String>`, `serde_json::Value`)
16    pub type_ref: TokenStream,
17    /// Any struct definitions that need to be emitted (for inline object schemas)
18    pub definitions: Vec<TokenStream>,
19}
20
21use crate::openapi::{Operation, OperationParam, ParamLocation, ParsedSpec, RequestBody};
22use crate::{Error, GeneratedTypeKind, Result, TypeOverride, TypeOverrides};
23
24/// Type generator that wraps typify's TypeSpace.
25///
26/// Uses a two-pass approach:
27/// 1. Pass 1: Generate all types (component schemas + auto-generated types)
28/// 2. Pass 2: Generate code that references types using the registry built in Pass 1
29pub struct TypeGenerator {
30    type_space: TypeSpace,
31    /// Map from schema name (as in OpenAPI spec) to final type name (after typify processing)
32    schema_to_type: HashMap<String, String>,
33}
34
35impl TypeGenerator {
36    /// Create a new type generator from a parsed spec with default settings.
37    pub fn new(spec: &ParsedSpec) -> Result<Self> {
38        Self::with_settings(spec, TypeSpaceSettings::default(), HashMap::new())
39    }
40
41    /// Create a new type generator from a parsed spec with custom settings and renames.
42    ///
43    /// The renames map should contain original schema name -> new name mappings.
44    /// These must match the patches configured in TypeSpaceSettings.
45    pub fn with_settings(
46        spec: &ParsedSpec,
47        settings: TypeSpaceSettings,
48        renames: HashMap<String, String>,
49    ) -> Result<Self> {
50        let mut type_space = TypeSpace::new(&settings);
51
52        // Collect schema names before adding to type space
53        let schema_names: Vec<String> = spec
54            .components
55            .as_ref()
56            .map(|c| c.schemas.keys().cloned().collect())
57            .unwrap_or_default();
58
59        // Add all component schemas to the type space
60        if let Some(components) = &spec.components {
61            let schemas = components
62                .schemas
63                .iter()
64                .map(|(name, schema)| {
65                    let schema = convert_to_schemars(schema)?;
66                    Ok((name.clone(), schema))
67                })
68                .collect::<Result<Vec<_>>>()?;
69
70            type_space
71                .add_ref_types(schemas.into_iter())
72                .map_err(|e| Error::TypeGenError(e.to_string()))?;
73        }
74
75        // First validate that all renames reference schemas that exist in the spec
76        for original_name in renames.keys() {
77            if !schema_names.contains(original_name) {
78                return Err(Error::UnknownSchema {
79                    name: original_name.clone(),
80                    available: schema_names.join(", "),
81                });
82            }
83        }
84
85        // Build schema_to_type map by iterating TypeSpace to see what names typify generated
86        let generated_names: std::collections::HashSet<String> =
87            type_space.iter_types().map(|t| t.name()).collect();
88
89        let mut schema_to_type = HashMap::new();
90        for schema_name in &schema_names {
91            // Check if there's a rename for this schema
92            let expected_name = if let Some(renamed) = renames.get(schema_name) {
93                renamed.to_upper_camel_case()
94            } else {
95                schema_name.to_upper_camel_case()
96            };
97
98            // Verify the type exists in TypeSpace
99            if generated_names.contains(&expected_name) {
100                schema_to_type.insert(schema_name.clone(), expected_name);
101            } else {
102                // Type doesn't exist - this shouldn't happen if typify is working correctly
103                return Err(Error::TypeGenError(format!(
104                    "typify did not generate expected type '{}' for schema '{}'",
105                    expected_name, schema_name
106                )));
107            }
108        }
109
110        Ok(Self {
111            type_space,
112            schema_to_type,
113        })
114    }
115
116    /// Generate all types as a TokenStream.
117    pub fn generate_all_types(&self) -> TokenStream {
118        self.type_space.to_stream()
119    }
120
121    /// Get the type name for a schema reference.
122    /// Returns the actual name from TypeSpace (after any renames applied by typify).
123    pub fn get_type_name(&self, reference: &str) -> Option<String> {
124        // Extract the schema name from the reference
125        let schema_name = reference.strip_prefix("#/components/schemas/")?;
126
127        // Look up in our map (built from TypeSpace)
128        self.schema_to_type.get(schema_name).cloned()
129    }
130
131    /// Check if a schema is inline (not a reference).
132    pub fn is_inline_schema(schema: &ReferenceOr<Schema>) -> bool {
133        matches!(schema, ReferenceOr::Item(_))
134    }
135
136    /// Generate a type for an inline schema (returns only the type reference, discarding definitions).
137    /// For inline object types that need struct definitions, use `type_for_schema_with_definitions`.
138    pub fn type_for_schema(&self, schema: &ReferenceOr<Schema>, name_hint: &str) -> TokenStream {
139        self.type_for_schema_with_definitions(schema, name_hint)
140            .type_ref
141    }
142
143    /// Generate a type for a boxed schema reference.
144    pub fn type_for_boxed_schema(
145        &self,
146        schema: &ReferenceOr<Box<Schema>>,
147        name_hint: &str,
148    ) -> TokenStream {
149        match schema {
150            ReferenceOr::Reference { reference } => {
151                if let Some(type_name) = self.get_type_name(reference) {
152                    let ident = format_ident!("{}", type_name);
153                    quote! { #ident }
154                } else {
155                    quote! { serde_json::Value }
156                }
157            }
158            ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint).type_ref,
159        }
160    }
161
162    /// Generate a type for a schema, returning both the type reference and any generated definitions.
163    /// Use this when you need to collect inline struct definitions.
164    pub fn type_for_schema_with_definitions(
165        &self,
166        schema: &ReferenceOr<Schema>,
167        name_hint: &str,
168    ) -> GeneratedType {
169        match schema {
170            ReferenceOr::Reference { reference } => {
171                let type_ref = if let Some(type_name) = self.get_type_name(reference) {
172                    let ident = format_ident!("{}", type_name);
173                    quote! { #ident }
174                } else {
175                    quote! { serde_json::Value }
176                };
177                GeneratedType {
178                    type_ref,
179                    definitions: vec![],
180                }
181            }
182            ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, name_hint),
183        }
184    }
185
186    /// Generate a type for an inline schema, returning both the type reference and any definitions.
187    fn type_for_inline_schema(&self, schema: &Schema, name_hint: &str) -> GeneratedType {
188        match &schema.schema_kind {
189            SchemaKind::Type(Type::String(_)) => GeneratedType {
190                type_ref: quote! { String },
191                definitions: vec![],
192            },
193            SchemaKind::Type(Type::Integer(int_type)) => {
194                let type_ref = match &int_type.format {
195                    VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) => quote! { i32 },
196                    VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64) => quote! { i64 },
197                    _ => quote! { i64 },
198                };
199                GeneratedType {
200                    type_ref,
201                    definitions: vec![],
202                }
203            }
204            SchemaKind::Type(Type::Number(num_type)) => {
205                let type_ref = match &num_type.format {
206                    VariantOrUnknownOrEmpty::Item(NumberFormat::Float) => quote! { f32 },
207                    VariantOrUnknownOrEmpty::Item(NumberFormat::Double) => quote! { f64 },
208                    _ => quote! { f64 },
209                };
210                GeneratedType {
211                    type_ref,
212                    definitions: vec![],
213                }
214            }
215            SchemaKind::Type(Type::Boolean(_)) => GeneratedType {
216                type_ref: quote! { bool },
217                definitions: vec![],
218            },
219            SchemaKind::Type(Type::Array(arr)) => {
220                if let Some(items) = &arr.items {
221                    let inner = self.type_for_boxed_schema(items, &format!("{}Item", name_hint));
222                    GeneratedType {
223                        type_ref: quote! { Vec<#inner> },
224                        definitions: vec![],
225                    }
226                } else {
227                    GeneratedType {
228                        type_ref: quote! { Vec<serde_json::Value> },
229                        definitions: vec![],
230                    }
231                }
232            }
233            SchemaKind::Type(Type::Object(obj)) => {
234                self.generate_inline_struct(obj, &schema.schema_data, name_hint)
235            }
236            _ => GeneratedType {
237                type_ref: quote! { serde_json::Value },
238                definitions: vec![],
239            },
240        }
241    }
242
243    /// Generate an inline struct for an object schema.
244    fn generate_inline_struct(
245        &self,
246        obj: &openapiv3::ObjectType,
247        _schema_data: &openapiv3::SchemaData,
248        name_hint: &str,
249    ) -> GeneratedType {
250        let struct_name = format_ident!("{}", name_hint.to_upper_camel_case());
251        let mut definitions = Vec::new();
252
253        // Generate fields for each property
254        let fields: Vec<_> = obj
255            .properties
256            .iter()
257            .map(|(prop_name, prop_schema)| {
258                let field_name = format_ident!("{}", AsSnakeCase(prop_name).to_string());
259                let is_required = obj.required.contains(prop_name);
260
261                // Generate the inner type, collecting any nested definitions
262                let inner_hint = format!("{}{}", name_hint, prop_name.to_upper_camel_case());
263                let generated = match prop_schema {
264                    ReferenceOr::Reference { reference } => {
265                        let type_ref = if let Some(type_name) = self.get_type_name(reference) {
266                            let ident = format_ident!("{}", type_name);
267                            quote! { #ident }
268                        } else {
269                            quote! { serde_json::Value }
270                        };
271                        GeneratedType {
272                            type_ref,
273                            definitions: vec![],
274                        }
275                    }
276                    ReferenceOr::Item(schema) => self.type_for_inline_schema(schema, &inner_hint),
277                };
278
279                // Collect nested definitions
280                definitions.extend(generated.definitions);
281
282                let field_type = if is_required {
283                    generated.type_ref
284                } else {
285                    let inner = generated.type_ref;
286                    quote! { Option<#inner> }
287                };
288
289                // Add serde rename if field name differs from property name
290                let snake_name = AsSnakeCase(prop_name).to_string();
291                if snake_name != *prop_name {
292                    quote! {
293                        #[serde(rename = #prop_name)]
294                        pub #field_name: #field_type
295                    }
296                } else {
297                    quote! { pub #field_name: #field_type }
298                }
299            })
300            .collect();
301
302        // Generate the struct definition
303        let struct_def = quote! {
304            #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
305            pub struct #struct_name {
306                #(#fields,)*
307            }
308        };
309
310        definitions.push(struct_def);
311
312        GeneratedType {
313            type_ref: quote! { #struct_name },
314            definitions,
315        }
316    }
317
318    /// Get the type for a path parameter.
319    pub fn path_param_type(&self, param: &OperationParam) -> TokenStream {
320        if let Some(schema) = &param.schema {
321            self.type_for_schema(schema, &param.name.to_upper_camel_case())
322        } else {
323            quote! { String }
324        }
325    }
326
327    /// Get the type for a query parameter.
328    pub fn query_param_type(&self, param: &OperationParam) -> TokenStream {
329        if let Some(schema) = &param.schema {
330            self.type_for_schema(schema, &param.name.to_upper_camel_case())
331        } else {
332            quote! { String }
333        }
334    }
335
336    /// Get the type for a request body.
337    pub fn request_body_type(&self, body: &RequestBody, op_name: &str) -> TokenStream {
338        if let Some(schema) = &body.schema {
339            self.type_for_schema(schema, &format!("{}Body", op_name.to_upper_camel_case()))
340        } else {
341            quote! { serde_json::Value }
342        }
343    }
344
345    /// Get the type for a response body.
346    pub fn response_type(
347        &self,
348        schema: &Option<ReferenceOr<Schema>>,
349        op_name: &str,
350        status: u16,
351    ) -> TokenStream {
352        if let Some(schema) = schema {
353            self.type_for_schema(
354                schema,
355                &format!("{}Response{}", op_name.to_upper_camel_case(), status),
356            )
357        } else {
358            quote! { () }
359        }
360    }
361
362    /// Generate a query params struct for an operation.
363    pub fn generate_query_struct(
364        &self,
365        op: &Operation,
366        overrides: &TypeOverrides,
367    ) -> Option<(syn::Ident, TokenStream)> {
368        // Check if replaced
369        if overrides.is_replaced(op.method, &op.path, GeneratedTypeKind::Query) {
370            return None;
371        }
372
373        let query_params: Vec<_> = op
374            .parameters
375            .iter()
376            .filter(|p| p.location == ParamLocation::Query)
377            .collect();
378
379        if query_params.is_empty() {
380            return None;
381        }
382
383        let default_name = format!(
384            "{}Query",
385            op.operation_id
386                .as_deref()
387                .unwrap_or(&op.path)
388                .to_upper_camel_case()
389        );
390
391        // Check for rename override
392        let struct_name = if let Some(TypeOverride::Rename { name, .. }) =
393            overrides.get(op.method, &op.path, GeneratedTypeKind::Query)
394        {
395            name.clone()
396        } else {
397            format_ident!("{}", default_name)
398        };
399
400        let fields = query_params.iter().map(|param| {
401            let name = format_ident!("{}", heck::AsSnakeCase(&param.name).to_string());
402            let ty = self.query_param_type(param);
403
404            if param.required {
405                quote! { pub #name: #ty }
406            } else {
407                quote! { pub #name: Option<#ty> }
408            }
409        });
410
411        // Use struct name span so errors point to user's override if provided
412        let definition = quote_spanned! { struct_name.span() =>
413            #[derive(Debug, Clone, serde::Deserialize)]
414            pub struct #struct_name {
415                #(#fields,)*
416            }
417        };
418
419        Some((struct_name, definition))
420    }
421
422    /// Generate a path params type for an operation (usually a tuple).
423    pub fn generate_path_type(&self, op: &Operation) -> TokenStream {
424        let path_params: Vec<_> = op
425            .parameters
426            .iter()
427            .filter(|p| p.location == ParamLocation::Path)
428            .collect();
429
430        if path_params.is_empty() {
431            return quote! { () };
432        }
433
434        if path_params.len() == 1 {
435            return self.path_param_type(path_params[0]);
436        }
437
438        let types = path_params.iter().map(|p| self.path_param_type(p));
439        quote! { (#(#types),*) }
440    }
441}
442
443/// Convert an OpenAPI schema to a schemars schema for typify.
444fn convert_to_schemars(schema: &ReferenceOr<Schema>) -> Result<schemars::schema::Schema> {
445    serde_value::to_value(schema)
446        .map_err(|e| Error::TypeGenError(format!("failed to serialize schema: {}", e)))?
447        .deserialize_into::<schemars::schema::Schema>()
448        .map_err(|e| Error::TypeGenError(format!("failed to deserialize schema: {}", e)))
449}