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