Skip to content

Commit

Permalink
Add support for wildcard routes: /foo/bar/{rest:.*} (#110)
Browse files Browse the repository at this point in the history
* Add support for wildcard routes: `/foo/bar/{rest:.*}`
This will match paths such as `/foo/bar` and `/foo/bar/baz.css`

We can use this to serve static assets such as css, html, and image
files.

Express uses this syntax: `/foo/bar/:rest(.*)`
Dropwizard / Jersey / JAX-RS does: `/foo/bar/{rest:.*}`
Actix does: `/foo/bar/{rest:.*}`

This also adds support for a tag (unpublished = true) in the #[endpoint]
macro to omit certain endpoints from the OpenAPI output. This is both
useful for non-API endpoints and necessary in that OpenAPI doesn't
support any type of multi-segment matching of routes.
  • Loading branch information
ahl authored Jul 1, 2021
1 parent feea258 commit 41060ad
Show file tree
Hide file tree
Showing 13 changed files with 1,006 additions and 491 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ https://github.com/oxidecomputer/dropshot/compare/v0.5.1\...HEAD[Full list of co

* https://github.com/oxidecomputer/dropshot/pull/105[#105] When generating an OpenAPI spec, Dropshot now uses references rather than inline schemas to represent request and response bodies.
* https://github.com/oxidecomputer/dropshot/pull/103[#103] When the Dropshot server is dropped before having been shut down, Dropshot now attempts to gracefully shut down rather than panic.
* https://github.com/oxidecomputer/dropshot/pull/110[#110] Wildcard paths are now supported. Consumers may take over routing (e.g. for file serving) by annotating a path component: `/static/{path:.*}`. The `path` member should then be of type `Vec<String>` and it will be filled in with all path components following `/static/`.

=== Breaking Changes

Expand Down
1 change: 1 addition & 0 deletions dropshot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ slog-async = "2.4.0"
slog-bunyan = "2.2.0"
slog-json = "2.3.0"
slog-term = "2.5.0"
syn = "1.0.73"
toml = "0.5.6"

[dependencies.chrono]
Expand Down
97 changes: 97 additions & 0 deletions dropshot/examples/index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2021 Oxide Computer Company
/*!
* Example use of Dropshot for matching wildcard paths to serve static content.
*/

use dropshot::ApiDescription;
use dropshot::ConfigDropshot;
use dropshot::ConfigLogging;
use dropshot::ConfigLoggingLevel;
use dropshot::HttpError;
use dropshot::HttpServerStarter;
use dropshot::RequestContext;
use dropshot::{endpoint, Path};
use http::{Response, StatusCode};
use hyper::Body;
use schemars::JsonSchema;
use serde::Deserialize;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), String> {
/*
* We must specify a configuration with a bind address. We'll use 127.0.0.1
* since it's available and won't expose this server outside the host. We
* request port 0, which allows the operating system to pick any available
* port.
*/
let config_dropshot: ConfigDropshot = Default::default();

/*
* For simplicity, we'll configure an "info"-level logger that writes to
* stderr assuming that it's a terminal.
*/
let config_logging = ConfigLogging::StderrTerminal {
level: ConfigLoggingLevel::Info,
};
let log = config_logging
.to_logger("example-basic")
.map_err(|error| format!("failed to create logger: {}", error))?;

/*
* Build a description of the API.
*/
let mut api = ApiDescription::new();
api.register(index).unwrap();

/*
* Set up the server.
*/
let server = HttpServerStarter::new(&config_dropshot, api, (), &log)
.map_err(|error| format!("failed to create server: {}", error))?
.start();

/*
* Wait for the server to stop. Note that there's not any code to shut down
* this server, so we should never get past this point.
*/
server.await
}

#[derive(Deserialize, JsonSchema)]
struct AllPath {
path: Vec<String>,
}

/**
* Return static content.for all paths.
*/
#[endpoint {
method = GET,
/*
* Match literally every path including the empty path.
*/
path = "/{path:.*}",
/*
* This isn't an API so we don't want this to appear in the OpenAPI
* description if we were to generate it.
*/
unpublished = true,
}]
async fn index(
_rqctx: Arc<RequestContext<()>>,
path: Path<AllPath>,
) -> Result<Response<Body>, HttpError> {
Ok(Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, "text/html")
.body(
format!(
"<HTML><HEAD>nothing at {:?}</HEAD></HTML>",
path.into_inner().path
)
.into(),
)?)
}
17 changes: 14 additions & 3 deletions dropshot/src/api_description.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Oxide Computer Company
// Copyright 2021 Oxide Computer Company
/*!
* Describes the endpoints and handler functions in your API
*/
Expand Down Expand Up @@ -36,6 +36,7 @@ pub struct ApiEndpoint<Context: ServerContext> {
pub description: Option<String>,
pub tags: Vec<String>,
pub paginated: bool,
pub visible: bool,
}

impl<'a, Context: ServerContext> ApiEndpoint<Context> {
Expand All @@ -61,6 +62,7 @@ impl<'a, Context: ServerContext> ApiEndpoint<Context> {
description: None,
tags: vec![],
paginated: func_parameters.paginated,
visible: true,
}
}

Expand All @@ -73,6 +75,11 @@ impl<'a, Context: ServerContext> ApiEndpoint<Context> {
self.tags.push(tag.to_string());
self
}

pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
}

/**
Expand Down Expand Up @@ -230,8 +237,9 @@ impl<Context: ServerContext> ApiDescription<Context> {
let path = route_path_to_segments(&e.path)
.iter()
.filter_map(|segment| match PathSegment::from(segment) {
PathSegment::Varname(v) => Some(v),
_ => None,
PathSegment::VarnameSegment(v) => Some(v),
PathSegment::VarnameWildcard(v) => Some(v),
PathSegment::Literal(_) => None,
})
.collect::<HashSet<_>>();
let vars = e
Expand Down Expand Up @@ -380,6 +388,9 @@ impl<Context: ServerContext> ApiDescription<Context> {
indexmap::IndexMap::<String, schemars::schema::Schema>::new();

for (path, method, endpoint) in &self.router {
if !endpoint.visible {
continue;
}
let path = openapi.paths.entry(path).or_insert(
openapiv3::ReferenceOr::Item(openapiv3::PathItem::default()),
);
Expand Down
Loading

0 comments on commit 41060ad

Please sign in to comment.