api: add context, some style improvements
This commit is contained in:
parent
8c0a60a4e8
commit
284ad260a4
|
@ -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<misc_types::Lang>,
|
||||
pub lang: Option<Lang>,
|
||||
}
|
||||
|
||||
pub enum ResponseError {
|
||||
|
@ -13,10 +15,10 @@ pub enum ResponseError {
|
|||
|
||||
pub fn execute(
|
||||
repo: &impl IngredientRepo,
|
||||
ctx: &Context,
|
||||
key: String,
|
||||
opts: RequestOpts,
|
||||
) -> Result<types::Ingredient, ResponseError> {
|
||||
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) => {}
|
||||
|
|
|
@ -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<misc_types::Lang>,
|
||||
pub keys: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub fn execute(repo: &impl IngredientRepo, opts: RequestOpts) -> Vec<types::Ingredient> {
|
||||
repo.get_ingredients(opts.into())
|
||||
pub fn execute(
|
||||
repo: &impl IngredientRepo,
|
||||
ctx: &Context,
|
||||
opts: RequestOpts,
|
||||
) -> Vec<types::Ingredient> {
|
||||
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()
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
pub mod ingredient;
|
||||
pub mod misc_types;
|
||||
pub mod recipe;
|
||||
|
||||
pub use misc_types::{Context, Lang};
|
||||
|
|
|
@ -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<types::Recipe> {
|
||||
repo.get_recipes()
|
||||
pub fn execute(repo: &impl RecipeRepo, ctx: &Context) -> Vec<types::Recipe> {
|
||||
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"));
|
||||
|
|
|
@ -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<fetch_by_key::RequestOpts> for GetIngredientOpts {
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct GetIngredientsOpts {
|
||||
pub lang: Option<misc_types::Lang>,
|
||||
pub keys: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<fetch_list::RequestOpts> 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<types::Ingredient>;
|
||||
fn get_ingredient_opt(&self, ctx: &Context, key: String) -> Option<types::Ingredient>;
|
||||
|
||||
fn get_ingredients(&self, opts: GetIngredientsOpts) -> Vec<types::Ingredient>;
|
||||
fn get_ingredients(&self, ctx: &Context, opts: GetIngredientsOpts) -> Vec<types::Ingredient>;
|
||||
}
|
||||
|
||||
pub struct StaticIngredientRepo;
|
||||
|
||||
impl IngredientRepo for StaticIngredientRepo {
|
||||
fn get_ingredient_opt(
|
||||
&self,
|
||||
key: String,
|
||||
opts: GetIngredientOpts,
|
||||
) -> Option<types::Ingredient> {
|
||||
fn get_ingredient_opt(&self, ctx: &Context, key: String) -> Option<types::Ingredient> {
|
||||
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<types::Ingredient> {
|
||||
let langs = [opts.lang.unwrap_or_default()].repeat(db::INGREDIENTS.len());
|
||||
fn get_ingredients(&self, ctx: &Context, opts: GetIngredientsOpts) -> Vec<types::Ingredient> {
|
||||
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<types::Ingredient> {
|
||||
fn get_ingredient_opt(&self, ctx: &Context, key: String) -> Option<types::Ingredient> {
|
||||
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<types::Ingredient> {
|
||||
let langs = [opts.lang.unwrap_or_default()].repeat(self.ingredients.len());
|
||||
fn get_ingredients(&self, ctx: &Context, opts: GetIngredientsOpts) -> Vec<types::Ingredient> {
|
||||
let langs = [ctx.lang].repeat(self.ingredients.len());
|
||||
self.ingredients
|
||||
.iter()
|
||||
.zip(langs)
|
||||
|
|
|
@ -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<types::Recipe>;
|
||||
fn get_recipes(&self, ctx: &Context) -> Vec<types::Recipe>;
|
||||
}
|
||||
|
||||
pub struct StaticRecipeRepo;
|
||||
|
||||
impl RecipeRepo for StaticRecipeRepo {
|
||||
fn get_recipes(&self) -> Vec<types::Recipe> {
|
||||
fn get_recipes(&self, ctx: &Context) -> Vec<types::Recipe> {
|
||||
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<types::Recipe> {
|
||||
fn get_recipes(&self, ctx: &Context) -> Vec<types::Recipe> {
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::domain;
|
||||
|
||||
use super::types::QueryParams;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Context {
|
||||
lang: Option<String>,
|
||||
}
|
||||
|
||||
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<Context> for domain::Context {
|
||||
fn from(rest: Context) -> Self {
|
||||
Self {
|
||||
lang: rest
|
||||
.lang
|
||||
.and_then(|l| domain::Lang::from_str(&l).ok())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> From<QueryParams<'a>> 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<QueryParams<'a>> for FetchIngredientsOpts<'a> {
|
|||
|
||||
impl From<FetchIngredientsOpts<'_>> 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<Cursor<Vec<u8>>> {
|
||||
pub fn fetch_list(rest_ctx: &rest::Context, url: &Url) -> Response<Cursor<Vec<u8>>> {
|
||||
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<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>>> {
|
||||
pub fn fetch_by_key(rest_ctx: &rest::Context, _url: &Url, key: &str) -> Response<Cursor<Vec<u8>>> {
|
||||
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)
|
||||
|
|
|
@ -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<Cursor<Vec<u8>>> {
|
||||
use crate::domain::recipe::fetch_list;
|
||||
pub fn fetch_list(rest_ctx: &rest::Context, _url: &Url) -> Response<Cursor<Vec<u8>>> {
|
||||
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)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
pub mod context;
|
||||
pub mod ctrl;
|
||||
pub mod server;
|
||||
pub mod types;
|
||||
|
||||
pub use context::Context;
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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<QueryParam> {
|
||||
self.query
|
||||
.map(|q| {
|
||||
q.split('&')
|
||||
.filter_map(|part| {
|
||||
if let [key, val] = part.splitn(2, '=').collect::<Vec<&str>>()[..] {
|
||||
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<QueryParam> {
|
||||
query
|
||||
.clone()
|
||||
.map(|q| {
|
||||
q.split('&')
|
||||
.filter_map(|part| {
|
||||
if let [key, val] = part.splitn(2, '=').collect::<Vec<&str>>()[..] {
|
||||
Some((key, val))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue