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}