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