#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] #[cfg_attr( feature = "utoipa", derive(utoipa::ToSchema, serde::Deserialize, serde::Serialize), schema(example = "v0"), serde(rename_all = "camelCase") )] pub enum Version { V0, } #[cfg(feature = "axum")] mod request { use super::*; use axum::RequestPartsExt; use axum::extract::{FromRequestParts, Path}; use axum::http::StatusCode; use axum::http::request::Parts; use axum::response::{IntoResponse, Response}; use std::collections::HashMap; impl FromRequestParts for Version where S: Send + Sync, { type Rejection = Response; async fn from_request_parts( parts: &mut Parts, _state: &S, ) -> Result { let params: Path> = parts.extract().await.map_err(IntoResponse::into_response)?; let version = params .get("apiVersion") .ok_or_else(|| (StatusCode::NOT_FOUND, "version param missing").into_response())?; match version.as_str() { "v0" => Ok(Version::V0), _ => Err((StatusCode::NOT_FOUND, "unknown version").into_response()), } } } } #[cfg(test)] mod tests { use super::*; use axum::{ Router, body::Body, http::{Request, StatusCode}, routing::get, }; use tower::ServiceExt; async fn handler(version: Version) -> &'static str { match version { Version::V0 => "ok", } } async fn check(endpoint: &str, expected: StatusCode) { let app = app(); let response = app .oneshot( Request::builder() .uri(endpoint) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(expected, response.status()); } fn app() -> Router { Router::new().route("/{apiVersion}/test", get(handler)) } #[tokio::test] async fn valid_version_v0() { check("/v0/test", StatusCode::OK).await } #[tokio::test] async fn unknown_version() { check("/v1/test", StatusCode::NOT_FOUND).await } #[tokio::test] async fn missing_version_param() { check("/test", StatusCode::NOT_FOUND).await } }