Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/aide-macro-impl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "aide-macro-impl"
version = "0.1.0"
edition = "2024"

[dependencies]
proc-macro2 = "1.0.103"
quote = "1.0.42"
syn = "2.0.110"
1 change: 1 addition & 0 deletions crates/aide-macro-impl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod operation_output;
132 changes: 132 additions & 0 deletions crates/aide-macro-impl/src/operation_output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Expr};

/// Derive macro implementation for OperationOutput trait on enum response types
pub fn operation_output_impl(input: DeriveInput) -> Result<TokenStream, syn::Error> {
let enum_name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

// Ensure this is an enum
let data_enum = match &input.data {
syn::Data::Enum(data) => data,
_ => {
return Err(syn::Error::new_spanned(
input,
"OperationOutput can only be derived for enums",
));
}
};

// Parse variants and extract status codes from attributes
let mut variants_with_status: Vec<(syn::Ident, syn::Type, Option<Expr>)> = Vec::new();

for variant in &data_enum.variants {
// Ensure the variant is a tuple variant with exactly one field
let field = match &variant.fields {
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => &fields.unnamed[0],
_ => {
return Err(syn::Error::new_spanned(
variant,
"OperationOutput can only be derived for enums with tuple variants containing exactly one field",
));
}
};

let variant_name = variant.ident.clone();
let field_type = field.ty.clone();

// Look for #[aide(status_code = ...)] attribute
let mut status_code = None;
for attr in &variant.attrs {
if attr.path().is_ident("aide") {
// Parse the attribute tokens directly to support both literals and const expressions
let parsed = attr.parse_args_with(|input: syn::parse::ParseStream| {
// Parse: status_code = <expr>
let name: syn::Ident = input.parse()?;
if name == "status_code" {
let _eq: syn::Token![=] = input.parse()?;
let expr: Expr = input.parse()?;
Ok(Some(expr))
} else {
Ok(None)
}
})?;

if parsed.is_some() {
status_code = parsed;
}
}
}

variants_with_status.push((variant_name, field_type, status_code));
}

// Generate the implementation
let operation_response_impl = if variants_with_status.len() == 1 {
let (_, field_type, _) = &variants_with_status[0];
quote! {
fn operation_response(
ctx: &mut ::aide::generate::GenContext,
operation: &mut ::aide::openapi::Operation,
) -> Option<::aide::openapi::Response> {
// For single variant enums, we can use the field type's operation_response
<#field_type as ::aide::OperationOutput>::operation_response(ctx, operation)
}
}
} else {
quote! {
fn operation_response(
_ctx: &mut ::aide::generate::GenContext,
_operation: &mut ::aide::openapi::Operation,
) -> Option<::aide::openapi::Response> {
// For enum responses with multiple variants, we return None here
// and let inferred_responses handle it
None
}
}
};

let inferred_responses_impl = {
let response_arms = variants_with_status.iter().map(|(_variant_name, field_type, status_code)| {
if let Some(code) = status_code {
// If status_code attribute is provided, use it and get the response from operation_response
quote! {
if let Some(err_resp) = <#field_type as ::aide::OperationOutput>::operation_response(ctx, operation) {
responses.push((
Some(::aide::openapi::StatusCode::Code(#code)),
err_resp,
));
}
}
} else {
// If no status_code attribute, use the type's inferred_responses
quote! {
responses.extend(<#field_type as ::aide::OperationOutput>::inferred_responses(ctx, operation));
}
}
});

quote! {
fn inferred_responses(
ctx: &mut ::aide::generate::GenContext,
operation: &mut ::aide::openapi::Operation,
) -> Vec<(Option<::aide::openapi::StatusCode>, ::aide::openapi::Response)> {
let mut responses = Vec::new();
#(#response_arms)*
responses
}
}
};

Ok(quote! {
impl #impl_generics ::aide::OperationOutput for #enum_name #ty_generics #where_clause {
type Inner = Self;

#operation_response_impl

#inferred_responses_impl
}
})
}
3 changes: 2 additions & 1 deletion crates/aide-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aide-macros"
version = "0.16.0-alpha.1" # remember to update the dependency in aide, even for patch releases
version = "0.16.0-alpha.1" # remember to update the dependency in aide, even for patch releases
authors = ["tamasfe"]
edition = "2021"
license = "MIT OR Apache-2.0"
Expand All @@ -14,6 +14,7 @@ darling = "0.20.0"
quote = "1.0.21"
syn = "2.0.15"
proc-macro2 = "1.0"
aide-macro-impl = { path = "../aide-macro-impl" }

[features]
axum-extra-typed-routing = []
64 changes: 64 additions & 0 deletions crates/aide-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use aide_macro_impl::operation_output::operation_output_impl;
use darling::FromDeriveInput;
use proc_macro::TokenStream;
use quote::quote;
Expand Down Expand Up @@ -189,3 +190,66 @@ pub fn axum_typed_path(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
.into()
}

/// Derive macro for OperationOutput trait.
///
/// NOTE: This is currently under development and is only available for enums
/// that contain only tuple variants with a single value. All values must already implement
/// OperationOutput.
///
/// ```ignore
/// use aide::axum::IntoApiResponse;
/// use aide::{OperationIo, derive::OperationOutput};
/// use axum::response::IntoResponse;
/// use schemars::JsonSchema;
/// use serde::Serialize;
///
/// pub const INTERNAL_SERVER_ERROR: u16 = 500;
///
/// // Using the simple implementation of OperationOutput here for brevity.
/// #[derive(Debug, Serialize, JsonSchema, OperationIo)]
/// #[aide(output)]
/// pub struct MyResponse {
/// pub success: bool,
/// }
///
/// #[derive(Debug, Serialize, OperationOutput)]
/// pub enum MyMultiResponse {
/// // Use a literal status code.
/// #[aide(status_code = 200)]
/// Success(MyResponse),
/// // Use any const status code.
/// //Hint: This includes http::StatusCode::as_u16()
/// #[aide(status_code=INTERNAL_SERVER_ERROR)]
/// Failure(MyResponse),
/// // Use an existing implementation's default status code.
/// DefaultStatusCode(String),
/// // Override an existing implementation's status code.
/// #[aide(status_code = 201)]
/// DefaultStatusCodeOverride(String),
/// // Use no status code.
/// NoStatusCode(MyResponse),
/// }
///
/// /// This implementation is required for the blanket implementation of [`IntoApiResponse`]
/// /// however it is not within the perview of this macro, so we will keep it short. (And
/// /// incorrect)
/// impl IntoResponse for MyMultiResponse {
/// fn into_response(self) -> axum::response::Response {
/// (StatusCode::OK, Body::from("Success")).into_response()
/// }
/// }
///
/// pub async fn my_handler() -> impl IntoApiResponse {
/// MyMultiResponse::Success(MyResponse { success: true })
/// }
/// ```
#[proc_macro_derive(OperationOutput, attributes(aide))]
pub fn derive_operation_output(item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as syn::DeriveInput);

match operation_output_impl(item) {
Ok(tokens) => tokens.into(),
Err(err) => TokenStream::from(err.to_compile_error()),
}
}
15 changes: 11 additions & 4 deletions crates/aide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ scalar = []
skip_serializing_defaults = []
serde_qs = ["dep:serde_qs"]

axum = ["dep:axum", "bytes", "http", "dep:tower-layer", "dep:tower-service", "serde_qs?/axum"]
axum = [
"dep:axum",
"bytes",
"http",
"dep:tower-layer",
"dep:tower-service",
"serde_qs?/axum",
]
axum-form = ["axum", "axum/form"]
axum-json = ["axum", "axum/json"]
axum-matched-path = ["axum", "axum/matched-path"]
Expand All @@ -56,9 +63,9 @@ axum-extra-query = ["axum-extra", "axum-extra/query"]
axum-extra-json-deserializer = ["axum-extra", "axum-extra/json-deserializer"]
axum-extra-routing = ["axum-extra", "axum-extra/routing"]
axum-extra-typed-routing = [
"axum-extra-routing",
"axum-extra/typed-routing",
"aide-macros?/axum-extra-typed-routing",
"axum-extra-routing",
"axum-extra/typed-routing",
"aide-macros?/axum-extra-typed-routing",
]
axum-extra-with-rejection = ["axum-extra", "axum-extra/with-rejection"]

Expand Down
6 changes: 6 additions & 0 deletions crates/aide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,9 @@ pub use operation::{OperationInput, OperationOutput};

#[cfg(feature = "macros")]
pub use aide_macros::OperationIo;

/// Derive macros that are under development.
#[cfg(feature = "macros")]
pub mod derive {
pub use aide_macros::OperationOutput;
}
51 changes: 51 additions & 0 deletions crates/aide/tests/test_op_out_derive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/// This is the same code that lives in the [`OperationOutputDerive`] doc.
#[cfg(feature = "axum")]
#[test]
fn test_op_out_derive() {
use aide::axum::IntoApiResponse;
use aide::{derive::OperationOutput, OperationIo};
use axum::{body::Body, response::IntoResponse};
use http::StatusCode;
use schemars::JsonSchema;
use serde::Serialize;

pub const INTERNAL_SERVER_ERROR: u16 = 500;

// Using the simple implementation of OperationOutput here for brevity.
#[derive(Debug, Serialize, JsonSchema, OperationIo)]
#[aide(output)]
pub struct MyResponse {
pub success: bool,
}

#[derive(Debug, Serialize, OperationOutput)]
pub enum MyMultiResponse {
// Use a literal status code.
#[aide(status_code = 200)]
Success(MyResponse),
// Use any const status code. Hint: This includes
// http::StatusCode::as_u16()
#[aide(status_code=INTERNAL_SERVER_ERROR)]
Failure(MyResponse),
// Use an existing implementation's default status code.
DefaultStatusCode(String),
// Override an existing implementation's status code.
#[aide(status_code = 201)]
DefaultStatusCodeOverride(String),
// Use no status code.
NoStatusCode(MyResponse),
}

/// This implementation is required for the blanket implementation of [`IntoApiResponse`]
/// however it is not within the perview of this macro, so we will keep it short. (And
/// incorrect)
impl IntoResponse for MyMultiResponse {
fn into_response(self) -> axum::response::Response {
(StatusCode::OK, Body::from("Success")).into_response()
}
}

pub async fn my_handler() -> impl IntoApiResponse {
MyMultiResponse::Success(MyResponse { success: true })
}
}