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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
//! An example key-value store service that stores bytes data.
//! It is meant to demonstrate the [`WebService`] and [`HttpServer`] capabilities in Rama at a high level.
//!
//! [`WebService`]: crate::http::service::web::WebService
//! [`HttpServer`]: crate::http::server::HttpServer
//!
//! This example demonstrates how to use rama to create a key-value store web service.
//! The service has the following endpoints:
//! - `GET /`: show this API documentation in Json Format
//! - `GET /keys`: list all keys for which (bytes) data is stored
//! - `GET /item/{key}`: return a 200 Ok containing the (bytes) data stored at <key>, and a 404 Not Found otherwise
//! - `HEAD /item/{key}`: return a 200 Ok if found and a 404 Not Found otherwise
//! - `POST /item/{key}`: store the given request payload as the value referenced by <key>, returning a 400 Bad Request if no payload was defined
//!
//! The service also has admin endpoints:
//! - `DELETE /keys`: clear all keys and their associated data
//! - `DELETE /item/{key}`: remove the data stored at <key>, returning a 200 Ok if the key was found, and a 404 Not Found otherwise
//!
//! # Run the example
//!
//! ```sh
//! cargo run --example http_key_value_store --features=compression,http-full
//! ```
//!
//! # Expected output
//!
//! The server will start and listen on `:62006`. You can use `curl` to interact with the service:
//!
//! ```sh
//! # show the API documentation
//! curl -v http://127.0.0.1:62006
//!
//! # store multiple key value pairs
//! curl -v -X POST http://127.0.0.1:62006/items \
//! -H 'content-type: application/json' \
//! -d '{"key1": "value1", "key2": "value2"}'
//!
//! # list all keys
//! curl -v http://127.0.0.1:62006/keys
//!
//! # store a single key value pair
//! curl -v -X POST http://127.0.0.1:62006/item/key3 -d "value3"
//!
//! # get the value for a key
//! curl -v http://127.0.0.1:62006/item/key3
//!
//! # check existence for a key
//! curl -v -XHEAD http://127.0.0.1:62006/item/key3
//!
//! # delete a key
//! curl -v -X DELETE http://127.0.0.1:62006/admin/item/key3 -H "Authorization: Bearer secret-token"
//! ```
#![expect(
clippy::unwrap_used,
reason = "example/test/bench: panic-on-error and print-for-output are the standard patterns for demos and harnesses"
)]
use rama::{
Layer,
conversion::FromRef,
http::{
Method, StatusCode,
layer::{
compression::CompressionLayer, trace::TraceLayer,
validate_request::ValidateRequestHeaderLayer,
},
matcher::HttpMatcher,
server::HttpServer,
service::web::{
IntoEndpointServiceWithState, WebService,
extract::{Bytes, Path, State},
response::{IntoResponse, Json},
},
},
net::{address::SocketAddress, user::credentials::bearer},
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
utils::macros::impl_deref,
};
use ahash::HashMap;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone, Debug, Default, FromRef)]
/// Contains the global shared state
///
/// It's best practise to make sure this is splittable in
/// smaller more focussed parts so handlers can request
/// only the things they need
struct AppState {
db: Db,
}
#[derive(Debug, Clone, Default)]
struct Db(Arc<RwLock<HashMap<String, bytes::Bytes>>>);
impl_deref!(Db: Arc<RwLock<HashMap<String, bytes::Bytes>>>);
#[derive(Debug, Deserialize)]
struct ItemParam {
key: String,
}
#[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 addr = SocketAddress::local_ipv4(62006);
tracing::info!(
network.local.address = %addr.ip_addr,
network.local.port = %addr.port,
"running service",
);
let state = AppState::default();
HttpServer::default()
.listen(
addr,
(TraceLayer::new_for_http())
.into_layer(
WebService::new_with_state(state.clone())
.with_get("/", Json(json!({
"GET /": "show this API documentation in Json Format",
"GET /keys": "list all keys for which (bytes) data is stored",
"GET /item/{key}": "return a 200 Ok containing the (bytes) data stored at <key>, and a 404 Not Found otherwise",
"HEAD /item/{key}": "return a 200 Ok if found, and a 404 Not Found otherwise",
"POST /item/{key}": "store the given request payload as the value referenced by <key>, returning a 400 Bad Request if no payload was defined",
"admin": {
"DELETE /keys": "clear all keys and their associated data",
"DELETE /item/:key": "remove the data stored at <key>, returning a 200 Ok if the key was found, and a 404 Not Found otherwise"
}
})))
.with_get("/keys", list_keys)
.with_nest_service("/admin", ValidateRequestHeaderLayer::auth(bearer!("secret-token"))
.into_layer(WebService::new_with_state(state.clone())
.with_delete("/keys", async |State(db): State<Db>| {
db.write().await.clear();
})
.with_delete("/item/{key}", async |State(db): State<Db>, Path(params): Path<ItemParam>| {
match db.write().await.remove(¶ms.key) {
Some(_) => StatusCode::OK,
None => StatusCode::NOT_FOUND,
}
})))
.with_matcher(
HttpMatcher::method_get().or_method_head().and_path("/item/{key}"),
// only compress the get Action, not the Post Action
CompressionLayer::new()
.into_layer((async |State(db): State<Db>, Path(params): Path<ItemParam>, method: Method| {
match method {
Method::GET => {
match db.read().await.get(¶ms.key) {
Some(b) => b.clone().into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}
Method::HEAD => {
if db.read().await.contains_key(¶ms.key) {
StatusCode::OK
} else {
StatusCode::NOT_FOUND
}.into_response()
}
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}).into_endpoint_service_with_state(state.clone())),
)
.with_post("/items", async |State(db): State<Db>, Json(dict): Json<HashMap<String, String>>| {
let mut db = db.write().await;
for (k, v) in dict {
db.insert(k, bytes::Bytes::from(v));
}
StatusCode::OK
})
.with_post("/item/{key}", async |State(db): State<Db>, Path(params): Path<ItemParam>, Bytes(value): Bytes| {
if value.is_empty() {
return StatusCode::BAD_REQUEST;
}
db.write().await.insert(params.key, value);
StatusCode::OK
}),
),
)
.await
.unwrap();
}
/// a service_fn can be a regular fn, instead of a closure
async fn list_keys(State(db): State<Db>) -> impl IntoResponse {
db.read().await.keys().fold(String::new(), |a, b| {
if a.is_empty() {
b.clone()
} else {
format!("{a}, {b}")
}
})
}