-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from EspressoSystems/feat/interfaces
Define the basic server types and interfaces and add a minimal example.
- Loading branch information
Showing
9 changed files
with
740 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[route.greeting] | ||
PATH = ["greeting", "greeting/:name"] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use snafu::Snafu; | ||
use std::fs; | ||
use std::io; | ||
use tide_disco::{http::StatusCode, Api, App}; | ||
|
||
#[derive(Clone, Debug, Deserialize, Serialize, Snafu)] | ||
enum HelloError { | ||
Goodbye { | ||
status: StatusCode, | ||
farewell: String, | ||
}, | ||
} | ||
|
||
impl tide_disco::Error for HelloError { | ||
fn catch_all(status: StatusCode, farewell: String) -> Self { | ||
Self::Goodbye { status, farewell } | ||
} | ||
|
||
fn status(&self) -> StatusCode { | ||
match self { | ||
Self::Goodbye { status, .. } => *status, | ||
} | ||
} | ||
} | ||
|
||
#[async_std::main] | ||
async fn main() -> io::Result<()> { | ||
let greeting = "Hello, world!".to_string(); | ||
let mut app = App::<String, HelloError>::with_state(greeting); | ||
let mut api = Api::<String, HelloError>::new(toml::from_slice(&fs::read( | ||
"examples/hello-world/api.toml", | ||
)?)?) | ||
.unwrap(); | ||
api.at("greeting", |req| async move { Ok(req.state().clone()) }) | ||
.unwrap(); | ||
app.register_module("", api).unwrap(); | ||
app.serve("0.0.0.0:8080").await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
use crate::{ | ||
request::RequestParams, | ||
route::{Route, RouteParseError}, | ||
}; | ||
use futures::Future; | ||
use serde::Serialize; | ||
use snafu::{OptionExt, ResultExt, Snafu}; | ||
use std::collections::hash_map::{HashMap, IntoValues, Values}; | ||
use std::ops::Index; | ||
|
||
/// An error encountered when parsing or constructing an [Api]. | ||
#[derive(Clone, Debug, Snafu)] | ||
pub enum ApiError { | ||
Route { source: RouteParseError }, | ||
MissingRoutesTable, | ||
RoutesMustBeTable, | ||
UndefinedRoute, | ||
HandlerAlreadyRegistered, | ||
} | ||
|
||
/// A description of an API. | ||
/// | ||
/// An [Api] is a structured representation of an `api.toml` specification. It contains API-level | ||
/// metadata and descriptions of all of the routes in the specification. It can be parsed from a | ||
/// TOML file and registered as a module of an [App](crate::App). | ||
pub struct Api<State, Error> { | ||
routes: HashMap<String, Route<State, Error>>, | ||
} | ||
|
||
impl<'a, State, Error> IntoIterator for &'a Api<State, Error> { | ||
type Item = &'a Route<State, Error>; | ||
type IntoIter = Values<'a, String, Route<State, Error>>; | ||
|
||
fn into_iter(self) -> Self::IntoIter { | ||
self.routes.values() | ||
} | ||
} | ||
|
||
impl<State, Error> IntoIterator for Api<State, Error> { | ||
type Item = Route<State, Error>; | ||
type IntoIter = IntoValues<String, Route<State, Error>>; | ||
|
||
fn into_iter(self) -> Self::IntoIter { | ||
self.routes.into_values() | ||
} | ||
} | ||
|
||
impl<State, Error> Index<&str> for Api<State, Error> { | ||
type Output = Route<State, Error>; | ||
|
||
fn index(&self, index: &str) -> &Route<State, Error> { | ||
&self.routes[index] | ||
} | ||
} | ||
|
||
impl<State, Error> Api<State, Error> { | ||
/// Parse an API from a TOML specification. | ||
pub fn new(api: toml::Value) -> Result<Self, ApiError> { | ||
let routes = match api.get("route") { | ||
Some(routes) => routes.as_table().context(RoutesMustBeTableSnafu)?, | ||
None => return Err(ApiError::MissingRoutesTable), | ||
}; | ||
Ok(Self { | ||
routes: routes | ||
.into_iter() | ||
.map(|(name, spec)| { | ||
let route = Route::new(name.clone(), spec).context(RouteSnafu)?; | ||
Ok((route.name(), route)) | ||
}) | ||
.collect::<Result<_, _>>()?, | ||
}) | ||
} | ||
|
||
/// Register a handler for a route. | ||
/// | ||
/// When the server receives a request whose URL matches the pattern of the route `name`, | ||
/// `handler` will be invoked with the parameters of the request and the result will be | ||
/// serialized into a response. | ||
/// | ||
/// If the route `name` does not exist in the API specification, or if the route already has a | ||
/// handler registered, an error is returned. Note that all routes are initialized with a | ||
/// default handler that echoes parameters and shows documentation, but this default handler can | ||
/// replaced by this function without raising [ApiError::HandlerAlreadyRegistered]. | ||
pub fn at<F, Fut, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError> | ||
where | ||
F: 'static + Send + Sync + Fn(RequestParams<State>) -> Fut, | ||
Fut: 'static + Send + Sync + Future<Output = Result<T, Error>>, | ||
T: 'static + Send + Sync + Serialize, | ||
State: 'static + Send + Sync, | ||
{ | ||
let route = self.routes.get_mut(name).ok_or(ApiError::UndefinedRoute)?; | ||
if route.has_handler() { | ||
return Err(ApiError::HandlerAlreadyRegistered); | ||
} else { | ||
route.set_fn_handler(handler); | ||
} | ||
Ok(self) | ||
} | ||
|
||
/// Create a new [Api] which is just like this one, except has a transformed `Error` type. | ||
pub fn map_err<Error2>( | ||
self, | ||
f: impl 'static + Clone + Send + Sync + Fn(Error) -> Error2, | ||
) -> Api<State, Error2> | ||
where | ||
Error: 'static + Send + Sync, | ||
Error2: 'static, | ||
State: 'static + Send + Sync, | ||
{ | ||
Api { | ||
routes: self | ||
.routes | ||
.into_iter() | ||
.map(|(name, route)| (name, route.map_err(f.clone()))) | ||
.collect(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
use crate::{ | ||
api::Api, | ||
request::RequestParams, | ||
route::{Handler, RouteError}, | ||
}; | ||
use async_std::sync::Arc; | ||
use snafu::Snafu; | ||
use std::collections::hash_map::{Entry, HashMap}; | ||
use std::io; | ||
use tide::http::StatusCode; | ||
|
||
pub use tide::listener::{Listener, ToListener}; | ||
|
||
/// A tide-disco server application. | ||
/// | ||
/// An [App] is a collection of API modules, plus a global `State`. Modules can be registered by | ||
/// constructing an [Api] for each module and calling [App::register_module]. Once all of the | ||
/// desired modules are registered, the app can be converted into an asynchronous server task using | ||
/// [App::server]. | ||
pub struct App<State, Error> { | ||
// Map from base URL to module API. | ||
apis: HashMap<String, Api<State, Error>>, | ||
state: Arc<State>, | ||
} | ||
|
||
/// An error encountered while building an [App]. | ||
#[derive(Clone, Debug, Snafu)] | ||
pub enum AppError { | ||
ModuleAlreadyExists, | ||
} | ||
|
||
impl<State: Clone + Send + Sync + 'static, Error: 'static> App<State, Error> { | ||
/// Create a new [App] with a given state. | ||
pub fn with_state(state: State) -> Self { | ||
Self { | ||
apis: HashMap::new(), | ||
state: Arc::new(state), | ||
} | ||
} | ||
|
||
/// Register an API module. | ||
pub fn register_module<ModuleError>( | ||
&mut self, | ||
base_url: &str, | ||
api: Api<State, ModuleError>, | ||
) -> Result<&mut Self, AppError> | ||
where | ||
Error: From<ModuleError>, | ||
ModuleError: 'static + Send + Sync, | ||
{ | ||
match self.apis.entry(base_url.to_string()) { | ||
Entry::Occupied(_) => { | ||
return Err(AppError::ModuleAlreadyExists); | ||
} | ||
Entry::Vacant(e) => { | ||
e.insert(api.map_err(Error::from)); | ||
} | ||
} | ||
|
||
Ok(self) | ||
} | ||
} | ||
|
||
impl<State: Clone + Send + Sync + 'static, Error: 'static + crate::Error> App<State, Error> { | ||
/// Serve the [App] asynchronously. | ||
pub async fn serve<L: ToListener<Arc<Self>>>(self, listener: L) -> io::Result<()> { | ||
let state = Arc::new(self); | ||
let mut server = tide::Server::with_state(state.clone()); | ||
for (prefix, api) in &state.apis { | ||
for route in api { | ||
let name = route.name(); | ||
let prefix = prefix.clone(); | ||
server.at(&prefix).at(&name).method( | ||
route.method(), | ||
move |req: tide::Request<Arc<Self>>| { | ||
let name = name.clone(); | ||
let prefix = prefix.clone(); | ||
async move { | ||
let route = &req.state().apis[&prefix][&name]; | ||
let req = | ||
RequestParams::new(&req, req.state().state.clone(), route.params()) | ||
.map_err(|err| { | ||
Error::from_request_error(err).into_tide_error() | ||
})?; | ||
route | ||
.handle(req) | ||
.await | ||
.map_err(|err| match err { | ||
RouteError::AppSpecific(err) => err.into(), | ||
_ => Error::from_route_error(err), | ||
}) | ||
.map_err(|err| err.into_tide_error()) | ||
} | ||
}, | ||
); | ||
} | ||
} | ||
server.listen(listener).await | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
use crate::request::RequestError; | ||
use crate::route::RouteError; | ||
use serde::{de::DeserializeOwned, Serialize}; | ||
use std::fmt::Display; | ||
use tide::StatusCode; | ||
|
||
/// Errors which can be serialized in a response body. | ||
/// | ||
/// This trait can be used to define a standard error type returned by all API endpoints. When a | ||
/// request fails for any reason, the body of the response will contain a serialization of | ||
/// the error that caused the failure, upcasted into an anyhow::Error. If the error is an instance | ||
/// of the standard error type for that particular API, it can be deserialized and downcasted to | ||
/// this type on the client. | ||
/// | ||
/// Other errors (those which don't downcast to the API's error type, such as errors generated from | ||
/// the [tide] framework) will be serialized as strings using their [Display] instance and encoded | ||
/// as an API error using the [catch_all](Error::catch_all) function. | ||
pub trait Error: std::error::Error + Serialize + DeserializeOwned + Send + Sync + 'static { | ||
fn catch_all(status: StatusCode, msg: String) -> Self; | ||
fn status(&self) -> StatusCode; | ||
|
||
fn from_route_error<E: Display>(source: RouteError<E>) -> Self { | ||
Self::catch_all(source.status(), source.to_string()) | ||
} | ||
|
||
fn from_request_error(source: RequestError) -> Self { | ||
Self::catch_all(StatusCode::BadRequest, source.to_string()) | ||
} | ||
|
||
fn into_tide_error(self) -> tide::Error { | ||
tide::Error::new(self.status(), self) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.