diff --git a/example-app/src/api.rs b/example-app/src/api.rs index a9295cc..e600c19 100644 --- a/example-app/src/api.rs +++ b/example-app/src/api.rs @@ -5,11 +5,10 @@ use axum::{ routing::{get, post}, Json, Router, }; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; -use crate::fetcher::Feed; +use crate::models::{CreateFeed, Feed, Status}; use crate::scheduler_interface::ToScheduler; #[derive(Clone)] @@ -35,31 +34,10 @@ pub fn app(scheduler_interface: Arc) -> Router { .with_state(state) } -#[derive(Serialize)] -struct Status { - status: String, -} - -impl Status { - fn new(status: &str) -> Self { - Self { - status: status.to_string(), - } - } -} - async fn status_handler() -> impl IntoResponse { Json(Status::new("OK")) } -#[derive(Clone, Deserialize, Serialize)] -struct CreateFeed { - name: String, - url: String, - frequency: u64, - headers: HashMap, -} - async fn post_handler( state: State, Json(CreateFeed { @@ -68,8 +46,11 @@ async fn post_handler( frequency, headers, }): Json, -) -> impl IntoResponse { - let id = *(state.next_feed_id.read().unwrap()); +) -> Result { + let id = *(state + .next_feed_id + .read() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?); let feed = Feed { id, name, @@ -78,24 +59,37 @@ async fn post_handler( headers, }; - *(state.next_feed_id.write().unwrap()) += 1; - state.db.write().unwrap().insert(id, feed.clone()); + *(state + .next_feed_id + .write() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?) += 1; + state + .db + .write() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .insert(id, feed.clone()); state.scheduler_interface.create(feed.clone()); - (StatusCode::CREATED, Json(feed)) + Ok((StatusCode::CREATED, Json(feed))) } -async fn get_handler(path: Path, state: State) -> impl IntoResponse { - let id = path.parse::().map_err(|_| StatusCode::BAD_REQUEST)?; +async fn get_handler( + Path(id): Path, + state: State, +) -> Result { + let feed = state + .db + .read() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .get(&id) + .cloned() + .ok_or(StatusCode::NOT_FOUND)?; - match state.db.read().unwrap().get(&id).cloned() { - Some(feed) => Ok(Json(feed)), - None => Err(StatusCode::NOT_FOUND), - } + Ok(Json(feed)) } async fn put_handler( - path: Path, + Path(id): Path, state: State, Json(CreateFeed { name, @@ -104,8 +98,13 @@ async fn put_handler( headers, }): Json, ) -> impl IntoResponse { - let id = path.parse::().map_err(|_| StatusCode::BAD_REQUEST)?; - if state.db.read().unwrap().get(&id).is_none() { + if state + .db + .read() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .get(&id) + .is_none() + { return Err(StatusCode::NOT_FOUND); } @@ -117,28 +116,47 @@ async fn put_handler( headers, }; - state.db.write().unwrap().insert(id, feed.clone()); + state + .db + .write() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .insert(id, feed.clone()); state.scheduler_interface.update(feed.clone()); Ok(Json(feed)) } -async fn delete_handler(path: Path, state: State) -> impl IntoResponse { - let id = path.parse::().map_err(|_| StatusCode::BAD_REQUEST)?; - let feed = match state.db.read().unwrap().get(&id).cloned() { - Some(f) => f, - None => return Err(StatusCode::NOT_FOUND), - }; - - state.db.write().unwrap().remove(&feed.id); +async fn delete_handler( + Path(id): Path, + state: State, +) -> Result { + let feed = state + .db + .read() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .get(&id) + .cloned() + .ok_or(StatusCode::NOT_FOUND)?; + + state + .db + .write() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .remove(&feed.id); state.scheduler_interface.delete(feed); Ok(StatusCode::NO_CONTENT) } -async fn list_handler(state: State) -> impl IntoResponse { - let feeds: Vec = state.db.read().unwrap().values().cloned().collect(); - Json(feeds) +async fn list_handler(state: State) -> Result { + let feeds: Vec = state + .db + .read() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .values() + .cloned() + .collect(); + Ok(Json(feeds)) } #[cfg(test)] @@ -146,7 +164,6 @@ mod api_tests { #[cfg(not(feature = "use_dependencies"))] use crate::deps::mime; - use crate::fetcher::Feed; use crate::scheduler_interface::{SchedulerInterface, TaskSender}; use tokio::net::TcpListener; use tulsa::AsyncTask; @@ -227,7 +244,7 @@ mod api_tests { let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); - assert_eq!(body.len(), 0); + assert_eq!(&body[..], b"Invalid URL: Cannot parse `\"abc\"` to a `u64`"); let sender = Arc::new(Mutex::new(MockSender::new())); let interface = Arc::new(SchedulerInterface::new(sender)); @@ -246,7 +263,7 @@ mod api_tests { let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); - assert_eq!(body.len(), 0); + assert_eq!(&body[..], b"Invalid URL: Cannot parse `\"-1\"` to a `u64`"); } #[tokio::test] diff --git a/example-app/src/fetcher.rs b/example-app/src/fetcher.rs index d94a74e..8eaad86 100644 --- a/example-app/src/fetcher.rs +++ b/example-app/src/fetcher.rs @@ -8,30 +8,8 @@ use ureq; mod transit { include!(concat!(env!("OUT_DIR"), "/transit_realtime.rs")); } -use std::collections::HashMap; - -use reqwest::header::{HeaderMap, HeaderName}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Feed { - pub id: usize, - pub name: String, - pub url: String, - pub frequency: u64, - pub headers: HashMap, -} -impl Feed { - pub fn to_header_map(&self) -> HeaderMap { - let mut headers = HeaderMap::new(); - for (key, value) in self.headers.iter() { - let new_key: HeaderName = key.parse().unwrap(); - headers.insert(new_key, value.parse().unwrap()); - } - headers - } -} +use crate::models::Feed; async fn fetch(feed: &Feed) -> usize { println!("Fetching {}", feed.name); diff --git a/example-app/src/lib.rs b/example-app/src/lib.rs index 10ebddd..a628dc5 100644 --- a/example-app/src/lib.rs +++ b/example-app/src/lib.rs @@ -1,5 +1,6 @@ pub mod api; pub mod fetcher; +pub mod models; pub mod scheduler_interface; // The deps module is an effort to re-implement my third-party dependencies as diff --git a/example-app/src/models/mod.rs b/example-app/src/models/mod.rs new file mode 100644 index 0000000..7419fb0 --- /dev/null +++ b/example-app/src/models/mod.rs @@ -0,0 +1,45 @@ +use reqwest::header::{HeaderMap, HeaderName}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Feed { + pub id: usize, + pub name: String, + pub url: String, + pub frequency: u64, + pub headers: HashMap, +} + +impl Feed { + pub fn to_header_map(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + for (key, value) in self.headers.iter() { + let new_key: HeaderName = key.parse().unwrap(); + headers.insert(new_key, value.parse().unwrap()); + } + headers + } +} + +/// This represents a [`Feed`] but without an ID, which are used in POST bodies. +#[derive(Clone, Deserialize, Serialize)] +pub struct CreateFeed { + pub name: String, + pub url: String, + pub frequency: u64, + pub headers: HashMap, +} + +#[derive(Serialize)] +pub struct Status { + status: String, +} + +impl Status { + pub fn new(status: &str) -> Self { + Self { + status: status.to_string(), + } + } +} diff --git a/example-app/src/scheduler_interface.rs b/example-app/src/scheduler_interface.rs index f1f213c..c280b6c 100644 --- a/example-app/src/scheduler_interface.rs +++ b/example-app/src/scheduler_interface.rs @@ -3,7 +3,8 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use tulsa::{AsyncTask, Scheduler, SyncTask}; -use crate::fetcher::{fetch_sync, recurring_fetch, Feed}; +use crate::fetcher::{fetch_sync, recurring_fetch}; +use crate::models::Feed; pub enum Mode { Sync,