Skip to content

Commit

Permalink
Add Support for axum::extract::Path (#14)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
bachp and kurtbuilds authored Feb 15, 2024
1 parent 66657bb commit 23abc41
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 27 deletions.
3 changes: 2 additions & 1 deletion core/src/schema/axum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ impl<T: OaSchema> OaSchema for axum::extract::Path<T> {
}

fn parameters() -> Vec<ReferenceOr<oa::Parameter>> {
T::parameters()
let p = oa::Parameter::path("path", T::schema_ref());
vec![ReferenceOr::Item(p)]
}

fn body_schema() -> Option<ReferenceOr<Schema>> {
Expand Down
98 changes: 74 additions & 24 deletions oasgen/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -89,32 +89,25 @@ impl<Router: Default> Server<Router, OpenAPI> {
}

/// Add a handler to the OpenAPI spec (which is different than mounting it to a server).
fn add_handler_to_spec<F>(&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<Regex> = 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<F>(&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::<F>();
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),
}
}
Expand Down Expand Up @@ -220,4 +213,61 @@ impl<Router: Default> Server<Router, OpenAPI> {
swagger_ui: self.swagger_ui,
}
}
}
}

// 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<Regex> = 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}");
}
}
3 changes: 2 additions & 1 deletion oasgen/tests/test-axum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
File renamed without changes.
29 changes: 29 additions & 0 deletions oasgen/tests/test-axum/03-path.rs
Original file line number Diff line number Diff line change
@@ -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<u64>) -> 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();
}
28 changes: 28 additions & 0 deletions oasgen/tests/test-axum/03-path.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 23abc41

Please sign in to comment.