From 665ca4f0b90e1cab78b653ec5135e4c88e6dfc2d Mon Sep 17 00:00:00 2001 From: Kurt Wolf Date: Fri, 27 Sep 2024 15:14:39 -0700 Subject: [PATCH] fix query path issue --- core/src/operation.rs | 24 ++++- core/src/schema.rs | 64 ++----------- core/src/schema/actix.rs | 82 +++++++---------- core/src/schema/axum.rs | 128 ++++++++------------------ core/src/schema/cookies.rs | 6 +- core/src/schema/tuple.rs | 36 ++++++++ macro/src/lib.rs | 8 +- oasgen/src/lib.rs | 5 +- oasgen/src/server/actix.rs | 20 ++-- oasgen/tests/test-actix/01-hello.yaml | 6 ++ oasgen/tests/test-axum/01-hello.rs | 4 +- oasgen/tests/test-axum/02-query.yaml | 9 +- 12 files changed, 163 insertions(+), 229 deletions(-) create mode 100644 core/src/schema/tuple.rs diff --git a/core/src/operation.rs b/core/src/operation.rs index 2a8a615..9deb294 100644 --- a/core/src/operation.rs +++ b/core/src/operation.rs @@ -1,9 +1,29 @@ -use openapiv3::Operation; +use openapiv3::{Operation, Parameter, RefOr, Schema}; pub struct OperationRegister { - // module_path: &'static str, pub name: &'static str, pub constructor: &'static (dyn Sync + Send + Fn() -> Operation), } +pub trait OaParameter { + fn body_schema() -> Option> { + None + } + fn parameter_schemas() -> Vec> { + Vec::new() + } + fn parameters() -> Vec> { + Vec::new() + } +} + +impl OaParameter for Result +where + T: OaParameter, +{ + fn body_schema() -> Option> { + T::body_schema() + } +} + inventory::collect!(OperationRegister); \ No newline at end of file diff --git a/core/src/schema.rs b/core/src/schema.rs index f488cc3..6a67752 100644 --- a/core/src/schema.rs +++ b/core/src/schema.rs @@ -1,6 +1,4 @@ use std::collections::HashMap; - -use openapiv3 as oa; use openapiv3::{ReferenceOr, Schema}; #[cfg(feature = "actix")] @@ -25,6 +23,7 @@ mod bigdecimal; mod http; #[cfg(feature = "sid")] mod sid; +mod tuple; pub trait OaSchema { fn schema() -> Schema; @@ -32,11 +31,8 @@ pub trait OaSchema { fn schema_ref() -> ReferenceOr { ReferenceOr::Item(Self::schema()) } - - fn parameters() -> Vec> { - Vec::new() - } - + /// You should rarely if ever implement this method. + #[doc(hidden)] fn body_schema() -> Option> { Some(Self::schema_ref()) } @@ -78,46 +74,6 @@ macro_rules! impl_oa_schema_passthrough { }; } -// We have to define this macro instead of defining OaSchema for tuples because -// the Path types have to implement parameters(). parameters calls out to T::schema_ref() -// because we need to implement something like Path : OaSchema, -// but tuples don't work the same way, because schema doesn't return multiple schemas. - -// The alternative is a second trait interface like OaSchemaTuple, and we'd impl -// for axum::extract::Path and friends -#[macro_export] -macro_rules! impl_parameters { - // Pattern for generic axum types with tuple generics (A1, A2, etc.) - ($($path:ident)::+, $($A:ident),+) => { - impl<$($A: $crate::OaSchema),+> $crate::OaSchema for $($path)::+<($($A,)+)> { - fn schema() -> $crate::Schema { - panic!("Call parameters() for this type, not schema()."); - } - - fn parameters() -> Vec<$crate::ReferenceOr<$crate::Parameter>> { - vec![ - $( - $crate::ReferenceOr::Item($crate::Parameter::path(stringify!($A), $A::schema_ref())), - )+ - ] - } - - fn body_schema() -> Option<$crate::ReferenceOr<$crate::Schema>> { - None - } - } - }; -} - -impl OaSchema for () { - fn schema() -> Schema { - panic!("Call body_schema() for (), not schema().") - } - - fn body_schema() -> Option> { - None - } -} impl_oa_schema!(bool, Schema::new_bool()); @@ -176,20 +132,12 @@ where } } -impl OaSchema for Result -where - T: OaSchema, -{ +impl OaSchema for () { fn schema() -> Schema { - T::schema() - } - - fn schema_ref() -> ReferenceOr { - T::schema_ref() + panic!("Unit type has no schema") } - fn body_schema() -> Option> { - T::body_schema() + None } } diff --git a/core/src/schema/actix.rs b/core/src/schema/actix.rs index 7d0a897..a722f09 100644 --- a/core/src/schema/actix.rs +++ b/core/src/schema/actix.rs @@ -1,74 +1,54 @@ use openapiv3 as oa; -use openapiv3::{ReferenceOr, Schema}; +use openapiv3::{RefOr, Schema, SchemaKind, Type}; -use crate::OaSchema; +use crate::{OaParameter, OaSchema}; -impl OaSchema for actix_web::web::Json { - fn schema() -> Schema { - panic!("Call body_schema() for Json, not schema().") - } - - fn body_schema() -> Option> { +impl OaParameter for actix_web::web::Json { + fn body_schema() -> Option> { T::body_schema() } } -impl OaSchema for actix_web::web::Data { - fn schema() -> Schema { - panic!("Call parameters() for Data, not schema()."); - } - fn body_schema() -> Option> { - None - } -} - -impl OaSchema for actix_web::HttpRequest { - fn schema() -> Schema { - panic!("Call parameters() for HttpRequest, not schema()."); - } -} - impl OaSchema for actix_web::HttpResponse { fn schema() -> Schema { - panic!("Call body_schema() for HttpResponse, not schema()."); + Schema::new_any() } } -impl OaSchema for actix_web::web::Path { - fn schema() -> Schema { - panic!("Call parameters() for Path, not schema()."); - } +impl OaParameter for actix_web::web::Data {} +impl OaParameter for actix_web::HttpRequest {} - fn parameters() -> Vec> { - T::parameters() - } - fn body_schema() -> Option> { - None +impl OaParameter for actix_web::web::Path { + fn parameters() -> Vec> { + T::parameter_schemas() + .into_iter() + .map(|s| RefOr::Item(oa::Parameter::path("path", s))) + .collect() } } -impl OaSchema for actix_web::web::Query { - fn schema() -> Schema { - panic!("Call parameters() for Query, not schema()."); - } - - fn parameters() -> Vec> { - T::parameters() - } - fn body_schema() -> Option> { - None +impl OaParameter for actix_web::web::Query { + fn parameters() -> Vec> { + T::parameter_schemas() + .into_iter() + .flat_map(|s| s.into_item()) + .flat_map(|s| match s.kind { + SchemaKind::Type(Type::Object(o)) => { Some(o.properties) } + _ => None + }) + .flatten() + .map(|(k, v)| RefOr::Item(oa::Parameter::query(k, v))) + .collect() } } #[cfg(feature = "qs")] -impl OaSchema for serde_qs::actix::QsQuery { - fn schema() -> Schema { - panic!("Call parameters() for QsQuery, not schema()."); - } - - fn parameters() -> Vec> { - let p = oa::Parameter::query("query", T::schema_ref()); - vec![ReferenceOr::Item(p)] +impl OaParameter for serde_qs::actix::QsQuery { + fn parameters() -> Vec> { + T::parameter_schemas() + .into_iter() + .map(|s| RefOr::Item(oa::Parameter::query("query", s))) + .collect() } } diff --git a/core/src/schema/axum.rs b/core/src/schema/axum.rs index 89c6af6..92994a6 100644 --- a/core/src/schema/axum.rs +++ b/core/src/schema/axum.rs @@ -1,34 +1,7 @@ -use openapiv3::{ReferenceOr, Schema}; +use openapiv3::{RefOr, Schema, SchemaKind, Type}; use openapiv3 as oa; -use crate::{impl_parameters, OaSchema}; - -impl OaSchema for axum::extract::Json { - fn schema() -> Schema { - panic!("Call body_schema() for Json, not schema().") - } - - fn body_schema() -> Option> { - T::body_schema() - } -} - -impl OaSchema for axum::extract::Extension { - fn schema() -> Schema { - panic!("Call parameters() for Extension, not schema().") - } - fn body_schema() -> Option> { None } -} - -impl OaSchema for axum::extract::State { - fn schema() -> Schema { - panic!("Call parameters() for State, not schema().") - } - - fn body_schema() -> Option> { - None - } -} +use crate::{OaParameter, OaSchema}; impl OaSchema for http::Response { fn schema() -> Schema { @@ -36,75 +9,48 @@ impl OaSchema for http::Response { } } -impl OaSchema for http::Request { - fn schema() -> Schema { - panic!("Call parameters() for Request, not schema().") - } - fn body_schema() -> Option> { None } -} - -impl OaSchema for axum::extract::ConnectInfo { - fn body_schema() -> Option> { None } - fn schema() -> Schema { - panic!("Call parameters() for ConnectInfo, not schema().") - } -} - -impl OaSchema for http::HeaderMap { - fn schema() -> Schema { - panic!("Call parameters() for HeaderMap, not schema().") +impl OaParameter for axum::extract::Json { + fn body_schema() -> Option> { + T::body_schema() } - fn body_schema() -> Option> { None } } +impl OaParameter for axum::extract::Extension {} +impl OaParameter for axum::extract::State {} +impl OaParameter for http::Request {} +impl OaParameter for axum::extract::ConnectInfo {} +impl OaParameter for http::HeaderMap {} +impl OaParameter for http::request::Parts {} -impl OaSchema for axum::extract::Query { - fn schema() -> Schema { - panic!("Call parameters() for Query, not schema().") - } - - fn parameters() -> Vec> { - let p = oa::Parameter::query("query", T::schema_ref()); - vec![ReferenceOr::Item(p)] +impl OaParameter for axum::extract::Query { + fn parameters() -> Vec> { + T::parameter_schemas() + .into_iter() + .flat_map(|s| s.into_item()) + .flat_map(|s| match s.kind { + SchemaKind::Type(Type::Object(o)) => { Some(o.properties) } + _ => None + }) + .flatten() + .map(|(k, v)| RefOr::Item(oa::Parameter::query(k, v))) + .collect() } - - fn body_schema() -> Option> { None } } -impl OaSchema for axum::extract::Path { - fn schema() -> Schema { - panic!("Call parameters() for Path, not schema()."); - } - - fn parameters() -> Vec> { - let p = oa::Parameter::path("path", T::schema_ref()); - vec![ReferenceOr::Item(p)] +impl OaParameter for axum::extract::Path { + fn parameters() -> Vec> { + T::parameter_schemas() + .into_iter() + .map(|s| RefOr::Item(oa::Parameter::path("path", s))) + .collect() } - - fn body_schema() -> Option> { - None - } -} - -impl OaSchema for http::request::Parts { - fn schema() -> Schema { - panic!("Call parameters() for Parts, not schema().") - } - fn body_schema() -> Option> { None } } #[cfg(feature = "qs")] -impl OaSchema for serde_qs::axum::QsQuery { - fn schema() -> Schema { - panic!("Call parameters() for QsQuery, not schema().") - } - - fn parameters() -> Vec> { - let p = oa::Parameter::query("query", T::schema_ref()); - vec![ReferenceOr::Item(p)] - } - fn body_schema() -> Option> { None } -} - -impl_parameters!(axum::extract::Path, A1); -impl_parameters!(axum::extract::Path, A1, A2); -impl_parameters!(axum::extract::Path, A1, A2, A3); \ No newline at end of file +impl OaParameter for serde_qs::axum::QsQuery { + fn parameters() -> Vec> { + T::parameter_schemas() + .into_iter() + .map(|s| RefOr::Item(oa::Parameter::query("query", s))) + .collect() + } +} \ No newline at end of file diff --git a/core/src/schema/cookies.rs b/core/src/schema/cookies.rs index 771c273..32930c0 100644 --- a/core/src/schema/cookies.rs +++ b/core/src/schema/cookies.rs @@ -1,11 +1,7 @@ -use openapiv3::{ReferenceOr, Schema}; +use openapiv3::{Schema}; impl crate::OaSchema for tower_cookies::Cookies { fn schema() -> Schema { Schema::new_object() } - - fn body_schema() -> Option> { - None - } } \ No newline at end of file diff --git a/core/src/schema/tuple.rs b/core/src/schema/tuple.rs new file mode 100644 index 0000000..6583d53 --- /dev/null +++ b/core/src/schema/tuple.rs @@ -0,0 +1,36 @@ +use openapiv3::{RefOr, Schema}; +use crate::{OaParameter, OaSchema}; + +impl OaParameter for A { + fn parameter_schemas() -> Vec> { + vec![RefOr::Item(A::schema())] + } + fn body_schema() -> Option> { + A::body_schema() + } +} + +impl OaParameter for (A1,) { + fn parameter_schemas() -> Vec> { + vec![A1::schema_ref()] + } +} + +impl OaParameter for (A1, A2) { + fn parameter_schemas() -> Vec> { + vec![ + A1::schema_ref(), + A2::schema_ref(), + ] + } +} + +impl OaParameter for (A1, A2, A3) { + fn parameter_schemas() -> Vec> { + vec![ + A1::schema_ref(), + A2::schema_ref(), + A3::schema_ref(), + ] + } +} \ No newline at end of file diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 37e212a..897cddf 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -61,9 +61,9 @@ pub fn oasgen(attr: TokenStream, input: TokenStream) -> TokenStream { }; let body = args.last().map(|t| { quote! { - let body = #t::body_schema(); + let body = <#t as ::oasgen::OaParameter>::body_schema(); if body.is_some() { - op.add_request_body_json(#t::body_schema()); + op.add_request_body_json(body); } } }).unwrap_or_default(); @@ -74,7 +74,7 @@ pub fn oasgen(attr: TokenStream, input: TokenStream) -> TokenStream { }).unwrap_or_default(); let ret = ret.map(|t| { quote! { - let body = #t::body_schema(); + let body = <#t as ::oasgen::OaParameter>::body_schema(); if body.is_some() { op.add_response_success_json(body); } @@ -105,7 +105,7 @@ pub fn oasgen(attr: TokenStream, input: TokenStream) -> TokenStream { let submit = quote! { ::oasgen::register_operation!(concat!(module_path!(), "::", #name), || { let parameters: Vec>> = vec![ - #( #args::parameters(), )* + #( <#args as ::oasgen::OaParameter>::parameters(), )* ]; let parameters = parameters .into_iter() diff --git a/oasgen/src/lib.rs b/oasgen/src/lib.rs index 37af1f5..1ccb37b 100644 --- a/oasgen/src/lib.rs +++ b/oasgen/src/lib.rs @@ -4,13 +4,10 @@ mod server; mod format; -pub use openapiv3::*; pub use format::*; pub use oasgen_macro::{OaSchema, oasgen}; -pub use oasgen_core::{OaSchema}; -pub use oasgen_core as core; pub use server::Server; -pub use oasgen_core::{impl_parameters, impl_oa_schema, impl_oa_schema_passthrough}; +pub use oasgen_core::*; #[cfg(feature = "swagger-ui")] #[cfg_attr(docsrs, doc(cfg(feature = "swagger-ui")))] diff --git a/oasgen/src/server/actix.rs b/oasgen/src/server/actix.rs index 6b27ff3..56063af 100644 --- a/oasgen/src/server/actix.rs +++ b/oasgen/src/server/actix.rs @@ -6,12 +6,12 @@ use actix_web::http::Method; use futures::future::{ok, Ready}; use openapiv3::OpenAPI; use std::sync::Arc; - +use actix_web::http::header::CONTENT_TYPE; use crate::Format; use super::Server; -use oasgen_core::OaSchema; +use oasgen_core::{OaParameter, OaSchema}; #[derive(Default)] pub struct ActixRouter(Vec>); @@ -40,7 +40,7 @@ where } } -pub type InnerResourceFactory<'a> = Box>; +pub type InnerResourceFactory<'a> = Box>; fn build_inner_resource( path: String, @@ -68,7 +68,7 @@ impl Server { F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static, - >::Output: OaSchema, + >::Output: OaParameter, F: Copy + Send, { self.add_handler_to_spec(path, http::Method::GET, &handler); @@ -83,7 +83,7 @@ impl Server { F: Handler + Copy + Send, Args: FromRequest + 'static, F::Output: Responder + 'static, - >::Output: OaSchema, + >::Output: OaParameter, { self.add_handler_to_spec(path, http::Method::POST, &handler); self.router.0.push(build_inner_resource( @@ -124,9 +124,9 @@ impl Server> { } #[derive(Clone)] -struct OaSpecJsonHandler(Arc); +struct OaSpecJsonHandler(Arc); -impl actix_web::dev::Handler<()> for OaSpecJsonHandler { +impl Handler<()> for OaSpecJsonHandler { type Output = Result; type Future = Ready; @@ -136,16 +136,16 @@ impl actix_web::dev::Handler<()> for OaSpecJsonHandler { } #[derive(Clone)] -struct OaSpecYamlHandler(Arc); +struct OaSpecYamlHandler(Arc); -impl actix_web::dev::Handler<()> for OaSpecYamlHandler { +impl Handler<()> for OaSpecYamlHandler { type Output = Result; type Future = Ready; fn call(&self, _: ()) -> Self::Future { let yaml = serde_yaml::to_string(&*self.0).unwrap(); ok(HttpResponse::Ok() - .insert_header(("Content-Type", "text/yaml")) + .insert_header((CONTENT_TYPE, "text/yaml")) .body(yaml)) } } diff --git a/oasgen/tests/test-actix/01-hello.yaml b/oasgen/tests/test-actix/01-hello.yaml index ca463d4..bb3613d 100644 --- a/oasgen/tests/test-actix/01-hello.yaml +++ b/oasgen/tests/test-actix/01-hello.yaml @@ -22,6 +22,12 @@ paths: /get-code: get: operationId: get_code + parameters: + - name: code + schema: + type: string + in: query + style: form responses: '200': description: '' diff --git a/oasgen/tests/test-axum/01-hello.rs b/oasgen/tests/test-axum/01-hello.rs index db634ad..febf05e 100644 --- a/oasgen/tests/test-axum/01-hello.rs +++ b/oasgen/tests/test-axum/01-hello.rs @@ -15,8 +15,8 @@ pub struct SendCodeResponse { /// Endpoint to login by sending a code to the given mobile number #[oasgen(tags("auth"), summary = "A shorter description")] -async fn send_code(_body: Json) -> Json { - Json(SendCodeResponse { found_account: false }) +async fn send_code(_body: Json) -> Result, String> { + Ok(Json(SendCodeResponse { found_account: false })) } fn main() { diff --git a/oasgen/tests/test-axum/02-query.yaml b/oasgen/tests/test-axum/02-query.yaml index bb8cd37..dfc4f0e 100644 --- a/oasgen/tests/test-axum/02-query.yaml +++ b/oasgen/tests/test-axum/02-query.yaml @@ -7,9 +7,14 @@ paths: get: operationId: list_tasks parameters: - - name: query + - name: completed schema: - $ref: '#/components/schemas/TaskFilter' + type: boolean + in: query + style: form + - name: assigned_to + schema: + type: integer in: query style: form responses: {}