- Rust 100%
| typed_web_routes | ||
| typed_web_routes_macro | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
| rust-toolchain | ||
Typed web routes
This crate provides a type-safe way to declare web routes and convert them to their URL, based on path and query
parameters. This crate is not tied with any specific web framework but works especially well with axum.
Think of something along the line of Django's urls.reverse(), but verified by the compiler at compile time :)
To declare the routes, decorate an enum with #[derive(WebRoutes)] and use #[route(pattern = "")] to describe the
path. For example, these three routes:
use typed_web_routes::WebRoutes;
#[derive(WebRoutes)]
enum Routes {
#[route(pattern = "/")]
Home,
#[route(pattern = "/post/{post}")]
Post { post: u32 },
#[route(pattern = "/search")]
Search { term: String, page: Option<i32> },
}
This crate adds a builder() method to the enum. This builder has one method and one struct for each route:
fn example() {
let builder = Routes::builder();
assert_eq!(builder.home().to_string(), "/");
assert_eq!(builder.post(17).to_string(), "/post/17");
assert_eq!(builder.search("buga!").to_string(), "/search?term=buga%21");
assert_eq!(builder.search("abc").with_page(2).to_string(), "/search?term=abc&page=2");
}
You can also generate absolute URLs:
fn example() {
let builder = Routes::builder_with_url_prefix("http://localhost:8080");
assert_eq!(builder.post(17).to_string(), "http://localhost:8080/post/17");
}
All non-Option<_> fields (like post and term in the example) are required by the builder method. All Option<_>
fields (like page in the example) can be provided using .with_() methods.
Path parameters and query names and values are URL escaped: any byte other than 0-9 a-z A-Z - . _ ~ are percentage
encoded.
The syntax of the URL pattern is the same as the one used by matchit:
- path parameter like
/photo-{id}.jpg - catch-all parameter like
/post/{*rest} - escaping braces like
/not{{a parameter}}
Dynamic usage
This crate also provides information about the routes that can be used to dynamically generate the URLs:
fn example() {
let route = Routes::info_by_name("Search").unwrap();
let url = route.format(
Some("http://localhost:8080"),
&[("term", &"abc"), ("page", &2)]
).unwrap();
assert_eq!(url, "http://localhost:8080/search?term=abc&page=2");
}
When using the type-safe alternative (like builder.search()) no error can occur, since the compiler will check the
name, types and number of parameters. When using the dynamic alternative (like info_by_name() and format()), errors
can occur, like invalid route name or missing required argument.
Use with axum
Activate the crate feature axum to have access to the extractor PathQuery<T>, that behaves like Path<T> and
Query<T> together:
use typed_web_routes::PathQuery;
async fn main() {
let app = axum::routing::Router::new()
.route(Routes::HOME.pattern, get(home))
.route(Routes::POST.pattern, get(post))
.route(Routes::SEARCH.pattern, get(search));
axum::serve(listener, app).await;
}
async fn home() {}
async fn post(PathQuery(request): PathQuery<Routes_Post>) {
// request.post was parsed from the path
}
async fn search(PathQuery(request): PathQuery<Routes_Search>) {
// request.term and request.page were parsed from the query
}
The WebRoutes derive macro in details
The #[derive(WebRoutes)] can only be applied to an enum. Each enum variant must be either empty (like Home in the
example) or a struct variant with fields.
It will generate code to:
- implement the trait
WebRoutesfor your enum - declare a new struct
YourEnum__Builder
Then for each enum variant it will generate code to:
- add a constant
YourEnum::VARIANTwith information about that route - declare a new struct
YourEnum_Variantwith the same fields as the enum variant, if any. This struct implements the traitWebRoute - add a method in
YourEnum__Builder::variant()that takes as parameters all the non-Option<_>fields of the variant - add one method in
YourEnum_Variant::with_field()for eachOption<_>fieldof the variant - if needed, declare new structs to deserialize the path and query
The #[route(...)] attribute can be used in three places:
#[derive(WebRoutes)]
#[route(typed_web_routes = some_other_name)] // <-- enum
enum Routes {
#[route(rename = "show-post", pattern = "/post/{post}")] // <-- variant
Post {
#[route(rename = "post")] // <-- field
post_id: u32
},
}
- enum:
typed_web_routes(optional): the name of this crate, if you imported it into your project with another name
- variant:
rename(optional): the name of this route. This impacts the string used inRoutes::info_by_name()pattern(required): the URL pattern used for this route
- field:
rename(optional): the name of this field in the path or query.
For reference, the following simple example:
#[derive(WebRoutes)]
enum Routes {
#[route(pattern = "/post/{post}")]
Post { post: u32 }
}
expands to something like:
impl WebRoutes for Routes {
const ROUTES: &'static [RouteInfo] = &[Routes_Post::INFO];
type Builder = Routes__Builder;
fn builder() -> Routes__Builder {
Routes__Builder { url_prefix: None }
}
fn builder_with_url_prefix(url_prefix: impl Into<String>) -> Routes__Builder {
Routes__Builder { url_prefix: Some(url_prefix.into()) }
}
}
#[derive(Clone, Debug)]
struct Routes__Builder {
url_prefix: Option<String>,
}
impl Routes {
pub const POST: RouteInfo = Routes_Post::INFO;
}
#[derive(Debug)]
struct Routes_Post {
_url_prefix: Option<String>,
pub post: u32,
}
impl Display for Routes_Post {
fn fmt(&self, f: &mut Formatter<'_>) -> Result { /* ... */ }
}
impl WebRoute for Routes_Post {
type Path = Routes_Post__Path;
type Query = ();
const INFO: RouteInfo = RouteInfo {
name: "Post",
pattern: "/post/{post}",
pattern_tokens: &[
PatternToken::Fixed("/post/"),
PatternToken::Parameter { name: "post", is_catch_all: false },
],
path_terms: &["post"],
query_terms: &[],
};
fn from_path_query(path: Self::Path, query: Self::Query) -> Self {
Self { _url_prefix: None, post: path.post }
}
fn with_url_prefix(mut self, url_prefix: impl Into<String>) -> Self {
self._url_prefix = Some(url_prefix.into());
self
}
}
impl Routes__Builder {
pub fn post(&self, post: impl Into<u32>) -> Routes_Post {
Routes_Post {
_url_prefix: self.url_prefix.clone(),
post: post.into(),
}
}
}
#[derive(Debug, Deserialize)]
struct Routes_Post__Path {
pub post: u32,
}