刚发现,大家围观一下这个小框架,感觉不错的。
Vial is a small web framework for making small web "sites" in Rust. It includes just a handful of basic features for delivering old school, server-side rendered HTML: request routing, form data parsing, response building, and serving static file assets.
The goal is a small, lean core that compiles quickly and has as few dependencies as possible. Use it for HTML stuff: prototyping ideas, testing out concepts, or, perhaps, even writing tiny personal apps. Nothing serious though, got it?
This manual is an overview of Vial’s built-in features, as well as the few optional features you can enable. It also includes suggestions for some "common tasks", like using a database to store information.
Here's the bare minimum:
vial::routes! {
GET "/" => |_| "Greetings, creature.";
}
fn main() {
vial::run!();
}
That should tell you a lot, in that there isn't a lot to Vial.
Now here's a bigger bear, showing off more of Vial's features:
use vial::prelude::*;
routes! {
GET "/" => hello_world;
POST "/" => redirect_to_greeting;
GET "/:name" => hello_name;
GET "/*path" => |req|
Response::from(404).with_body(
format!("<h1>404 Not Found: {}</h1>",
req.arg("path").unwrap_or("")));
}
fn hello_world(_req: Request) -> &'static str {
"<h1>Hello, world!</h1>
<p><strong>What's your name?</strong></p>
<form method='POST' action='/'>
<p><input name='name' type='text'/></p>
<p><input type='submit'/></p>
</form>"
}
fn redirect_to_greeting(req: Request) -> Option<impl Responder> {
let name = req.form("name")?;
Some(Response::redirect_to(format!("/{}", name)))
}
fn hello_name(req: Request) -> String {
format!(
"<h1>Why hello there, {}!</h1>",
req.arg("name").unwrap()
)
}
fn main() {
run!().unwrap();
}
You can run the above example from the root of this repository:
$ cargo run --example manual
Vial comes with a handful of examples in the examples/
directory, so be sure to peruse them skeptically - either alongside
or after digesting this manual.
Like most web library thingy-jingies that only focus on server-side rendering, there are three main parts to a Vial application:
Routing: You write actions that take a Request and return either a Response or a Responder, then map them to URLs and URL patterns using the vial::routes! macro.
[Requests]: The Request object provides information about each client's humble request.
[Responses]: Your actions return either a Response struct,
which can be easily built, or a type that implements the Responder
trait, like String
or Option<Response>
.
Vial should work on any recent, stable version of Rust on Linux or macOS.
To begin, add Vial to your project's Cargo.toml
:
[dependencies]
vial = "0.1"
Now all you have to do is call vial::routes!
to define your routes
and vial::run!
to start the server in src/main.rs
:
vial::routes! {
GET "/" => |_| "It works!";
}
fn main() {
vial::run!();
}
This should start a server at http://0.0.0.0:7667 and tell you that it did. Congratulations! You're on your way.
Routing is the real gravy and potatoes of any web framework, if you think about it. In Vial, routes are defined with the vial::routes! macro in this format:
HTTP_METHOD ROUTE_PATTERN => ACTION;
The order in which routes are written matters - routes written first
will be checked for matches first, meaning you can declare many routes
that point to "/"
, but only the first one defined will ever match.
HTTP_METHOD
can be one of:
GET
HEAD
POST
PUT
DELETE
PATCH
ROUTE_PATTERN
can be an exact match, such as "/user"
or
"/v2/search.php3"
, or it can include a named parameter:
"/:name"
— This will match anything except paths with /
or .
in them."/:name.md"
— Use this format to match on a specific file extension."/*name"
— This will match everything, including /
and .
In the three examples above, calling request.arg("name")
in an
Action will return Some(&str)
.
Note that you can have multiple parameters in the same route, as long as the "match all" pattern occurs last:
vial::routes! {
GET "/:category/:id/*name" => |req| format!(
"<p>Category: {}</p>
<p>ID: {}</p>
<p>Name: {}</p>",
req.arg("category").unwrap_or("None"),
req.arg("id").unwrap_or("None"),
req.arg("name").unwrap_or("None"),
);
}
fn main() {
vial::run!();
}
Actions are what routes actually route to. They are functions or closures take a Request and return either a Response or something that implements the Responder trait:
use vial::prelude::*;
routes! {
GET "/info" => |req| format!(
"<p>Name: {}</p>", req.query("name").unwrap_or("None")
);
GET "/" => index;
}
fn index(req: Request) -> &'static str {
"<form method='GET'>
<p>Enter your name: <input type='text' name='name'/></p>
<input type='submit'/>
</form>"
}
fn main() {
run!();
}
Returning impl Responder
is easy - Responder is a Vial trait
that defines a single conversion method that returns a Response:
pub trait Responder {
fn to_response(self) -> Response;
}
These types implement Responder
by default:
&str
String
usize
- Empty response with this number as the status code.Option<impl Responder>
- 404 on None
Result<impl Responder, Error>
- 500 on ErrorFilters are functions that are run before actions. They can either
modify the existing request before it's sent to your action, or they
can return a Response
that will be delivered to the client without any
actions being called:
fn(req: &mut Request) -> Option<Response>;
Like Rust's attributes, they can either apply to all routes defined in
the same vial::routes!
macro call or just a specific route:
use std::sync::atomic::{AtomicUsize, Ordering};
use vial::prelude::*;
routes! {
// `count` will run before all routes in this block
#![filter(count)]
GET "/" => |_| "Hey there!";
GET "/hits" => hits;
// `count` will run again when /double is visited
#[filter(count)]
GET "/double" => double;
// `echo` will be called when /echo is visited
#[filter(echo)]
GET "/echo" => |_| "Is there an echo in here?";
}
fn hits(req: Request) -> impl Responder {
format!("Hits: {}", req.counter().count())
}
fn double(req: Request) -> impl Responder {
"Double trouble."
}
fn echo(req: &mut Request) -> Option<Response> {
println!("{:#?}", req);
None
}
fn count(req: &mut Request) -> Option<Response> {
req.counter().incr();
None
}
#[derive(Debug, Default)]
struct Counter(AtomicUsize);
impl Counter {
fn count(&self) -> String {
self.0.load(Ordering::Relaxed).to_string()
}
fn incr(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
trait WithCounter {
fn counter(&self) -> &Counter;
}
impl WithCounter for Request {
fn counter(&self) -> &Counter {
self.state::<Counter>()
}
}
fn main() {
use_state!(Counter::default());
run!().unwrap();
}
Routes can be defined in different modules and combined together with
vial::run!
:
mod blog;
mod wiki {
vial::routes! {
GET "/wiki" => |_| "This is the wiki.";
}
}
vial::routes! {
GET "/" => |_| "Index page.";
}
fn main() {
vial::run!(self, blog, wiki);
}
When a route matches and an Action is called, it's passed a
Request object. Request
contains information about
the request itself, as well as a number of helper methods.
As mentioned in the Routing section above, you can define parameters
in a route and access their value for a given request using
request.arg()
:
vial::routes! {
GET "/:animal" => |req| format!(
"Animal: {}", req.arg("animal").unwrap_or("None")
);
}
In addition to route parameters, Vial will also parse good ol' fashioned query string parameters for you:
vial::routes! {
GET "/info" => |req| format!(
"Version: v{}",
req.query("version").unwrap_or("?")
);
}
fn main() {
vial::run!();
}
Running this and visiting /info
will show:
Version: v?
But visiting /info?version=1.0
will show:
Version: v1.0
Like arg()
, query()
returns Option<&str>
.
What's the web without open ended <textarea>s
? Perish the thought.
POSTed form data follows the same pattern as query and route
parameters: use request.form()
to access a form parameter:
use vial::prelude::*;
use db;
routes! {
GET "/show/:id" => show;
GET "/new" => new;
POST "/new" => create;
}
fn new(_req: Request) -> impl Responder {
"<form method='POST'>
<p>Name: <input type='text' name='name'/></p>
<p>Location: <input type='text' name='location'/></p>
<p><input type='submit'/></p>
</form>"
}
fn create(req: Request) -> Result<impl Responder, io::Error> {
let id = db::insert!(
"name" => req.form("name").unwrap(),
"location" => req.form("location").unwrap()
)?;
Ok(Response::redirect_to(format!("/show/{}", id)))
}
fn show(req: Request) -> Option<impl Responder> {
let record = db::query!("id" => id).ok()?;
format!(
"<p>Name: {}</p>
<p>Location: {}</p>",
record.get("name").unwrap_or("None"),
record.get("location").unwrap_or("None"),
)
}
fn main() {
run!();
}
Headers are available without any of the peksy conveniences of type
safety. Just give request.header()
a string and hope you get one
back!
use vial::prelude::*;
use std::{fs, path::Path};
routes! {
GET "/:file" => show;
}
fn show(req: Request) -> Option<impl Responder> {
let path = format!("./{}", req.arg("file")?);
if Path::new(&path).exists() {
if req.header("Accept").unwrap_or("?").starts_with("text/plain") {
Some(Response::from_header("Content-Type", "text/plain")
.with_file(&path))
} else {
let file = fs::read_to_string(&path).unwrap();
Some(Response::from_body(format!(
"<html><body>
<pre style='width:50%;margin:0 auto'>{}</pre>
</body></html>", file)))
}
} else {
None
}
}
fn main() {
run!();
}
Header names are case insensitive, though, so at least you don't have to worry about that.
Beyond the headers, Request
also surfaces a few more basic bits of
information such as the request.method()
and request.path()
:
impl Request {
// "GET", "POST", etc. Always uppercase.
fn method(&self) -> &str;
// Always starts with "/"
fn path(&self) -> &str;
}
Every Action returns either a Response or a type that implements the Responder trait's single method:
pub trait Responder {
fn to_response(self) -> Response;
}
Common types like &str
and Option<String>
already implement this,
so you are free to be lazy and return simple types in your Actions.
If, however, you want to set headers and do other fancy jazz, you'll
need to build and return a Response directly.
Rather than use the "Builder" pattern like more mature and better designed libraries, Vial's Response lets you set properties either directly or using Builder-style methods:
vial::routes! {
GET "/404" => |_| Response::from(404).with_text("404 Not Found");
}
Each Response
defaults to a Content-Type
of text/html; charset=utf8
, so you can build HTML with your bare hands:
fn index(_req: Request) -> impl Responder {
Response::from("<marquee>Coming soon!</marquee>")
}
To produce plain text, set the header using with_header()
or
set_header()
, or use with_text()
instead of with_body()
or
from_body()
:
fn readme(_req: Request) -> Response {
// This will be rendered as plain text.
Response::from_file("README.md")
.with_header("Content-Type", "text/plain")
}
The Response documentation contains more information on all the methods available, but here are some of the properties you can set on a Response in your actions:
fn from_text<S: AsRef<str>>(text: S) -> Response;
fn with_code(mut self, code: usize) -> Response;
fn with_body<S: AsRef<str>>(mut self, body: S) -> Response;
fn with_text<S: AsRef<str>>(self, text: S) -> Response;
fn with_json<T: serde::Serialize>(self, value: T) -> Response;
(requires
the json_serde
feature)
fn with_reader(mut self, reader: Box<dyn io::Read>) -> Response;
fn with_asset(mut self, path: &str) -> Response;
fn with_file(mut self, path: &str) -> Response;
fn with_error<E: error::Error>(self, err: E) -> Response;
fn with_header(mut self, key: &str, value: &str) -> Response;
To issue a 302 redirect, use the redirect_to
static method:
fn search(req: Request) -> Option<impl Responder> {
let url = format!(
"https://en.wikipedia.org/wiki/Special:Search?search={}",
req.arg("search")?
)
Some(Response::redirect_to(url))
}
Empty responses with status codes can be created from usize
:
fn fourohno(_req: Request) -> impl Responder {
Response::from(404)
}
fn pay_me(_req: Request) -> impl Responder {
402
}
Headers can be set Builder-style using with_header
or
imperative-style using set_header
:
fn not_found(_req: Request) -> Response {
Response::from(404)
.with_header("Content-Type", "text/plain")
.with_body("404 Not Found")
}
fn download(req: Request) -> Option<impl Responder> {
Response::from_file(req.arg("file")?)
.with_header("Content-Type", "application/octet-stream")
}
fn perm_redirect(url: &str) -> Response {
Response::from(301).with_header("Location", url)
}
Vial can automatically serve static files out of an asset
directory, complete with proper ETag handling, if you tell it which
directory to use with the vial::asset_dir!
macro. It can also
optionally bundle those assets them into your application in
--release
mode, producing a single binary that was developed as if
it used separate CSS and JS files.
vial::asset_dir!
To get started, put all your .js
and .css
and other static
assets into a directory in the root of your project, such as assets/
.
In your HTML, include those files as if the "assets" directory were
the root of your application. So if you have "assets/app.js", in your
<script>
tag you would include just "/app.js".
Next call vial::asset_dir!() with the
path to your asset directory (maybe assets/
?) before starting
your application with vial::run!:
If we had a directory structure like this:
. ├── README.md ├── assets │ └── img │ ├── banker.png │ └── doctor.png └── src └── main.rs
We could serve our images like so:
vial::routes! {
GET "/" => |_| "
<p><img src='/img/doctor.png'/></p>
<p><img src='/img/banker.png'/></p>
";
}
fn main() {
vial::asset_dir!("assets/");
vial::run!().unwrap();
}
asset::methods()
By setting an asset directory, either through the
vial::asset_dir!() or
[vial::bundle_assets!()][bundle_assets api] macro, you can then use
the methods in the asset::
module to work with them:
asset::etag(): Get the ETag for an asset. Used automatically by the Router if a web request matches an asset's path.
asset::exists(): Does an asset exist? Works regardless of whether the asset is bundled or not.
asset::is_bundled(): Are assets
bundled? Only true in --release
mode and when used with the
vial::bundle_assets!()
macro.
asset::to_string(): Like
fs::read_to_string()
, delivers the content of an asset as a
String
.
asset::as_reader(): Like
asset::to_string()
but provides an io::Read
of an asset,
whether or not it's bundled.
Vial is meant to be small and swift, like a ninja star. Part of
that means Vial apps should be able to compile into standalone
binaries that don't rely on the filesystem. In order to accomplish
this, Vial can bundle your assets into your final --release
binary for you. As long as you the asset::()
API described above to
access them, all your code will work the same whether your assets are
bundled or not.
Combined with the ETag support, this means all assets will be reloaded by your browser whenever they're modified in dev mode for a smoother developmental experience.
To bundle your assets into your final binary in release mode, you must:
REMOVE any calls to the vial::asset_dir!()
macro.
Add vial
as to [build-dependencies]
in your Cargo.toml
:
[build-dependencies]
vial = "0.1"
(Yes, you should now have vial
in there twice. Once for
build-dependencies
and once for dependencies
. Only the one under
dependencies
needs to list any optional features you want to use,
however.)
build.rs
in the root of your project and call vial::bundle_assets!()
in it,
passing your asset directory as the sole argument:fn main() {
vial::bundle_assets!("assets/").unwrap();
}
️ Note: Bundling assets and setting an asset path using
vial::asset_dir!()
are mutually exclusive - you can't do both, as
enabling bundling will set the asset path for you. Therefor if you
are making the transition from using-assets-but-not-bundling to
using-assets-and-bundling-them, make sure to remove your call to vial::asset_dir!
.
Other than that, you're all set! Your application will now bundle your
assets in --release
mode and use the disk in debug and test mode.
All calls to functions in the assets module should work with the files in your asset directory. Add more and get to it!
There are two types of state available in Vial:
Local State - Built-in to Request. Allows caching of expensive algorithms (like DB lookups) on a per-request basis.
Global State - Allows you to share any Send + Sync + 'static
types (like database connections) across all requests.
Local state lives for only a single Request, but can be useful to
prevent looking up the same data over and over. The cache is based on
the return type of the function or closure you pass to cache()
, so
make sure to create little wrapper structs if you want different
functions to return the same type, like Vec<String>
:
struct PageNames(Vec<String>);
struct UserNames(Vec<String>);
Here's an example:
use vial::prelude::*;
use page::Page;
use db;
routes! {
GET "/" => list;
}
struct PageNames(Vec<String>);
fn all_pages(_: &Request) -> Vec<Page> {
db::lookup("select * from pages")
}
fn page_names(req: &Request) -> PageNames {
PageNames(req.cache(all_pages)
.iter()
.map(|page| page.name.clone())
.collect::<Vec<_>>())
}
fn list_of_names(req: &Request) -> String {
req.cache(page_names)
.0
.iter()
.map(|name| format!("<li>{}</li>", name))
.collect::<Vec<_>>()
.join("\n")
}
fn list(req: Request) -> impl Responder {
format!(
"<html>
<head><title>{title}</title></head>
<body>
<h1>{title}</h1>
<h3>There are {page_count} pages:</h3>
<ul>
{pages}
</ul>
</body>
</html>",
title = "List Pages",
page_count = req.cache(all_pages).len(),
pages = req.cache(list_of_names),
)
}
fn main() {
run!().unwrap();
}
There are two steps involved in setting up shared, global state in Vial:
Send + Sync
to hold your application's
shared state:use vial;
use std::sync::{Arc, Mutx, atomic::{AtomicUsize, Ordering}};
use some_db_crate::DB;
struct MyConfig {
db: Arc<Mutex<DB>>,
counter: AtomicUsize,
}
impl MyConfig {
pub fn new(db: DB) {
MyConfig {
db: Arc::new(Mutex::new(db)),
counter: AtomicUsize::new(0),
}
}
}
run!
:fn main() {
let db = DB::new();
vial::use_state!(MyConfig::new(db));
vial::run!();
}
Now your actions and filters can access MyConfig
by calling the
state() method on Request:
use vial::prelude::*;
routes! {
GET "/list" => list;
}
fn find_names(db: Arc<Mutex<DB>>) -> Result<Vec<String>, db::Error> {
Ok(db.lock()?.query("SELECT name FROM names")?
.map(|row| row.get("name")?)
.collect::<Vec<_>>())
}
fn list(req: Request) -> Result<String> {
Ok(
find_names(req.state::<MyConfig>().db.clone())?
.map(|name| format!("<li>{}</li>", name))
.join("\n")
)
}
You might find it more convenient to define and implement a local
trait on the Request struct instead of calling request.state()
directly:
use vial::prelude::*;
routes! {
GET "/list" => list;
}
// ...
trait WithConfig {
fn config(&self) -> &MyConfig;
}
impl WithConfig for vial::Request {
fn config(&self) -> &MyConfig {
self.state::<MyConfig>()
}
}
fn list(req: Request) -> Result<String> {
Ok(
find_names(req.config().db.clone())?
.map(|name| format!("<li>{}</li>", name))
.join("\n")
)
}
Optional Feature: Coming soon.
Extremely basic cookie support is available by enabling the cookies
feature in your Cargo.toml
:
[Dependencies]
vial = { version = "*", features = ['cookies'] }
Once it's enabled you can access cookies the client sent using
req.cookie(name)
, set cookies using a similar API to the Request
Headers API, or remove cookies using
remove_cookie()
or without_cookie()
:
use vial::prelude::*;
routes! {
GET "/" => show;
GET "/clear" => clear;
GET "/set/:count" => set;
}
fn show(req: Request) -> impl Responder {
let count: usize = req.cookie("count").unwrap_or("0").parse().unwrap();
let new_count = count + 1;
Response::from(format!("count: {}", count))
.with_cookie("count", new_count.to_string())
}
fn clear(req: Request) -> impl Responder {
Response::redirect_to("/").without_cookie("count")
}
fn set(req: Request) -> Option<impl Responder> {
let val: usize = req.arg("count")?.parse().unwrap();
Response::redirect_to("/")
.with_cookie("count", val.to_string()).into()
}
fn main() {
run!();
}
Like all HTTP key/value pairs, cookie names are case insensitive.
Optional Feature: Coming soon.
Vial supports JSON requests and responses via Serde.
First, enable the json_serde
feature in your Cargo.toml
:
[Dependencies]
vial = { version = "*", features = ['json_serde'] }
Now, you can use Request::json
to deserialize a JSON request body, and
Response::with_json
to serialize a JSON response body:
use vial::prelude::*;
routes! {
POST "/json" => post;
}
fn post(req: Request) -> impl Responder {
match req
.json::<serde_json::Value>()
.ok()
.as_ref()
.and_then(|val| val.as_object())
.and_then(|obj| obj.get("message"))
.and_then(|val| val.as_str())
.map(|message| message.to_string())
{
Some(message) => Response::from(200).with_json(serde_json::json!({
"message": format!("Echo: {}", message)
})),
None => Response::from(400).with_body("json request parse error"),
}
}
fn main() {
vial::run!().unwrap();
}
Serde's derive macro can conveniently generate code to serialize and
deserialize structs and enums, including helpful error messages. To use it, add
a dependency on serde
with the derive
feature enabled in your Cargo.toml
:
[Dependencies]
serde = { version = "*", features=["derive"] }
Now, you can use Request::json
and Response::with_json
with any type
implementing serde::Deserialize
and serde::Serialize
, respectively:
use vial::prelude::*;
routes! {
POST "/json" => post;
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Echo {
message: String,
}
fn post(req: Request) -> impl Responder {
match req.json::<Echo>() {
Ok(echo) => Response::from(200).with_json(Echo {
message: format!("Echo: {}", echo.message),
}),
Err(e) => Response::from(400).with_body(e.to_string()),
}
}
fn main() {
vial::run!().unwrap();
}
"Pro Tip": Coming soon.
"Pro Tip": Coming soon.