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