aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorYigit Sever2021-04-07 01:08:31 +0300
committerYigit Sever2021-04-07 01:08:31 +0300
commitc3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc (patch)
tree43345c12a7caf4c94532a7b54638e756af10b3af /src
downloadgradecoin-c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc.tar.gz
gradecoin-c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc.tar.bz2
gradecoin-c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc.zip
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/custom_filters.rs21
-rw-r--r--src/handlers.rs122
-rw-r--r--src/main.rs26
-rw-r--r--src/routes.rs359
-rw-r--r--src/schema.rs223
-rw-r--r--src/validators.rs44
6 files changed, 795 insertions, 0 deletions
diff --git a/src/custom_filters.rs b/src/custom_filters.rs
new file mode 100644
index 0000000..86a78d4
--- /dev/null
+++ b/src/custom_filters.rs
@@ -0,0 +1,21 @@
1// Common filters ment to be shared between many endpoints
2
3use std::convert::Infallible;
4use warp::{Filter, Rejection};
5
6use crate::schema::{Db, Transaction}; // `Block` coming later
7
8// Database context for routes
9pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone {
10 warp::any().map(move || db.clone())
11}
12
13// Optional query params to allow pagination
14// pub fn list_options() -> impl Filter<Extract = (ListOptions,), Error = Rejection> + Clone {
15// warp::query::<ListOptions>()
16// }
17
18// Accept only JSON body and reject big payloads
19pub fn json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone {
20 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
21}
diff --git a/src/handlers.rs b/src/handlers.rs
new file mode 100644
index 0000000..51c7b63
--- /dev/null
+++ b/src/handlers.rs
@@ -0,0 +1,122 @@
1// API handlers, the ends of each filter chain
2
3use log::debug;
4use std::convert::Infallible;
5use warp::{http::StatusCode, reply};
6
7use crate::schema::{Db, Transaction}; // `Block` coming later
8
9// PROPOSE Transaction
10// POST /transaction
11pub async fn propose_transaction(
12 new_transaction: Transaction,
13 db: Db,
14) -> Result<impl warp::Reply, warp::Rejection> {
15 debug!("new transaction request {:?}", new_transaction);
16
17 let mut transactions = db.lock().await;
18
19 transactions.push(new_transaction);
20
21 Ok(StatusCode::CREATED)
22}
23
24// GET Transaction List
25// GET /transaction
26// Returns JSON array of transactions
27// Cannot fail?
28pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> {
29 debug!("list all transactions");
30
31 let transactions = db.lock().await;
32
33 let transactions: Vec<Transaction> = transactions.clone().into_iter().collect();
34
35 Ok(reply::with_status(
36 reply::json(&transactions),
37 StatusCode::OK,
38 ))
39}
40
41// PROPOSE Block
42// POST /block
43
44// `GET /games`
45// Returns JSON array of todos
46// Allows pagination, for example: `GET /games?offset=10&limit=5`
47// pub async fn list_games(options: ListOptions, db: Db) -> Result<impl Reply, Infallible> {
48// debug!("list all games");
49
50// let games = db.lock().await;
51// let games: Vec<Game> = games
52// .clone()
53// .into_iter()
54// .skip(options.offset.unwrap_or(0))
55// .take(options.limit.unwrap_or(std::usize::MAX))
56// .collect();
57
58// Ok(warp::reply::json(&games))
59// }
60
61// `POST /games`
62// Create new game entry with JSON body
63// pub async fn create_game(new_game: Game, db: Db) -> Result<impl Reply, Infallible> {
64// debug!("create new game: {:?}", new_game);
65
66// let mut games = db.lock().await;
67
68// match games.iter().find(|game| game.id == new_game.id) {
69// Some(game) => {
70// debug!("game of given id already exists: {}", game.id);
71
72// Ok(StatusCode::BAD_REQUEST)
73// }
74// None => {
75// games.push(new_game);
76// Ok(StatusCode::CREATED)
77// }
78// }
79// }
80
81// `PUT /games/:id`
82// pub async fn update_game(id: u64, updated_game: Game, db: Db) -> Result<impl Reply, Infallible> {
83// debug!("update existing game: id={}, game={:?}", id, updated_game);
84
85// let mut games = db.lock().await;
86
87// match games.iter_mut().find(|game| game.id == id) {
88// Some(game) => {
89// *game = updated_game;
90
91// Ok(StatusCode::OK)
92// }
93// None => {
94// debug!("game of given id not found");
95
96// Ok(StatusCode::NOT_FOUND)
97// }
98// }
99// }
100
101// `DELETE /games/:id`
102// pub async fn delete_game(id: u64, db: Db) -> Result<impl Reply, Infallible> {
103// debug!("delete game: id={}", id);
104
105// let mut games = db.lock().await;
106
107// let len = games.len();
108
109// // Removes all games with given id
110// games.retain(|game| game.id != id);
111
112// // If games length was smaller that means specyfic game was found and removed
113// let deleted = games.len() != len;
114
115// if deleted {
116// Ok(StatusCode::NO_CONTENT)
117// } else {
118// debug!("game of given id not found");
119
120// Ok(StatusCode::NOT_FOUND)
121// }
122// }
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..bcd4173
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,26 @@
1use std::env;
2use warp::Filter;
3
4mod custom_filters;
5mod handlers;
6mod routes;
7mod schema;
8// mod validators;
9
10#[tokio::main]
11async fn main() {
12 // Show debug logs by default by setting `RUST_LOG=restful_rust=debug`
13 if env::var_os("RUST_LOG").is_none() {
14 env::set_var("RUST_LOG", "restful_rust=debug");
15 }
16 pretty_env_logger::init();
17
18 let db = schema::ledger(); // 1. we need this to return a _simple_ db
19
20 let api = routes::consensus_routes(db);
21
22 let routes = api.with(warp::log("restful_rust"));
23
24 // Start the server
25 warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
26}
diff --git a/src/routes.rs b/src/routes.rs
new file mode 100644
index 0000000..fc4426a
--- /dev/null
+++ b/src/routes.rs
@@ -0,0 +1,359 @@
1use warp::{Filter, Rejection, Reply};
2
3use crate::custom_filters;
4use crate::handlers;
5use crate::schema::Db;
6
7// Root, all routes combined
8pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
9 transaction_list(db.clone()).or(transaction_propose(db.clone()))
10}
11
12// GET /transaction
13pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
14 warp::path!("transaction")
15 .and(warp::get())
16 .and(custom_filters::with_db(db))
17 .and_then(handlers::list_transactions)
18}
19
20// POST /transaction
21pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
22 warp::path!("transaction")
23 .and(warp::post())
24 .and(custom_filters::json_body())
25 .and(custom_filters::with_db(db))
26 .and_then(handlers::propose_transaction)
27}
28
29///////////////////////////
30// below are not mine. //
31///////////////////////////
32
33// Root, all routes combined
34//pub fn games_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
35// games_list(db.clone())
36// .or(games_create(db.clone()))
37// .or(games_update(db.clone()))
38// .or(games_delete(db))
39//}
40
41//// `GET /games?offset=3&limit=5`
42//pub fn games_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
43// warp::path!("games")
44// .and(warp::get())
45// .and(custom_filters::list_options())
46// .and(custom_filters::with_db(db))
47// .and_then(handlers::list_games)
48//}
49
50//// `POST /games`
51//pub fn games_create(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
52// warp::path!("games")
53// .and(warp::post())
54// .and(custom_filters::json_body())
55// .and(custom_filters::with_db(db))
56// .and_then(handlers::create_game)
57//}
58
59//// `PUT /games/:id`
60//pub fn games_update(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
61// warp::path!("games" / u64)
62// .and(warp::put())
63// .and(custom_filters::json_body())
64// .and(custom_filters::with_db(db))
65// .and_then(handlers::update_game)
66//}
67
68//// `DELETE /games/:id`
69//pub fn games_delete(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
70// warp::path!("games" / u64)
71// .and(warp::delete())
72// .and(custom_filters::with_db(db))
73// .and_then(handlers::delete_game)
74//}
75
76////////////////////////////////
77//// tests below, it's fine //
78////////////////////////////////
79
80/////////////////////////////////////
81// of course I'll write tests... //
82/////////////////////////////////////
83
84// TODO: write tests <07-04-21, yigit> //
85
86//#[cfg(test)]
87//mod tests {
88// use super::*;
89
90// use chrono::prelude::*;
91// use std::sync::Arc;
92// use tokio::sync::Mutex;
93// use warp::http::StatusCode;
94
95// use crate::schema::{Game, Genre};
96
97// // Mocked dataset for each test
98
99// fn mocked_db() -> Db {
100// Arc::new(Mutex::new(vec![
101// Game {
102// id: 1,
103// title: String::from("Crappy title"),
104// rating: 35,
105// genre: Genre::RolePlaying,
106// description: Some(String::from("Test description...")),
107// release_date: NaiveDate::from_ymd(2011, 9, 22).and_hms(0, 0, 0),
108// },
109// Game {
110// id: 2,
111// title: String::from("Decent game"),
112// rating: 84,
113// genre: Genre::Strategy,
114// description: None,
115// release_date: NaiveDate::from_ymd(2014, 3, 11).and_hms(0, 0, 0),
116// },
117// ]))
118// }
119
120// fn mocked_game() -> Game {
121// Game {
122// id: 3,
123// title: String::from("Another game"),
124// rating: 65,
125// description: None,
126// genre: Genre::Strategy,
127// release_date: NaiveDate::from_ymd(2016, 3, 11).and_hms(0, 0, 0),
128// }
129// }
130
131// #[tokio::test]
132// async fn get_list_of_games_200() {
133// let db = mocked_db();
134// let filter = games_routes(db);
135
136// let res = warp::test::request().method("GET").path("/games").reply(&filter).await;
137
138// assert_eq!(res.status(), StatusCode::OK);
139
140// let expected_json_body = r#"[{"id":1,"title":"Crappy title","rating":35,"genre":"ROLE_PLAYING","description":"Test description...","releaseDate":"2011-09-22T00:00:00"},{"id":2,"title":"Decent game","rating":84,"genre":"STRATEGY","description":null,"releaseDate":"2014-03-11T00:00:00"}]"#;
141// assert_eq!(res.body(), expected_json_body);
142// }
143
144// #[tokio::test]
145// async fn get_list_of_games_with_options_200() {
146// let db = mocked_db();
147// let filter = games_routes(db);
148
149// let res = warp::test::request()
150// .method("GET")
151// .path("/games?offset=1&limit=5")
152// .reply(&filter)
153// .await;
154
155// assert_eq!(res.status(), StatusCode::OK);
156
157// let expected_json_body = r#"[{"id":2,"title":"Decent game","rating":84,"genre":"STRATEGY","description":null,"releaseDate":"2014-03-11T00:00:00"}]"#;
158// assert_eq!(res.body(), expected_json_body);
159// }
160
161// #[tokio::test]
162// async fn get_empty_list_with_offset_overshot_200() {
163// let db = mocked_db();
164// let filter = games_routes(db);
165
166// let res = warp::test::request()
167// .method("GET")
168// .path("/games?offset=5&limit=5")
169// .reply(&filter)
170// .await;
171
172// assert_eq!(res.status(), StatusCode::OK);
173
174// let expected_json_body = r#"[]"#;
175// assert_eq!(res.body(), expected_json_body);
176// }
177
178// #[tokio::test]
179// async fn get_incorrect_options_400() {
180// let db = mocked_db();
181// let filter = games_routes(db);
182
183// let res = warp::test::request()
184// .method("GET")
185// .path("/games?offset=a&limit=b")
186// .reply(&filter)
187// .await;
188
189// assert_eq!(res.status(), StatusCode::BAD_REQUEST);
190// }
191
192// #[tokio::test]
193// async fn get_wrong_path_405() {
194// let db = mocked_db();
195// let filter = games_routes(db);
196
197// let res = warp::test::request()
198// .method("GET")
199// .path("/games/42")
200// .reply(&filter)
201// .await;
202
203// assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
204// }
205
206// #[tokio::test]
207// async fn post_json_201() {
208// let db = mocked_db();
209// let filter = games_routes(db.clone());
210
211// let res = warp::test::request()
212// .method("POST")
213// .json(&mocked_game())
214// .path("/games")
215// .reply(&filter)
216// .await;
217
218// assert_eq!(res.status(), StatusCode::CREATED);
219// assert_eq!(db.lock().await.len(), 3);
220// }
221
222// #[tokio::test]
223// async fn post_too_long_content_413() {
224// let db = mocked_db();
225// let filter = games_routes(db);
226
227// let res = warp::test::request()
228// .method("POST")
229// .header("content-length", 1024 * 36)
230// .path("/games")
231// .reply(&filter)
232// .await;
233
234// assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
235// }
236
237// #[tokio::test]
238// async fn post_wrong_payload_400() {
239// let db = mocked_db();
240// let filter = games_routes(db);
241
242// let res = warp::test::request()
243// .method("POST")
244// .body(&r#"{"id":4}"#)
245// .path("/games")
246// .reply(&filter)
247// .await;
248
249// assert_eq!(res.status(), StatusCode::BAD_REQUEST);
250// }
251
252// #[tokio::test]
253// async fn post_wrong_path_405() {
254// let db = mocked_db();
255// let filter = games_routes(db);
256
257// let res = warp::test::request()
258// .method("POST")
259// .path("/games/42")
260// .reply(&filter)
261// .await;
262
263// assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
264// }
265
266// #[tokio::test]
267// async fn put_json_200() {
268// let db = mocked_db();
269// let filter = games_routes(db.clone());
270
271// let res = warp::test::request()
272// .method("PUT")
273// .json(&mocked_game())
274// .path("/games/2")
275// .reply(&filter)
276// .await;
277
278// assert_eq!(res.status(), StatusCode::OK);
279
280// let db = db.lock().await;
281// let ref title = db[1].title;
282// assert_eq!(title, "Another game");
283// }
284
285// #[tokio::test]
286// async fn put_wrong_id_404() {
287// let db = mocked_db();
288// let filter = games_routes(db);
289
290// let res = warp::test::request()
291// .method("PUT")
292// .json(&mocked_game())
293// .path("/games/42")
294// .reply(&filter)
295// .await;
296
297// assert_eq!(res.status(), StatusCode::NOT_FOUND);
298// }
299
300// #[tokio::test]
301// async fn put_wrong_payload_400() {
302// let db = mocked_db();
303// let filter = games_routes(db);
304
305// let res = warp::test::request()
306// .method("PUT")
307// .header("content-length", 1024 * 16)
308// .body(&r#"{"id":2"#)
309// .path("/games/2")
310// .reply(&filter)
311// .await;
312
313// assert_eq!(res.status(), StatusCode::BAD_REQUEST);
314// }
315
316// #[tokio::test]
317// async fn put_too_long_content_413() {
318// let db = mocked_db();
319// let filter = games_routes(db);
320
321// let res = warp::test::request()
322// .method("PUT")
323// .header("content-length", 1024 * 36)
324// .path("/games/2")
325// .reply(&filter)
326// .await;
327
328// assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
329// }
330
331// #[tokio::test]
332// async fn delete_wrong_id_404() {
333// let db = mocked_db();
334// let filter = games_routes(db);
335
336// let res = warp::test::request()
337// .method("DELETE")
338// .path("/games/42")
339// .reply(&filter)
340// .await;
341
342// assert_eq!(res.status(), StatusCode::NOT_FOUND);
343// }
344
345// #[tokio::test]
346// async fn delete_game_204() {
347// let db = mocked_db();
348// let filter = games_routes(db.clone());
349
350// let res = warp::test::request()
351// .method("DELETE")
352// .path("/games/1")
353// .reply(&filter)
354// .await;
355
356// assert_eq!(res.status(), StatusCode::NO_CONTENT);
357// assert_eq!(db.lock().await.len(), 1);
358// }
359//}
diff --git a/src/schema.rs b/src/schema.rs
new file mode 100644
index 0000000..ea36a70
--- /dev/null
+++ b/src/schema.rs
@@ -0,0 +1,223 @@
1// Common types used across API
2
3use chrono::{NaiveDate, NaiveDateTime};
4use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6use tokio::sync::Mutex;
7
8// use crate::validators;
9
10pub fn ledger() -> Db {
11 // TODO: there was something simpler in one of the other tutorials? <07-04-21, yigit> //
12
13 Arc::new(Mutex::new(vec![
14 Transaction {
15 source: String::from("Myself"),
16 target: String::from("Nobody"),
17 amount: 4,
18 timestamp: NaiveDate::from_ymd(2021, 4, 7).and_hms(00, 17, 00),
19 },
20 ]))
21}
22
23
24// For presentation purposes keep mocked data in in-memory structure
25// In real life scenario connection with regular database would be established
26
27pub type Db = Arc<Mutex<Vec<Transaction>>>;
28
29#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
30pub struct Transaction {
31 pub source: String,
32 pub target: String,
33 pub amount: i32,
34 pub timestamp: NaiveDateTime,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
38pub struct Block {
39 pub transaction_list: Vec<Transaction>, // [Transaction; N]
40 pub nonce: i32,
41 pub timestamp: NaiveDateTime,
42 pub hash: String, // future proof'd baby
43}
44
45// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
46// #[serde(rename_all = "camelCase")]
47// pub struct Game {
48// pub id: u64,
49// pub title: String,
50// #[serde(with = "validators::validate_game_rating")]
51// pub rating: u8,
52// pub genre: Genre,
53// pub description: Option<String>,
54// pub release_date: NaiveDateTime,
55// }
56
57// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
58// #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
59// pub enum Genre {
60// RolePlaying,
61// Strategy,
62// Shooter,
63// }
64
65// #[derive(Deserialize, Debug, PartialEq)]
66// pub struct ListOptions {
67// pub offset: Option<usize>,
68// pub limit: Option<usize>,
69// }
70
71// pub fn example_db() -> Db {
72// Arc::new(Mutex::new(
73// vec![
74// Game {
75// id: 1,
76// title: String::from("Dark Souls"),
77// rating: 91,
78// genre: Genre::RolePlaying,
79// description: Some(String::from("Takes place in the fictional kingdom of Lordran, where players assume the role of a cursed undead character who begins a pilgrimage to discover the fate of their kind.")),
80// release_date: NaiveDate::from_ymd(2011, 9, 22).and_hms(0, 0, 0),
81// },
82// Game {
83// id: 2,
84// title: String::from("Dark Souls 2"),
85// rating: 87,
86// genre: Genre::RolePlaying,
87// description: None,
88// release_date: NaiveDate::from_ymd(2014, 3, 11).and_hms(0, 0, 0),
89// },
90// Game {
91// id: 3,
92// title: String::from("Dark Souls 3"),
93// rating: 89,
94// genre: Genre::RolePlaying,
95// description: Some(String::from("The latest chapter in the series with its trademark sword and sorcery combat and rewarding action RPG gameplay.")),
96// release_date: NaiveDate::from_ymd(2016, 3, 24).and_hms(0, 0, 0),
97// },
98// ]
99// ))
100// }
101
102// #[cfg(test)]
103// mod tests {
104// use super::*;
105
106// use serde_json::error::Error;
107// use serde_test::{assert_tokens, Token};
108
109// #[test]
110// fn game_serialize_correctly() {
111// let game = Game {
112// id: 1,
113// title: String::from("Test"),
114// rating: 90,
115// genre: Genre::Shooter,
116// description: None,
117// release_date: NaiveDate::from_ymd(2019, 11, 12).and_hms(0, 0, 0),
118// };
119
120// assert_tokens(
121// &game,
122// &[
123// Token::Struct {
124// name: "Game",
125// len: 6,
126// },
127// Token::String("id"),
128// Token::U64(1),
129// Token::String("title"),
130// Token::String("Test"),
131// Token::String("rating"),
132// Token::U8(90),
133// Token::String("genre"),
134// Token::UnitVariant {
135// name: "Genre",
136// variant: "SHOOTER",
137// },
138// Token::String("description"),
139// Token::None,
140// Token::String("releaseDate"),
141// Token::String("2019-11-12T00:00:00"),
142// Token::StructEnd,
143// ],
144// );
145// }
146
147// #[test]
148// fn game_deserialize_correctly() {
149// let data = r#"{"id":3,"title":"Another game","rating":65,"genre":"STRATEGY","description":null,"releaseDate":"2016-03-11T00:00:00"}"#;
150// let game: Game = serde_json::from_str(data).unwrap();
151// let expected_game = Game {
152// id: 3,
153// title: String::from("Another game"),
154// rating: 65,
155// genre: Genre::Strategy,
156// description: None,
157// release_date: NaiveDate::from_ymd(2016, 3, 11).and_hms(0, 0, 0),
158// };
159
160// assert_eq!(game, expected_game);
161// }
162
163// #[test]
164// fn game_error_when_wrong_rating_passed() {
165// let data = r#"{"id":3,"title":"Another game","rating":120,"genre":"STRATEGY","description":null,"releaseDate":"2016-03-11T00:00:00"}"#;
166// let err: Error = serde_json::from_str::<Game>(data).unwrap_err();
167
168// assert_eq!(err.is_data(), true);
169// }
170
171// #[test]
172// fn genre_serialize_correctly() {
173// let genre = Genre::Shooter;
174// assert_tokens(
175// &genre,
176// &[Token::UnitVariant {
177// name: "Genre",
178// variant: "SHOOTER",
179// }],
180// );
181
182// let genre = Genre::RolePlaying;
183// assert_tokens(
184// &genre,
185// &[Token::UnitVariant {
186// name: "Genre",
187// variant: "ROLE_PLAYING",
188// }],
189// );
190
191// let genre = Genre::Strategy;
192// assert_tokens(
193// &genre,
194// &[Token::UnitVariant {
195// name: "Genre",
196// variant: "STRATEGY",
197// }],
198// );
199// }
200
201// #[test]
202// fn genre_deserialize_correctly() {
203// let data = r#""SHOOTER""#;
204// let genre: Genre = serde_json::from_str(data).unwrap();
205// let expected_genre = Genre::Shooter;
206
207// assert_eq!(genre, expected_genre);
208
209// let data = r#""ROLE_PLAYING""#;
210// let genre: Genre = serde_json::from_str(data).unwrap();
211// let expected_genre = Genre::RolePlaying;
212
213// assert_eq!(genre, expected_genre);
214// }
215
216// #[test]
217// fn genre_error_when_wrong_rating_passed() {
218// let data = r#""SPORT""#;
219// let err: Error = serde_json::from_str::<Genre>(data).unwrap_err();
220
221// assert_eq!(err.is_data(), true);
222// }
223// }
diff --git a/src/validators.rs b/src/validators.rs
new file mode 100644
index 0000000..dbebee8
--- /dev/null
+++ b/src/validators.rs
@@ -0,0 +1,44 @@
1// Custom validators incoming data
2
3use log::error;
4use serde::de::{Deserializer, Error as DeserializerError, Unexpected};
5use serde::ser::{Error as SerializerError, Serializer};
6use serde::Deserialize;
7
8pub mod validate_game_rating {
9 use super::*;
10
11 const ERROR_MESSAGE: &str = "rating must be a number between 0 and 100";
12
13 pub fn deserialize<'de, D>(deserializer: D) -> Result<u8, D::Error>
14 where
15 D: Deserializer<'de>,
16 {
17 let value = u8::deserialize(deserializer)?;
18
19 if value > 100 {
20 error!("{}", ERROR_MESSAGE);
21
22 return Err(DeserializerError::invalid_value(
23 Unexpected::Unsigned(u64::from(value)),
24 &ERROR_MESSAGE,
25 ));
26 }
27
28 Ok(value)
29 }
30
31 #[allow(clippy::trivially_copy_pass_by_ref)]
32 pub fn serialize<S>(value: &u8, serializer: S) -> Result<S::Ok, S::Error>
33 where
34 S: Serializer,
35 {
36 if *value > 100 {
37 error!("{}", ERROR_MESSAGE);
38
39 return Err(SerializerError::custom(ERROR_MESSAGE));
40 }
41
42 serializer.serialize_u8(*value)
43 }
44}