Skip to content
Merged
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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,46 @@ fn main() {
}
```

## Path parameters from function signature (axum)

With the `axum` feature enabled, path parameters are inferred from the
function signature, so handlers that use the axum `Path<...>` extractor no
longer need to repeat their names in `parameters(path(...))`. Two binding
shapes are recognized:

```rust,no_run
use axum::extract::Path;
use okapi_operation::{axum_integration::*, *};

// Single path parameter — inferred as `system: String`.
#[openapi(operation_id = "get_system")]
async fn get_system(Path(system): Path<String>) -> String {
system
}

// Tuple — inferred in order: `system: String`, `backup_name: String`.
#[openapi(operation_id = "abort_backup")]
async fn abort_backup(Path((system, backup_name)): Path<(String, String)>) {
let _ = (system, backup_name);
}
```

Anything more involved (struct extractors, wildcard `_` bindings, references,
…) is left to explicit `parameters(path(...))` declarations, which always
take precedence over inferred entries with the same name.

## Cookie parameters

Cookie parameters are declared the same way as the other kinds:

```rust,no_run
use okapi_operation::*;

#[openapi(
operation_id = "me",
parameters(cookie(name = "session", required = true, schema = "String")),
)]
async fn me() {}
## Parameters

Parameters (`path`, `query`, `header`, `cookie`) support two mutually exclusive ways to describe the parameter value:
Expand Down
17 changes: 17 additions & 0 deletions okapi-operation-macro/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ This project follows the [Semantic Versioning standard](https://semver.org/).

### Added

- Path parameters are now inferred from the function signature when the `axum`
feature is enabled. The axum `Path<...>` extractor is recognized in two
forms: `Path(name): Path<T>` produces a single path parameter, and
`Path((a, b, ...)): Path<(T1, T2, ...)>` produces one parameter per tuple
position. Parameters declared explicitly via `parameters(path(...))` win
over inferred ones with the same name, so existing code is unaffected.

### Fixed

- `parameters(cookie(...))` entries are now actually emitted into the
generated operation. Previously cookie parameters were parsed but silently
dropped, so declared cookies never appeared in the spec.
- The cookie style field is now wrapped in `Some(...)`, fixing a compile
error that previously prevented `parameters(cookie(...))` from being used
at all.
- The cookie parameter `location` is now `"cookie"` (lowercase) as required
by OpenAPI 3.0.
- Support `content` field for `path`, `query`, `header`, and `cookie` parameters — allows specifying a parameter value via `ParameterValue::Content` (a media type map) instead of `ParameterValue::Schema`.

## [0.3.0] - 2025-06-15
Expand Down
4 changes: 2 additions & 2 deletions okapi-operation-macro/src/operation/cookie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl ToTokens for Cookie {
let allow_reserved = false;
quote! {
okapi::openapi3::ParameterValue::Schema {
style: #style,
style: Some(#style),
explode: #explode,
allow_reserved: #allow_reserved,
schema: components.schema_for::<#ty>(),
Expand All @@ -68,7 +68,7 @@ impl ToTokens for Cookie {
tokens.extend(quote! {
okapi::openapi3::Parameter {
name: #name.into(),
location: "Cookie".into(),
location: "cookie".into(),
description: #description,
required: #required,
deprecated: #deprecated,
Expand Down
4 changes: 4 additions & 0 deletions okapi-operation-macro/src/operation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod external_docs;
mod header;
mod parameters;
mod path;
mod path_inference;
mod query;
mod reference;
mod request_body;
Expand Down Expand Up @@ -138,6 +139,9 @@ pub(crate) fn openapi(
operation_attrs
.responses
.add_return_type(&input, operation_attrs.responses.ignore_return_type);
operation_attrs
.parameters
.add_inferred_from_signature(&input);
let request_body = RequestBody::from_item_fn(&mut input)?;
let openapi_generator_fn =
build_openapi_generator_fn(&input.sig.ident, &input.vis, operation_attrs, request_body)?;
Expand Down
36 changes: 31 additions & 5 deletions okapi-operation-macro/src/operation/parameters.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Meta, Token, punctuated::Punctuated};
use syn::{ItemFn, Meta, Token, punctuated::Punctuated};

use super::cookie::{COOKIE_ATTRIBUTE_NAME, Cookie};
use crate::{
Expand All @@ -14,9 +14,6 @@ use crate::{
utils::meta_to_meta_list,
};

// TODO: support cookie parameters
// TODO: support parameters from function signature

#[derive(Debug, FromMeta)]
#[darling(rename_all = "camelCase")]
pub(super) enum ParameterStyle {
Expand Down Expand Up @@ -44,7 +41,7 @@ impl ToTokens for ParameterStyle {
}
}

/// Parameters description (header/path/query) .
/// Parameters description (header/path/query/cookie).
#[derive(Default, Debug)]
pub(super) struct Parameters {
header_parameters: Vec<Header>,
Expand All @@ -54,6 +51,33 @@ pub(super) struct Parameters {
ref_parameters: Vec<Reference>,
}

impl Parameters {
/// Append parameters that can be inferred from the function signature
/// (currently: axum `Path<...>` extractor). Names already declared via the
/// macro arguments win — inferred entries with a duplicate name are
/// dropped.
pub(super) fn add_inferred_from_signature(&mut self, item_fn: &ItemFn) {
#[cfg(feature = "axum")]
{
let inferred = super::path_inference::infer_path_parameters(item_fn);
for param in inferred {
if self
.path_parameters
.iter()
.any(|p| p.name() == param.name())
{
continue;
}
self.path_parameters.push(param);
}
}
#[cfg(not(feature = "axum"))]
{
let _ = item_fn;
}
}
}

impl FromMeta for Parameters {
fn from_meta(meta: &Meta) -> Result<Self, darling::Error> {
let meta_list = meta_to_meta_list(meta)?;
Expand Down Expand Up @@ -88,13 +112,15 @@ impl ToTokens for Parameters {
let header_parameters = self.header_parameters.iter().map(|x| x.for_parameter());
let path_parameters = &self.path_parameters;
let query_parameters = &self.query_parameters;
let cookie_parameters = &self.cookie_parameters;
let ref_parameters = &self.ref_parameters;
tokens.extend(quote! {
parameters: {
let mut v = Vec::new();
#(v.push(okapi::openapi3::RefOr::Object(#header_parameters));)*
#(v.push(okapi::openapi3::RefOr::Object(#path_parameters));)*
#(v.push(okapi::openapi3::RefOr::Object(#query_parameters));)*
#(v.push(okapi::openapi3::RefOr::Object(#cookie_parameters));)*
#(v.push(#ref_parameters);)*
v
},
Expand Down
18 changes: 18 additions & 0 deletions okapi-operation-macro/src/operation/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ pub(super) struct Path {
content: Option<String>,
}

impl Path {
pub(super) fn name(&self) -> &str {
&self.name
}

/// Build a path parameter inferred from a function argument.
pub(super) fn new_inferred(name: String, schema: syn::Path) -> Self {
Self {
name,
description: None,
deprecated: false,
style: None,
schema,
content: None,
}
}
}

impl ToTokens for Path {
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = &self.name;
Expand Down
119 changes: 119 additions & 0 deletions okapi-operation-macro/src/operation/path_inference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//! Infer path parameters from a function signature.
//!
//! Currently only the axum-style `Path<...>` extractor is recognized, behind
//! the `axum` feature. Two binding shapes are supported:
//!
//! - `Path(name): Path<T>` — produces a single path parameter named `name`
//! with schema `T`.
//! - `Path((a, b, ...)): Path<(T1, T2, ...)>` — produces one parameter per
//! tuple position; the name comes from the binding, the schema from the
//! corresponding tuple element.
//!
//! Anything more complex (struct extractors, `_` bindings, references, etc.)
//! is silently skipped — callers fall back to declaring the parameters
//! explicitly via `parameters(path(...))`.

#![cfg(feature = "axum")]

use syn::{FnArg, GenericArgument, ItemFn, Pat, PathArguments, Type};

use super::path::Path;

/// Walk the function signature and produce inferred path parameters.
pub(super) fn infer_path_parameters(item_fn: &ItemFn) -> Vec<Path> {
let mut result = Vec::new();
for arg in &item_fn.sig.inputs {
let FnArg::Typed(pt) = arg else { continue };
let Some(params) = extract_from_arg(&pt.pat, &pt.ty) else {
continue;
};
result.extend(params);
}
result
}

fn extract_from_arg(pat: &Pat, ty: &Type) -> Option<Vec<Path>> {
let inner_ty = unwrap_axum_path_type(ty)?;
let names = extract_names_from_path_pat(pat)?;

match inner_ty {
Type::Tuple(tuple) if names.len() == tuple.elems.len() => {
let mut params = Vec::with_capacity(names.len());
for (name, elem_ty) in names.into_iter().zip(tuple.elems.iter()) {
let schema = type_to_simple_path(elem_ty)?;
params.push(Path::new_inferred(name, schema));
}
Some(params)
}
// Tuples with mismatched arity vs binding — skip rather than guess.
Type::Tuple(_) => None,
// Single non-tuple type with a single binding name.
single if names.len() == 1 => {
let schema = type_to_simple_path(single)?;
Some(vec![Path::new_inferred(
names.into_iter().next().unwrap(),
schema,
)])
}
_ => None,
}
}

/// If the type is `Path<T>` (last segment ident is `Path` with one generic
/// argument), return the inner type.
fn unwrap_axum_path_type(ty: &Type) -> Option<&Type> {
let Type::Path(tp) = ty else { return None };
let last = tp.path.segments.last()?;
if last.ident != "Path" {
return None;
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
if args.args.len() != 1 {
return None;
}
match args.args.first()? {
GenericArgument::Type(inner) => Some(inner),
_ => None,
}
}

/// Pull binding names out of a `Path(...)` pattern. Recognizes:
/// `Path(ident)` → `[ident]`
/// `Path((a, b, ...))` → `[a, b, ...]`
/// Returns `None` for anything else, including patterns containing `_`.
fn extract_names_from_path_pat(pat: &Pat) -> Option<Vec<String>> {
let Pat::TupleStruct(ts) = pat else {
return None;
};
let last = ts.path.segments.last()?;
if last.ident != "Path" {
return None;
}
if ts.elems.len() != 1 {
return None;
}
match ts.elems.first()? {
Pat::Ident(pi) => Some(vec![pi.ident.to_string()]),
Pat::Tuple(pt) => pt
.elems
.iter()
.map(|e| match e {
Pat::Ident(pi) => Some(pi.ident.to_string()),
_ => None,
})
.collect(),
_ => None,
}
}

/// Accept simple type-path schemas (`String`, `u32`, `my::Type`, …) and reject
/// everything else (references, slices, nested tuples). The downstream code
/// expects a `syn::Path` because it splices the type into a turbofish.
fn type_to_simple_path(ty: &Type) -> Option<syn::Path> {
match ty {
Type::Path(tp) if tp.qself.is_none() => Some(tp.path.clone()),
_ => None,
}
}
11 changes: 11 additions & 0 deletions okapi-operation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ This project follows the [Semantic Versioning standard](https://semver.org/).
### Added

- Now you can enable inferring `operation_id` from the function name, using `OpenApiBuilder.infer_operation_id()`.
- Path parameters are now inferred from the function signature when the `axum`
feature is enabled (`Path(name): Path<T>` and
`Path((a, b)): Path<(T1, T2)>`). Explicit `parameters(path(...))` declarations
with the same name take precedence.

### Fixed

- `parameters(cookie(...))` is now actually emitted into the generated
operation, with `location` set to `"cookie"` per OpenAPI 3.0. Previously
cookie entries didn't compile, and even if they did they were silently
dropped from the spec.

## [0.3.0] - 2025-06-13

Expand Down
Loading
Loading