1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! This example demonstrates how to create a web router.
//!
//! Within this example you also find the use of middleware
//! that can be used to redirect incoming uris to dynamic
//! uri's (derived from request Uri), as well as static one.
//! This can be useful to direct requests to a central destination
//! or to migrate users to new versions.
//!
//! ```sh
//! cargo run --example http_web_router --features=http-full
//! ```
//!
//! # Expected output
//!
//! The server will start and listen on `:62018`. You can use your browser to interact with the service:
//!
//! ```sh
//! open http://127.0.0.1:62018
//! curl -v -X POST http://127.0.0.1:62018/greet/world
//! curl -v http://127.0.0.1:62018/lang/fr
//! curl -v -L 'http://127.0.0.1:62018/greet?lang=fr'
//! curl -v -L http://127.0.0.1:62018/api/v1/status
//! curl -v http://127.0.0.1:62018/api/v2/status
//! ```
//!
//! You should see the homepage in your browser with the title "Rama Web Router".
// rama provides everything out of the box to build a complete web service.
#![expect(
clippy::unwrap_used,
clippy::expect_used,
reason = "example/test/bench: panic-on-error and print-for-output are the standard patterns for demos and harnesses"
)]
use rama::{
Layer,
http::{
Method,
headers::exotic::XClacksOverhead,
layer::{
error_handling::ErrorHandlerLayer, match_redirect::UriMatchRedirectLayer,
set_header::SetResponseHeaderLayer, trace::TraceLayer,
},
server::HttpServer,
service::web::{
Router,
extract::Path,
response::{Html, Json, Redirect},
},
},
net::http::uri::UriMatchReplaceRule,
rt::Executor,
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
};
/// Everything else we need is provided by the standard library, community crates or tokio.
use serde::Deserialize;
use serde_json::json;
use std::{sync::Arc, time::Duration};
const ADDRESS: &str = "127.0.0.1:62018";
#[tokio::main]
async fn main() {
tracing::subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
let graceful = rama::graceful::Shutdown::default();
#[derive(Debug, Deserialize)]
struct PostGreetForPathParams {
name: String,
}
#[derive(Debug, Deserialize)]
struct GetGreetingPathParams {
code: String,
}
let router = Router::new()
.with_get("/", Html(r##"<h1>Rama - Web Router</h1>"##.to_owned()))
// route with a parameter
.with_post(
"/greet/{name}",
async |method: Method, Path(PostGreetForPathParams { name }): Path<PostGreetForPathParams>| {
Json(json!({
"method": method.as_str(),
"message": format!("Hello, {name}!"),
}))
},
)
// catch-all route
.with_get(
"/lang/{*code}",
async |Path(GetGreetingPathParams { code }): Path<GetGreetingPathParams>| {
let translations = [
("en", "Welcome to our site!"),
("fr", "Bienvenue sur notre site!"),
("es", "¡Bienvenido a nuestro sitio!"),
];
let message = translations
.iter()
.find(|(lang, _)| *lang == code)
.map(|(_, message)| *message)
.unwrap_or("Language not supported");
Json(json!({
"message": message,
}))
},
)
// sub route support - api version health check
.with_sub_router_make_fn("/api", |router| {
router.with_sub_router_make_fn("/v2", |router| {
router.with_get("/status", async || {
Json(json!({
"status": "API v2 is up and running",
}))
})
})
})
.with_not_found(Redirect::temporary("/"));
let middlewares = (
TraceLayer::new_for_http(),
SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
UriMatchRedirectLayer::permanent([
UriMatchReplaceRule::try_new("*/v1/*", "$1/v2/$2").unwrap(), // upgrade users as-is to v2 (backwards compatible)
// this is now a new endpoint,
// NOTE though that query matches are pretty fragile,
// and instead you should try to keep it to the authority, scheme and path only,
// and instead either preserve or drop the query parameter
UriMatchReplaceRule::try_new("*/greet\\?lang=*", "$1/lang/$2").unwrap(),
]),
ErrorHandlerLayer::new(),
);
graceful.spawn_task_fn(async |guard| {
tracing::info!("running service at: {ADDRESS}");
let exec = Executor::graceful(guard);
HttpServer::auto(exec)
.listen(ADDRESS, Arc::new(middlewares.into_layer(router)))
.await
.unwrap();
});
graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}