Crate tide_disco

Crate tide_disco 

Source
Expand description

Tide Disco is a web server framework with built-in discoverability support for Tide

§Overview

We say a system is discoverable if guesses and mistakes regarding usage are rewarded with relevant documentation and assistance at producing correct requests. To offer this capability in a practical way, it is helpful to specify the API in data files, rather than code, so that all relevant text can be edited in one concise readable specification.

Tide Disco leverages TOML to specify

  • Routes with typed parameters
  • Route documentation
  • Route error messages
  • General documentation

§Goals

  • Context-sensitive help
  • Spelling suggestions
  • Reference documentation assembled from route documentation
  • Forms and other user interfaces to aid in the construction of correct inputs
  • Localization
  • Novice and expert help
  • Flexible route parsing, e.g. named parameters rather than positional parameters
  • API fuzz testing automation based on parameter types

§Future work

  • WebSocket support
  • Runtime control over logging

§Getting started

A Tide Disco app is composed of one or more API modules. An API module consists of a TOML specification and a set of route handlers – Rust functions – to provide the behavior of the routes defined in the TOML. You can learn the format of the TOML file by looking at the examples in this crate. Once you have it, you can load it into an API description using Api::new:

use tide_disco::Api;
use tide_disco::error::ServerError;
use vbs::version::StaticVersion;

type State = ();
type Error = ServerError;
type StaticVer01 = StaticVersion<0, 1>;

let spec: toml::Value = toml::from_str(
    std::str::from_utf8(&std::fs::read("/path/to/api.toml").unwrap()).unwrap(),
).unwrap();
let mut api = Api::<State, Error, StaticVer01>::new(spec)?;

Once you have an Api, you can define route handlers for any routes in your TOML specification. Suppose you have the following route definition:

[route.hello]
PATH = ["hello"]
METHOD = "GET"

Register a handler for it like this:

use futures::FutureExt;

api.get("hello", |req, state| async move { Ok("Hello, world!") }.boxed())?;

See the API reference for more details on what you can do to create an Api.

Once you have registered all of your route handlers, you need to register your Api module with an App:

use tide_disco::App;
use vbs::version::{StaticVersion, StaticVersionType};

type StaticVer01 = StaticVersion<0, 1>;

let mut app = App::<State, Error>::with_state(());
app.register_module("api", api);
app.serve("http://localhost:8080", StaticVer01::instance()).await;

Then you can use your application:

curl http://localhost:8080/api/hello

§Boxed futures

As a web server framework, Tide Disco naturally includes many interfaces that take functions as arguments. For example, route handlers are registered by passing a handler function to an Api object. Also naturally, many of these function parameters are async, which of course just means that they are regular functions returning some type F that implements the Future trait. This is all perfectly usual, but throughout the interfaces in this crate, you may notice something that is a bit unusual: many of these functions are required to return not just any Future, but a BoxFuture. This is due to a limitation that currently exists in the Rust compiler.

The problem arises with functions where the returned future is not 'static, but rather borrows from the function parameters. Consider the following route definition, for example:

type State = RwLock<u64>;
type Error = ();

api.at("someroute", |_req, state: &State| async {
    Ok(*state.read().await)
})

The async block in the route handler uses the state reference, so the resulting future is only valid for as long as the reference state is valid. We could write the signature of the route handler like this:

use futures::Future;
use tide_disco::RequestParams;

type State = async_std::sync::RwLock<u64>;
type Error = ();

fn handler<'a>(
    req: RequestParams,
    state: &'a State,
) -> impl 'a + Future<Output = Result<u64, Error>> {
    // ...
}

Notice how we explicitly constrain the future type by the lifetime 'a using impl syntax. Unfortunately, while we can write a function signature like this, we cannot write a type bound that uses the Fn trait and represents the equivalent function signature. This is a problem, since interfaces like at would like to consume any function-like object which implements Fn, not just static function pointers. Here is what we would like to write:

impl<State, Error, VER: StaticVersionType> Api<State, Error, VER> {
    pub fn at<F, T>(&mut self, route: &str, handler: F)
    where
        F: for<'a> Fn<(RequestParams, &'a State)>,
        for<'a> <F as Fn<(RequestParams, &'a State)>>::Output:
            'a + Future<Output = Result<T, Error>>,
    {...}
}

Here we are using a higher-rank trait bound on the associated type Output of the Fn implementation for F in order to constrain the future by the lifetime 'a, which is the lifetime of the State reference. It is actually possible to write this function signature today in unstable Rust (using the raw Fn trait as a bound is unstable), but even then, no associated type will be able to implement the HRTB due to a bug in the compiler. This limitation is described in detail in this post.

As a workaround until this is fixed, we require the function F to return a concrete future type with an explicit lifetime parameter: BoxFuture. This allows us to specify the lifetime constraint within the HRTB on F itself, rather than resorting to a separate HRTB on the associated type Output in order to be able to name the return type of F. Here is the actual (partial) signature of at:

impl<State, Error, VER: StaticVersionType> Api<State, Error, VER> {
    pub fn at<F, T>(&mut self, route: &str, handler: F)
    where
        F: for<'a> Fn(RequestParams, &'a State) -> BoxFuture<'a, Result<T, Error>>,
    {...}
}

What this means for your code is that functions you pass to the Tide Disco framework must return a boxed future. When passing a closure, you can simply add .boxed() to your async block, like this:

use async_std::sync::RwLock;
use futures::FutureExt;
use tide_disco::Api;
use vbs::version::StaticVersion;

type State = RwLock<u64>;
type Error = ();

type StaticVer01 = StaticVersion<0, 1>;

fn define_routes(api: &mut Api<State, Error, StaticVer01>) {
    api.at("someroute", |_req, state: &State| async {
        Ok(*state.read().await)
    }.boxed());
}

This also means that you cannot pass the name of an async fn directly, since async functions declared with the async fn syntax do not return a boxed future. Instead, you can wrap the function in a closure:

use async_std::sync::RwLock;
use futures::FutureExt;
use tide_disco::{Api, RequestParams};
use vbs::version::StaticVersion;

type State = RwLock<u64>;
type Error = ();
type StaticVer01 = StaticVersion<0, 1>;

async fn handler(_req: RequestParams, state: &State) -> Result<u64, Error> {
    Ok(*state.read().await)
}

fn register(api: &mut Api<State, Error, StaticVer01>) {
    api.at("someroute", |req, state: &State| handler(req, state).boxed());
}

In the future, we may create an attribute macro which can rewrite an async fn to return a boxed future directly, like

#[boxed_future]
async fn handler(_req: RequestParams, state: &State) -> Result<u64, Error> {
    Ok(*state.read().await)
}

Re-exports§

pub use api::Api;
pub use app::App;
pub use error::Error;
pub use method::Method;
pub use request::RequestError;
pub use request::RequestParam;
pub use request::RequestParamType;
pub use request::RequestParamValue;
pub use request::RequestParams;
pub use status::StatusCode;
pub use tide::http;

Modules§

api
app
error
healthcheck
listener
method
Interfaces for methods of accessing to state.
metrics
Support for routes using the Prometheus metrics format.
request
socket
An interface for asynchronous communication with clients, using WebSockets.
status
testing

Macros§

join

Structs§

DiscoArgs
ServerState
Url
A parsed URL record.

Enums§

DiscoKey
Configuration keys for Tide Disco settings
HealthStatus
UrlSegment

Constants§

SERVER_STARTUP_RETRIES
Number of times to poll before failing
SERVER_STARTUP_SLEEP_MS
Number of milliseconds to sleep between attempts

Functions§

app_api_path
check_api
Check api.toml for schema compliance errors
compose_config_path
Compose the path to the application’s configuration file
compose_settings
Get the application configuration
configure_router
Add routes from api.toml to the routefinder instance in tide-disco
get_api_path
Get the path to api.toml
healthcheck
Return a JSON expression with status 200 indicating the server is up and running. The JSON expression is normally {“status”: “Available”} When the server is running but unable to process requests normally, a response with status 503 and payload {“status”: “unavailable”} should be added.
init_logging
load_api
Load the web API or panic
org_data_path
wait_for_server
Wait for the server to respond to a connection request

Type Aliases§

AppServerState
AppState
Html