Skip to main content

oxapi_impl/
lib.rs

1//! Core implementation for the oxapi OpenAPI server stub generator.
2
3use std::collections::{HashMap, HashSet};
4
5use openapiv3::OpenAPI;
6use proc_macro2::TokenStream;
7use thiserror::Error;
8
9mod method;
10mod openapi;
11mod responses;
12mod router;
13mod types;
14
15pub use method::{MethodTransformer, ParamRole};
16pub use openapi::{HttpMethod, Operation, OperationParam, ParamLocation, ParsedSpec};
17pub use responses::ResponseGenerator;
18pub use router::RouterGenerator;
19pub use types::TypeGenerator;
20
21// Re-export typify types for use in the macro crate
22pub use typify::{TypeSpacePatch, TypeSpaceSettings};
23
24/// Configuration for response type suffixes and derives.
25#[derive(Debug, Clone)]
26pub struct ResponseSuffixes {
27    /// Suffix for success response types (e.g., "Response" → `GetPetResponse`).
28    pub ok_suffix: String,
29    /// Suffix for error response types (e.g., "Error" → `GetPetError`).
30    pub err_suffix: String,
31    /// Default derive attributes for generated enums (used when no derive is specified).
32    pub default_derives: TokenStream,
33}
34
35impl Default for ResponseSuffixes {
36    fn default() -> Self {
37        Self {
38            ok_suffix: "Response".to_string(),
39            err_suffix: "Error".to_string(),
40            default_derives: quote::quote! { #[derive(Debug)] },
41        }
42    }
43}
44
45/// Kind of generated type for an operation.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum GeneratedTypeKind {
48    /// Success response enum ({Op}{ok_suffix}, default: {Op}Response)
49    Ok,
50    /// Error response enum ({Op}{err_suffix}, default: {Op}Error)
51    Err,
52    /// Query parameters struct ({Op}Query)
53    Query,
54    /// Path parameters struct ({Op}Path)
55    Path,
56}
57
58/// Key for looking up type overrides.
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub struct TypeOverrideKey {
61    pub method: HttpMethod,
62    pub path: String,
63    pub kind: GeneratedTypeKind,
64}
65
66/// Variant override info: name, inner type name, and attributes to pass through.
67#[derive(Debug, Clone)]
68pub struct VariantOverride {
69    /// The variant name (as an Ident for better error spans).
70    pub name: proc_macro2::Ident,
71    /// Optional inner type name override for inline schemas.
72    /// If specified, inline schemas for this variant will use this name instead of the default.
73    pub inner_type_name: Option<proc_macro2::Ident>,
74    /// Attributes to apply to the variant (excluding the consumed `#[status()]`)
75    pub attrs: Vec<TokenStream>,
76}
77
78/// Override for a generated type - either rename or replace.
79#[derive(Debug, Clone)]
80pub enum TypeOverride {
81    /// Rename the type to a new name, optionally with variant renames
82    Rename {
83        /// The new name (as an Ident for better error spans).
84        name: proc_macro2::Ident,
85        /// Attributes to apply to the enum (excluding consumed `#[rename(...)]`)
86        /// If this contains a `#[derive(...)]`, it overrides the default.
87        attrs: Vec<TokenStream>,
88        /// Status code → variant override (name + attributes)
89        variant_overrides: HashMap<u16, VariantOverride>,
90    },
91    /// Replace the type with an existing type (as TokenStream)
92    Replace(TokenStream),
93}
94
95/// Key for query unknown field storage.
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97struct QueryUnknownFieldKey {
98    method: HttpMethod,
99    path: String,
100}
101
102/// Collection of type overrides for generated types.
103#[derive(Debug, Clone, Default)]
104pub struct TypeOverrides {
105    overrides: HashMap<TypeOverrideKey, TypeOverride>,
106    /// Unknown field names for query structs, set via `#[oxapi(query, field_name)]`.
107    query_unknown_fields: HashMap<QueryUnknownFieldKey, proc_macro2::Ident>,
108}
109
110impl TypeOverrides {
111    /// Create a new empty TypeOverrides.
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Add a rename override.
117    pub fn add_rename(
118        &mut self,
119        method: HttpMethod,
120        path: impl Into<String>,
121        kind: GeneratedTypeKind,
122        new_name: proc_macro2::Ident,
123    ) {
124        self.overrides.insert(
125            TypeOverrideKey {
126                method,
127                path: path.into(),
128                kind,
129            },
130            TypeOverride::Rename {
131                name: new_name,
132                attrs: Vec::new(),
133                variant_overrides: HashMap::new(),
134            },
135        );
136    }
137
138    /// Add a rename override with attributes and variant overrides.
139    pub fn add_rename_with_overrides(
140        &mut self,
141        method: HttpMethod,
142        path: impl Into<String>,
143        kind: GeneratedTypeKind,
144        new_name: proc_macro2::Ident,
145        attrs: Vec<TokenStream>,
146        variant_overrides: HashMap<u16, VariantOverride>,
147    ) {
148        self.overrides.insert(
149            TypeOverrideKey {
150                method,
151                path: path.into(),
152                kind,
153            },
154            TypeOverride::Rename {
155                name: new_name,
156                attrs,
157                variant_overrides,
158            },
159        );
160    }
161
162    /// Add a replace override.
163    pub fn add_replace(
164        &mut self,
165        method: HttpMethod,
166        path: impl Into<String>,
167        kind: GeneratedTypeKind,
168        replacement: TokenStream,
169    ) {
170        self.overrides.insert(
171            TypeOverrideKey {
172                method,
173                path: path.into(),
174                kind,
175            },
176            TypeOverride::Replace(replacement),
177        );
178    }
179
180    /// Get an override for a specific operation and kind.
181    pub fn get(
182        &self,
183        method: HttpMethod,
184        path: &str,
185        kind: GeneratedTypeKind,
186    ) -> Option<&TypeOverride> {
187        self.overrides.get(&TypeOverrideKey {
188            method,
189            path: path.to_string(),
190            kind,
191        })
192    }
193
194    /// Check if there's a replacement for this operation/kind.
195    pub fn is_replaced(&self, method: HttpMethod, path: &str, kind: GeneratedTypeKind) -> bool {
196        matches!(self.get(method, path, kind), Some(TypeOverride::Replace(_)))
197    }
198
199    /// Set the unknown field name for a query struct.
200    ///
201    /// This is used when `#[oxapi(query, field_name)]` is specified on a parameter.
202    pub fn set_query_unknown_field(
203        &mut self,
204        method: HttpMethod,
205        path: impl Into<String>,
206        field_name: proc_macro2::Ident,
207    ) {
208        self.query_unknown_fields.insert(
209            QueryUnknownFieldKey {
210                method,
211                path: path.into(),
212            },
213            field_name,
214        );
215    }
216
217    /// Get the unknown field name for a query struct, if set.
218    pub fn get_query_unknown_field(
219        &self,
220        method: HttpMethod,
221        path: &str,
222    ) -> Option<&proc_macro2::Ident> {
223        self.query_unknown_fields.get(&QueryUnknownFieldKey {
224            method,
225            path: path.to_string(),
226        })
227    }
228}
229
230#[derive(Error, Debug)]
231pub enum Error {
232    #[error("failed to parse OpenAPI spec: {0}")]
233    ParseError(String),
234
235    #[error("operation not found: {method} {path}")]
236    OperationNotFound { method: String, path: String },
237
238    #[error("missing operations in trait: {0:?}")]
239    MissingOperations(Vec<String>),
240
241    #[error("type generation error: {0}")]
242    TypeGenError(String),
243
244    #[error("invalid attribute: {0}")]
245    InvalidAttribute(String),
246
247    #[error("unsupported feature: {0}")]
248    Unsupported(String),
249
250    #[error("unknown schema '{name}'. Available schemas: {available}")]
251    UnknownSchema { name: String, available: String },
252}
253
254pub type Result<T> = std::result::Result<T, Error>;
255
256/// Builder for configuring and creating a Generator.
257pub struct GeneratorBuilder {
258    spec: OpenAPI,
259    settings: TypeSpaceSettings,
260    type_overrides: TypeOverrides,
261    response_suffixes: ResponseSuffixes,
262    schema_renames: HashMap<String, String>,
263}
264
265impl GeneratorBuilder {
266    /// Create a builder from an OpenAPI spec.
267    pub fn new(spec: OpenAPI) -> Self {
268        Self {
269            spec,
270            settings: TypeSpaceSettings::default(),
271            type_overrides: TypeOverrides::default(),
272            response_suffixes: ResponseSuffixes::default(),
273            schema_renames: HashMap::new(),
274        }
275    }
276
277    /// Create a builder by loading an OpenAPI spec from a file.
278    pub fn from_file(path: &std::path::Path) -> Result<Self> {
279        let spec = openapi::load_spec(path)?;
280        Ok(Self::new(spec))
281    }
282
283    /// Set custom type space settings.
284    pub fn settings(mut self, settings: TypeSpaceSettings) -> Self {
285        self.settings = settings;
286        self
287    }
288
289    /// Set type overrides for generated types.
290    pub fn type_overrides(mut self, overrides: TypeOverrides) -> Self {
291        self.type_overrides = overrides;
292        self
293    }
294
295    /// Set response suffixes configuration.
296    pub fn response_suffixes(mut self, suffixes: ResponseSuffixes) -> Self {
297        self.response_suffixes = suffixes;
298        self
299    }
300
301    /// Set schema renames (original name → new name).
302    pub fn schema_renames(mut self, renames: HashMap<String, String>) -> Self {
303        self.schema_renames = renames;
304        self
305    }
306
307    /// Build the Generator.
308    pub fn build(self) -> Result<Generator> {
309        let parsed = ParsedSpec::from_openapi(self.spec)?;
310        let type_gen = TypeGenerator::with_settings(&parsed, self.settings, self.schema_renames)?;
311
312        Ok(Generator {
313            spec: parsed,
314            type_gen,
315            type_overrides: self.type_overrides,
316            response_suffixes: self.response_suffixes,
317        })
318    }
319}
320
321/// Main generator that coordinates all the pieces.
322pub struct Generator {
323    spec: ParsedSpec,
324    type_gen: TypeGenerator,
325    type_overrides: TypeOverrides,
326    response_suffixes: ResponseSuffixes,
327}
328
329impl Generator {
330    /// Create a builder from an OpenAPI spec.
331    pub fn builder(spec: OpenAPI) -> GeneratorBuilder {
332        GeneratorBuilder::new(spec)
333    }
334
335    /// Create a builder by loading an OpenAPI spec from a file.
336    pub fn builder_from_file(path: &std::path::Path) -> Result<GeneratorBuilder> {
337        GeneratorBuilder::from_file(path)
338    }
339
340    /// Create a new generator from an OpenAPI spec with default settings.
341    pub fn new(spec: OpenAPI) -> Result<Self> {
342        GeneratorBuilder::new(spec).build()
343    }
344
345    /// Create a new generator from an OpenAPI spec with custom type settings.
346    pub fn with_settings(spec: OpenAPI, settings: TypeSpaceSettings) -> Result<Self> {
347        GeneratorBuilder::new(spec).settings(settings).build()
348    }
349
350    /// Load and parse an OpenAPI spec from a file path.
351    pub fn from_file(path: &std::path::Path) -> Result<Self> {
352        GeneratorBuilder::from_file(path)?.build()
353    }
354
355    /// Get the parsed spec.
356    pub fn spec(&self) -> &ParsedSpec {
357        &self.spec
358    }
359
360    /// Get the type generator.
361    pub fn type_generator(&self) -> &TypeGenerator {
362        &self.type_gen
363    }
364
365    /// Generate all types as a TokenStream.
366    pub fn generate_types(&self) -> TokenStream {
367        self.type_gen.generate_all_types()
368    }
369
370    /// Generate response enums for all operations.
371    pub fn generate_responses(&self) -> TokenStream {
372        ResponseGenerator::new(
373            &self.spec,
374            &self.type_gen,
375            &self.type_overrides,
376            &self.response_suffixes,
377        )
378        .generate_all()
379    }
380
381    /// Generate query parameter structs for all operations.
382    pub fn generate_query_structs(&self) -> TokenStream {
383        let mut structs = Vec::new();
384        for op in self.spec.operations() {
385            // Check if there's an unknown field specified for this operation's query struct
386            let unknown_field = self
387                .type_overrides
388                .get_query_unknown_field(op.method, &op.path);
389            if let Some((_, definition)) =
390                self.type_gen
391                    .generate_query_struct(op, &self.type_overrides, unknown_field)
392            {
393                structs.push(definition);
394            }
395        }
396        quote::quote! { #(#structs)* }
397    }
398
399    /// Generate path parameter structs for all operations.
400    pub fn generate_path_structs(&self) -> TokenStream {
401        let mut structs = Vec::new();
402        for op in self.spec.operations() {
403            if let Some((_, definition)) =
404                self.type_gen.generate_path_struct(op, &self.type_overrides)
405            {
406                structs.push(definition);
407            }
408        }
409        quote::quote! { #(#structs)* }
410    }
411
412    /// Get the type overrides.
413    pub fn type_overrides(&self) -> &TypeOverrides {
414        &self.type_overrides
415    }
416
417    /// Get the response suffixes.
418    pub fn response_suffixes(&self) -> &ResponseSuffixes {
419        &self.response_suffixes
420    }
421
422    /// Look up an operation by HTTP method and path.
423    pub fn get_operation(&self, method: HttpMethod, path: &str) -> Option<&Operation> {
424        self.spec.get_operation(method, path)
425    }
426
427    /// Get all operations.
428    pub fn operations(&self) -> impl Iterator<Item = &Operation> {
429        self.spec.operations()
430    }
431
432    /// Validate that all operations are covered by trait methods.
433    pub fn validate_coverage(&self, covered: &HashSet<(HttpMethod, String)>) -> Result<()> {
434        let mut missing = Vec::new();
435
436        for op in self.spec.operations() {
437            let key = (op.method, op.path.clone());
438            if !covered.contains(&key) {
439                missing.push(format!("{} {}", op.method, op.path));
440            }
441        }
442
443        if missing.is_empty() {
444            Ok(())
445        } else {
446            Err(Error::MissingOperations(missing))
447        }
448    }
449}