api: fetch ingredient by key

This commit is contained in:
Dmitriy Pleshevskiy 2022-05-13 19:33:08 +03:00
parent ad2c142203
commit 1bc6689104
5 changed files with 183 additions and 5 deletions

View File

@ -0,0 +1,96 @@
use super::types;
use crate::repo::ingredient::IngredientRepo;
#[derive(Default, Debug)]
pub struct RequestOpts {
pub lang: Option<types::Lang>,
}
pub enum ResponseError {
NotFound,
}
pub fn execute(
repo: &impl IngredientRepo,
key: String,
opts: RequestOpts,
) -> Result<types::Ingredient, ResponseError> {
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!(),
}
}
}

View File

@ -1,2 +1,3 @@
pub mod fetch_by_key;
pub mod fetch_list;
pub mod types;

View File

@ -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<types::Lang>,
}
impl From<fetch_by_key::RequestOpts> for GetIngredientOpts {
fn from(app: fetch_by_key::RequestOpts) -> Self {
Self { lang: app.lang }
}
}
#[derive(Default)]
pub struct GetIngredientsOpts {
pub lang: Option<types::Lang>,
pub keys: Option<Vec<String>>,
}
impl From<fetch_list::RequestOpts> for GetIngredientOpts {
impl From<fetch_list::RequestOpts> for GetIngredientsOpts {
fn from(app: fetch_list::RequestOpts) -> Self {
Self {
lang: app.lang,
@ -16,13 +27,27 @@ impl From<fetch_list::RequestOpts> for GetIngredientOpts {
}
pub trait IngredientRepo {
fn get_ingredients(&self, opts: GetIngredientOpts) -> Vec<types::Ingredient>;
fn get_ingredient_opt(&self, key: String, opts: GetIngredientOpts)
-> Option<types::Ingredient>;
fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec<types::Ingredient>;
}
pub struct StaticIngredientRepo;
impl IngredientRepo for StaticIngredientRepo {
fn get_ingredients(&self, opts: GetIngredientOpts) -> Vec<types::Ingredient> {
fn get_ingredient_opt(
&self,
key: String,
opts: GetIngredientOpts,
) -> Option<types::Ingredient> {
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<types::Ingredient> {
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<types::Ingredient> {
fn get_ingredient_opt(
&self,
key: String,
opts: GetIngredientOpts,
) -> Option<types::Ingredient> {
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<types::Ingredient> {
let langs = [opts.lang.unwrap_or_default()].repeat(self.ingredients.len());
self.ingredients
.iter()

View File

@ -55,3 +55,44 @@ pub fn fetch_list(url: &Url) -> Response<Cursor<Vec<u8>>> {
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<QueryParams<'a>> 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<FetchIngredientOpts<'_>> 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<Cursor<Vec<u8>>> {
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())
}

View File

@ -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")),
};
}