Skip to main content

oxapi_macro/
lib.rs

1//! Procedural macros for the oxapi OpenAPI server stub generator.
2//!
3//! This crate provides the `#[oxapi]` attribute macro for generating type-safe
4//! server stubs from OpenAPI specifications. See [`oxapi`] for full documentation.
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as TokenStream2;
8use quote::{ToTokens, quote, quote_spanned};
9use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
10use syn::{Ident, ItemMod, ItemTrait, LitStr, Token};
11
12/// Generate type-safe server stubs from OpenAPI specifications.
13///
14/// # Syntax
15///
16/// ```text
17/// #[oxapi(framework, "spec_path")]
18/// #[oxapi(framework, "spec_path", unwrap)]
19/// #[oxapi(framework, "spec_path", ok_suffix = "Response", err_suffix = "Error")]
20/// #[oxapi(framework, "spec_path", derive = (Debug, Clone))]
21/// #[oxapi(framework, "spec_path", unwrap, ok_suffix = "Ok", err_suffix = "Err")]
22/// ```
23///
24/// ## Arguments
25///
26/// - `framework`: The web framework to generate code for. Currently only `axum` is supported.
27/// - `spec_path`: Path to the OpenAPI specification file (JSON or YAML), relative to `Cargo.toml`.
28/// - `unwrap` (optional): Emit contents directly without wrapping in a module.
29/// - `ok_suffix` (optional): Suffix for success response types. Defaults to `"Response"`.
30/// - `err_suffix` (optional): Suffix for error response types. Defaults to `"Error"`.
31/// - `derive` (optional): Default derives for generated response enums. Defaults to `(Debug)`.
32///
33/// All options after the spec path can be specified in any order.
34///
35/// # Usage Modes
36///
37/// ## Single Trait Mode
38///
39/// Apply directly to a trait to generate types in a sibling `{trait_name}_types` module:
40///
41/// ```ignore
42/// #[oxapi::oxapi(axum, "spec.json")]
43/// trait PetService<S> {
44///     #[oxapi(map)]
45///     fn map_routes(router: Router<S>) -> Router<S>;
46///
47///     #[oxapi(get, "/pet/{petId}")]
48///     async fn get_pet(state: State<S>, pet_id: Path<_>) -> Result<GetPetResponse, GetPetError>;
49/// }
50/// ```
51///
52/// ## Module Mode
53///
54/// Apply to a module containing one or more traits. Types are generated in a shared `types` submodule:
55///
56/// ```ignore
57/// #[oxapi::oxapi(axum, "spec.json")]
58/// mod api {
59///     trait PetService<S> {
60///         #[oxapi(map)]
61///         fn map_routes(router: Router<S>) -> Router<S>;
62///
63///         #[oxapi(get, "/pet/{petId}")]
64///         async fn get_pet(state: State<S>, pet_id: Path<_>);
65///     }
66///
67///     trait StoreService<S> {
68///         #[oxapi(map)]
69///         fn map_routes(router: Router<S>) -> Router<S>;
70///
71///         #[oxapi(get, "/store/inventory")]
72///         async fn get_inventory(state: State<S>);
73///     }
74/// }
75/// ```
76///
77/// ## Unwrap Mode
78///
79/// Use `unwrap` to emit contents directly without a module wrapper:
80///
81/// ```ignore
82/// #[oxapi::oxapi(axum, "spec.json", unwrap)]
83/// mod api {
84///     trait PetService<S> { /* ... */ }
85/// }
86/// // Generates: pub mod types { ... } pub trait PetService<S> { ... }
87/// // Instead of: mod api { pub mod types { ... } pub trait PetService<S> { ... } }
88/// ```
89///
90/// # Method Attributes
91///
92/// ## `#[oxapi(map)]`
93///
94/// Marks a method as the route mapper. Must have signature `fn map_routes(router: Router<S>) -> Router<S>`.
95/// The macro fills in the body to register all routes:
96///
97/// ```ignore
98/// #[oxapi(map)]
99/// fn map_routes(router: Router<S>) -> Router<S>;
100/// // Generates body: router.route("/pet/{petId}", get(Self::get_pet)).route(...)
101/// ```
102///
103/// ## `#[oxapi(method, "path")]`
104///
105/// Maps a trait method to an OpenAPI operation. Supported methods: `get`, `post`, `put`, `delete`, `patch`, `head`, `options`.
106///
107/// ```ignore
108/// #[oxapi(get, "/pet/{petId}")]
109/// async fn get_pet(state: State<S>, pet_id: Path<_>);
110///
111/// #[oxapi(post, "/pet")]
112/// async fn add_pet(state: State<S>, body: Json<_>);
113/// ```
114///
115/// ## `#[oxapi(spec, "endpoint_path")]`
116///
117/// Serves the embedded OpenAPI spec at the given endpoint path. The method must be completely bare
118/// (no parameters, no return type, not async, no generics). The macro generates:
119/// - A method that returns `&'static str` containing the spec contents via `include_str!`
120/// - A GET route at the specified path (added to `map_routes`)
121///
122/// This endpoint does not appear in the OpenAPI spec itself but can be used for validation purposes.
123///
124/// ```ignore
125/// #[oxapi(spec, "/openapi.yaml")]
126/// fn spec();
127/// // Generates: fn spec() -> &'static str { include_str!("path/to/spec.yaml") }
128/// // And adds: .route("/openapi.yaml", get(|| async { Self::spec() }))
129/// ```
130///
131/// # Type Elision
132///
133/// Use `_` as a type parameter to have the macro infer the correct type from the OpenAPI spec:
134///
135/// - `Path<_>` → Inferred from path parameters (single type or tuple for multiple)
136/// - `Query<_>` → Generated query struct `{OperationId}Query`
137/// - `Json<_>` → Inferred from request body schema
138/// - `State<S>` → Passed through unchanged (user-provided)
139///
140/// ```ignore
141/// #[oxapi(get, "/pet/{petId}")]
142/// async fn get_pet(
143///     state: State<S>,       // User state, unchanged
144///     pet_id: Path<_>,       // Becomes Path<i64> based on spec
145///     query: Query<_>,       // Becomes Query<GetPetQuery>
146/// );
147///
148/// #[oxapi(post, "/pet")]
149/// async fn add_pet(
150///     state: State<S>,
151///     body: Json<_>,         // Becomes Json<Pet> based on spec
152/// );
153/// ```
154///
155/// # Generated Types
156///
157/// For each operation, the macro generates:
158///
159/// - `{OperationId}{ok_suffix}` - Enum with success response variants (2xx status codes), default suffix is `Response`
160/// - `{OperationId}{err_suffix}` - Enum with error response variants (4xx, 5xx, default), default suffix is `Error`
161/// - `{OperationId}Query` - Struct for query parameters (if operation has query params)
162///
163/// All response enums implement `axum::response::IntoResponse`.
164///
165/// # Type Customization
166///
167/// ## Schema Type Conversion
168///
169/// Replace JSON schema constructs with custom types using `#[convert(...)]` on the module:
170///
171/// ```ignore
172/// #[oxapi(axum, "spec.json")]
173/// #[convert(into = uuid::Uuid, type = "string", format = "uuid")]
174/// #[convert(into = rust_decimal::Decimal, type = "number")]
175/// mod api {
176///     // All schemas with type="string", format="uuid" use uuid::Uuid
177///     // All schemas with type="number" use rust_decimal::Decimal
178/// }
179/// ```
180///
181/// ### Convert Attribute Fields
182///
183/// - `into`: The replacement Rust type (required)
184/// - `type`: JSON schema type (`"string"`, `"number"`, `"integer"`, `"boolean"`, `"array"`, `"object"`)
185/// - `format`: JSON schema format (e.g., `"uuid"`, `"date-time"`, `"uri"`)
186///
187/// ## Schema Type Patching (Rename/Derive)
188///
189/// Rename schema types or add derives using struct declarations:
190///
191/// ```ignore
192/// #[oxapi(axum, "spec.json")]
193/// mod api {
194///     // Rename "Veggie" from the schema to "Vegetable" in generated code
195///     #[oxapi(Veggie)]
196///     struct Vegetable;
197///
198///     // Add extra derives to a schema type
199///     #[oxapi(Veggie)]
200///     #[derive(schemars::JsonSchema, PartialEq, Eq, Hash)]
201///     struct Vegetable;
202/// }
203/// ```
204///
205/// ## Schema Type Replacement
206///
207/// Replace a schema type entirely with an existing Rust type:
208///
209/// ```ignore
210/// #[oxapi(axum, "spec.json")]
211/// mod api {
212///     // Use my_networking::Ipv6Cidr instead of generating Ipv6Cidr
213///     #[oxapi]
214///     type Ipv6Cidr = my_networking::Ipv6Cidr;
215/// }
216/// ```
217///
218/// ## Generated Type Customization
219///
220/// Rename or replace the auto-generated `Response`/`Error`/`Query` types using operation coordinates:
221///
222/// ```ignore
223/// #[oxapi(axum, "spec.json")]
224/// mod api {
225///     // Rename GetPetByIdResponse to PetResponse
226///     #[oxapi(get, "/pet/{petId}", ok)]
227///     struct PetResponse;
228///
229///     // Rename GetPetByIdError to PetError
230///     #[oxapi(get, "/pet/{petId}", err)]
231///     struct PetError;
232///
233///     // Rename FindPetsByStatusQuery to PetSearchParams
234///     #[oxapi(get, "/pet/findByStatus", query)]
235///     struct PetSearchParams;
236///
237///     // Replace GetPetByIdResponse with a custom type (skips generation)
238///     #[oxapi(get, "/pet/{petId}", ok)]
239///     type _ = my_types::PetResponse;
240///
241///     // Replace GetPetByIdError with a custom type
242///     #[oxapi(get, "/pet/{petId}", err)]
243///     type _ = my_types::PetError;
244/// }
245/// ```
246///
247/// ## Enum Variant Renaming
248///
249/// Rename individual status code variants within response enums using the enum syntax:
250///
251/// ```ignore
252/// #[oxapi(axum, "spec.json")]
253/// mod api {
254///     // Rename the enum to PetError and customize specific variant names
255///     #[oxapi(get, "/pet/{petId}", err)]
256///     enum PetError {
257///         #[oxapi(status = 401)]
258///         Unauthorized,
259///         #[oxapi(status = 404)]
260///         NotFound,
261///     }
262///     // Generates: enum PetError { Unauthorized(...), NotFound(...), Status500(...), ... }
263///     // instead of: enum GetPetByIdError { Status401(...), Status404(...), Status500(...), ... }
264/// }
265/// ```
266///
267/// Only status codes you specify will be renamed; others retain their default `Status{code}` names.
268///
269/// ### Inline Type Naming
270///
271/// When a response has an inline schema (not a `$ref`), oxapi generates a struct for it.
272/// The default name is `{EnumName}{VariantName}`. You can override this by specifying
273/// a type name in the variant:
274///
275/// ```ignore
276/// #[oxapi(axum, "spec.json")]
277/// mod api {
278///     #[oxapi(get, "/store/inventory", ok)]
279///     enum InventoryResponse {
280///         #[oxapi(status = 200)]
281///         Success(Inventory),  // The inline schema struct will be named "Inventory"
282///     }
283/// }
284/// ```
285///
286/// Note: This only works for inline schemas. Using a type name with a `$ref` schema
287/// will result in a compile error.
288///
289/// ### Attribute Pass-Through
290///
291/// All attributes on the enum (except `#[oxapi(...)]`) and on variants (except `#[oxapi(...)]`)
292/// are passed through to the generated code. If you specify a `#[derive(...)]` on the enum,
293/// it completely overrides the default derives.
294///
295/// ```ignore
296/// #[oxapi(axum, "spec.json")]
297/// mod api {
298///     #[oxapi(get, "/pet/{petId}", err)]
299///     #[derive(Debug, thiserror::Error)]  // Overrides default, must include Debug if needed
300///     enum PetError {
301///         #[oxapi(status = 401)]
302///         #[error("Unauthorized access")]
303///         Unauthorized,
304///
305///         #[oxapi(status = 404)]
306///         #[error("Pet not found: {0}")]
307///         NotFound(String),
308///     }
309/// }
310/// ```
311///
312/// ### Kind Values
313///
314/// - `ok` - Success response enum (`{OperationId}{ok_suffix}`, default: `{OperationId}Response`). Supports variant renames with enum syntax.
315/// - `err` - Error response enum (`{OperationId}{err_suffix}`, default: `{OperationId}Error`). Supports variant renames with enum syntax.
316/// - `query` - Query parameters struct (`{OperationId}Query`)
317///
318/// # Complete Example
319///
320/// ```ignore
321/// use axum::{Router, extract::{State, Path, Query, Json}};
322///
323/// #[oxapi::oxapi(axum, "petstore.json")]
324/// #[convert(into = uuid::Uuid, type = "string", format = "uuid")]
325/// mod api {
326///     // Rename a schema type (Pet from OpenAPI becomes Animal in Rust)
327///     #[oxapi(Pet)]
328///     #[derive(Clone)]
329///     struct Animal;
330///
331///     // Replace a generated response type
332///     #[oxapi(get, "/pet/{petId}", err)]
333///     type _ = crate::errors::PetNotFound;
334///
335///     trait PetService<S: Clone + Send + Sync + 'static> {
336///         #[oxapi(map)]
337///         fn map_routes(router: Router<S>) -> Router<S>;
338///
339///         #[oxapi(get, "/pet/{petId}")]
340///         async fn get_pet(state: State<S>, pet_id: Path<_>);
341///
342///         #[oxapi(post, "/pet")]
343///         async fn add_pet(state: State<S>, body: Json<_>);
344///
345///         #[oxapi(get, "/pet/findByStatus")]
346///         async fn find_by_status(state: State<S>, query: Query<_>);
347///     }
348/// }
349///
350/// // Implement the trait
351/// struct MyPetService;
352///
353/// impl api::PetService<AppState> for MyPetService {
354///     fn map_routes(router: Router<AppState>) -> Router<AppState> {
355///         <Self as api::PetService<AppState>>::map_routes(router)
356///     }
357///
358///     async fn get_pet(
359///         State(state): State<AppState>,
360///         Path(pet_id): Path<i64>,
361///     ) -> Result<api::types::GetPetByIdResponse, crate::errors::PetNotFound> {
362///         // Implementation
363///     }
364///     // ...
365/// }
366/// ```
367#[proc_macro_attribute]
368pub fn oxapi(attr: TokenStream, item: TokenStream) -> TokenStream {
369    match do_oxapi(attr.into(), item.into()) {
370        Ok(tokens) => tokens.into(),
371        Err(err) => err.to_compile_error().into(),
372    }
373}
374
375fn do_oxapi(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
376    // Parse the attribute arguments: (framework, "spec_path")
377    let args = syn::parse2::<MacroArgs>(attr)?;
378
379    // Build response suffixes from macro args
380    let response_suffixes = oxapi_impl::ResponseSuffixes {
381        ok_suffix: args.ok_suffix.clone(),
382        err_suffix: args.err_suffix.clone(),
383        default_derives: args
384            .default_derives
385            .clone()
386            .unwrap_or_else(|| quote! { #[derive(Debug)] }),
387    };
388
389    // Try to parse as trait first, then as module
390    if let Ok(trait_item) = syn::parse2::<ItemTrait>(item.clone()) {
391        // Load the OpenAPI spec with default settings for traits
392        let spec_path = resolve_spec_path(&args.spec_path)?;
393        let generator = oxapi_impl::Generator::builder_from_file(&spec_path)
394            .map_err(|e| {
395                syn::Error::new(args.spec_path.span(), format!("failed to load spec: {}", e))
396            })?
397            .response_suffixes(response_suffixes)
398            .build()
399            .map_err(|e| {
400                syn::Error::new(args.spec_path.span(), format!("failed to load spec: {}", e))
401            })?;
402        let processor = TraitProcessor::new(generator, trait_item, spec_path)?;
403        processor.generate()
404    } else if let Ok(mod_item) = syn::parse2::<ItemMod>(item) {
405        // Parse convert attributes from module and build settings
406        let (settings, overrides, schema_renames) = build_type_settings(&mod_item)?;
407
408        // Convert to HashMap<String, String> for the generator (original_name -> new_name)
409        let schema_renames_for_gen: std::collections::HashMap<String, String> = schema_renames
410            .iter()
411            .map(|(k, rename)| (k.clone(), rename.new.to_string()))
412            .collect();
413
414        // Load the OpenAPI spec with custom settings
415        // Validation of schema renames happens during TypeGenerator construction
416        let spec_path = resolve_spec_path(&args.spec_path)?;
417        let generator = oxapi_impl::Generator::builder_from_file(&spec_path)
418            .map_err(|e| syn::Error::new(args.spec_path.span(), format!("{}", e)))?
419            .settings(settings)
420            .type_overrides(overrides)
421            .response_suffixes(response_suffixes)
422            .schema_renames(schema_renames_for_gen)
423            .build()
424            .map_err(|e| {
425                // Check if this is an UnknownSchema error and use the span from the original ident
426                if let oxapi_impl::Error::UnknownSchema { ref name, .. } = e
427                    && let Some(rename) = schema_renames.get(name)
428                {
429                    return syn::Error::new(rename.original.span(), format!("{}", e));
430                }
431                syn::Error::new(args.spec_path.span(), format!("{}", e))
432            })?;
433
434        let processor = ModuleProcessor::new(generator, mod_item, args.unwrap, spec_path)?;
435        processor.generate()
436    } else {
437        Err(syn::Error::new(
438            proc_macro2::Span::call_site(),
439            "expected trait or mod item",
440        ))
441    }
442}
443
444/// Resolve the spec path relative to CARGO_MANIFEST_DIR.
445fn resolve_spec_path(lit: &LitStr) -> syn::Result<std::path::PathBuf> {
446    let dir = std::env::var("CARGO_MANIFEST_DIR")
447        .map_err(|_| syn::Error::new(lit.span(), "CARGO_MANIFEST_DIR not set"))?;
448
449    let path = std::path::Path::new(&dir).join(lit.value());
450    Ok(path)
451}
452
453/// Rename tracking original and new tokens for name resolution and error reporting.
454struct Rename<T> {
455    original: T,
456    new: T,
457}
458
459/// Build TypeSpaceSettings, TypeOverrides, and schema renames from module attributes and items.
460fn build_type_settings(
461    mod_item: &ItemMod,
462) -> syn::Result<(
463    oxapi_impl::TypeSpaceSettings,
464    oxapi_impl::TypeOverrides,
465    std::collections::HashMap<String, Rename<Ident>>,
466)> {
467    let mut settings = oxapi_impl::TypeSpaceSettings::default();
468    let mut overrides = oxapi_impl::TypeOverrides::new();
469    let mut schema_renames = std::collections::HashMap::new();
470
471    // Parse #[convert(...)] attributes on the module
472    for attr in find_convert_attrs(&mod_item.attrs)? {
473        let schema = attr.to_schema();
474        let type_name = attr.into.to_token_stream().to_string();
475        settings.with_conversion(schema, type_name, std::iter::empty());
476    }
477
478    // Parse items inside the module for patches and replacements
479    if let Some((_, content)) = &mod_item.content {
480        for item in content {
481            match item {
482                // Struct with #[oxapi(...)] for renames
483                syn::Item::Struct(s) => {
484                    if let Some(oxapi_attr) = find_struct_oxapi_attr(&s.attrs)? {
485                        match oxapi_attr {
486                            StructOxapiAttr::Schema(original_ident) => {
487                                // Schema type rename: #[oxapi(OriginalName)] struct NewName;
488                                // The attribute specifies the schema name from OpenAPI,
489                                // the struct ident is what it gets renamed to.
490                                let original_name = original_ident.to_string();
491                                let new_name = s.ident.to_string();
492                                let mut patch = oxapi_impl::TypeSpacePatch::default();
493                                patch.with_rename(&new_name);
494
495                                // Also add any derives
496                                for derive_path in find_derives(&s.attrs)? {
497                                    patch.with_derive(derive_path.to_token_stream().to_string());
498                                }
499
500                                settings.with_patch(&original_name, &patch);
501                                schema_renames.insert(
502                                    original_name,
503                                    Rename {
504                                        original: original_ident,
505                                        new: s.ident.clone(),
506                                    },
507                                );
508                            }
509                            StructOxapiAttr::Operation(op) => {
510                                // Generated type rename: #[oxapi(get, "/path", ok)] struct NewName;
511                                overrides.add_rename(
512                                    op.method,
513                                    op.path.value(),
514                                    op.kind,
515                                    s.ident.clone(),
516                                );
517                            }
518                        }
519                    }
520                }
521
522                // Enum with #[oxapi(method, "path", kind)] for variant renames
523                syn::Item::Enum(e) => {
524                    if let Some(op) = find_enum_oxapi_attr(&e.attrs)? {
525                        let attrs = extract_passthrough_attrs(&e.attrs);
526                        let variant_overrides = parse_variant_overrides(&e.variants)?;
527                        overrides.add_rename_with_overrides(
528                            op.method,
529                            op.path.value(),
530                            op.kind,
531                            e.ident.clone(),
532                            attrs,
533                            variant_overrides,
534                        );
535                    }
536                }
537
538                // Type alias with #[oxapi] or #[oxapi(method, path, kind)] for replacements
539                syn::Item::Type(t) => {
540                    if let Some(oxapi_attr) = find_type_alias_oxapi_attr(&t.attrs)? {
541                        match oxapi_attr {
542                            TypeAliasOxapiAttr::Operation(op) => {
543                                // Generated type replacement: #[oxapi(get, "/path", ok)] type _ = T;
544                                let replacement = t.ty.to_token_stream();
545                                overrides.add_replace(
546                                    op.method,
547                                    op.path.value(),
548                                    op.kind,
549                                    replacement,
550                                );
551                            }
552                            TypeAliasOxapiAttr::Schema => {
553                                // Schema type replacement: #[oxapi] type Name = T;
554                                let type_name = t.ident.to_string();
555                                let replace_type = t.ty.to_token_stream().to_string();
556                                settings.with_replacement(
557                                    &type_name,
558                                    &replace_type,
559                                    std::iter::empty(),
560                                );
561                            }
562                        }
563                    }
564                }
565
566                _ => {}
567            }
568        }
569    }
570
571    Ok((settings, overrides, schema_renames))
572}
573
574/// Parsed macro arguments.
575struct MacroArgs {
576    spec_path: LitStr,
577    unwrap: bool,
578    ok_suffix: String,
579    err_suffix: String,
580    /// Default derives for generated enums (None means use Debug)
581    default_derives: Option<TokenStream2>,
582}
583
584impl syn::parse::Parse for MacroArgs {
585    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
586        let framework: Ident = input.parse()?;
587        input.parse::<Token![,]>()?;
588        let spec_path: LitStr = input.parse()?;
589
590        // Parse optional arguments (unordered)
591        let mut unwrap = false;
592        let mut ok_suffix: Option<String> = None;
593        let mut err_suffix: Option<String> = None;
594        let mut default_derives: Option<TokenStream2> = None;
595
596        while input.peek(Token![,]) {
597            input.parse::<Token![,]>()?;
598
599            // Check if this is a key = value pair or a bare identifier
600            let ident: Ident = input.parse()?;
601
602            if input.peek(Token![=]) {
603                // key = value syntax
604                input.parse::<Token![=]>()?;
605
606                match ident.to_string().as_str() {
607                    "ok_suffix" => {
608                        let value: LitStr = input.parse()?;
609                        if ok_suffix.is_some() {
610                            return Err(syn::Error::new(
611                                ident.span(),
612                                "ok_suffix specified more than once",
613                            ));
614                        }
615                        ok_suffix = Some(value.value());
616                    }
617                    "err_suffix" => {
618                        let value: LitStr = input.parse()?;
619                        if err_suffix.is_some() {
620                            return Err(syn::Error::new(
621                                ident.span(),
622                                "err_suffix specified more than once",
623                            ));
624                        }
625                        err_suffix = Some(value.value());
626                    }
627                    "derive" => {
628                        // Parse derive = (Trait1, Trait2, ...)
629                        if default_derives.is_some() {
630                            return Err(syn::Error::new(
631                                ident.span(),
632                                "derive specified more than once",
633                            ));
634                        }
635                        let content;
636                        syn::parenthesized!(content in input);
637                        let derives: syn::punctuated::Punctuated<syn::Path, Token![,]> =
638                            content.parse_terminated(syn::Path::parse, Token![,])?;
639                        default_derives = Some(quote! { #[derive(#derives)] });
640                    }
641                    other => {
642                        return Err(syn::Error::new(
643                            ident.span(),
644                            format!("unknown option: {}", other),
645                        ));
646                    }
647                }
648            } else {
649                // Bare identifier (flag)
650                match ident.to_string().as_str() {
651                    "unwrap" => {
652                        if unwrap {
653                            return Err(syn::Error::new(
654                                ident.span(),
655                                "unwrap specified more than once",
656                            ));
657                        }
658                        unwrap = true;
659                    }
660                    other => {
661                        return Err(syn::Error::new(
662                            ident.span(),
663                            format!(
664                                "unknown option: {} (expected 'unwrap', 'ok_suffix = \"...\"', 'err_suffix = \"...\"', or 'derive = (...)')",
665                                other
666                            ),
667                        ));
668                    }
669                }
670            }
671        }
672
673        // Only axum is supported for now
674        if framework.to_string().as_str() != "axum" {
675            return Err(syn::Error::new(
676                framework.span(),
677                format!("unsupported framework: {}", framework),
678            ));
679        }
680
681        Ok(MacroArgs {
682            spec_path,
683            unwrap,
684            ok_suffix: ok_suffix.unwrap_or_else(|| "Response".to_string()),
685            err_suffix: err_suffix.unwrap_or_else(|| "Error".to_string()),
686            default_derives,
687        })
688    }
689}
690
691/// Processes a trait and generates the output.
692struct TraitProcessor {
693    generator: oxapi_impl::Generator,
694    trait_item: ItemTrait,
695    /// Resolved path to the spec file for include_str!
696    spec_path: std::path::PathBuf,
697}
698
699impl TraitProcessor {
700    fn new(
701        generator: oxapi_impl::Generator,
702        trait_item: ItemTrait,
703        spec_path: std::path::PathBuf,
704    ) -> syn::Result<Self> {
705        Ok(Self {
706            generator,
707            trait_item,
708            spec_path,
709        })
710    }
711
712    fn generate(self) -> syn::Result<TokenStream2> {
713        use std::collections::HashMap;
714
715        let trait_name = &self.trait_item.ident;
716        let types_mod_name = syn::Ident::new(
717            &format!("{}_types", heck::AsSnakeCase(trait_name.to_string())),
718            trait_name.span(),
719        );
720
721        // Parse all method attributes and collect coverage info
722        let mut covered: HashMap<(oxapi_impl::HttpMethod, String), ()> = HashMap::new();
723        let mut map_method: Option<&syn::TraitItemFn> = None;
724        let mut spec_method: Option<(&syn::TraitItemFn, String)> = None;
725        let mut handler_methods: Vec<(&syn::TraitItemFn, oxapi_impl::HttpMethod, String)> =
726            Vec::new();
727
728        for item in &self.trait_item.items {
729            if let syn::TraitItem::Fn(method) = item {
730                if let Some(attr) = find_oxapi_attr(&method.attrs)? {
731                    match attr {
732                        OxapiAttr::Map => {
733                            map_method = Some(method);
734                        }
735                        OxapiAttr::Route {
736                            method: http_method,
737                            path,
738                        } => {
739                            covered.insert((http_method, path.clone()), ());
740                            handler_methods.push((method, http_method, path));
741                        }
742                        OxapiAttr::Spec { path } => {
743                            validate_bare_spec_method(method)?;
744                            // Spec routes count as GET coverage if the path exists in OpenAPI
745                            covered.insert((oxapi_impl::HttpMethod::Get, path.clone()), ());
746                            spec_method = Some((method, path));
747                        }
748                    }
749                } else {
750                    return Err(syn::Error::new_spanned(
751                        method,
752                        "all trait methods must have #[oxapi(...)] attribute",
753                    ));
754                }
755            }
756        }
757
758        // Validate coverage
759        self.generator
760            .validate_coverage(&covered)
761            .map_err(|e| syn::Error::new_spanned(&self.trait_item, e.to_string()))?;
762
763        // Generate types module
764        let types = self.generator.generate_types();
765        let responses = self.generator.generate_responses();
766        let query_structs = self.generator.generate_query_structs();
767
768        // Generate transformed methods
769        let mut transformed_methods = Vec::new();
770
771        // Generate map_routes if present
772        if let Some(map_fn) = map_method {
773            let map_body = oxapi_impl::RouterGenerator.generate_map_routes(
774                &handler_methods
775                    .iter()
776                    .map(|(m, method, path)| (m.sig.ident.clone(), *method, path.clone()))
777                    .collect::<Vec<_>>(),
778                spec_method
779                    .as_ref()
780                    .map(|(m, path)| (m.sig.ident.clone(), path.clone())),
781            );
782
783            let sig = &map_fn.sig;
784            transformed_methods.push(quote! {
785                #sig {
786                    #map_body
787                }
788            });
789        }
790
791        // Generate handler methods
792        let method_transformer =
793            oxapi_impl::MethodTransformer::new(&self.generator, &types_mod_name);
794        for (method, http_method, path) in &handler_methods {
795            let op = self
796                .generator
797                .get_operation(*http_method, path)
798                .ok_or_else(|| {
799                    syn::Error::new_spanned(
800                        method,
801                        format!("operation not found: {} {}", http_method, path),
802                    )
803                })?;
804
805            let transformed = method_transformer.transform(method, op)?;
806            transformed_methods.push(transformed);
807        }
808
809        // Generate spec method if present
810        if let Some((method, _endpoint_path)) = &spec_method {
811            let method_name = &method.sig.ident;
812            let spec_file_path = self.spec_path.to_string_lossy();
813            transformed_methods.push(quote! {
814                fn #method_name() -> &'static str {
815                    include_str!(#spec_file_path)
816                }
817            });
818        }
819
820        // Generate the full output
821        let vis = &self.trait_item.vis;
822        let trait_attrs: Vec<_> = self
823            .trait_item
824            .attrs
825            .iter()
826            .filter(|a| !a.path().is_ident("oxapi"))
827            .collect();
828
829        // Preserve generic parameters from the original trait
830        let generics = &self.trait_item.generics;
831        let where_clause = &generics.where_clause;
832
833        // Use trait name span so errors point to user's trait definition
834        let trait_def = quote_spanned! { trait_name.span() =>
835            #(#trait_attrs)*
836            #vis trait #trait_name #generics: 'static #where_clause {
837                #(#transformed_methods)*
838            }
839        };
840
841        let output = quote! {
842            #vis mod #types_mod_name {
843                use super::*;
844
845                #types
846                #responses
847                #query_structs
848            }
849
850            #trait_def
851        };
852
853        Ok(output)
854    }
855}
856
857/// Processes a module containing multiple traits.
858struct ModuleProcessor {
859    generator: oxapi_impl::Generator,
860    /// Module wrapper (visibility, name) - None if unwrap mode
861    mod_wrapper: Option<(syn::Visibility, Ident)>,
862    /// Module content items
863    content: Vec<syn::Item>,
864    /// Module span for error reporting
865    mod_span: proc_macro2::Span,
866    /// Resolved path to the spec file for include_str!
867    spec_path: std::path::PathBuf,
868}
869
870impl ModuleProcessor {
871    fn new(
872        generator: oxapi_impl::Generator,
873        mod_item: ItemMod,
874        unwrap: bool,
875        spec_path: std::path::PathBuf,
876    ) -> syn::Result<Self> {
877        // Module must have content (not just a declaration)
878        let mod_span = mod_item.ident.span();
879        let (_, content) = mod_item.content.ok_or_else(|| {
880            syn::Error::new(
881                mod_span,
882                "module must have inline content, not just a declaration",
883            )
884        })?;
885
886        let mod_wrapper = if unwrap {
887            None
888        } else {
889            Some((mod_item.vis, mod_item.ident))
890        };
891
892        Ok(Self {
893            generator,
894            mod_wrapper,
895            content,
896            mod_span,
897            spec_path,
898        })
899    }
900
901    fn generate(self) -> syn::Result<TokenStream2> {
902        use std::collections::HashMap;
903
904        // Find all traits in the module, filtering out patch/replace items
905        let mut traits: Vec<&ItemTrait> = Vec::new();
906        let mut other_items: Vec<&syn::Item> = Vec::new();
907
908        for item in &self.content {
909            match item {
910                syn::Item::Trait(t) => traits.push(t),
911                // Skip structs with #[oxapi(...)] (patch/override items)
912                syn::Item::Struct(s) if find_struct_oxapi_attr(&s.attrs)?.is_some() => {}
913                // Skip enums with #[oxapi(...)] (variant rename items)
914                syn::Item::Enum(e) if find_enum_oxapi_attr(&e.attrs)?.is_some() => {}
915                // Skip type aliases with #[oxapi] or #[oxapi(...)] (replacement items)
916                syn::Item::Type(t) if has_oxapi_attr(&t.attrs) => {}
917                other => other_items.push(other),
918            }
919        }
920
921        if traits.is_empty() {
922            return Err(syn::Error::new(
923                self.mod_span,
924                "module must contain at least one trait",
925            ));
926        }
927
928        // Collect all operations across all traits for coverage validation
929        let mut all_covered: HashMap<(oxapi_impl::HttpMethod, String), ()> = HashMap::new();
930
931        // Process each trait and collect handler info
932        struct TraitInfo<'a> {
933            trait_item: &'a ItemTrait,
934            map_method: Option<&'a syn::TraitItemFn>,
935            spec_method: Option<(&'a syn::TraitItemFn, String)>,
936            handler_methods: Vec<(&'a syn::TraitItemFn, oxapi_impl::HttpMethod, String)>,
937        }
938
939        let mut trait_infos: Vec<TraitInfo> = Vec::new();
940
941        for trait_item in &traits {
942            let mut map_method: Option<&syn::TraitItemFn> = None;
943            let mut spec_method: Option<(&syn::TraitItemFn, String)> = None;
944            let mut handler_methods: Vec<(&syn::TraitItemFn, oxapi_impl::HttpMethod, String)> =
945                Vec::new();
946
947            for item in &trait_item.items {
948                if let syn::TraitItem::Fn(method) = item {
949                    if let Some(attr) = find_oxapi_attr(&method.attrs)? {
950                        match attr {
951                            OxapiAttr::Map => {
952                                map_method = Some(method);
953                            }
954                            OxapiAttr::Route {
955                                method: http_method,
956                                path,
957                            } => {
958                                // Check for duplicates across traits
959                                let key = (http_method, path.clone());
960                                if all_covered.contains_key(&key) {
961                                    return Err(syn::Error::new_spanned(
962                                        method,
963                                        format!(
964                                            "operation {} {} is already defined in another trait",
965                                            http_method, path
966                                        ),
967                                    ));
968                                }
969                                all_covered.insert(key, ());
970                                handler_methods.push((method, http_method, path));
971                            }
972                            OxapiAttr::Spec { path } => {
973                                validate_bare_spec_method(method)?;
974                                // Spec routes count as GET coverage if the path exists in OpenAPI
975                                all_covered.insert((oxapi_impl::HttpMethod::Get, path.clone()), ());
976                                spec_method = Some((method, path));
977                            }
978                        }
979                    } else {
980                        return Err(syn::Error::new_spanned(
981                            method,
982                            "all trait methods must have #[oxapi(...)] attribute",
983                        ));
984                    }
985                }
986            }
987
988            trait_infos.push(TraitInfo {
989                trait_item,
990                map_method,
991                spec_method,
992                handler_methods,
993            });
994        }
995
996        // Validate coverage against spec
997        self.generator
998            .validate_coverage(&all_covered)
999            .map_err(|e| syn::Error::new(self.mod_span, e.to_string()))?;
1000
1001        // Generate shared types module
1002        let types = self.generator.generate_types();
1003        let responses = self.generator.generate_responses();
1004        let query_structs = self.generator.generate_query_structs();
1005
1006        // Generate each trait
1007        let types_mod_name = syn::Ident::new("types", proc_macro2::Span::call_site());
1008        let method_transformer =
1009            oxapi_impl::MethodTransformer::new(&self.generator, &types_mod_name);
1010
1011        let mut generated_traits = Vec::new();
1012
1013        for info in &trait_infos {
1014            let trait_name = &info.trait_item.ident;
1015            let _trait_vis = &info.trait_item.vis;
1016            let trait_attrs: Vec<_> = info
1017                .trait_item
1018                .attrs
1019                .iter()
1020                .filter(|a| !a.path().is_ident("oxapi"))
1021                .collect();
1022
1023            let mut transformed_methods = Vec::new();
1024
1025            // Generate map_routes if present
1026            if let Some(map_fn) = info.map_method {
1027                let map_body = oxapi_impl::RouterGenerator.generate_map_routes(
1028                    &info
1029                        .handler_methods
1030                        .iter()
1031                        .map(|(m, method, path)| (m.sig.ident.clone(), *method, path.clone()))
1032                        .collect::<Vec<_>>(),
1033                    info.spec_method
1034                        .as_ref()
1035                        .map(|(m, path)| (m.sig.ident.clone(), path.clone())),
1036                );
1037
1038                let sig = &map_fn.sig;
1039                transformed_methods.push(quote! {
1040                    #sig {
1041                        #map_body
1042                    }
1043                });
1044            }
1045
1046            // Generate handler methods
1047            for (method, http_method, path) in &info.handler_methods {
1048                let op = self
1049                    .generator
1050                    .get_operation(*http_method, path)
1051                    .ok_or_else(|| {
1052                        syn::Error::new_spanned(
1053                            method,
1054                            format!("operation not found: {} {}", http_method, path),
1055                        )
1056                    })?;
1057
1058                let transformed = method_transformer.transform(method, op)?;
1059                transformed_methods.push(transformed);
1060            }
1061
1062            // Generate spec method if present
1063            if let Some((method, _endpoint_path)) = &info.spec_method {
1064                let method_name = &method.sig.ident;
1065                let spec_file_path = self.spec_path.to_string_lossy();
1066                transformed_methods.push(quote! {
1067                    fn #method_name() -> &'static str {
1068                        include_str!(#spec_file_path)
1069                    }
1070                });
1071            }
1072
1073            // Traits in module are always pub (for external use)
1074            // Preserve generic parameters from the original trait
1075            let generics = &info.trait_item.generics;
1076            let where_clause = &generics.where_clause;
1077            // Use trait name span so errors point to user's trait definition
1078            generated_traits.push(quote_spanned! { trait_name.span() =>
1079                #(#trait_attrs)*
1080                pub trait #trait_name #generics: 'static #where_clause {
1081                    #(#transformed_methods)*
1082                }
1083            });
1084        }
1085
1086        // Generate the output
1087        let inner = quote! {
1088            pub mod #types_mod_name {
1089                use super::*;
1090
1091                #types
1092                #responses
1093                #query_structs
1094            }
1095
1096            #(#generated_traits)*
1097        };
1098
1099        let output = if let Some((vis, name)) = &self.mod_wrapper {
1100            quote! {
1101                #vis mod #name {
1102                    use super::*;
1103                    #inner
1104                }
1105            }
1106        } else {
1107            inner
1108        };
1109
1110        Ok(output)
1111    }
1112}
1113
1114/// Find and parse the #[oxapi(...)] attribute on a method.
1115fn find_oxapi_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<OxapiAttr>> {
1116    for attr in attrs {
1117        if attr.path().is_ident("oxapi") {
1118            return attr.parse_args::<OxapiAttr>().map(Some);
1119        }
1120    }
1121    Ok(None)
1122}
1123
1124/// Parsed #[oxapi(...)] attribute.
1125enum OxapiAttr {
1126    Map,
1127    Route {
1128        method: oxapi_impl::HttpMethod,
1129        path: String,
1130    },
1131    /// Spec endpoint: `#[oxapi(spec, "/openapi.yaml")]`
1132    /// Returns the embedded spec contents at the given path.
1133    Spec {
1134        path: String,
1135    },
1136}
1137
1138impl syn::parse::Parse for OxapiAttr {
1139    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1140        let ident: Ident = input.parse()?;
1141
1142        match ident.to_string().as_str() {
1143            "map" => Ok(OxapiAttr::Map),
1144            "spec" => {
1145                input.parse::<Token![,]>()?;
1146                let path: LitStr = input.parse()?;
1147                Ok(OxapiAttr::Spec { path: path.value() })
1148            }
1149            _ => {
1150                let method = parse_http_method(&ident)?;
1151                input.parse::<Token![,]>()?;
1152                let path: LitStr = input.parse()?;
1153                Ok(OxapiAttr::Route {
1154                    method,
1155                    path: path.value(),
1156                })
1157            }
1158        }
1159    }
1160}
1161
1162fn parse_http_method(ident: &Ident) -> syn::Result<oxapi_impl::HttpMethod> {
1163    match ident.to_string().as_str() {
1164        "get" => Ok(oxapi_impl::HttpMethod::Get),
1165        "post" => Ok(oxapi_impl::HttpMethod::Post),
1166        "put" => Ok(oxapi_impl::HttpMethod::Put),
1167        "delete" => Ok(oxapi_impl::HttpMethod::Delete),
1168        "patch" => Ok(oxapi_impl::HttpMethod::Patch),
1169        "head" => Ok(oxapi_impl::HttpMethod::Head),
1170        "options" => Ok(oxapi_impl::HttpMethod::Options),
1171        other => Err(syn::Error::new(
1172            ident.span(),
1173            format!("unknown HTTP method: {}", other),
1174        )),
1175    }
1176}
1177
1178/// Validate that a spec method is completely bare: no parameters, no return type, not async.
1179fn validate_bare_spec_method(method: &syn::TraitItemFn) -> syn::Result<()> {
1180    // Check for async
1181    if method.sig.asyncness.is_some() {
1182        return Err(syn::Error::new_spanned(
1183            method.sig.asyncness,
1184            "spec method must not be async",
1185        ));
1186    }
1187
1188    // Check for parameters
1189    if !method.sig.inputs.is_empty() {
1190        return Err(syn::Error::new_spanned(
1191            &method.sig.inputs,
1192            "spec method must have no parameters",
1193        ));
1194    }
1195
1196    // Check for return type
1197    if !matches!(method.sig.output, syn::ReturnType::Default) {
1198        return Err(syn::Error::new_spanned(
1199            &method.sig.output,
1200            "spec method must have no return type (it will be generated)",
1201        ));
1202    }
1203
1204    // Check for generics
1205    if !method.sig.generics.params.is_empty() {
1206        return Err(syn::Error::new_spanned(
1207            &method.sig.generics,
1208            "spec method must not have generics",
1209        ));
1210    }
1211
1212    Ok(())
1213}
1214
1215/// Parsed #[convert(into = Type, type = "...", format = "...")] attribute.
1216struct ConvertAttr {
1217    into: syn::Type,
1218    type_field: Option<LitStr>,
1219    format: Option<LitStr>,
1220}
1221
1222impl syn::parse::Parse for ConvertAttr {
1223    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1224        let mut into: Option<syn::Type> = None;
1225        let mut type_field: Option<LitStr> = None;
1226        let mut format: Option<LitStr> = None;
1227
1228        while !input.is_empty() {
1229            let key: Ident = input.parse()?;
1230            input.parse::<Token![=]>()?;
1231
1232            match key.to_string().as_str() {
1233                "into" => {
1234                    into = Some(input.parse()?);
1235                }
1236                "type" => {
1237                    type_field = Some(input.parse()?);
1238                }
1239                "format" => {
1240                    format = Some(input.parse()?);
1241                }
1242                other => {
1243                    return Err(syn::Error::new(
1244                        key.span(),
1245                        format!("unknown convert attribute: {}", other),
1246                    ));
1247                }
1248            }
1249
1250            if !input.is_empty() {
1251                input.parse::<Token![,]>()?;
1252            }
1253        }
1254
1255        let into =
1256            into.ok_or_else(|| syn::Error::new(input.span(), "convert requires 'into' field"))?;
1257
1258        Ok(ConvertAttr {
1259            into,
1260            type_field,
1261            format,
1262        })
1263    }
1264}
1265
1266impl ConvertAttr {
1267    /// Build a schemars SchemaObject from the type/format fields.
1268    fn to_schema(&self) -> SchemaObject {
1269        let instance_type = self.type_field.as_ref().map(|t| {
1270            let ty = match t.value().as_str() {
1271                "string" => InstanceType::String,
1272                "number" => InstanceType::Number,
1273                "integer" => InstanceType::Integer,
1274                "boolean" => InstanceType::Boolean,
1275                "array" => InstanceType::Array,
1276                "object" => InstanceType::Object,
1277                "null" => InstanceType::Null,
1278                _ => InstanceType::String, // fallback
1279            };
1280            SingleOrVec::Single(Box::new(ty))
1281        });
1282
1283        SchemaObject {
1284            instance_type,
1285            format: self.format.as_ref().map(|f| f.value()),
1286            ..Default::default()
1287        }
1288    }
1289}
1290
1291/// Find all #[convert(...)] attributes on an item.
1292fn find_convert_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<ConvertAttr>> {
1293    let mut result = Vec::new();
1294    for attr in attrs {
1295        if attr.path().is_ident("convert") {
1296            result.push(attr.parse_args::<ConvertAttr>()?);
1297        }
1298    }
1299    Ok(result)
1300}
1301
1302/// Parsed `#[oxapi(SchemaName)]` attribute for schema type renames.
1303struct SchemaRenameAttr(Ident);
1304
1305impl syn::parse::Parse for SchemaRenameAttr {
1306    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1307        let ident: Ident = input.parse()?;
1308        Ok(SchemaRenameAttr(ident))
1309    }
1310}
1311
1312/// Parsed `#[oxapi(method, "path", kind)]` attribute for generated type renames/replaces.
1313struct OpAttr {
1314    method: oxapi_impl::HttpMethod,
1315    path: LitStr,
1316    kind: oxapi_impl::GeneratedTypeKind,
1317}
1318
1319impl syn::parse::Parse for OpAttr {
1320    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1321        let method_ident: Ident = input.parse()?;
1322        let method = parse_http_method(&method_ident)?;
1323        input.parse::<Token![,]>()?;
1324        let path: LitStr = input.parse()?;
1325        input.parse::<Token![,]>()?;
1326        let kind_ident: Ident = input.parse()?;
1327        let kind = parse_type_kind(&kind_ident)?;
1328        Ok(OpAttr { method, path, kind })
1329    }
1330}
1331
1332fn parse_type_kind(ident: &Ident) -> syn::Result<oxapi_impl::GeneratedTypeKind> {
1333    match ident.to_string().as_str() {
1334        "ok" => Ok(oxapi_impl::GeneratedTypeKind::Ok),
1335        "err" => Ok(oxapi_impl::GeneratedTypeKind::Err),
1336        "query" => Ok(oxapi_impl::GeneratedTypeKind::Query),
1337        other => Err(syn::Error::new(
1338            ident.span(),
1339            format!("unknown type kind: {} (expected ok, err, or query)", other),
1340        )),
1341    }
1342}
1343
1344/// Parsed `#[oxapi(status = code)]` attribute on enum variant.
1345struct VariantStatusAttr(u16);
1346
1347impl syn::parse::Parse for VariantStatusAttr {
1348    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1349        let ident: Ident = input.parse()?;
1350        if ident != "status" {
1351            return Err(syn::Error::new(
1352                ident.span(),
1353                format!("expected 'status', found '{}'", ident),
1354            ));
1355        }
1356        input.parse::<Token![=]>()?;
1357        let lit: syn::LitInt = input.parse()?;
1358        let code: u16 = lit.base10_parse()?;
1359        Ok(VariantStatusAttr(code))
1360    }
1361}
1362
1363/// Find `#[oxapi(status = code)]` attribute and return remaining attributes.
1364/// Returns (status_code, pass_through_attrs) where pass_through_attrs excludes the `#[oxapi]`.
1365/// Returns an error if more than one `#[oxapi]` attribute is found.
1366fn extract_variant_status_and_attrs(
1367    attrs: &[syn::Attribute],
1368) -> syn::Result<Option<(u16, Vec<proc_macro2::TokenStream>)>> {
1369    let mut found_status: Option<(u16, proc_macro2::Span)> = None;
1370    let mut pass_through = Vec::new();
1371
1372    for attr in attrs {
1373        if attr.path().is_ident("oxapi") {
1374            if found_status.is_some() {
1375                return Err(syn::Error::new_spanned(
1376                    attr,
1377                    "duplicate #[oxapi] attribute; only one #[oxapi(status = ...)] allowed per variant",
1378                ));
1379            }
1380            let status: VariantStatusAttr = attr.parse_args()?;
1381            found_status = Some((status.0, attr.path().get_ident().unwrap().span()));
1382        } else {
1383            pass_through.push(quote::quote! { #attr });
1384        }
1385    }
1386
1387    Ok(found_status.map(|(s, _)| (s, pass_through)))
1388}
1389
1390/// Parse variant overrides from an enum's variants.
1391/// Extracts variant name, optional inner type name (from tuple variant), and attributes.
1392fn parse_variant_overrides(
1393    variants: &syn::punctuated::Punctuated<syn::Variant, Token![,]>,
1394) -> syn::Result<std::collections::HashMap<u16, oxapi_impl::VariantOverride>> {
1395    let mut overrides = std::collections::HashMap::new();
1396    for variant in variants {
1397        if let Some((status, attrs)) = extract_variant_status_and_attrs(&variant.attrs)? {
1398            // Extract inner type name from tuple variant like `Success(TheResponse)`
1399            let inner_type_name = extract_inner_type_ident(&variant.fields)?;
1400
1401            overrides.insert(
1402                status,
1403                oxapi_impl::VariantOverride {
1404                    name: variant.ident.clone(),
1405                    inner_type_name,
1406                    attrs,
1407                },
1408            );
1409        }
1410    }
1411    Ok(overrides)
1412}
1413
1414/// Extract the inner type identifier from a tuple variant's single field.
1415/// For `Success(TheResponse)`, returns `Some(TheResponse)`.
1416/// For `Success` (unit variant), returns `None`.
1417fn extract_inner_type_ident(fields: &syn::Fields) -> syn::Result<Option<syn::Ident>> {
1418    match fields {
1419        syn::Fields::Unit => Ok(None),
1420        syn::Fields::Unnamed(unnamed) => {
1421            if unnamed.unnamed.len() != 1 {
1422                return Err(syn::Error::new_spanned(
1423                    unnamed,
1424                    "variant must have exactly one field for inline type override",
1425                ));
1426            }
1427            let field = unnamed.unnamed.first().unwrap();
1428            // The type should be a simple path like `TheResponse`
1429            if let syn::Type::Path(type_path) = &field.ty
1430                && type_path.qself.is_none()
1431                && type_path.path.segments.len() == 1
1432            {
1433                let segment = type_path.path.segments.first().unwrap();
1434                if segment.arguments.is_empty() {
1435                    return Ok(Some(segment.ident.clone()));
1436                }
1437            }
1438            Err(syn::Error::new_spanned(
1439                &field.ty,
1440                "inner type must be a simple identifier (e.g., `MyTypeName`)",
1441            ))
1442        }
1443        syn::Fields::Named(named) => Err(syn::Error::new_spanned(
1444            named,
1445            "named fields not supported for variant overrides; use tuple variant like `Success(TypeName)`",
1446        )),
1447    }
1448}
1449
1450/// Extract all attributes except `#[oxapi(...)]` as TokenStreams.
1451fn extract_passthrough_attrs(attrs: &[syn::Attribute]) -> Vec<proc_macro2::TokenStream> {
1452    attrs
1453        .iter()
1454        .filter(|attr| !attr.path().is_ident("oxapi"))
1455        .map(|attr| quote::quote! { #attr })
1456        .collect()
1457}
1458
1459/// Result of parsing a `#[oxapi(...)]` attribute on a struct (rename context).
1460enum StructOxapiAttr {
1461    /// Simple rename for schema types: `#[oxapi(SchemaName)]`
1462    Schema(Ident),
1463    /// Operation rename for generated types: `#[oxapi(get, "/path", ok)]`
1464    Operation(OpAttr),
1465}
1466
1467/// Find a single `#[oxapi(...)]` attribute and parse it with the given parser.
1468/// Returns an error if more than one `#[oxapi]` attribute is found.
1469fn find_single_oxapi_attr<T, F>(attrs: &[syn::Attribute], parser: F) -> syn::Result<Option<T>>
1470where
1471    F: FnOnce(&syn::Attribute) -> syn::Result<T>,
1472{
1473    let mut oxapi_attr: Option<&syn::Attribute> = None;
1474
1475    for attr in attrs {
1476        if attr.path().is_ident("oxapi") {
1477            if oxapi_attr.is_some() {
1478                return Err(syn::Error::new_spanned(
1479                    attr,
1480                    "duplicate #[oxapi] attribute; only one allowed per item",
1481                ));
1482            }
1483            oxapi_attr = Some(attr);
1484        }
1485    }
1486
1487    oxapi_attr.map(parser).transpose()
1488}
1489
1490/// Find `#[oxapi(...)]` attribute on a struct (for renames).
1491fn find_struct_oxapi_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<StructOxapiAttr>> {
1492    find_single_oxapi_attr(attrs, |attr| {
1493        // Try operation syntax first, then schema syntax (single ident)
1494        if let Ok(op) = attr.parse_args::<OpAttr>() {
1495            Ok(StructOxapiAttr::Operation(op))
1496        } else {
1497            let schema: SchemaRenameAttr = attr.parse_args()?;
1498            Ok(StructOxapiAttr::Schema(schema.0))
1499        }
1500    })
1501}
1502
1503/// Result of parsing a `#[oxapi(...)]` attribute on a type alias (replace context).
1504enum TypeAliasOxapiAttr {
1505    /// Schema type replacement: `#[oxapi]` (no args)
1506    Schema,
1507    /// Operation replacement: `#[oxapi(get, "/path", ok)]`
1508    Operation(OpAttr),
1509}
1510
1511/// Find `#[oxapi(...)]` attribute on a type alias (for replacements).
1512fn find_type_alias_oxapi_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<TypeAliasOxapiAttr>> {
1513    find_single_oxapi_attr(attrs, |attr| {
1514        // Check if it has arguments (operation style) or not (schema style)
1515        if let syn::Meta::Path(_) = &attr.meta {
1516            // No args: #[oxapi]
1517            Ok(TypeAliasOxapiAttr::Schema)
1518        } else if let Ok(op) = attr.parse_args::<OpAttr>() {
1519            Ok(TypeAliasOxapiAttr::Operation(op))
1520        } else {
1521            // No args but in list form: #[oxapi()]
1522            Ok(TypeAliasOxapiAttr::Schema)
1523        }
1524    })
1525}
1526
1527/// Check if an item has `#[oxapi]` attribute (any style).
1528fn has_oxapi_attr(attrs: &[syn::Attribute]) -> bool {
1529    attrs.iter().any(|attr| attr.path().is_ident("oxapi"))
1530}
1531
1532/// Find `#[oxapi(...)]` attribute on an enum (for renames).
1533/// For enums, we only support operation syntax: `#[oxapi(get, "/path", err)]`
1534fn find_enum_oxapi_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<OpAttr>> {
1535    find_single_oxapi_attr(attrs, |attr| attr.parse_args())
1536}
1537
1538/// Find #[derive(...)] attributes and extract the derive paths.
1539fn find_derives(attrs: &[syn::Attribute]) -> syn::Result<Vec<syn::Path>> {
1540    let mut derives = Vec::new();
1541    for attr in attrs {
1542        if attr.path().is_ident("derive") {
1543            let nested: syn::punctuated::Punctuated<syn::Path, Token![,]> =
1544                attr.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
1545            derives.extend(nested);
1546        }
1547    }
1548    Ok(derives)
1549}