1use fastapi_rust::core::{
48 App, Body, Request, RequestContext, Response, ResponseBody, StatusCode, TestClient,
49};
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52use std::sync::Mutex;
53
54#[derive(Debug, Clone, Deserialize)]
59struct UserInput {
60 name: String,
61 email: String,
62}
63
64#[derive(Debug, Clone, Serialize)]
65struct User {
66 id: u64,
67 name: String,
68 email: String,
69}
70
71struct UserDb {
72 users: HashMap<u64, User>,
73 next_id: u64,
74}
75
76static STORE: Mutex<Option<UserDb>> = Mutex::new(None);
79
80fn with_db<R>(f: impl FnOnce(&mut UserDb) -> R) -> R {
81 let mut guard = STORE
82 .lock()
83 .unwrap_or_else(std::sync::PoisonError::into_inner);
84 let db = guard.get_or_insert_with(|| UserDb {
85 users: HashMap::new(),
86 next_id: 1,
87 });
88 f(db)
89}
90
91fn parse_json_body<T: serde::de::DeserializeOwned>(req: &mut Request) -> Result<T, Response> {
96 let is_json = req
97 .headers()
98 .get("content-type")
99 .is_some_and(|ct| ct.starts_with(b"application/json"));
100
101 if !is_json {
102 return Err(json_error(
103 StatusCode::UNSUPPORTED_MEDIA_TYPE,
104 "Content-Type must be application/json",
105 ));
106 }
107
108 let Body::Bytes(body) = req.take_body() else {
109 return Err(json_error(StatusCode::BAD_REQUEST, "Missing request body"));
110 };
111 serde_json::from_slice(&body)
112 .map_err(|e| json_error(StatusCode::BAD_REQUEST, &format!("Invalid JSON: {e}")))
113}
114
115fn extract_user_id(req: &Request) -> Result<u64, Response> {
116 let path = req.path();
117 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
118 segments
119 .get(1)
120 .and_then(|s| s.parse::<u64>().ok())
121 .ok_or_else(|| json_error(StatusCode::BAD_REQUEST, "Invalid user ID"))
122}
123
124fn json_error(status: StatusCode, detail: &str) -> Response {
125 let body = serde_json::json!({ "detail": detail });
126 Response::with_status(status)
127 .header("content-type", b"application/json".to_vec())
128 .body(ResponseBody::Bytes(body.to_string().into_bytes()))
129}
130
131fn json_response(status: StatusCode, value: &impl Serialize) -> Response {
132 match serde_json::to_string(value) {
133 Ok(text) => Response::with_status(status)
134 .header("content-type", b"application/json".to_vec())
135 .body(ResponseBody::Bytes(text.into_bytes())),
136 Err(err) => json_error(
137 StatusCode::INTERNAL_SERVER_ERROR,
138 &format!("JSON serialization failed: {err}"),
139 ),
140 }
141}
142
143fn validate_input(input: &UserInput) -> Option<Response> {
144 if input.name.trim().is_empty() {
145 return Some(json_error(
146 StatusCode::BAD_REQUEST,
147 "name must not be empty",
148 ));
149 }
150 if !input.email.contains('@') {
151 return Some(json_error(
152 StatusCode::BAD_REQUEST,
153 "email must contain '@'",
154 ));
155 }
156 None
157}
158
159fn create_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
164 let input = match parse_json_body::<UserInput>(req) {
165 Ok(v) => v,
166 Err(r) => return std::future::ready(r),
167 };
168 if let Some(r) = validate_input(&input) {
169 return std::future::ready(r);
170 }
171
172 let user = with_db(|db| {
173 let id = db.next_id;
174 db.next_id += 1;
175 let user = User {
176 id,
177 name: input.name,
178 email: input.email,
179 };
180 db.users.insert(id, user.clone());
181 user
182 });
183
184 std::future::ready(json_response(StatusCode::CREATED, &user))
185}
186
187fn list_users(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
188 let users = with_db(|db| {
189 let mut v: Vec<User> = db.users.values().cloned().collect();
190 v.sort_by_key(|u| u.id);
191 v
192 });
193 std::future::ready(json_response(StatusCode::OK, &users))
194}
195
196fn get_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
197 let id = match extract_user_id(req) {
198 Ok(id) => id,
199 Err(r) => return std::future::ready(r),
200 };
201 let result = with_db(|db| db.users.get(&id).cloned());
202 match result {
203 Some(user) => std::future::ready(json_response(StatusCode::OK, &user)),
204 None => std::future::ready(json_error(
205 StatusCode::NOT_FOUND,
206 &format!("User {id} not found"),
207 )),
208 }
209}
210
211fn update_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
212 let id = match extract_user_id(req) {
213 Ok(id) => id,
214 Err(r) => return std::future::ready(r),
215 };
216 let input = match parse_json_body::<UserInput>(req) {
217 Ok(v) => v,
218 Err(r) => return std::future::ready(r),
219 };
220 if let Some(r) = validate_input(&input) {
221 return std::future::ready(r);
222 }
223
224 let result = with_db(|db| {
225 db.users.get_mut(&id).map(|user| {
226 user.name = input.name;
227 user.email = input.email;
228 user.clone()
229 })
230 });
231 match result {
232 Some(user) => std::future::ready(json_response(StatusCode::OK, &user)),
233 None => std::future::ready(json_error(
234 StatusCode::NOT_FOUND,
235 &format!("User {id} not found"),
236 )),
237 }
238}
239
240fn delete_user(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
241 let id = match extract_user_id(req) {
242 Ok(id) => id,
243 Err(r) => return std::future::ready(r),
244 };
245 let removed = with_db(|db| db.users.remove(&id).is_some());
246 if removed {
247 std::future::ready(Response::with_status(StatusCode::NO_CONTENT))
248 } else {
249 std::future::ready(json_error(
250 StatusCode::NOT_FOUND,
251 &format!("User {id} not found"),
252 ))
253 }
254}
255
256#[allow(clippy::needless_pass_by_value)]
261fn check_eq<T: PartialEq + std::fmt::Debug>(left: T, right: T, message: &str) -> bool {
262 if left == right {
263 true
264 } else {
265 eprintln!("Check failed: {message}. left={left:?} right={right:?}");
266 false
267 }
268}
269
270#[allow(clippy::too_many_lines)]
271fn main() {
272 let mut guard = STORE
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 *guard = Some(UserDb {
277 users: HashMap::new(),
278 next_id: 1,
279 });
280
281 println!("fastapi_rust CRUD API Example");
282 println!("=============================\n");
283
284 let app = App::builder()
285 .post("/users", create_user)
286 .get("/users", list_users)
287 .get("/users/{id}", get_user)
288 .put("/users/{id}", update_user)
289 .delete("/users/{id}", delete_user)
290 .build();
291
292 println!("App created with {} route(s)\n", app.route_count());
293 let client = TestClient::new(app);
294
295 println!("1. Create users");
297 let resp = client
298 .post("/users")
299 .header("content-type", "application/json")
300 .body(r#"{"name": "Alice", "email": "alice@example.com"}"#)
301 .send();
302 println!(
303 " POST /users -> {} {}",
304 resp.status().as_u16(),
305 resp.text()
306 );
307 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
308 return;
309 }
310
311 let resp = client
312 .post("/users")
313 .header("content-type", "application/json")
314 .body(r#"{"name": "Bob", "email": "bob@example.com"}"#)
315 .send();
316 println!(
317 " POST /users -> {} {}",
318 resp.status().as_u16(),
319 resp.text()
320 );
321 if !check_eq(resp.status().as_u16(), 201, "Create user should return 201") {
322 return;
323 }
324
325 println!("\n2. List all users");
327 let resp = client.get("/users").send();
328 println!(
329 " GET /users -> {} {}",
330 resp.status().as_u16(),
331 resp.text()
332 );
333 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
334 return;
335 }
336
337 println!("\n3. Get user by ID");
339 let resp = client.get("/users/1").send();
340 println!(
341 " GET /users/1 -> {} {}",
342 resp.status().as_u16(),
343 resp.text()
344 );
345 if !check_eq(resp.status().as_u16(), 200, "Get user should return 200") {
346 return;
347 }
348
349 println!("\n4. Get nonexistent user");
351 let resp = client.get("/users/999").send();
352 println!(
353 " GET /users/999 -> {} {}",
354 resp.status().as_u16(),
355 resp.text()
356 );
357 if !check_eq(
358 resp.status().as_u16(),
359 404,
360 "Missing user should return 404",
361 ) {
362 return;
363 }
364
365 println!("\n5. Update user");
367 let resp = client
368 .put("/users/1")
369 .header("content-type", "application/json")
370 .body(r#"{"name": "Alice Smith", "email": "alice.smith@example.com"}"#)
371 .send();
372 println!(
373 " PUT /users/1 -> {} {}",
374 resp.status().as_u16(),
375 resp.text()
376 );
377 if !check_eq(resp.status().as_u16(), 200, "Update user should return 200") {
378 return;
379 }
380
381 println!("\n6. Validation error (empty name)");
383 let resp = client
384 .post("/users")
385 .header("content-type", "application/json")
386 .body(r#"{"name": "", "email": "bad@example.com"}"#)
387 .send();
388 println!(
389 " POST /users -> {} {}",
390 resp.status().as_u16(),
391 resp.text()
392 );
393 if !check_eq(resp.status().as_u16(), 400, "Empty name should return 400") {
394 return;
395 }
396
397 println!("\n7. Validation error (invalid email)");
399 let resp = client
400 .post("/users")
401 .header("content-type", "application/json")
402 .body(r#"{"name": "Charlie", "email": "not-an-email"}"#)
403 .send();
404 println!(
405 " POST /users -> {} {}",
406 resp.status().as_u16(),
407 resp.text()
408 );
409 if !check_eq(
410 resp.status().as_u16(),
411 400,
412 "Invalid email should return 400",
413 ) {
414 return;
415 }
416
417 println!("\n8. Wrong Content-Type");
419 let resp = client
420 .post("/users")
421 .header("content-type", "text/plain")
422 .body(r#"{"name": "Dan", "email": "dan@example.com"}"#)
423 .send();
424 println!(
425 " POST /users -> {} {}",
426 resp.status().as_u16(),
427 resp.text()
428 );
429 if !check_eq(
430 resp.status().as_u16(),
431 415,
432 "Wrong Content-Type should return 415",
433 ) {
434 return;
435 }
436
437 println!("\n9. Delete user");
439 let resp = client.delete("/users/2").send();
440 println!(" DELETE /users/2 -> {}", resp.status().as_u16());
441 if !check_eq(resp.status().as_u16(), 204, "Delete user should return 204") {
442 return;
443 }
444
445 println!("\n10. Verify deletion");
447 let resp = client.get("/users/2").send();
448 println!(
449 " GET /users/2 -> {} {}",
450 resp.status().as_u16(),
451 resp.text()
452 );
453 if !check_eq(
454 resp.status().as_u16(),
455 404,
456 "Deleted user should return 404",
457 ) {
458 return;
459 }
460
461 let resp = client.get("/users").send();
462 println!(
463 " GET /users -> {} {}",
464 resp.status().as_u16(),
465 resp.text()
466 );
467 if !check_eq(resp.status().as_u16(), 200, "List users should return 200") {
468 return;
469 }
470
471 println!("\nAll CRUD operations passed!");
472}