Web frameworks
Rama extractors and a JSON validation layer for vld schemas.

Extractors for Rama 0.4 WebService handlers, plus a ValidateJsonLayer for proxy and gateway service stacks. Shared rules (status codes, error body, coercion, headers and cookies) are on the web adapters overview.

Install

Cargo.toml
[dependencies]
vld = "0.5"
vld-rama = "0.5"
rama = { version = "0.4", features = ["http"] }

The crate has no features of its own; it pulls rama with http only. MSRV 1.96 (Rama's own), see MSRV.

Example

use rama::http::service::web::WebService;
use vld_rama::{VldJson, VldPath, VldQuery};

vld::schema! {
    #[derive(Debug)]
    pub struct CreateUser {
        pub name: String => vld::string().min(2).max(50),
        pub email: String => vld::string().email(),
    }
}

vld::schema! {
    #[derive(Debug)]
    pub struct Search {
        pub q: String => vld::string().min(1),
        pub page: Option<i64> => vld::number().int().min(1).optional(),
    }
}

vld::schema! {
    #[derive(Debug)]
    pub struct UserPath {
        pub id: i64 => vld::number().int().min(1),
    }
}

async fn create(VldJson(user): VldJson<CreateUser>) -> String {
    format!("created {}", user.name)
}

async fn search(VldQuery(q): VldQuery<Search>) -> String {
    format!("q={} page={}", q.q, q.page.unwrap_or(1))
}

async fn get_user(VldPath(p): VldPath<UserPath>) -> String {
    format!("user #{}", p.id)
}

let svc = WebService::default()
    .with_post("/users", create)
    .with_get("/search", search)
    .with_get("/users/{id}", get_user);

As a layer

ValidateJsonLayer<T> validates JSON bodies before the inner service runs and stores the value as Validated<T> in the request extensions. Read it with validated::<T>(&req):

use rama::http::{Body, Request, Response};
use rama::{Layer, Service};
use vld_rama::{validated, ValidateJsonLayer};

#[derive(Clone)]
struct Backend;

impl Service<Request> for Backend {
    type Output = Response;
    type Error = std::convert::Infallible;

    async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
        // None for non-JSON requests: the layer lets them through unread.
        let name = validated::<CreateUser>(&req).map_or("-".into(), |u| u.name);
        Ok(Response::new(Body::from(name)))
    }
}

let gateway = ValidateJsonLayer::<CreateUser>::new().layer(Backend);

CreateUser must be Clone + Debug + Send + Sync + 'static for the layer.

API

ItemPurpose
VldJson<T>JSON body (FromRequest / FromRequestBody); 415 without a JSON Content-Type
VldQuery<T>Query string, coerced
VldPath<T>Route parameters (/users/{id}), coerced; {} on a route without parameters
VldForm<T>application/x-www-form-urlencoded body (never the query string)
VldHeaders<T>Request headers
VldCookie<T>Cookies from every Cookie header
ValidateJsonLayer<T>Layer; new() (2 MiB) or with_limit(bytes)
ValidateJsonService<S, T>The service the layer produces
Validated<T>Extension wrapper holding the validated value
validated::<T>(&req)Option<T> from the extensions
VldBodyLimit(usize)Request extension that sets the extractor body limit
VldRejectionRejection: status(), body(), error(); implements IntoResponse
preludeAll of the above plus vld::prelude::*

All extractors deref to T.

Body limit

Extractors read at most 2 MiB. Set another limit per router with the VldBodyLimit extension:

use rama::layer::AddInputExtensionLayer;
use rama::Layer;
use vld_rama::VldBodyLimit;

let svc = AddInputExtensionLayer::new(VldBodyLimit(64 * 1024)).into_layer(
    WebService::default().with_post("/users", create),
);

The layer takes its own limit: ValidateJsonLayer::<T>::with_limit(64 * 1024).

Gotchas

  • validated() returns Option<T>.
  • The layer passes requests without a JSON Content-Type through without reading the body, so a handler behind it must handle None.
  • An over-limit body is 413 only when the error is Rama's LengthLimitError; any other body read error is a 422.