versioning - path parameter with fixed values
Sometimes the path-parameter is from a fixed set of values. For example if we are build an API and the first part of the path is the
version number of the API then we might accept the strings v1
, v2
, v3
, but no other value.
In this example we see exactly that.
[package]
name = "example-versioning"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
axum = { path = "../../axum" }
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
http-body-util = "0.1.0"
tower = { version = "0.5.2", features = ["util"] }
//! Run with //! //! ```not_rust //! cargo run -p example-versioning //! ``` use axum::{ extract::{FromRequestParts, Path}, http::{request::Parts, StatusCode}, response::{Html, IntoResponse, Response}, routing::get, RequestPartsExt, Router, }; use std::collections::HashMap; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // build our application with some routes let app = app(); // run it let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } fn app() -> Router { Router::new().route("/{version}/foo", get(handler)) } async fn handler(version: Version) -> Html<String> { Html(format!("received request with version {version:?}")) } #[derive(Debug)] enum Version { V1, V2, V3, } impl<S> FromRequestParts<S> for Version where S: Send + Sync, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { let params: Path<HashMap<String, String>> = parts.extract().await.map_err(IntoResponse::into_response)?; let version = params .get("version") .ok_or_else(|| (StatusCode::NOT_FOUND, "version param missing").into_response())?; match version.as_str() { "v1" => Ok(Version::V1), "v2" => Ok(Version::V2), "v3" => Ok(Version::V3), _ => Err((StatusCode::NOT_FOUND, "unknown version").into_response()), } } } #[cfg(test)] mod tests { use super::*; use axum::{body::Body, http::Request, http::StatusCode}; use http_body_util::BodyExt; use tower::ServiceExt; #[tokio::test] async fn test_v1() { let response = app() .oneshot( Request::builder() .uri("/v1/foo") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body(); let bytes = body.collect().await.unwrap().to_bytes(); let html = String::from_utf8(bytes.to_vec()).unwrap(); assert_eq!(html, "received request with version V1"); } #[tokio::test] async fn test_v4() { let response = app() .oneshot( Request::builder() .uri("/v4/foo") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); let body = response.into_body(); let bytes = body.collect().await.unwrap().to_bytes(); let html = String::from_utf8(bytes.to_vec()).unwrap(); assert_eq!(html, "unknown version"); } }