Skip to content

Commit

Permalink
Implement default route handler
Browse files Browse the repository at this point in the history
  • Loading branch information
jbearer committed Dec 13, 2023
1 parent 0863f71 commit 6feaf5f
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 28 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async-trait = "0.1.51"
bincode = "1.3.3"
clap = { version = "4.0", features = ["derive"] }
config = "0.13.1"
derivative = "2.2"
derive_more = "0.99"
dirs = "5.0.1"
edit-distance = "2.1.0"
Expand Down
21 changes: 15 additions & 6 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use crate::{
route::{self, *},
socket, Html,
};
use async_std::sync::Arc;
use async_trait::async_trait;
use derivative::Derivative;
use derive_more::From;
use futures::{future::BoxFuture, stream::BoxStream};
use maud::{html, PreEscaped};
Expand Down Expand Up @@ -245,10 +247,14 @@ mod meta_defaults {
/// 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).
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct Api<State, Error> {
meta: ApiMetadata,
meta: Arc<ApiMetadata>,
name: String,
routes: HashMap<String, Route<State, Error>>,
routes_by_path: HashMap<String, Vec<String>>,
#[derivative(Debug = "ignore")]
health_check: Option<HealthCheckHandler<State>>,
api_version: Option<Version>,
public: Option<PathBuf>,
Expand Down Expand Up @@ -311,6 +317,7 @@ impl<State, Error> Api<State, Error> {
.map_err(|source| ApiError::InvalidMetaTable { source })?,
None => ApiMetadata::default(),
};
let meta = Arc::new(meta);
let routes = match api.get("route") {
Some(routes) => routes.as_table().context(RoutesMustBeTableSnafu)?,
None => return Err(ApiError::MissingRoutesTable),
Expand All @@ -319,7 +326,7 @@ impl<State, Error> Api<State, Error> {
let routes = routes
.into_iter()
.map(|(name, spec)| {
let route = Route::new(name.clone(), spec).context(RouteSnafu)?;
let route = Route::new(name.clone(), spec, meta.clone()).context(RouteSnafu)?;
Ok((route.name(), route))
})
.collect::<Result<HashMap<_, _>, _>>()?;
Expand Down Expand Up @@ -348,6 +355,7 @@ impl<State, Error> Api<State, Error> {
}
}
Ok(Self {
name: meta.name.clone(),
meta,
routes,
routes_by_path,
Expand Down Expand Up @@ -1045,6 +1053,7 @@ impl<State, Error> Api<State, Error> {
{
Api {
meta: self.meta,
name: self.name,
routes: self
.routes
.into_iter()
Expand All @@ -1058,23 +1067,23 @@ impl<State, Error> Api<State, Error> {
}

pub(crate) fn set_name(&mut self, name: String) {
self.meta.name = name;
self.name = name;
}

/// Compose an HTML page documenting all the routes in this API.
pub fn documentation(&self) -> Html {
html! {
(PreEscaped(self.meta.html_top
.replace("{{NAME}}", &self.meta.name)
.replace("{{NAME}}", &self.name)
.replace("{{DESCRIPTION}}", &self.meta.description)
.replace("{{VERSION}}", &match &self.api_version {
Some(version) => version.to_string(),
None => "(no version)".to_string(),
})
.replace("{{FORMAT_VERSION}}", &self.meta.format_version.to_string())
.replace("{{PUBLIC}}", &format!("/public/{}", self.meta.name))))
.replace("{{PUBLIC}}", &format!("/public/{}", self.name))))
@for route in self.routes.values() {
(route.documentation(&self.meta))
(route.documentation())
}
(PreEscaped(&self.meta.html_bottom))
}
Expand Down
6 changes: 2 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,8 @@ impl<State: Send + Sync + 'static, Error: 'static + crate::Error> App<State, Err
let api = api.clone();
async move {
let route = &req.state().clone().apis[&api][&name];
let state = &*req.state().clone().state;
let req = request_params(req, route.params()).await?;
route
.default_handler(req, state)
.default_handler()
.map_err(|err| match err {
RouteError::AppSpecific(err) => err,
_ => Error::from_route_error(err),
Expand Down Expand Up @@ -507,7 +505,7 @@ fn add_error_body<T: Clone + Send + Sync + 'static, E: crate::Error>(
let mut res = next.run(req).await;
if let Some(error) = res.take_error() {
let error = E::from_server_error(error);
tracing::warn!("responding with error: {}", error);
tracing::info!("responding with error: {}", error);
// Try to add the error to the response body using a format accepted by the client. If
// we cannot do that (for example, if the client requested a format that is incompatible
// with a serialized error) just add the error as a string using plaintext.
Expand Down
44 changes: 26 additions & 18 deletions src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use crate::{
socket::{self, SocketError},
Html,
};
use async_std::sync::Arc;
use async_trait::async_trait;
use derivative::Derivative;
use derive_more::From;
use futures::future::{BoxFuture, FutureExt};
use maud::{html, PreEscaped};
Expand Down Expand Up @@ -213,11 +215,15 @@ impl<State, Error> RouteImplementation<State, Error> {
/// It can be parsed from a TOML specification, and it also includes an optional handler function
/// which the Rust server can register. Routes with no handler will use a default handler that
/// simply returns information about the route.
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
pub struct Route<State, Error> {
name: String,
patterns: Vec<String>,
params: Vec<RequestParam>,
doc: String,
meta: Arc<ApiMetadata>,
#[derivative(Debug = "ignore")]
handler: RouteImplementation<State, Error>,
}

Expand Down Expand Up @@ -247,7 +253,11 @@ impl<State, Error> Route<State, Error> {
/// In addition, the following optional keys may be specified:
/// * `METHOD`: the method to use to dispatch the route (default `GET`)
/// * `DOC`: Markdown description of the route
pub fn new(name: String, spec: &toml::Value) -> Result<Self, RouteParseError> {
pub fn new(
name: String,
spec: &toml::Value,
meta: Arc<ApiMetadata>,
) -> Result<Self, RouteParseError> {
let paths: Vec<String> = spec["PATH"]
.as_array()
.ok_or(RouteParseError::MissingPathArray)?
Expand Down Expand Up @@ -310,6 +320,7 @@ impl<State, Error> Route<State, Error> {
Some(doc) => markdown::to_html(doc.as_str().context(IncorrectDocTypeSnafu)?),
None => String::new(),
},
meta,
})
}

Expand Down Expand Up @@ -363,31 +374,32 @@ impl<State, Error> Route<State, Error> {
patterns: self.patterns,
params: self.params,
doc: self.doc,
meta: self.meta,
}
}

/// Compose an HTML fragment documenting all the variations on this route.
pub fn documentation(&self, meta: &ApiMetadata) -> Html {
pub fn documentation(&self) -> Html {
html! {
(PreEscaped(meta.heading_entry
(PreEscaped(self.meta.heading_entry
.replace("{{METHOD}}", &self.method().to_string())
.replace("{{NAME}}", &self.name())))
(PreEscaped(&meta.heading_routes))
(PreEscaped(&self.meta.heading_routes))
@for path in self.patterns() {
(PreEscaped(meta.route_path.replace("{{PATH}}", &format!("/{}/{}", meta.name, path))))
(PreEscaped(self.meta.route_path.replace("{{PATH}}", &format!("/{}/{}", self.meta.name, path))))
}
(PreEscaped(&meta.heading_parameters))
(PreEscaped(&meta.parameter_table_open))
(PreEscaped(&self.meta.heading_parameters))
(PreEscaped(&self.meta.parameter_table_open))
@for param in self.params() {
(PreEscaped(meta.parameter_row
(PreEscaped(self.meta.parameter_row
.replace("{{NAME}}", &param.name)
.replace("{{TYPE}}", &param.param_type.to_string())))
}
@if self.params().is_empty() {
(PreEscaped(&meta.parameter_none))
(PreEscaped(&self.meta.parameter_none))
}
(PreEscaped(&meta.parameter_table_close))
(PreEscaped(&meta.heading_description))
(PreEscaped(&self.meta.parameter_table_close))
(PreEscaped(&self.meta.heading_description))
(PreEscaped(&self.doc))
}
}
Expand Down Expand Up @@ -435,12 +447,8 @@ impl<State, Error> Route<State, Error> {

/// Print documentation about the route, to aid the developer when the route is not yet
/// implemented.
pub(crate) fn default_handler(
&self,
_req: RequestParams,
_state: &State,
) -> Result<tide::Response, RouteError<Error>> {
unimplemented!()
pub(crate) fn default_handler(&self) -> Result<tide::Response, RouteError<Error>> {
Ok(self.documentation().into())
}

pub(crate) async fn handle_socket(
Expand Down Expand Up @@ -480,7 +488,7 @@ where
match &self.handler {
RouteImplementation::Http { handler, .. } => match handler {
Some(handler) => handler.handle(req, state).await,
None => self.default_handler(req, state),
None => self.default_handler(),
},
RouteImplementation::Socket { .. } => Err(RouteError::IncorrectMethod {
expected: self.method(),
Expand Down

0 comments on commit 6feaf5f

Please sign in to comment.