mise-server 0.1.11

MIcro SErvice
Documentation
use crate::{
    routes::{RequestProcessor, RouteContext, RouteMethod},
    server::Server,
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use std::{collections::HashMap, error::Error, net::SocketAddr};

pub struct Request(pub http::Request<Value>);
pub struct Response(pub http::Response<Value>);

/// Final routes are always in JSON.
pub trait Route: FnMut(Request) -> Response + 'static {}
impl<T> Route for T where T: FnMut(Request) -> Response + 'static {}

/// In some cases it's possible to install a route that only returns text.
/// this is used for certain side behavior such as returning prometheus scrape
/// renders. These are meant to be static and have no parameter and respond only
/// on absolute paths: no request object is available and the response is always
/// a string.
pub trait TextRoute: FnMut() -> String + 'static {}
impl<T> TextRoute for T where T: FnMut() -> String + 'static {}

/// Server resource and entry point.
///
/// Example:
///
/// ```no_run
/// use mise_server::prelude::*;
/// use serde_json::json;
///
/// Mise::new()
///     .get("/found", |_| json!("hello world").into())
///     .get("/not", |_| StatusCode::NOT_FOUND.into())
///     .get("/param", |r| json!(r.query_param("a").unwrap()).into())
///     .get("/error", |_| panic!("error"))
///     .text("/text", || "result".to_string())
///     .post("/echo", |r| r.body().clone().into())
///     .get("/get_echo/*", |r| json!(r.name()).into())
///     .serve("127.0.0.1:8080".parse().unwrap());
/// ```
#[derive(Default)]
pub struct Mise {
    routes: HashMap<RouteMethod, HashMap<String, RouteContext>>,
    text_routes: HashMap<String, Box<dyn TextRoute>>,
}

impl Mise {
    /// Create a new default server.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a text result only.
    /// This is used for cases such as prometheus scrape renders.
    /// Text routes always take precedence even if the route is already defined
    /// for any other methods.
    #[must_use]
    pub fn text<F: TextRoute>(mut self, path: &str, f: F) -> Self {
        self.text_routes.insert(path.to_string(), Box::new(f));
        self
    }

    /// Register a get route.
    #[must_use]
    pub fn get<F: Route>(mut self, path: &str, f: F) -> Self {
        self.regiser_method(RouteMethod::Get, path, f);
        self
    }

    /// Register a delete route.
    #[must_use]
    pub fn delete<F: Route>(mut self, path: &str, f: F) -> Self {
        self.regiser_method(RouteMethod::Delete, path, f);
        self
    }

    /// Registers a post route. Body is obtained from [`Request::body`] and it is
    /// always a json [Value].
    #[must_use]
    pub fn post<F: Route>(mut self, path: &str, f: F) -> Self {
        self.regiser_method(RouteMethod::Post, path, f);
        self
    }

    /// Registers a put route. Body is obtained from [`Request::body`] and it is
    /// always a json [Value].
    #[must_use]
    pub fn put<F: Route>(mut self, path: &str, f: F) -> Self {
        self.regiser_method(RouteMethod::Put, path, f);
        self
    }

    /// Registers a patch route. Body is obtained from [`Request::body`] and it is
    /// always a json [Value].
    #[must_use]
    pub fn patch<F: Route>(mut self, path: &str, f: F) -> Self {
        self.regiser_method(RouteMethod::Patch, path, f);
        self
    }

    /// Starts the server. Blocks until the server quits.
    /// Can panic if cannot bind the server.
    pub fn serve(self, addr: SocketAddr) {
        Server::serve(
            RequestProcessor {
                routes: self.routes,
                text_routes: self.text_routes,
            },
            addr,
        )
        .run();
    }

    fn regiser_method<F: Route>(&mut self, method: RouteMethod, path: &str, f: F) {
        let routes = self.routes.entry(method).or_default();
        let d: Box<dyn Route> = Box::new(f);
        routes.insert(path.to_string(), (path, d).into());
    }
}

impl From<Request> for Value {
    fn from(value: Request) -> Self {
        value.0.body().to_owned()
    }
}

impl From<Value> for Response {
    fn from(value: Value) -> Self {
        Response(http::Response::new(value))
    }
}

impl From<http::StatusCode> for Response {
    fn from(value: http::StatusCode) -> Self {
        Response(
            http::Response::builder()
                .status(value)
                .body(Value::Null)
                .expect("Statically built body should not fail"),
        )
    }
}

pub trait Serializable: Serialize {
    /// # Errors
    ///
    /// Errors if is not serializable.
    fn to_response(&self) -> Result<Response, Box<dyn Error>> {
        Ok(Response(http::Response::new(serde_json::to_value(self)?)))
    }
}
impl<T> Serializable for T where T: Serialize {}

pub trait Deserializable: DeserializeOwned {
    /// # Errors
    ///
    /// Errors if is not serializable.
    fn from_request(r: Request) -> Result<Self, Box<dyn Error>> {
        Ok(serde_json::from_value(r.body().to_owned())?)
    }
}
impl<T> Deserializable for T where T: DeserializeOwned {}

impl Request {
    /// Returns the uri path, without the query params
    pub fn path(&self) -> &str {
        self.0.uri().path()
    }

    pub fn query(&self) -> Option<&str> {
        self.0.uri().query()
    }

    /// Returns the query param by name.
    pub fn query_param(&self, name: &str) -> Option<&str> {
        let q = self.0.uri().query()?;
        let f = format!("{name}=");
        let idx = q.find(&f)?;
        let end = q[idx..].find('&').unwrap_or(q.len());
        Some(&q[idx + f.len()..end])
    }

    /// Returns the base of the path which is the path without the last item
    ///
    /// eg from /p1/p2/p3 returns '/p1/p2'
    ///
    /// empty string when is unavailable ("/a" = "")
    pub fn base(&self) -> &str {
        let p = self.0.uri().path();
        let n = self.name();
        p[..p.len() - n.len()].trim_end_matches('/')
    }

    /// Returns the last item of the path
    ///
    /// eg from /p1/p2/p3 returns 'p3'
    ///
    /// empty string when is unavailable
    pub fn name(&self) -> &str {
        if !self.0.uri().path().contains('/') {
            return "";
        }
        self.0.uri().path().split('/').next_back().unwrap_or("")
    }

    /// If there is a body in the request, then this will be the json of that
    ///
    /// If not available returns [`Value::Null`]
    pub fn body(&self) -> &Value {
        self.0.body()
    }

    pub(crate) fn base_star(&self) -> Option<String> {
        if self.name().is_empty() {
            // Cannot have a wildcard: no last item
            return None;
        }
        Some(format!("{}/*", self.base()))
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use serde::Deserialize;
    use serde_json::json;

    #[derive(Serialize, Deserialize)]
    struct My {
        val: usize,
    }

    #[test]
    fn test_serde() {
        let m = My { val: 1 };
        let r = m.to_response().unwrap();
        assert_eq!(r.0.body()["val"].clone(), 1);

        let r = Request(http::Request::new(json!({"val":2})));
        let m = My::from_request(r).unwrap();
        assert_eq!(m.val, 2);
    }

    #[test]
    fn test_paths() {
        assert_eq!(requri("/").name(), "");
        assert_eq!(requri("/a").name(), "a");
        assert_eq!(requri("/a/b").name(), "b");

        assert_eq!(requri("/").base(), "");
        assert_eq!(requri("/a").base(), "");
        assert_eq!(requri("/a/b").base(), "/a");

        assert_eq!(requri("/").base_star(), None);
        assert_eq!(requri("/a").base_star(), Some("/*".to_string()));
        assert_eq!(requri("/a/b").base_star(), Some("/a/*".to_string()));

        assert_eq!(requri("/?b=a").query_param("b"), Some("a"));
    }

    fn requri(uri: &str) -> Request {
        Request(http::Request::builder().uri(uri).body(Value::Null).unwrap())
    }
}