diff options
author | Yigit Sever | 2021-04-07 01:08:31 +0300 |
---|---|---|
committer | Yigit Sever | 2021-04-07 01:08:31 +0300 |
commit | c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc (patch) | |
tree | 43345c12a7caf4c94532a7b54638e756af10b3af /src | |
download | gradecoin-c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc.tar.gz gradecoin-c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc.tar.bz2 gradecoin-c3ba5ad5ebe1d5bb28ed0a340af93e8547b1c5bc.zip |
Initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/custom_filters.rs | 21 | ||||
-rw-r--r-- | src/handlers.rs | 122 | ||||
-rw-r--r-- | src/main.rs | 26 | ||||
-rw-r--r-- | src/routes.rs | 359 | ||||
-rw-r--r-- | src/schema.rs | 223 | ||||
-rw-r--r-- | src/validators.rs | 44 |
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 | |||
3 | use std::convert::Infallible; | ||
4 | use warp::{Filter, Rejection}; | ||
5 | |||
6 | use crate::schema::{Db, Transaction}; // `Block` coming later | ||
7 | |||
8 | // Database context for routes | ||
9 | pub 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 | ||
19 | pub 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 | |||
3 | use log::debug; | ||
4 | use std::convert::Infallible; | ||
5 | use warp::{http::StatusCode, reply}; | ||
6 | |||
7 | use crate::schema::{Db, Transaction}; // `Block` coming later | ||
8 | |||
9 | // PROPOSE Transaction | ||
10 | // POST /transaction | ||
11 | pub 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? | ||
28 | pub 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 @@ | |||
1 | use std::env; | ||
2 | use warp::Filter; | ||
3 | |||
4 | mod custom_filters; | ||
5 | mod handlers; | ||
6 | mod routes; | ||
7 | mod schema; | ||
8 | // mod validators; | ||
9 | |||
10 | #[tokio::main] | ||
11 | async 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 @@ | |||
1 | use warp::{Filter, Rejection, Reply}; | ||
2 | |||
3 | use crate::custom_filters; | ||
4 | use crate::handlers; | ||
5 | use crate::schema::Db; | ||
6 | |||
7 | // Root, all routes combined | ||
8 | pub 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 | ||
13 | pub 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 | ||
21 | pub 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 | |||
3 | use chrono::{NaiveDate, NaiveDateTime}; | ||
4 | use serde::{Deserialize, Serialize}; | ||
5 | use std::sync::Arc; | ||
6 | use tokio::sync::Mutex; | ||
7 | |||
8 | // use crate::validators; | ||
9 | |||
10 | pub 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 | |||
27 | pub type Db = Arc<Mutex<Vec<Transaction>>>; | ||
28 | |||
29 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | ||
30 | pub 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)] | ||
38 | pub 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 | |||
3 | use log::error; | ||
4 | use serde::de::{Deserializer, Error as DeserializerError, Unexpected}; | ||
5 | use serde::ser::{Error as SerializerError, Serializer}; | ||
6 | use serde::Deserialize; | ||
7 | |||
8 | pub 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 | } | ||