From 8eacbf212accb3c2e9fa869abd7c7d47ec3c05e8 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 27 Sep 2023 13:22:20 +0200 Subject: [PATCH 1/4] Infer operation_id from function name --- okapi-examples/src/main.rs | 1 - okapi-operation-macro/src/operation/mod.rs | 3 +++ okapi-operation/src/builder.rs | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/okapi-examples/src/main.rs b/okapi-examples/src/main.rs index 7405b3b..3152305 100644 --- a/okapi-examples/src/main.rs +++ b/okapi-examples/src/main.rs @@ -24,7 +24,6 @@ async fn echo_get(query: Query) -> Json { #[openapi( summary = "Echo using POST request", - operation_id = "echo_post", tags = "echo" )] async fn echo_post( diff --git a/okapi-operation-macro/src/operation/mod.rs b/okapi-operation-macro/src/operation/mod.rs index b51b37f..bc3bd21 100644 --- a/okapi-operation-macro/src/operation/mod.rs +++ b/okapi-operation-macro/src/operation/mod.rs @@ -92,6 +92,9 @@ pub(crate) fn openapi(mut attrs: AttributeArgs, mut input: ItemFn) -> Result, } impl OpenApiBuilder { @@ -24,6 +28,7 @@ impl OpenApiBuilder { Self { spec, components: Components::new(Default::default()), + known_operation_ids: Default::default(), } } @@ -78,6 +83,15 @@ impl OpenApiBuilder { generator: OperationGenerator, ) -> Result<&mut Self, anyhow::Error> { let operation_schema = generator(&mut self.components)?; + + // 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); From fd0805836bab8f121b6fc5a11fe6627ac94472a0 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 1 Nov 2023 16:47:27 +0100 Subject: [PATCH 2/4] Make infer_operation_id feature optional --- okapi-examples/src/main.rs | 11 ++++----- okapi-operation-macro/src/operation/mod.rs | 22 +++++++++++++---- okapi-operation/src/builder.rs | 28 ++++++++++++++++++++-- okapi-operation/src/lib.rs | 5 +++- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/okapi-examples/src/main.rs b/okapi-examples/src/main.rs index 3152305..32fcae2 100644 --- a/okapi-examples/src/main.rs +++ b/okapi-examples/src/main.rs @@ -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",), @@ -22,10 +21,7 @@ async fn echo_get(query: Query) -> Json { Json(query.0.data) } -#[openapi( - summary = "Echo using POST request", - tags = "echo" -)] +#[openapi(summary = "Echo using POST request", tags = "echo")] async fn echo_post( #[request_body(description = "Echo data", required = true)] body: Json, ) -> Json { @@ -35,7 +31,10 @@ async fn echo_post( #[tokio::main] async fn main() { // Here you can also add security schemes, other operations, modify internal OpenApi object. - let oas_builder = OpenApiBuilder::new("Demo", "1.0.0"); + let mut oas_builder = OpenApiBuilder::new("Demo", "1.0.0"); + + // Enable infer operation_id from function names. + oas_builder.infer_operation_id(); let app = Router::new() .route("/echo/get", get(openapi_handler!(echo_get))) diff --git a/okapi-operation-macro/src/operation/mod.rs b/okapi-operation-macro/src/operation/mod.rs index bc3bd21..4e91904 100644 --- a/okapi-operation-macro/src/operation/mod.rs +++ b/okapi-operation-macro/src/operation/mod.rs @@ -39,6 +39,8 @@ struct OperationAttrs { #[darling(default)] operation_id: Option, #[darling(default)] + inferred_operation_id: String, + #[darling(default)] tags: Option, #[darling(default)] deprecated: bool, @@ -56,7 +58,18 @@ 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 = { @@ -92,9 +105,7 @@ pub(crate) fn openapi(mut attrs: AttributeArgs, mut input: ItemFn) -> Result std::result::Result { let mut operation = okapi::openapi3::Operation { #attrs diff --git a/okapi-operation/src/builder.rs b/okapi-operation/src/builder.rs index 106c751..a33f831 100644 --- a/okapi-operation/src/builder.rs +++ b/okapi-operation/src/builder.rs @@ -5,12 +5,27 @@ use okapi::openapi3::{Info, OpenApi, SecurityRequirement, SecurityScheme}; use crate::{components::Components, utils::convert_axum_path_to_openapi, OperationGenerator}; -/// OpenAPI specificatrion builder. +/// OpenAPI specification builder. pub struct OpenApiBuilder { spec: OpenApi, components: Components, // To validate operation ids known_operation_ids: HashSet, + + builder_options: BuilderOptions +} + +#[derive(Default)] +pub struct BuilderOptions { + // If true, infer the operation id from the method name + pub infer_operation_id: bool +} + +impl BuilderOptions { + // If true, infer the operation id from the method name + pub fn infer_operation_id(&self) -> bool { + self.infer_operation_id + } } impl OpenApiBuilder { @@ -29,6 +44,7 @@ impl OpenApiBuilder { spec, components: Components::new(Default::default()), known_operation_ids: Default::default(), + builder_options: Default::default(), } } @@ -75,6 +91,14 @@ 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 infer_operation_id(&mut self) -> &mut Self { + self.builder_options.infer_operation_id = true; + self + } + /// Add single operation. pub fn add_operation( &mut self, @@ -82,7 +106,7 @@ impl OpenApiBuilder { method: Method, generator: OperationGenerator, ) -> Result<&mut Self, anyhow::Error> { - let operation_schema = generator(&mut self.components)?; + 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() { diff --git a/okapi-operation/src/lib.rs b/okapi-operation/src/lib.rs index b1bcf75..41236b5 100644 --- a/okapi-operation/src/lib.rs +++ b/okapi-operation/src/lib.rs @@ -22,6 +22,9 @@ pub use self::{ to_responses::ToResponses, }; +// This is public to let macros access it +pub use self::builder::BuilderOptions as InternalBuilderOptions; + use okapi::openapi3::Operation; mod builder; @@ -35,4 +38,4 @@ pub type Empty = (); // TODO: allow return RefOr /// Operation generator signature. -pub type OperationGenerator = fn(&mut Components) -> Result; +pub type OperationGenerator = fn(&mut Components, &InternalBuilderOptions) -> Result; \ No newline at end of file From 1a7dadcbec2e1d872442a5202262eba662ed1522 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 1 Nov 2023 16:48:45 +0100 Subject: [PATCH 3/4] Changelog --- okapi-operation/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/okapi-operation/CHANGELOG.md b/okapi-operation/CHANGELOG.md index 3f52e18..cc48668 100644 --- a/okapi-operation/CHANGELOG.md +++ b/okapi-operation/CHANGELOG.md @@ -2,6 +2,9 @@ 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.2.1] - 2023-05-09 ### Added From 68e6adbfbb0d7d156b2af404efc67770339cf4c6 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 1 Nov 2023 16:54:31 +0100 Subject: [PATCH 4/4] Fmt + fix tests --- okapi-operation-macro/src/operation/mod.rs | 2 +- okapi-operation/src/axum_integration/handler_traits.rs | 7 +++++-- okapi-operation/src/axum_integration/router.rs | 7 +++++-- okapi-operation/src/builder.rs | 6 +++--- okapi-operation/src/lib.rs | 3 ++- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/okapi-operation-macro/src/operation/mod.rs b/okapi-operation-macro/src/operation/mod.rs index 4e91904..5d6012f 100644 --- a/okapi-operation-macro/src/operation/mod.rs +++ b/okapi-operation-macro/src/operation/mod.rs @@ -62,7 +62,7 @@ impl ToTokens for OperationAttrs { let operation_id = quote_option(&self.operation_id); let inferred_operation_id = &self.inferred_operation_id; - quote!{ + quote! { if (builder_options.infer_operation_id) { #operation_id.or_else(|| Some(String::from(#inferred_operation_id))) } else { diff --git a/okapi-operation/src/axum_integration/handler_traits.rs b/okapi-operation/src/axum_integration/handler_traits.rs index 2a7d468..474cb7b 100644 --- a/okapi-operation/src/axum_integration/handler_traits.rs +++ b/okapi-operation/src/axum_integration/handler_traits.rs @@ -177,10 +177,13 @@ mod tests { use super::*; use crate::{ axum_integration::{MethodRouter, Router}, - Components, + Components, InternalBuilderOptions, }; - fn openapi_generator(_: &mut Components) -> Result { + fn openapi_generator( + _: &mut Components, + _: &InternalBuilderOptions, + ) -> Result { unimplemented!() } diff --git a/okapi-operation/src/axum_integration/router.rs b/okapi-operation/src/axum_integration/router.rs index 822c9d9..d8018b2 100644 --- a/okapi-operation/src/axum_integration/router.rs +++ b/okapi-operation/src/axum_integration/router.rs @@ -350,10 +350,13 @@ mod tests { use super::*; use crate::{ axum_integration::{get, HandlerExt}, - Components, + Components, InternalBuilderOptions, }; - fn openapi_generator(_: &mut Components) -> Result { + fn openapi_generator( + _: &mut Components, + _: &InternalBuilderOptions, + ) -> Result { unimplemented!() } diff --git a/okapi-operation/src/builder.rs b/okapi-operation/src/builder.rs index a33f831..c4cfa67 100644 --- a/okapi-operation/src/builder.rs +++ b/okapi-operation/src/builder.rs @@ -1,7 +1,7 @@ -use std::collections::HashSet; use anyhow::anyhow; use http::Method; use okapi::openapi3::{Info, OpenApi, SecurityRequirement, SecurityScheme}; +use std::collections::HashSet; use crate::{components::Components, utils::convert_axum_path_to_openapi, OperationGenerator}; @@ -12,13 +12,13 @@ pub struct OpenApiBuilder { // To validate operation ids known_operation_ids: HashSet, - builder_options: BuilderOptions + builder_options: BuilderOptions, } #[derive(Default)] pub struct BuilderOptions { // If true, infer the operation id from the method name - pub infer_operation_id: bool + pub infer_operation_id: bool, } impl BuilderOptions { diff --git a/okapi-operation/src/lib.rs b/okapi-operation/src/lib.rs index 41236b5..750c109 100644 --- a/okapi-operation/src/lib.rs +++ b/okapi-operation/src/lib.rs @@ -38,4 +38,5 @@ pub type Empty = (); // TODO: allow return RefOr /// Operation generator signature. -pub type OperationGenerator = fn(&mut Components, &InternalBuilderOptions) -> Result; \ No newline at end of file +pub type OperationGenerator = + fn(&mut Components, &InternalBuilderOptions) -> Result;