diff --git a/api/src/main.rs b/api/src/main.rs index b71c700..7f345b5 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -5,8 +5,8 @@ extern crate serde; mod domain; mod repo; -mod server; +mod rest; fn main() { - server::start(); + rest::server::start(); } diff --git a/api/src/rest/ctrl/ingredient.rs b/api/src/rest/ctrl/ingredient.rs new file mode 100644 index 0000000..5529e27 --- /dev/null +++ b/api/src/rest/ctrl/ingredient.rs @@ -0,0 +1,57 @@ +use std::io::Cursor; +use std::str::FromStr; + +use tiny_http::{Header, Response}; + +use crate::domain; +use crate::domain::ingredient::types::Lang; +use crate::repo::ingredient::StaticIngredientRepo; +use crate::rest::types::{QueryParams, Url}; + +#[derive(Default, Debug)] +struct FetchIngredientsOpts<'a> { + lang: Option<&'a str>, + keys: Option>, +} + +impl<'a> From> for FetchIngredientsOpts<'a> { + fn from(params: QueryParams<'a>) -> Self { + params + .into_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); + opts.keys = Some(keys) + } + _ => {} + }; + + opts + }) + } +} + +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 } + } +} + +pub fn fetch_list(url: &Url) -> Response>> { + use domain::ingredient::fetch_list; + let opts = FetchIngredientsOpts::from(url.query_params()); + + let repo = StaticIngredientRepo; + let ingredients = fetch_list::execute(&repo, opts.into()); + let data = serde_json::to_string(&ingredients).unwrap(); + + Response::from_string(data) + .with_header(Header::from_str("content-type: application/json").unwrap()) +} diff --git a/api/src/rest/ctrl/mod.rs b/api/src/rest/ctrl/mod.rs new file mode 100644 index 0000000..ac67972 --- /dev/null +++ b/api/src/rest/ctrl/mod.rs @@ -0,0 +1 @@ +pub mod ingredient; diff --git a/api/src/rest/mod.rs b/api/src/rest/mod.rs new file mode 100644 index 0000000..f4cf26a --- /dev/null +++ b/api/src/rest/mod.rs @@ -0,0 +1,3 @@ +pub mod ctrl; +pub mod server; +pub mod types; diff --git a/api/src/rest/server.rs b/api/src/rest/server.rs new file mode 100644 index 0000000..0342b82 --- /dev/null +++ b/api/src/rest/server.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; +use std::thread; +use tiny_http::{Response, Server}; + +use crate::rest; +use crate::rest::types::Url; + +pub fn start() { + let server = Arc::new(Server::http("0.0.0.0:33333").unwrap()); + println!("Server listening on http://localhost:33333"); + + let mut handles = Vec::with_capacity(4); + + for _ in 0..4 { + let server = server.clone(); + + handles.push(thread::spawn(move || { + for rq in server.incoming_requests() { + let url = Url::parse(rq.url()); + let _ = match url.path_segments()[..] { + ["api", "ingredients"] => { + let res = rest::ctrl::ingredient::fetch_list(&url); + rq.respond(res) + } + _ => rq.respond(Response::from_string("Not found")), + }; + } + })); + } + + for h in handles { + h.join().unwrap(); + } +} diff --git a/api/src/rest/types.rs b/api/src/rest/types.rs new file mode 100644 index 0000000..5d1f8e0 --- /dev/null +++ b/api/src/rest/types.rs @@ -0,0 +1,40 @@ +#[derive(Debug)] +pub struct Url<'a> { + path: &'a str, + query: Option<&'a str>, +} + +pub type QueryParam<'a> = (&'a str, &'a str); + +pub type QueryParams<'a> = Vec>; + +impl Url<'_> { + pub fn parse(url: &str) -> Url { + let mut parts = url.splitn(2, '?'); + + let path = parts.next().unwrap_or_default().trim_matches('/'); + let query = parts.next(); + + Url { path, query } + } + + pub fn path_segments(&self) -> Vec<&str> { + self.path.split('/').collect() + } + + 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() + } +} diff --git a/api/src/server.rs b/api/src/server.rs deleted file mode 100644 index 363b5e4..0000000 --- a/api/src/server.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::str::FromStr; -use std::sync::Arc; -use std::thread; - -use crate::domain; -use crate::domain::ingredient::types::Lang; -use crate::repo::ingredient::StaticIngredientRepo; - -pub fn start() { - use tiny_http::{Header, Response, Server}; - let server = Arc::new(Server::http("0.0.0.0:33333").unwrap()); - println!("Server listening on http://localhost:33333"); - - let mut handles = Vec::with_capacity(4); - - for _ in 0..4 { - let server = server.clone(); - - handles.push(thread::spawn(move || { - for rq in server.incoming_requests() { - let url = Url::parse(rq.url()); - let _ = match url.path_segments()[..] { - ["ingredients"] => { - use domain::ingredient::fetch_list; - let opts = FetchIngredientsOpts::from(url.query_params()); - - let repo = StaticIngredientRepo; - let ingredients = fetch_list::execute(&repo, opts.into()); - let data = serde_json::to_string(&ingredients).unwrap(); - - let response = Response::from_string(data).with_header( - Header::from_str("content-type: application/json").unwrap(), - ); - rq.respond(response) - } - _ => rq.respond(Response::from_string("Not found")), - }; - } - })); - } - - for h in handles { - h.join().unwrap(); - } -} - -#[derive(Default, Debug)] -struct FetchIngredientsOpts<'a> { - lang: Option<&'a str>, - keys: Option>, -} - -impl<'a> From> for FetchIngredientsOpts<'a> { - fn from(params: QueryParams<'a>) -> Self { - params - .into_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); - opts.keys = Some(keys) - } - _ => {} - }; - - opts - }) - } -} - -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 } - } -} - -#[derive(Debug)] -struct Url<'a> { - path: &'a str, - query: Option<&'a str>, -} - -type QueryParam<'a> = (&'a str, &'a str); - -type QueryParams<'a> = Vec>; - -impl Url<'_> { - pub fn parse(url: &str) -> Url { - let mut parts = url.splitn(2, '?'); - - let path = parts.next().unwrap_or_default(); - let query = parts.next(); - - Url { path, query } - } - - pub fn path_segments(&self) -> Vec<&str> { - self.path.split('/').skip(1).collect() - } - - 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() - } -}