Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for axum::extract::Path #14

Merged
merged 3 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading