Skip to content

Commit

Permalink
Add support for wildcard routes: /foo/bar/{rest:.*}
Browse files Browse the repository at this point in the history
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 committed May 28, 2021
1 parent bdbda2f commit f4eeb34
Show file tree
Hide file tree
Showing 5 changed files with 541 additions and 97 deletions.
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: 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 = 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::VarnameRegEx(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 f4eeb34

Please sign in to comment.