From 23abc4184904576a0317abcdfcf92ab5857ecac2 Mon Sep 17 00:00:00 2001 From: Pascal Bach Date: Thu, 15 Feb 2024 21:13:17 +0100 Subject: [PATCH] Add Support for axum::extract::Path (#14) * rename axum-path test to axum-query test The test does use axum Query not Path * add axum Path extraction capability This turns a axum::extract::Path into a parameter * add modifying parameter names during route/operation registration --------- Co-authored-by: Kurt Wolf --- core/src/schema/axum.rs | 3 +- oasgen/src/server.rs | 98 ++++++++++++++----- oasgen/tests/test-axum.rs | 3 +- .../test-axum/{02-path.rs => 02-query.rs} | 2 +- .../test-axum/{02-path.yaml => 02-query.yaml} | 0 oasgen/tests/test-axum/03-path.rs | 29 ++++++ oasgen/tests/test-axum/03-path.yaml | 28 ++++++ 7 files changed, 136 insertions(+), 27 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%) 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..215331a 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("path", T::schema_ref()); + vec![ReferenceOr::Item(p)] } fn body_schema() -> Option> { 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}"); + } +} diff --git a/oasgen/tests/test-axum.rs b/oasgen/tests/test-axum.rs index c8c109a..d687967 100644 --- a/oasgen/tests/test-axum.rs +++ b/oasgen/tests/test-axum.rs @@ -2,5 +2,6 @@ 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"); + t.pass("tests/test-axum/03-path.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 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