From 284ad260a45c9c2197b2349860692ea9b1c3186d Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 15 May 2022 15:50:09 +0300 Subject: [PATCH] api: add context, some style improvements --- api/src/domain/ingredient/fetch_by_key.rs | 37 +++++++-------- api/src/domain/ingredient/fetch_list.rs | 23 +++++----- api/src/domain/misc_types.rs | 14 +++++- api/src/domain/mod.rs | 2 + api/src/domain/recipe/fetch_list.rs | 15 ++++--- api/src/repo/ingredient.rs | 37 +++++---------- api/src/repo/recipe.rs | 20 +++++---- api/src/rest/context.rs | 34 ++++++++++++++ api/src/rest/ctrl/ingredient.rs | 55 ++++++----------------- api/src/rest/ctrl/recipe.rs | 9 ++-- api/src/rest/mod.rs | 3 ++ api/src/rest/server.rs | 9 ++-- api/src/rest/types.rs | 50 +++++++++++++-------- 13 files changed, 163 insertions(+), 145 deletions(-) create mode 100644 api/src/rest/context.rs diff --git a/api/src/domain/ingredient/fetch_by_key.rs b/api/src/domain/ingredient/fetch_by_key.rs index 20cd7a4..156688e 100644 --- a/api/src/domain/ingredient/fetch_by_key.rs +++ b/api/src/domain/ingredient/fetch_by_key.rs @@ -1,10 +1,12 @@ use super::types; -use crate::domain::misc_types; -use crate::repo::ingredient::IngredientRepo; +use crate::{ + domain::{Context, Lang}, + repo::ingredient::IngredientRepo, +}; #[derive(Default, Debug)] pub struct RequestOpts { - pub lang: Option, + pub lang: Option, } pub enum ResponseError { @@ -13,10 +15,10 @@ pub enum ResponseError { pub fn execute( repo: &impl IngredientRepo, + ctx: &Context, key: String, - opts: RequestOpts, ) -> Result { - match repo.get_ingredient_opt(key, opts.into()) { + match repo.get_ingredient_opt(ctx, key) { Some(ing) => Ok(ing), _ => Err(ResponseError::NotFound), } @@ -25,12 +27,15 @@ pub fn execute( #[cfg(test)] mod tests { use super::*; - use crate::{domain::misc_types::Lang, repo::ingredient::InMemoryIngredientRepo}; + use crate::{ + domain::{Context, Lang}, + repo::ingredient::InMemoryIngredientRepo, + }; #[test] fn should_return_ingredient() { let repo = InMemoryIngredientRepo::new(); - let res = execute(&repo, String::from("apple"), RequestOpts::default()); + let res = execute(&repo, &Context::default(), String::from("apple")); match res { Ok(apple) => { @@ -45,13 +50,7 @@ mod tests { #[test] fn should_return_ingredient_with_choosed_lang() { let repo = InMemoryIngredientRepo::new(); - let res = execute( - &repo, - String::from("apple"), - RequestOpts { - lang: Some(Lang::Eng), - }, - ); + let res = execute(&repo, &Context::eng(), String::from("apple")); match res { Ok(apple) => { @@ -66,13 +65,7 @@ mod tests { #[test] fn should_return_ingredient_with_fallback_lang() { let repo = InMemoryIngredientRepo::new(); - let res = execute( - &repo, - String::from("orange"), - RequestOpts { - lang: Some(Lang::Eng), - }, - ); + let res = execute(&repo, &Context::eng(), String::from("orange")); match res { Ok(orange) => { @@ -87,7 +80,7 @@ mod tests { #[test] fn should_throw_not_found_error() { let repo = InMemoryIngredientRepo::new(); - let res = execute(&repo, String::from("wildberries"), RequestOpts::default()); + let res = execute(&repo, &Context::default(), String::from("wildberries")); match res { Err(ResponseError::NotFound) => {} diff --git a/api/src/domain/ingredient/fetch_list.rs b/api/src/domain/ingredient/fetch_list.rs index 2188e9a..dbd0164 100644 --- a/api/src/domain/ingredient/fetch_list.rs +++ b/api/src/domain/ingredient/fetch_list.rs @@ -1,15 +1,18 @@ use super::types; -use crate::domain::misc_types; +use crate::domain::misc_types::Context; use crate::repo::ingredient::IngredientRepo; #[derive(Default, Debug)] pub struct RequestOpts { - pub lang: Option, pub keys: Option>, } -pub fn execute(repo: &impl IngredientRepo, opts: RequestOpts) -> Vec { - repo.get_ingredients(opts.into()) +pub fn execute( + repo: &impl IngredientRepo, + ctx: &Context, + opts: RequestOpts, +) -> Vec { + repo.get_ingredients(ctx, opts.into()) } #[cfg(test)] @@ -20,7 +23,7 @@ mod tests { #[test] fn should_return_all_ingredients() { let repo = InMemoryIngredientRepo::new(); - let res = execute(&repo, RequestOpts::default()); + let res = execute(&repo, &Context::default(), RequestOpts::default()); match res.as_slice() { [banana, apple, orange, salt, sugar] => { @@ -51,13 +54,7 @@ mod tests { #[test] fn should_return_preferred_lang_with_fallback() { let repo = InMemoryIngredientRepo::new(); - let res = execute( - &repo, - RequestOpts { - lang: Some(Lang::Eng), - ..RequestOpts::default() - }, - ); + let res = execute(&repo, &Context::eng(), RequestOpts::default()); match res.as_slice() { [banana, apple, orange, salt, sugar] => { @@ -90,9 +87,9 @@ mod tests { let repo = InMemoryIngredientRepo::new(); let res = execute( &repo, + &Context::default(), RequestOpts { keys: Some(vec![String::from("apple")]), - ..RequestOpts::default() }, ); diff --git a/api/src/domain/misc_types.rs b/api/src/domain/misc_types.rs index 4197fc9..31f293a 100644 --- a/api/src/domain/misc_types.rs +++ b/api/src/domain/misc_types.rs @@ -1,6 +1,18 @@ use std::str::FromStr; -#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)] +#[derive(Debug, Default)] +pub struct Context { + pub lang: Lang, +} + +#[cfg(test)] +impl Context { + pub fn eng() -> Self { + Self { lang: Lang::Eng } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] pub enum Lang { Rus, #[allow(dead_code)] diff --git a/api/src/domain/mod.rs b/api/src/domain/mod.rs index 4e45794..10a63a7 100644 --- a/api/src/domain/mod.rs +++ b/api/src/domain/mod.rs @@ -1,3 +1,5 @@ pub mod ingredient; pub mod misc_types; pub mod recipe; + +pub use misc_types::{Context, Lang}; diff --git a/api/src/domain/recipe/fetch_list.rs b/api/src/domain/recipe/fetch_list.rs index c616991..5cffb4b 100644 --- a/api/src/domain/recipe/fetch_list.rs +++ b/api/src/domain/recipe/fetch_list.rs @@ -1,23 +1,23 @@ -use crate::repo::recipe::RecipeRepo; +use crate::{domain::Context, repo::recipe::RecipeRepo}; use super::types; -pub fn execute(repo: &impl RecipeRepo) -> Vec { - repo.get_recipes() +pub fn execute(repo: &impl RecipeRepo, ctx: &Context) -> Vec { + repo.get_recipes(ctx) } #[cfg(test)] mod tests { use super::*; use crate::{ - domain::{misc_types::Lang, recipe::types::RecipeIngredientMeasure}, + domain::{recipe::types::RecipeIngredientMeasure, Lang}, repo::recipe::InMemoryRecipeRepo, }; #[test] fn should_return_all_recipes() { let repo = InMemoryRecipeRepo::new(); - let res = execute(&repo); + let res = execute(&repo, &Context::default()); match res.as_slice() { [salad] => { @@ -52,7 +52,8 @@ mod tests { #[test] fn should_not_return_recipes_if_ingredients_not_exist_in_db() { let mut repo = InMemoryRecipeRepo::new().with_no_ingredients_found(); - let res = execute(&repo); + let ctx = Context::default(); + let res = execute(&repo, &ctx); match res.as_slice() { [salad] => { @@ -65,7 +66,7 @@ mod tests { rec.ingredients.pop(); // remove wheat flour from ingredients } - let res = execute(&repo); + let res = execute(&repo, &ctx); match res.as_slice() { [salad, no_found] => { assert_eq!(salad.key, String::from("fruit_salad")); diff --git a/api/src/repo/ingredient.rs b/api/src/repo/ingredient.rs index f0d75fb..e4ccb14 100644 --- a/api/src/repo/ingredient.rs +++ b/api/src/repo/ingredient.rs @@ -1,5 +1,5 @@ use crate::domain::ingredient::{fetch_by_key, fetch_list, types}; -use crate::domain::misc_types; +use crate::domain::misc_types::{self, Context}; #[derive(Default)] pub struct GetIngredientOpts { @@ -14,42 +14,33 @@ impl From for GetIngredientOpts { #[derive(Default)] pub struct GetIngredientsOpts { - pub lang: Option, pub keys: Option>, } impl From for GetIngredientsOpts { fn from(app: fetch_list::RequestOpts) -> Self { - Self { - lang: app.lang, - keys: app.keys, - } + Self { keys: app.keys } } } pub trait IngredientRepo { - fn get_ingredient_opt(&self, key: String, opts: GetIngredientOpts) - -> Option; + fn get_ingredient_opt(&self, ctx: &Context, key: String) -> Option; - fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec; + fn get_ingredients(&self, ctx: &Context, opts: GetIngredientsOpts) -> Vec; } pub struct StaticIngredientRepo; impl IngredientRepo for StaticIngredientRepo { - fn get_ingredient_opt( - &self, - key: String, - opts: GetIngredientOpts, - ) -> Option { + fn get_ingredient_opt(&self, ctx: &Context, key: String) -> Option { db::INGREDIENTS .iter() .find(|ing| ing.key == &key) - .map(|ing| types::Ingredient::from((ing, opts.lang.unwrap_or_default()))) + .map(|ing| types::Ingredient::from((ing, ctx.lang))) } - fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec { - let langs = [opts.lang.unwrap_or_default()].repeat(db::INGREDIENTS.len()); + fn get_ingredients(&self, ctx: &Context, opts: GetIngredientsOpts) -> Vec { + let langs = [ctx.lang].repeat(db::INGREDIENTS.len()); db::INGREDIENTS .iter() .zip(langs) @@ -111,19 +102,15 @@ impl InMemoryIngredientRepo { #[cfg(test)] impl IngredientRepo for InMemoryIngredientRepo { - fn get_ingredient_opt( - &self, - key: String, - opts: GetIngredientOpts, - ) -> Option { + fn get_ingredient_opt(&self, ctx: &Context, key: String) -> Option { self.ingredients .iter() .find(|ing| ing.key == &key) - .map(|ing| types::Ingredient::from((ing, opts.lang.unwrap_or_default()))) + .map(|ing| types::Ingredient::from((ing, ctx.lang))) } - fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec { - let langs = [opts.lang.unwrap_or_default()].repeat(self.ingredients.len()); + fn get_ingredients(&self, ctx: &Context, opts: GetIngredientsOpts) -> Vec { + let langs = [ctx.lang].repeat(self.ingredients.len()); self.ingredients .iter() .zip(langs) diff --git a/api/src/repo/recipe.rs b/api/src/repo/recipe.rs index 06969e3..661d903 100644 --- a/api/src/repo/recipe.rs +++ b/api/src/repo/recipe.rs @@ -1,23 +1,25 @@ -use crate::domain::{misc_types::Lang, recipe::types}; +use crate::domain::{recipe::types, Context}; use crate::repo; use super::ingredient::IngredientRepo; +#[cfg(test)] use db::data as Db; -use Db::RecipeIngredientMeasure as DbRIM; +#[cfg(test)] +use db::data::RecipeIngredientMeasure as DbRIM; pub trait RecipeRepo { - fn get_recipes(&self) -> Vec; + fn get_recipes(&self, ctx: &Context) -> Vec; } pub struct StaticRecipeRepo; impl RecipeRepo for StaticRecipeRepo { - fn get_recipes(&self) -> Vec { + fn get_recipes(&self, ctx: &Context) -> Vec { let ings_repo = repo::ingredient::StaticIngredientRepo; - let ings = ings_repo.get_ingredients(Default::default()); + let ings = ings_repo.get_ingredients(ctx, Default::default()); - let langs = [Lang::default()].repeat(db::RECIPES.len()); + let langs = [ctx.lang].repeat(db::RECIPES.len()); db::RECIPES .iter() .zip(langs) @@ -99,11 +101,11 @@ impl InMemoryRecipeRepo { #[cfg(test)] impl RecipeRepo for InMemoryRecipeRepo { - fn get_recipes(&self) -> Vec { + fn get_recipes(&self, ctx: &Context) -> Vec { let ings_repo = repo::ingredient::InMemoryIngredientRepo::new(); - let ings = ings_repo.get_ingredients(Default::default()); + let ings = ings_repo.get_ingredients(ctx, Default::default()); - let langs = [Lang::default()].repeat(self.recipes.len()); + let langs = [ctx.lang].repeat(self.recipes.len()); self.recipes .iter() .zip(langs) diff --git a/api/src/rest/context.rs b/api/src/rest/context.rs new file mode 100644 index 0000000..aaafb1c --- /dev/null +++ b/api/src/rest/context.rs @@ -0,0 +1,34 @@ +use std::str::FromStr; + +use crate::domain; + +use super::types::QueryParams; + +#[derive(Debug, Default, Clone)] +pub struct Context { + lang: Option, +} + +impl<'a> From<&QueryParams<'a>> for Context { + fn from(params: &QueryParams<'a>) -> Self { + params.iter().fold(Context::default(), |mut opts, p| { + match p { + ("lang", val) => opts.lang = Some(String::from(*val)), + _ => {} + }; + + opts + }) + } +} + +impl From for domain::Context { + fn from(rest: Context) -> Self { + Self { + lang: rest + .lang + .and_then(|l| domain::Lang::from_str(&l).ok()) + .unwrap_or_default(), + } + } +} diff --git a/api/src/rest/ctrl/ingredient.rs b/api/src/rest/ctrl/ingredient.rs index 6643a7a..abe430b 100644 --- a/api/src/rest/ctrl/ingredient.rs +++ b/api/src/rest/ctrl/ingredient.rs @@ -3,24 +3,21 @@ use std::str::FromStr; use tiny_http::{Header, Response}; -use crate::domain; -use crate::domain::misc_types::Lang; use crate::repo::ingredient::StaticIngredientRepo; use crate::rest::types::{QueryParams, Url}; +use crate::{domain, rest}; #[derive(Default, Debug)] struct FetchIngredientsOpts<'a> { - lang: Option<&'a str>, keys: Option>, } -impl<'a> From> for FetchIngredientsOpts<'a> { - fn from(params: QueryParams<'a>) -> Self { +impl<'a> From<&QueryParams<'a>> for FetchIngredientsOpts<'a> { + fn from(params: &QueryParams<'a>) -> Self { params - .into_iter() + .iter() .fold(FetchIngredientsOpts::default(), |mut opts, p| { match p { - ("lang", val) => opts.lang = Some(val), ("key", val) => { let mut keys = opts.keys.unwrap_or_default(); keys.push(val); @@ -36,61 +33,35 @@ impl<'a> From> for FetchIngredientsOpts<'a> { impl From> for domain::ingredient::fetch_list::RequestOpts { fn from(rest: FetchIngredientsOpts) -> Self { - let lang = rest.lang.and_then(|l| Lang::from_str(l).ok()); let keys = rest .keys .map(|keys| keys.into_iter().map(String::from).collect()); - Self { lang, keys } + Self { keys } } } -pub fn fetch_list(url: &Url) -> Response>> { +pub fn fetch_list(rest_ctx: &rest::Context, url: &Url) -> Response>> { use domain::ingredient::fetch_list; let opts = FetchIngredientsOpts::from(url.query_params()); + let ctx = rest_ctx.clone().into(); + let repo = StaticIngredientRepo; - let ingredients = fetch_list::execute(&repo, opts.into()); + let ingredients = fetch_list::execute(&repo, &ctx, opts.into()); let data = serde_json::to_string(&ingredients).unwrap(); 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>> { +pub fn fetch_by_key(rest_ctx: &rest::Context, _url: &Url, key: &str) -> Response>> { use domain::ingredient::fetch_by_key; - let opts = FetchIngredientOpts::from(url.query_params()); - let repo = StaticIngredientRepo; + let ctx = rest_ctx.clone().into(); + // TODO: catch notfound error - let ingredient = fetch_by_key::execute(&repo, key.to_string(), opts.into()).ok(); + let ingredient = fetch_by_key::execute(&repo, &ctx, key.to_string()).ok(); let data = serde_json::to_string(&ingredient).unwrap(); Response::from_string(data) diff --git a/api/src/rest/ctrl/recipe.rs b/api/src/rest/ctrl/recipe.rs index b937ae9..d97bfd2 100644 --- a/api/src/rest/ctrl/recipe.rs +++ b/api/src/rest/ctrl/recipe.rs @@ -5,13 +5,16 @@ use tiny_http::{Header, Response}; use crate::repo::recipe::StaticRecipeRepo; use crate::rest::types::Url; +use crate::{domain, rest}; -pub fn fetch_list(url: &Url) -> Response>> { - use crate::domain::recipe::fetch_list; +pub fn fetch_list(rest_ctx: &rest::Context, _url: &Url) -> Response>> { + use domain::recipe::fetch_list; let repo = StaticRecipeRepo; + let ctx = rest_ctx.clone().into(); + // TODO: catch notfound error - let recipe = fetch_list::execute(&repo); + let recipe = fetch_list::execute(&repo, &ctx); let data = serde_json::to_string(&recipe).unwrap(); Response::from_string(data) diff --git a/api/src/rest/mod.rs b/api/src/rest/mod.rs index f4cf26a..c0c2281 100644 --- a/api/src/rest/mod.rs +++ b/api/src/rest/mod.rs @@ -1,3 +1,6 @@ +pub mod context; pub mod ctrl; pub mod server; pub mod types; + +pub use context::Context; diff --git a/api/src/rest/server.rs b/api/src/rest/server.rs index 5d5dce8..83cbc41 100644 --- a/api/src/rest/server.rs +++ b/api/src/rest/server.rs @@ -17,17 +17,18 @@ pub fn start() { handles.push(thread::spawn(move || { for rq in server.incoming_requests() { let url = Url::parse(rq.url()); - let _ = match url.path_segments()[..] { + let ctx = rest::Context::from(url.query_params()); + let _ = match url.path_segments() { ["api", "ingredients"] => { - let res = rest::ctrl::ingredient::fetch_list(&url); + let res = rest::ctrl::ingredient::fetch_list(&ctx, &url); rq.respond(res) } ["api", "ingredients", key] => { - let res = rest::ctrl::ingredient::fetch_by_key(&url, key); + let res = rest::ctrl::ingredient::fetch_by_key(&ctx, &url, key); rq.respond(res) } ["api", "recipes"] => { - let res = rest::ctrl::recipe::fetch_list(&url); + let res = rest::ctrl::recipe::fetch_list(&ctx, &url); rq.respond(res) } _ => rq.respond(Response::from_string("Not found")), diff --git a/api/src/rest/types.rs b/api/src/rest/types.rs index ef087e9..95f5b04 100644 --- a/api/src/rest/types.rs +++ b/api/src/rest/types.rs @@ -1,7 +1,7 @@ #[derive(Debug)] pub struct Url<'a> { - path: &'a str, - query: Option<&'a str>, + path_segments: Vec<&'a str>, + query_params: QueryParams<'a>, } pub type QueryParam<'a> = (&'a str, &'a str); @@ -15,26 +15,38 @@ impl Url<'_> { let path = parts.next().unwrap_or_default(); let query = parts.next(); - Url { path, query } + Url { + path_segments: extract_path_segments(path), + query_params: extract_query_params(query), + } } - pub fn path_segments(&self) -> Vec<&str> { - self.path.split('/').skip(1).collect() + pub fn path_segments(&self) -> &[&str] { + self.path_segments.as_slice() } - pub fn query_params(&self) -> Vec { - self.query - .map(|q| { - q.split('&') - .filter_map(|part| { - if let [key, val] = part.splitn(2, '=').collect::>()[..] { - Some((key, val)) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default() + pub fn query_params(&self) -> &QueryParams { + &self.query_params } } + +fn extract_path_segments(path: &str) -> Vec<&str> { + path.split('/').skip(1).collect() +} + +fn extract_query_params(query: Option<&str>) -> Vec { + query + .clone() + .map(|q| { + q.split('&') + .filter_map(|part| { + if let [key, val] = part.splitn(2, '=').collect::>()[..] { + Some((key, val)) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() +}