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