From 1bc6689104c632eda2785e3e6580862887f2cb2f Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Fri, 13 May 2022 19:33:08 +0300 Subject: [PATCH] api: fetch ingredient by key --- api/src/domain/ingredient/fetch_by_key.rs | 96 +++++++++++++++++++++++ api/src/domain/ingredient/mod.rs | 1 + api/src/repo/ingredient.rs | 46 +++++++++-- api/src/rest/ctrl/ingredient.rs | 41 ++++++++++ api/src/rest/server.rs | 4 + 5 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 api/src/domain/ingredient/fetch_by_key.rs diff --git a/api/src/domain/ingredient/fetch_by_key.rs b/api/src/domain/ingredient/fetch_by_key.rs new file mode 100644 index 0000000..df51f14 --- /dev/null +++ b/api/src/domain/ingredient/fetch_by_key.rs @@ -0,0 +1,96 @@ +use super::types; +use crate::repo::ingredient::IngredientRepo; + +#[derive(Default, Debug)] +pub struct RequestOpts { + pub lang: Option, +} + +pub enum ResponseError { + NotFound, +} + +pub fn execute( + repo: &impl IngredientRepo, + key: String, + opts: RequestOpts, +) -> Result { + match repo.get_ingredient_opt(key, opts.into()) { + Some(ing) => Ok(ing), + _ => Err(ResponseError::NotFound), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::ingredient::types::Lang; + + #[test] + fn should_return_ingredient() { + let repo = crate::repo::ingredient::InMemoryIngredientRepo::new(); + let res = execute(&repo, String::from("apple"), RequestOpts::default()); + + match res { + Ok(apple) => { + assert_eq!(apple.key, String::from("apple")); + assert_eq!(apple.lang, Lang::Rus); + assert_eq!(apple.name, String::from("Яблоко")); + } + _ => unimplemented!(), + } + } + + #[test] + fn should_return_ingredient_with_choosed_lang() { + let repo = crate::repo::ingredient::InMemoryIngredientRepo::new(); + let res = execute( + &repo, + String::from("apple"), + RequestOpts { + lang: Some(Lang::Eng), + }, + ); + + match res { + Ok(apple) => { + assert_eq!(apple.key, String::from("apple")); + assert_eq!(apple.lang, Lang::Eng); + assert_eq!(apple.name, String::from("Apple")); + } + _ => unimplemented!(), + } + } + + #[test] + fn should_return_ingredient_with_fallback_lang() { + let repo = crate::repo::ingredient::InMemoryIngredientRepo::new(); + let res = execute( + &repo, + String::from("orange"), + RequestOpts { + lang: Some(Lang::Eng), + }, + ); + + match res { + Ok(orange) => { + assert_eq!(orange.key, String::from("orange")); + assert_eq!(orange.lang, Lang::Rus); + assert_eq!(orange.name, String::from("Апельсин")); + } + _ => unimplemented!(), + } + } + + #[test] + fn should_throw_not_found_error() { + let repo = crate::repo::ingredient::InMemoryIngredientRepo::new(); + let res = execute(&repo, String::from("wildberries"), RequestOpts::default()); + + match res { + Err(ResponseError::NotFound) => {} + _ => unimplemented!(), + } + } +} diff --git a/api/src/domain/ingredient/mod.rs b/api/src/domain/ingredient/mod.rs index 1f3c4a4..d017b5b 100644 --- a/api/src/domain/ingredient/mod.rs +++ b/api/src/domain/ingredient/mod.rs @@ -1,2 +1,3 @@ +pub mod fetch_by_key; pub mod fetch_list; pub mod types; diff --git a/api/src/repo/ingredient.rs b/api/src/repo/ingredient.rs index fefcb11..0dbb1fd 100644 --- a/api/src/repo/ingredient.rs +++ b/api/src/repo/ingredient.rs @@ -1,12 +1,23 @@ -use crate::domain::ingredient::{fetch_list, types}; +use crate::domain::ingredient::{fetch_by_key, fetch_list, types}; #[derive(Default)] pub struct GetIngredientOpts { pub lang: Option, +} + +impl From for GetIngredientOpts { + fn from(app: fetch_by_key::RequestOpts) -> Self { + Self { lang: app.lang } + } +} + +#[derive(Default)] +pub struct GetIngredientsOpts { + pub lang: Option, pub keys: Option>, } -impl From for GetIngredientOpts { +impl From for GetIngredientsOpts { fn from(app: fetch_list::RequestOpts) -> Self { Self { lang: app.lang, @@ -16,13 +27,27 @@ impl From for GetIngredientOpts { } pub trait IngredientRepo { - fn get_ingredients(&self, opts: GetIngredientOpts) -> Vec; + fn get_ingredient_opt(&self, key: String, opts: GetIngredientOpts) + -> Option; + + fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec; } pub struct StaticIngredientRepo; impl IngredientRepo for StaticIngredientRepo { - fn get_ingredients(&self, opts: GetIngredientOpts) -> Vec { + fn get_ingredient_opt( + &self, + key: String, + opts: GetIngredientOpts, + ) -> Option { + db::INGREDIENTS + .iter() + .find(|ing| ing.key == &key) + .map(|ing| types::Ingredient::from((ing, opts.lang.unwrap_or_default()))) + } + + fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec { let langs = [opts.lang.unwrap_or_default()].repeat(db::INGREDIENTS.len()); db::INGREDIENTS .iter() @@ -78,7 +103,18 @@ impl InMemoryIngredientRepo { #[cfg(test)] impl IngredientRepo for InMemoryIngredientRepo { - fn get_ingredients(&self, opts: GetIngredientOpts) -> Vec { + fn get_ingredient_opt( + &self, + key: String, + opts: GetIngredientOpts, + ) -> Option { + self.ingredients + .iter() + .find(|ing| ing.key == &key) + .map(|ing| types::Ingredient::from((ing, opts.lang.unwrap_or_default()))) + } + + fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec { let langs = [opts.lang.unwrap_or_default()].repeat(self.ingredients.len()); self.ingredients .iter() diff --git a/api/src/rest/ctrl/ingredient.rs b/api/src/rest/ctrl/ingredient.rs index 5529e27..8b566e4 100644 --- a/api/src/rest/ctrl/ingredient.rs +++ b/api/src/rest/ctrl/ingredient.rs @@ -55,3 +55,44 @@ pub fn fetch_list(url: &Url) -> Response>> { Response::from_string(data) .with_header(Header::from_str("content-type: application/json").unwrap()) } + +#[derive(Default, Debug)] +struct FetchIngredientOpts<'a> { + lang: Option<&'a str>, +} + +impl<'a> From> for FetchIngredientOpts<'a> { + fn from(params: QueryParams<'a>) -> Self { + params + .into_iter() + .fold(FetchIngredientOpts::default(), |mut opts, p| { + match p { + ("lang", val) => opts.lang = Some(val), + _ => {} + }; + + opts + }) + } +} + +impl From> for domain::ingredient::fetch_by_key::RequestOpts { + fn from(rest: FetchIngredientOpts) -> Self { + let lang = rest.lang.and_then(|l| Lang::from_str(l).ok()); + Self { lang } + } +} + +pub fn fetch_by_key(url: &Url, key: &str) -> Response>> { + use domain::ingredient::fetch_by_key; + let opts = FetchIngredientOpts::from(url.query_params()); + + let repo = StaticIngredientRepo; + + // TODO: catch notfound error + let ingredient = fetch_by_key::execute(&repo, key.to_string(), opts.into()).ok(); + let data = serde_json::to_string(&ingredient).unwrap(); + + Response::from_string(data) + .with_header(Header::from_str("content-type: application/json").unwrap()) +} diff --git a/api/src/rest/server.rs b/api/src/rest/server.rs index 0342b82..6ccf016 100644 --- a/api/src/rest/server.rs +++ b/api/src/rest/server.rs @@ -22,6 +22,10 @@ pub fn start() { let res = rest::ctrl::ingredient::fetch_list(&url); rq.respond(res) } + ["api", "ingredients", key] => { + let res = rest::ctrl::ingredient::fetch_by_key(&url, key); + rq.respond(res) + } _ => rq.respond(Response::from_string("Not found")), }; }