From 02386d5732210093c68aea548b8564d617cbd311 Mon Sep 17 00:00:00 2001 From: Pascal Bach Date: Thu, 15 Feb 2024 15:55:11 +0100 Subject: [PATCH 1/3] rename axum-path test to axum-query test The test does use axum Query not Path --- oasgen/tests/test-axum.rs | 2 +- oasgen/tests/test-axum/{02-path.rs => 02-query.rs} | 2 +- oasgen/tests/test-axum/{02-path.yaml => 02-query.yaml} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename oasgen/tests/test-axum/{02-path.rs => 02-query.rs} (93%) rename oasgen/tests/test-axum/{02-path.yaml => 02-query.yaml} (100%) diff --git a/oasgen/tests/test-axum.rs b/oasgen/tests/test-axum.rs index c8c109a..0f767ef 100644 --- a/oasgen/tests/test-axum.rs +++ b/oasgen/tests/test-axum.rs @@ -2,5 +2,5 @@ fn run_tests() { let t = trybuild::TestCases::new(); t.pass("tests/test-axum/01-hello.rs"); - t.pass("tests/test-axum/02-path.rs"); + t.pass("tests/test-axum/02-query.rs"); } \ No newline at end of file diff --git a/oasgen/tests/test-axum/02-path.rs b/oasgen/tests/test-axum/02-query.rs similarity index 93% rename from oasgen/tests/test-axum/02-path.rs rename to oasgen/tests/test-axum/02-query.rs index e2b131d..e54e3b1 100644 --- a/oasgen/tests/test-axum/02-path.rs +++ b/oasgen/tests/test-axum/02-query.rs @@ -21,7 +21,7 @@ fn main() { ; let spec = serde_yaml::to_string(&server.openapi).unwrap(); - let other = include_str!("02-path.yaml"); + let other = include_str!("02-query.yaml"); assert_eq!(spec.trim(), other); let router = axum::Router::new() .merge(server.freeze().into_router()); diff --git a/oasgen/tests/test-axum/02-path.yaml b/oasgen/tests/test-axum/02-query.yaml similarity index 100% rename from oasgen/tests/test-axum/02-path.yaml rename to oasgen/tests/test-axum/02-query.yaml From ed97d8c41a5abe3b345a2c4018af2f37c8cf4303 Mon Sep 17 00:00:00 2001 From: Pascal Bach Date: Thu, 15 Feb 2024 15:56:23 +0100 Subject: [PATCH 2/3] add axum Path extraction capability This turns a axum::extract::Path into a parameter --- core/src/schema/axum.rs | 3 ++- oasgen/tests/test-axum.rs | 1 + oasgen/tests/test-axum/03-path.rs | 29 +++++++++++++++++++++++++++++ oasgen/tests/test-axum/03-path.yaml | 28 ++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 oasgen/tests/test-axum/03-path.rs create mode 100644 oasgen/tests/test-axum/03-path.yaml diff --git a/core/src/schema/axum.rs b/core/src/schema/axum.rs index 08f7a41..a4cac31 100644 --- a/core/src/schema/axum.rs +++ b/core/src/schema/axum.rs @@ -76,7 +76,8 @@ impl OaSchema for axum::extract::Path { } fn parameters() -> Vec> { - T::parameters() + let p = oa::Parameter::path("WHERE?", T::schema_ref()); + vec![ReferenceOr::Item(p)] } fn body_schema() -> Option> { diff --git a/oasgen/tests/test-axum.rs b/oasgen/tests/test-axum.rs index 0f767ef..d687967 100644 --- a/oasgen/tests/test-axum.rs +++ b/oasgen/tests/test-axum.rs @@ -3,4 +3,5 @@ fn run_tests() { let t = trybuild::TestCases::new(); t.pass("tests/test-axum/01-hello.rs"); t.pass("tests/test-axum/02-query.rs"); + t.pass("tests/test-axum/03-path.rs"); } \ No newline at end of file diff --git a/oasgen/tests/test-axum/03-path.rs b/oasgen/tests/test-axum/03-path.rs new file mode 100644 index 0000000..2411862 --- /dev/null +++ b/oasgen/tests/test-axum/03-path.rs @@ -0,0 +1,29 @@ +use axum::extract::{Path, Json}; +use oasgen::{OaSchema, oasgen, Server}; +use serde::{Deserialize}; + +/// Send a code to a mobile number +#[derive(Deserialize, OaSchema)] +pub struct TaskFilter { + pub completed: bool, + pub assigned_to: i32, +} + +#[oasgen] +async fn get_task(Path(_id): Path) -> Json<()> { + Json(()) +} + +fn main() { + use pretty_assertions::assert_eq; + let server = Server::axum() + .get("/tasks/:id/", get_task) + ; + + let spec = serde_yaml::to_string(&server.openapi).unwrap(); + let other = include_str!("03-path.yaml"); + assert_eq!(spec.trim(), other); + let router = axum::Router::new() + .merge(server.freeze().into_router()); + router.into_make_service(); +} \ No newline at end of file diff --git a/oasgen/tests/test-axum/03-path.yaml b/oasgen/tests/test-axum/03-path.yaml new file mode 100644 index 0000000..1001aed --- /dev/null +++ b/oasgen/tests/test-axum/03-path.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.3 +info: + title: '' + version: '' +paths: + /tasks/{id}/: + get: + operationId: get_task + parameters: + - name: id + schema: + type: integer + in: path + style: simple + responses: {} +components: + schemas: + TaskFilter: + description: Send a code to a mobile number + type: object + properties: + completed: + type: boolean + assigned_to: + type: integer + required: + - completed + - assigned_to \ No newline at end of file From 2dcbde4e2148d241f54a54ff955af0e8788b4df5 Mon Sep 17 00:00:00 2001 From: Kurt Wolf Date: Thu, 15 Feb 2024 11:47:43 -0500 Subject: [PATCH 3/3] add modifying parameter names during route/operation registration --- core/src/schema/axum.rs | 2 +- oasgen/src/server.rs | 98 +++++++++++++++++++++++++++++++---------- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/core/src/schema/axum.rs b/core/src/schema/axum.rs index a4cac31..215331a 100644 --- a/core/src/schema/axum.rs +++ b/core/src/schema/axum.rs @@ -76,7 +76,7 @@ impl OaSchema for axum::extract::Path { } fn parameters() -> Vec> { - let p = oa::Parameter::path("WHERE?", T::schema_ref()); + let p = oa::Parameter::path("path", T::schema_ref()); vec![ReferenceOr::Item(p)] } diff --git a/oasgen/src/server.rs b/oasgen/src/server.rs index 73c82a2..0fd8a35 100644 --- a/oasgen/src/server.rs +++ b/oasgen/src/server.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use http::Method; use once_cell::sync::Lazy; -use openapiv3::{OpenAPI, Operation, ReferenceOr}; +use openapiv3::{OpenAPI, Operation, ReferenceOr, Parameter, ParameterKind}; use oasgen_core::{OaSchema}; @@ -89,32 +89,25 @@ impl Server { } /// Add a handler to the OpenAPI spec (which is different than mounting it to a server). - fn add_handler_to_spec(&mut self, path: &str, method: Method, _handler: &F) - where - { - let mut path = path.to_string(); - if path.contains(':') { - use once_cell::sync::OnceCell; - use regex::Regex; - static REMAP: OnceCell = OnceCell::new(); - let remap = REMAP.get_or_init(|| Regex::new("/:([a-zA-Z0-9_]+)/").unwrap()); - path = remap.replace_all(&path, "/{$1}/").to_string(); - } - let item = self.openapi.paths.paths.entry(path.to_string()).or_default(); + fn add_handler_to_spec(&mut self, path: &str, method: Method, _handler: &F) { + use http::Method; + let path = replace_path_params(path); + let item = self.openapi.paths.paths.entry(path.clone()).or_default(); let item = item.as_mut().expect("Currently don't support references for PathItem"); let type_name = std::any::type_name::(); - let operation = OPERATION_LOOKUP.get(type_name) + let mut operation = OPERATION_LOOKUP.get(type_name) .expect(&format!("Operation {} not found in OpenAPI spec.", type_name))(); - match method.as_str() { - "GET" => item.get = Some(operation), - "POST" => item.post = Some(operation), - "PUT" => item.put = Some(operation), - "DELETE" => item.delete = Some(operation), - "OPTIONS" => item.options = Some(operation), - "HEAD" => item.head = Some(operation), - "PATCH" => item.patch = Some(operation), - "TRACE" => item.trace = Some(operation), + modify_parameter_names(&mut operation, &path); + match method { + Method::GET => item.get = Some(operation), + Method::POST => item.post = Some(operation), + Method::PUT => item.put = Some(operation), + Method::DELETE => item.delete = Some(operation), + Method::OPTIONS => item.options = Some(operation), + Method::HEAD => item.head = Some(operation), + Method::PATCH => item.patch = Some(operation), + Method::TRACE => item.trace = Some(operation), _ => panic!("Unsupported method: {}", method), } } @@ -220,4 +213,61 @@ impl Server { swagger_ui: self.swagger_ui, } } -} \ No newline at end of file +} + +// Note: this takes an OpenAPI url, which parameterizes like: /path/{param} +fn modify_parameter_names(operation: &mut Operation, path: &str) { + if !path.contains("{") { + return; + } + let path_parts = path.split("/") + .filter(|part| part.starts_with("{")) + .map(|part| &part[1..part.len() - 1]); + let path_params = operation.parameters.iter_mut() + .filter_map(|mut p| p.as_mut()) + .filter(|p| matches!(p.kind, ParameterKind::Path { .. })); + + for (part, param) in path_parts.zip(path_params) { + param.name = part.to_string(); + } +} + +// Note: this takes an axum/actix url, which parameterizes like: /path/:param +fn replace_path_params(path: &str) -> String { + if !path.contains(':') { + return path.to_string(); + } + use once_cell::sync::OnceCell; + use regex::Regex; + static REMAP: OnceCell = OnceCell::new(); + let remap = REMAP.get_or_init(|| Regex::new("/:([a-zA-Z0-9_]+)").unwrap()); + remap.replace_all(&path, "/{$1}").to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use openapiv3 as oa; + + #[test] + fn test_modify_parameter_names() { + let path = "/api/v1/pet/{id}/"; + let mut operation = Operation::default(); + operation.parameters.push(Parameter::path("path", oa::Schema::new_number()).into()); + operation.parameters.push(Parameter::query("query", oa::Schema::new_number()).into()); + modify_parameter_names(&mut operation, path); + assert_eq!(operation.parameters[0].as_item().unwrap().name, "id", "path param name is updated"); + assert_eq!(operation.parameters[1].as_item().unwrap().name, "query", "leave query param alone"); + } + + #[test] + fn test_replace_path_params() { + let path = "/api/v1/pet/:id/"; + let path = replace_path_params(path); + assert_eq!(path, "/api/v1/pet/{id}/"); + + let path = "/api/v1/pet/:id"; + let path = replace_path_params(path); + assert_eq!(path, "/api/v1/pet/{id}"); + } +}