seamless/api/
api.rs

1use std::collections::HashMap;
2use http::{ Request, Response, method::Method };
3use serde::Serialize;
4use super::info::ApiBodyInfo;
5use super::error::ApiError;
6use crate::handler::{ Handler, IntoHandler, request::AsyncReadBody };
7
8/// The entry point; you can create an instance of this and then add API routes to it
9/// using [`Self::add()`]. You can then get information about the routes that have been added
10/// using [`Self::info()`], or handle an [`http::Request`] using [`Self::handle()`].
11pub struct Api {
12    base_path: String,
13    routes: HashMap<(Method,String),ResolvedApiRoute>
14}
15
16// An API route has the contents of `ResolvedHandler` but also a description.
17struct ResolvedApiRoute {
18    description: String,
19    resolved_handler: Handler
20}
21
22impl Api {
23
24    /// Instantiate a new API.
25    pub fn new() -> Api {
26        Api::new_with_base_path("")
27    }
28
29    /// Instantiate a new API that will handle requests that begin with the
30    /// provided base path.
31    ///
32    /// For example, if the provided `base_path` is "/foo/bar", and a route with
33    /// the path "hi" is added, then an incoming [`http::Request`] with the path
34    /// `"/foo/bar/hi"` will match it.
35    pub fn new_with_base_path<S: Into<String>>(base_path: S) -> Api {
36        Api {
37            base_path: base_path.into(),
38            routes: HashMap::new()
39        }
40    }
41
42    /// Add a new route to the API. You must provide a path to make this route available at,
43    /// and are given back a [`RouteBuilder`] which can be used to give the route a handler
44    /// and a description.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// # use seamless::{ Api, handler::{ body::FromJson, response::ToJson } };
50    /// # use std::convert::Infallible;
51    /// # let mut api = Api::new();
52    /// // This route expects a JSON formatted string to be provided, and echoes it straight back.
53    /// api.add("some/route/name")
54    ///    .description("This route takes some Foo's in and returns some Bar's")
55    ///    .handler(|body: FromJson<String>| ToJson(body.0));
56    ///
57    /// // This route delegates to an async fn to sum some values, so we can infer more types in the
58    /// // handler.
59    /// api.add("another.route")
60    ///    .description("This route takes an array of values and sums them")
61    ///    .handler(|body: FromJson<_>| sum(body.0));
62    ///
63    /// async fn sum(ns: Vec<u64>) -> ToJson<u64> {
64    ///     ToJson(ns.into_iter().sum())
65    /// }
66    /// ```
67    pub fn add<P: Into<String>>(&mut self, path: P) -> RouteBuilder {
68        RouteBuilder::new(self, path.into())
69    }
70
71    // Add a route given the individual parts (for internal use)
72    fn add_parts<A, P: Into<String>, HandlerFn: IntoHandler<A>>(&mut self, path: P, description: String, handler_fn: HandlerFn) {
73        let resolved_handler = handler_fn.into_handler();
74        let mut path: String = path.into();
75        path = path.trim_matches('/').to_owned();
76        self.routes.insert((resolved_handler.method.clone(), path.into()), ResolvedApiRoute {
77            description,
78            resolved_handler
79        });
80    }
81
82    /// Match an incoming [`http::Request`] against our API routes and run the relevant handler if a
83    /// matching one is found. We'll get back bytes representing a JSON response back if all goes ok,
84    /// else we'll get back a [`RouteError`], which will either be [`RouteError::NotFound`] if no matching
85    /// route was found, or a [`RouteError::Err`] if a matching route was found, but that handler emitted
86    /// an error.
87    pub async fn handle<Body: AsyncReadBody>(&self, req: Request<Body>) -> Result<Response<Vec<u8>>, RouteError<Body, ApiError>> {
88        let base_path = &self.base_path.trim_start_matches('/');
89        let req_path = req.uri().path().trim_start_matches('/');
90
91        if req_path.starts_with(base_path) {
92            // Ensure that the method and path suffix lines up as expected:
93            let req_method = req.method().into();
94            let req_path_tail = req_path[base_path.len()..].trim_start_matches('/').to_owned();
95
96            // Turn req body into &mut dyn AsyncReadBody:
97            let (req_parts, mut req_body) = req.into_parts();
98            let dyn_req = Request::from_parts(req_parts, &mut req_body as &mut dyn AsyncReadBody);
99
100            if let Some(route) = self.routes.get(&(req_method,req_path_tail)) {
101                (route.resolved_handler.handler)(dyn_req).await.map_err(RouteError::Err)
102            } else {
103                let (req_parts, _) = dyn_req.into_parts();
104                Err(RouteError::NotFound(Request::from_parts(req_parts, req_body)))
105            }
106        } else {
107            Err(RouteError::NotFound(req))
108        }
109    }
110
111    /// Return information about the API routes that have been defined so far.
112    pub fn info(&self) -> Vec<RouteInfo> {
113        let mut info = vec![];
114        for ((_method,key), val) in &self.routes {
115            info.push(RouteInfo {
116                name: key.to_owned(),
117                method: format!("{}", &val.resolved_handler.method),
118                description: val.description.clone(),
119                request_type: val.resolved_handler.request_type.clone(),
120                response_type: val.resolved_handler.response_type.clone()
121            });
122        }
123        info.sort_by(|a,b| a.name.cmp(&b.name));
124        info
125    }
126
127}
128
129/// Add a new API route by providing a description (optional but encouraged)
130/// and then a handler function.
131///
132/// # Examples
133///
134/// ```
135/// # use seamless::{ Api, handler::{ body::FromJson, response::ToJson } };
136/// # use std::convert::Infallible;
137/// # let mut api = Api::new();
138/// // This route expects a JSON formatted string to be provided, and echoes it straight back.
139/// api.add("echo")
140///    .description("Echo back the JSON encoded string provided")
141///    .handler(|body: FromJson<String>| ToJson(body.0));
142///
143/// // This route delegates to an async fn to sum some values, so we can infer more types in the handler.
144/// api.add("another.route")
145///    .description("This route takes an array of values and sums them")
146///    .handler(|FromJson(body)| sum(body));
147///
148/// async fn sum(ns: Vec<u64>) -> Result<ToJson<u64>, Infallible> {
149///     Ok(ToJson(ns.into_iter().sum()))
150/// }
151/// ```
152pub struct RouteBuilder<'a> {
153    api: &'a mut Api,
154    path: String,
155    description: String
156}
157impl <'a> RouteBuilder<'a> {
158    fn new(api: &'a mut Api, path: String) -> Self {
159        RouteBuilder { api, path, description: String::new() }
160    }
161    /// Add a description to the API route.
162    pub fn description<S: Into<String>>(mut self, desc: S) -> Self {
163        self.description = desc.into();
164        self
165    }
166    /// Add a handler to the API route. Until this has been added, the route
167    /// doesn't "exist".
168    pub fn handler<A, HandlerFn: IntoHandler<A>>(self, handler: HandlerFn) {
169        self.api.add_parts(self.path, self.description, handler);
170    }
171}
172
173/// A route is either not found, or we attempted to run it and ran into
174/// an issue.
175pub enum RouteError<B, E> {
176    /// No route matched the provided request,
177    /// so we hand it back.
178    NotFound(Request<B>),
179    /// The matching route failed; this is the error.
180    Err(E)
181}
182
183impl <B, E: std::fmt::Debug> std::fmt::Debug for RouteError<B, E> {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        match self {
186            RouteError::NotFound(..) => f.debug_tuple("RouteError::NotFound").finish(),
187            RouteError::Err(e) => f.debug_tuple("RouteError::Err").field(e).finish()
188        }
189    }
190}
191
192impl <B, E> RouteError<B, E> {
193    /// Assume that the `RouteError` contains an error and attempt to
194    /// unwrap this
195    ///
196    /// # Panics
197    ///
198    /// Panics if the RouteError does not contain an error
199    pub fn unwrap_err(self) -> E {
200        match self {
201            RouteError::Err(e) => e,
202            _ => panic!("Attempt to unwrap_api_err on RouteError that is NotFound")
203        }
204    }
205}
206
207/// Information about a single route.
208#[derive(Debug,Clone,PartialEq,Serialize)]
209pub struct RouteInfo {
210    /// The name/path that the [`http::Request`] needs to contain
211    /// in order to match this route.
212    pub name: String,
213    /// The HTTP method expected in order for a [`http::Request`] to
214    /// match this route, as a string.
215    pub method: String,
216    /// The description of the route as set by [`RouteBuilder::description()`]
217    pub description: String,
218    /// The shape of the data expected to be provided as part of the [`http::Request`]
219    /// for this route. This doesn't care about the wire format that the data is provided in,
220    /// though the type information is somewhat related to what the possible types that can
221    /// be sent and received via JSON.
222    ///
223    /// Types can use the [`macro@crate::ApiBody`] macro, or implement [`type@crate::api::ApiBody`]
224    /// manually in order to describe the shape and documentation that they should hand back.
225    pub request_type: ApiBodyInfo,
226    /// The shape of the data that is returned from this API route.
227    pub response_type: ApiBodyInfo
228}