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, ¶m_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}