Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handler APIs in a number of places #27

Merged
merged 7 commits into from
Nov 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions examples/static-content/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ fn main() -> Result<(), Box<dyn Error>> {
// Serve the "/" route with the specified file
.with_route("/", handlers::serve_file("./static/pages/index.html"))
// Serve the "/img/*" route with files stored in the "./static/images" directory.
// Strip the "/img/" prefix from the request URI before it is concatenated with the directory path.
// For example, "/img/ferris.png" would go to "ferris.png" and then to "./static/images/ferris.png".
.with_route("/img/*", handlers::serve_dir("./static/images", "/img/"))
.with_path_aware_route("/img/*", handlers::serve_dir("./static/images"))
// Serve a regular file path in the current directory.
// This means simply appending the request URI to the directory path and looking for a file there.
// This is equivalent to `serve_dir` with a strip prefix value of `""`.
.with_route("/src/*", handlers::serve_as_file_path("."));
.with_route("/src/*", handlers::serve_as_file_path("."))
// Redirect requests to "/ferris" to "/img/ferris.png"
.with_route("/ferris", handlers::redirect("/img/ferris.png"));

app.run("0.0.0.0:80")?;

Expand Down
3 changes: 3 additions & 0 deletions examples/static-content/static/pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ <h3>Code for this example (served from "/src/main.rs"):</h3>

<h3>Secret Page (index file example)</h3>
<a href="/img/secret_page">Click me</a>

<h3>Ferris Image Redirect (redirect example)</h3>
<a href="/ferris">Click me</a>
</body>

</html>
73 changes: 70 additions & 3 deletions humphrey/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub type ConnectionCondition<State> = fn(&mut TcpStream, Arc<State>) -> bool;
pub type WebsocketHandler<State> = fn(Request, TcpStream, Arc<State>);

/// Represents a function able to handle a request.
/// It is passed a the request as well as the app's state, and must return a response.
/// It is passed the request as well as the app's state, and must return a response.
///
/// ## Example
/// The most basic request handler would be as follows:
Expand All @@ -58,6 +58,37 @@ pub type WebsocketHandler<State> = fn(Request, TcpStream, Arc<State>);
pub trait RequestHandler<State>: Fn(Request, Arc<State>) -> Response + Send + Sync {}
impl<T, S> RequestHandler<S> for T where T: Fn(Request, Arc<S>) -> Response + Send + Sync {}

/// Represents a function able to handle a request.
/// It is passed only the request, and must return a response.
/// If you want access to the app's state, consider using the `RequestHandler` trait instead.
///
/// ## Example
/// The most basic stateless request handler would be as follows:
/// ```
/// fn handler(request: Request) -> Response {
/// Response::new(StatusCode::OK, b"Success", &request)
/// }
/// ```
pub trait StatelessRequestHandler<State>: Fn(Request) -> Response + Send + Sync {}
impl<T, S> StatelessRequestHandler<S> for T where T: Fn(Request) -> Response + Send + Sync {}

/// Represents a function able to handle a request with respect to the route it was called from.
/// It is passed the request, the app's state, and the route it was called from, and must return a response.
///
/// ## Example
/// The most basic path-aware request handler would be as follows:
/// ```
/// fn handler(request: Request, _: Arc<()>, route: &str) -> Response {
/// Response::new(StatusCode::OK, format!("Success matching route {}", route), &request)
/// }
/// ```
#[rustfmt::skip]
pub trait PathAwareRequestHandler<State>:
Fn(Request, Arc<State>, &str) -> Response + Send + Sync {}
#[rustfmt::skip]
impl<T, S> PathAwareRequestHandler<S> for T where
T: Fn(Request, Arc<S>, &str) -> Response + Send + Sync {}

/// Represents a function able to handle an error.
/// The first parameter of type `Option<Request>` will be `Some` if the request could be parsed.
/// Otherwise, it will be `None` and the status code will be `StatusCode::BadRequest`.
Expand Down Expand Up @@ -188,6 +219,42 @@ where
self
}

/// Adds a route and associated handler to the server.
/// Does not pass the state to the handler.
/// Routes can include wildcards, for example `/blog/*`.
///
/// If you want to access the app's state in the handler, consider using `with_route`.
///
/// ## Panics
/// This function will panic if the route string cannot be converted to a `Uri` object.
pub fn with_stateless_route<T>(mut self, route: &str, handler: T) -> Self
where
T: StatelessRequestHandler<State> + 'static,
{
self.routes.push(RouteHandler {
route: route.parse().unwrap(),
handler: Box::new(move |request, _| handler(request)),
});
self
}

/// Adds a path-aware route and associated handler to the server.
/// Routes can include wildcards, for example `/blog/*`.
/// Will also pass the route to the handler at runtime.
///
/// ## Panics
/// This function will panic if the route string cannot be converted to a `Uri` object.
pub fn with_path_aware_route<T>(mut self, route: &'static str, handler: T) -> Self
where
T: PathAwareRequestHandler<State> + 'static,
{
self.routes.push(RouteHandler {
route: route.parse().unwrap(),
handler: Box::new(move |request, state| handler(request, state, route)),
});
self
}

/// Sets the error handler for the server.
pub fn with_error_handler(mut self, handler: ErrorHandler) -> Self {
self.error_handler = handler;
Expand Down Expand Up @@ -217,8 +284,8 @@ where

/// Gets a reference to the app's state.
/// This should only be used in the main thread, as the state is passed to request handlers otherwise.
pub fn get_state(&self) -> &Arc<State> {
&self.state
pub fn get_state(&self) -> Arc<State> {
self.state.clone()
}
}

Expand Down
34 changes: 14 additions & 20 deletions humphrey/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ pub fn serve_file<T>(file_path: &'static str) -> impl Fn(Request, Arc<T>) -> Res
/// - directory path of `./static` will serve files from the static directory but with their whole URI,
/// for example a request to `/images/ferris.png` will map to the file `./static/images/ferris.png`.
///
/// This is **not** equivalent to `serve_dir` with a strip prefix value of `""`, as this does not respect
/// index files within nested directories.
/// This is **not** equivalent to `serve_dir`, as `serve_dir` respects index files within nested directories.
pub fn serve_as_file_path<T>(directory_path: &'static str) -> impl Fn(Request, Arc<T>) -> Response {
move |request: Request, _| {
let directory_path = directory_path.strip_suffix('/').unwrap_or(directory_path);
Expand All @@ -49,30 +48,20 @@ pub fn serve_as_file_path<T>(directory_path: &'static str) -> impl Fn(Request, A
}
}

/// Serves a directory of files, stripping the given prefix from the request URI before concatenating it with
/// the given directory path.
/// Serves a directory of files.
///
/// Respects index files with the following rules:
/// - requests to `/directory` will return either the file `directory`, 301 redirect to `/directory/` if it is a directory, or return 404
/// - requests to `/directory/` will return either the file `/directory/index.html` or `/directory/index.htm`, or return 404
///
/// ## Example
/// For example, if you want to serve files from a `./static/images` directory to the `/img` route of your server,
/// you would use `serve_dir` with a directory path of `./static/images` and a strip prefix value of `/img/` at
/// a route called `/img/*`. This would remove the `/img/` prefix from the request URI before concatenating it
/// with the directory path `./static/images`.
pub fn serve_dir<T>(
directory_path: &'static str,
strip_prefix: &'static str,
) -> impl Fn(Request, Arc<T>) -> Response {
move |request: Request, _| {
let strip_prefix_without_slash = strip_prefix.strip_prefix('/').unwrap_or(&request.uri);

let stripped_prefix = request.uri[1..]
.strip_prefix(strip_prefix_without_slash)
pub fn serve_dir<T>(directory_path: &'static str) -> impl Fn(Request, Arc<T>, &str) -> Response {
move |request: Request, _, route| {
let route_without_wildcard = route.strip_suffix('*').unwrap_or(route);
let uri_without_route = request
.uri
.strip_prefix(route_without_wildcard)
.unwrap_or(&request.uri);

let located = try_find_path(directory_path, stripped_prefix, &INDEX_FILES);
let located = try_find_path(directory_path, uri_without_route, &INDEX_FILES);

if let Some(located) = located {
match located {
Expand All @@ -96,3 +85,8 @@ pub fn serve_dir<T>(
}
}
}

/// Redirects requests to the given location with status code 301.
pub fn redirect<T>(location: &'static str) -> impl Fn(Request, Arc<T>) -> Response {
move |request: Request, _| Response::redirect(location, &request)
}
11 changes: 11 additions & 0 deletions humphrey/src/http/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ impl Response {
}
}

/// Creates a redirect response to the given location.
pub fn redirect<T>(location: T, request: &Request) -> Self
where
T: AsRef<str>,
{
Self::empty(StatusCode::MovedPermanently)
.with_header(ResponseHeader::Location, location.as_ref().to_string())
.with_request_compatibility(request)
.with_generated_headers()
}

/// Adds the given header to the response.
/// Returns itself for use in a builder pattern.
pub fn with_header(mut self, header: ResponseHeader, value: String) -> Self {
Expand Down