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
7 changes: 1 addition & 6 deletions okapi-examples/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ struct Request {

#[openapi(
summary = "Echo using GET request",
operation_id = "echo_get",
tags = "echo",
parameters(
query(name = "echo-data", required = true, schema = "std::string::String",),
Expand All @@ -22,11 +21,7 @@ async fn echo_get(query: Query<Request>) -> Json<String> {
Json(query.0.data)
}

#[openapi(
summary = "Echo using POST request",
operation_id = "echo_post",
tags = "echo"
)]
#[openapi(summary = "Echo using POST request", tags = "echo")]
async fn echo_post(
#[body(description = "Echo data", required = true)] body: Json<Request>,
) -> Json<String> {
Expand Down
24 changes: 19 additions & 5 deletions okapi-operation-macro/src/operation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,28 @@ struct OperationAttrs {
rename = "rename_attribute"
)]
attribute_name: String,

// Internal fields
#[darling(default, skip)]
inferred_operation_id: String,
}

impl ToTokens for OperationAttrs {
fn to_tokens(&self, tokens: &mut TokenStream) {
let summary = quote_option(&self.summary);
let description = quote_option(&self.description);
let operation_id = quote_option(&self.operation_id);
let operation_id = {
let operation_id = quote_option(&self.operation_id);
let inferred_operation_id = &self.inferred_operation_id;

quote! {
if (builder_options.infer_operation_id) {
#operation_id.or_else(|| Some(String::from(#inferred_operation_id)))
} else {
#operation_id
}
}
};
let external_docs = quote_option(&self.external_docs);
let deprecated = &self.deprecated;
let tags = {
Expand Down Expand Up @@ -118,9 +133,8 @@ pub(crate) fn openapi(
) -> Result<TokenStream, Error> {
let attrs = NestedMeta::parse_meta_list(attrs.into())?;
let mut operation_attrs = OperationAttrs::from_list(&attrs)?;

operation_attrs.inferred_operation_id = input.sig.ident.to_string();
set_current_attribute_name(operation_attrs.attribute_name.clone());

operation_attrs
.responses
.add_return_type(&input, operation_attrs.responses.ignore_return_type);
Expand Down Expand Up @@ -158,10 +172,10 @@ fn build_openapi_generator_fn(
Ok(quote! {
#[allow(non_snake_case, unused)]
#vis fn #name(
components: &mut #crate_name::Components
components: &mut #crate_name::Components,
builder_options: &#crate_name::BuilderOptions
) -> std::result::Result<#crate_name::okapi::openapi3::Operation, anyhow::Error> {
use #crate_name::_macro_prelude::*;

let mut operation = okapi::openapi3::Operation {
#attrs
#request_body
Expand Down
6 changes: 6 additions & 0 deletions okapi-operation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to this project will be documented in the changelog of the respective crates.
This project follows the [Semantic Versioning standard](https://semver.org/).

## [Unreleased]

### Added

- Now you can enable inferring `operation_id` from the function name, using `OpenApiBuilder.infer_operation_id()`.

## [0.3.0] - 2025-06-13

### Notable changes
Expand Down
7 changes: 5 additions & 2 deletions okapi-operation/src/axum_integration/handler_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,14 @@ mod tests {

use super::*;
use crate::{
Components,
BuilderOptions, Components,
axum_integration::{MethodRouter, Router},
};

fn openapi_generator(_: &mut Components) -> Result<Operation, anyhow::Error> {
fn openapi_generator(
_: &mut Components,
_: &BuilderOptions,
) -> Result<Operation, anyhow::Error> {
unimplemented!()
}

Expand Down
1 change: 0 additions & 1 deletion okapi-operation/src/axum_integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ mod method_router;
mod operations;
mod router;
mod trait_impls;
mod utils;

use axum::{
Json,
Expand Down
20 changes: 7 additions & 13 deletions okapi-operation/src/axum_integration/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use super::{
get,
method_router::{MethodRouter, MethodRouterOperations},
operations::RoutesOperations,
utils::convert_axum_path_to_openapi,
};
use crate::OpenApiBuilder;

Expand Down Expand Up @@ -335,11 +334,7 @@ where
let mut builder = self.openapi_builder_template.clone();
// Don't use try_operations since duplicates should be checked
// when mounting route to axum router.
builder.operations(
routes
.into_iter()
.map(|((x, y), z)| (convert_axum_path_to_openapi(&x), y, z)),
);
builder.operations(routes.into_iter().map(|((x, y), z)| (x, y, z)));
builder
}

Expand Down Expand Up @@ -404,11 +399,7 @@ where
// when mounting route to axum router.
let spec = self
.generate_openapi_builder()
.operation(
convert_axum_path_to_openapi(serve_path),
Method::GET,
super::serve_openapi_spec__openapi,
)
.operation(serve_path, Method::GET, super::serve_openapi_spec__openapi)
.title(title)
.version(version)
.build()?;
Expand All @@ -429,11 +420,14 @@ mod tests {

use super::*;
use crate::{
Components,
BuilderOptions, Components,
axum_integration::{HandlerExt, get, post},
};

fn openapi_generator(_: &mut Components) -> Result<Operation, anyhow::Error> {
fn openapi_generator(
_: &mut Components,
_: &BuilderOptions,
) -> Result<Operation, anyhow::Error> {
unimplemented!()
}

Expand Down
13 changes: 0 additions & 13 deletions okapi-operation/src/axum_integration/utils.rs

This file was deleted.

93 changes: 90 additions & 3 deletions okapi-operation/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use anyhow::{Context, bail};
use std::collections::HashSet;

use anyhow::{Context, anyhow, bail};
use http::Method;
use indexmap::IndexMap;
use okapi::openapi3::{
Expand All @@ -7,12 +9,34 @@ use okapi::openapi3::{

use crate::{OperationGenerator, components::Components};

#[derive(Clone)]
pub struct BuilderOptions {
pub infer_operation_id: bool,
}

#[allow(clippy::derivable_impls)]
impl Default for BuilderOptions {
fn default() -> Self {
Self {
infer_operation_id: false,
}
}
}

impl BuilderOptions {
pub fn infer_operation_id(&self) -> bool {
self.infer_operation_id
}
}

/// OpenAPI specificatrion builder.
#[derive(Clone)]
pub struct OpenApiBuilder {
spec: OpenApi,
components: Components,
operations: IndexMap<(String, Method), OperationGenerator>,
known_operation_ids: HashSet<String>, // Used to validate operation ids
builder_options: BuilderOptions,
}

impl Default for OpenApiBuilder {
Expand All @@ -25,6 +49,8 @@ impl Default for OpenApiBuilder {
spec,
components: Components::new(Default::default()),
operations: IndexMap::new(),
known_operation_ids: Default::default(),
builder_options: Default::default(),
}
}
}
Expand Down Expand Up @@ -140,6 +166,65 @@ impl OpenApiBuilder {
self
}

/// Infer the operation id for every operation based on the function name.
///
/// If the operation_id is specified in the macro, it will replace the inferred name.
pub fn set_infer_operation_id(&mut self, value: bool) -> &mut Self {
self.builder_options.infer_operation_id = value;
self
}

/// Add single operation.
pub fn add_operation(
&mut self,
path: &str,
method: Method,
generator: OperationGenerator,
) -> Result<&mut Self, anyhow::Error> {
let operation_schema = generator(&mut self.components, &self.builder_options)?;

// Check operation id doesn't exists
if let Some(operation_id) = operation_schema.operation_id.as_ref() {
if self.known_operation_ids.contains(operation_id) {
return Err(anyhow!("Found duplicate operation_id {operation_id}."));
}
self.known_operation_ids.insert(operation_id.clone());
}

let path = self.spec.paths.entry(path.into()).or_default();
if method == Method::DELETE {
path.delete = Some(operation_schema);
} else if method == Method::GET {
path.get = Some(operation_schema);
} else if method == Method::HEAD {
path.head = Some(operation_schema);
} else if method == Method::OPTIONS {
path.options = Some(operation_schema);
} else if method == Method::PATCH {
path.patch = Some(operation_schema);
} else if method == Method::POST {
path.post = Some(operation_schema);
} else if method == Method::PUT {
path.put = Some(operation_schema);
} else if method == Method::TRACE {
path.trace = Some(operation_schema);
} else {
return Err(anyhow::anyhow!("Unsupported method {}", method));
}
Ok(self)
}

/// Add multiple operations.
pub fn add_operations(
&mut self,
operations: impl Iterator<Item = (String, Method, OperationGenerator)>,
) -> Result<&mut Self, anyhow::Error> {
for (path, method, f) in operations {
self.add_operation(&path, method, f)?;
}
Ok(self)
}

/// Generate [`okapi::openapi3::OpenApi`] specification.
///
/// This method can be called repeatedly on the same object.
Expand All @@ -156,6 +241,7 @@ impl OpenApiBuilder {
try_add_path(
&mut spec,
&mut self.components,
&self.builder_options,
path,
method.clone(),
*generator,
Expand Down Expand Up @@ -241,11 +327,12 @@ impl OpenApiBuilder {
fn try_add_path(
spec: &mut OpenApi,
components: &mut Components,
builder_options: &BuilderOptions,
path: &str,
method: Method,
generator: OperationGenerator,
) -> Result<(), anyhow::Error> {
let operation_schema = generator(components)?;
let operation_schema = generator(components, builder_options)?;
let path_str = path;
let path = spec.paths.entry(path.into()).or_default();
if method == Method::DELETE {
Expand Down Expand Up @@ -284,7 +371,7 @@ fn ensure_builder_deterministic() {
for _ in 0..100 {
let mut builder = OpenApiBuilder::new("title", "version");
for i in 0..2 {
builder.operation(format!("/path/{}", i), Method::GET, |_| {
builder.operation(format!("/path/{}", i), Method::GET, |_, _| {
Ok(Operation::default())
});
}
Expand Down
5 changes: 4 additions & 1 deletion okapi-operation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub mod axum_integration;

use okapi::openapi3::Operation;

#[doc(hidden)]
pub use self::builder::BuilderOptions;
pub use self::{
builder::OpenApiBuilder,
components::{Components, ComponentsBuilder},
Expand All @@ -34,7 +36,8 @@ pub type Empty = ();

// TODO: allow return RefOr<Operation>
/// Operation generator signature.
pub type OperationGenerator = fn(&mut Components) -> Result<Operation, anyhow::Error>;
pub type OperationGenerator =
fn(&mut Components, &BuilderOptions) -> Result<Operation, anyhow::Error>;

#[cfg(feature = "macro")]
#[doc(hidden)]
Expand Down
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
unstable_features = true

edition = "2024"
style_edition = "2024"
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
Expand Down