use mime::Mime;
use serde::de::DeserializeOwned;
use std::str::Split;
use std::time::Duration;
use crate::response::{Cookie, SputnikHeaders, delete_cookie};
use crate::http::{HeaderMap, header, request::Parts};
pub trait SputnikParts {
fn query<X: DeserializeOwned>(&self) -> Result<X,QueryError>;
fn cookies(&self) -> CookieIter;
fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError>;
fn response_headers(&mut self) -> &mut HeaderMap;
}
pub struct CookieIter<'a>(Split<'a, char>);
impl<'a> Iterator for CookieIter<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<Self::Item> {
self.0.next().and_then(|str| {
let mut iter = str.splitn(2, '=');
let name = iter.next().expect("first splitn().next() returns Some");
let value = iter.next();
match value {
None => self.next(),
Some(mut value) => {
if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
value = &value[1..value.len()-1];
}
Some((name, value))
}
}
})
}
}
impl SputnikParts for Parts {
fn query<T: DeserializeOwned>(&self) -> Result<T,QueryError> {
serde_urlencoded::from_str::<T>(self.uri.query().unwrap_or("")).map_err(QueryError)
}
fn response_headers(&mut self) -> &mut HeaderMap {
if self.extensions.get::<HeaderMap>().is_none() {
self.extensions.insert(HeaderMap::new());
}
self.extensions.get_mut::<HeaderMap>().unwrap()
}
fn cookies(&self) -> CookieIter {
CookieIter(self.headers.get(header::COOKIE).and_then(|h| std::str::from_utf8(h.as_bytes()).ok()).unwrap_or("").split(';'))
}
fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError> {
if let Some(content_type) = self.headers.get(header::CONTENT_TYPE) {
if *content_type == mime.to_string() {
return Ok(())
}
}
Err(WrongContentTypeError{expected: mime, received: self.headers.get(header::CONTENT_TYPE).as_ref().and_then(|h| h.to_str().ok().map(|s| s.to_owned()))})
}
}
const FLASH_COOKIE_NAME: &str = "flash";
pub struct Flash {
name: String,
message: String,
}
impl From<Flash> for Cookie {
fn from(flash: Flash) -> Self {
Cookie {
name: FLASH_COOKIE_NAME.into(),
value: format!("{}:{}", flash.name, flash.message),
max_age: Some(Duration::from_secs(5 * 60)),
..Default::default()
}
}
}
impl Flash {
pub fn from_request(req: &mut Parts) -> Option<Self> {
let value = req.cookies().find(|(name, _value)| *name == FLASH_COOKIE_NAME)?.1.to_owned();
req.response_headers().set_cookie(delete_cookie(FLASH_COOKIE_NAME));
let mut iter = value.splitn(2, ':');
if let (Some(name), Some(message)) = (iter.next(), iter.next()) {
return Some(Flash{name: name.to_owned(), message: message.to_owned()})
}
None
}
pub fn new(name: String, message: String) -> Self {
Flash{name, message}
}
pub fn success(message: String) -> Self {
Flash{name: "success".to_owned(), message}
}
pub fn warning(message: String) -> Self {
Flash{name: "warning".to_owned(), message}
}
pub fn error(message: String) -> Self {
Flash{name: "error".to_owned(), message}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(thiserror::Error, Debug)]
#[error("query deserialize error: {0}")]
pub struct QueryError(pub serde_urlencoded::de::Error);
#[derive(thiserror::Error, Debug)]
#[error("expected Content-Type {expected} but received {}", received.as_ref().unwrap_or(&"nothing".to_owned()))]
pub struct WrongContentTypeError {
pub expected: Mime,
pub received: Option<String>,
}
#[cfg(test)]
mod tests {
use std::convert::TryInto;
use crate::http::{Request, header};
use super::SputnikParts;
#[test]
fn test_enforce_content_type() {
let (mut parts, _body) = Request::new("").into_parts();
assert!(parts.enforce_content_type(mime::APPLICATION_JSON).is_err());
parts.headers.append(header::CONTENT_TYPE, "application/json".try_into().unwrap());
assert!(parts.enforce_content_type(mime::APPLICATION_JSON).is_ok());
parts.headers.insert(header::CONTENT_TYPE, "text/html".try_into().unwrap());
assert!(parts.enforce_content_type(mime::APPLICATION_JSON).is_err());
}
}