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