Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions examples/function_router/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ pub fn route_meta(route: &Route) -> (&'static str, &'static str) {

#[derive(Routable, PartialEq, Eq, Clone, Debug)]
pub enum Route {
#[at("/posts/:id")]
#[at("/posts/{id}")]
Post { id: u32 },
#[at("/posts")]
Posts,
#[at("/authors/:id")]
#[at("/authors/{id}")]
Author { id: u32 },
#[at("/authors")]
Authors,
Expand Down
4 changes: 2 additions & 2 deletions examples/router/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ use yew::html::Scope;

#[derive(Routable, PartialEq, Eq, Clone, Debug)]
pub enum Route {
#[at("/posts/:id")]
#[at("/posts/{id}")]
Post { id: u64 },
#[at("/posts")]
Posts,
#[at("/authors/:id")]
#[at("/authors/{id}")]
Author { id: u64 },
#[at("/authors")]
Authors,
Expand Down
2 changes: 1 addition & 1 deletion examples/wasi_ssr_module/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub enum Route {
#[at("/")]
Portal,

#[at("/t/:id")]
#[at("/t/{id}")]
Thread { id: String },

#[not_found]
Expand Down
79 changes: 73 additions & 6 deletions packages/yew-router-macro/src/routable_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
const AT_ATTR_IDENT: &str = "at";
const NOT_FOUND_ATTR_IDENT: &str = "not_found";

/// Extract parameter names from a matchit-style route pattern.
/// E.g. `"/posts/{id}"` → `["id"]`, `"/files/{*path}"` → `["path"]`.
fn extract_route_params(route: &str) -> Vec<String> {
let mut params = Vec::new();
let mut chars = route.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
if chars.peek() == Some(&'*') {
chars.next();
}
let name: String = chars.by_ref().take_while(|&c| c != '}').collect();
if !name.is_empty() {
params.push(name);
}
}
}
params
}

pub struct Routable {
ident: Ident,
ats: Vec<LitStr>,
Expand Down Expand Up @@ -101,6 +120,55 @@ fn parse_variants_attributes(
));
}

// Reject old route-recognizer `:param` / `*param` syntax that would
// silently become literal path segments under matchit.
for segment in val.split('/') {
if let Some(name) = segment.strip_prefix(':') {
return Err(syn::Error::new_spanned(
&lit,
format!(
"route segments must not start with `:`. Use `{{{name}}}` to capture a \
parameter.",
),
));
}
if let Some(name) = segment.strip_prefix('*') {
return Err(syn::Error::new_spanned(
&lit,
format!(
"route segments must not start with `*`. Use `{{*{name}}}` to capture a \
wildcard.",
),
));
}
}

let route_params = extract_route_params(&val);
if !route_params.is_empty() {
let field_names: std::collections::HashSet<String> = match &variant.fields {
Fields::Named(fields) => fields
.named
.iter()
.filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
.collect(),
Fields::Unit => std::collections::HashSet::new(),
Fields::Unnamed(_) => unreachable!(),
};

for param in &route_params {
if !field_names.contains(param) {
return Err(syn::Error::new_spanned(
&lit,
format!(
"route parameter `{param}` does not have a corresponding field in \
variant `{}`",
variant.ident
),
));
}
}
}

ats.push(lit);

for attr in attrs.iter() {
Expand Down Expand Up @@ -174,14 +242,13 @@ impl Routable {

let mut wildcard_fields = std::collections::HashSet::new();
for field in fields.iter() {
if right.contains(&format!("*{field}")) {
if right.contains(&format!("{{*{field}}}")) {
wildcard_fields.insert((*field).clone());
}
// :param -> {param}
// *param -> {param}
// so we can pass it to `format!("...", param)`
right = right.replace(&format!(":{field}"), &format!("{{{field}}}"));
right = right.replace(&format!("*{field}"), &format!("{{{field}}}"));
// {*param} -> {param} so we can pass it to `format!("...", param)`
// {param} is already valid format syntax
right =
right.replace(&format!("{{*{field}}}"), &format!("{{{field}}}"));
}

let field_encodings = fields.iter().map(|field| {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[derive(yew_router::Routable, Debug, Clone, PartialEq)]
enum Routes {
#[at("/")]
Home,
#[at("/posts/:id")]
Post { id: u32 },
#[at("/files/*path")]
File { path: String },
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: route segments must not start with `:`. Use `{id}` to capture a parameter.
--> tests/routable_derive/old-param-syntax-fail.rs:5:10
|
5 | #[at("/posts/:id")]
| ^^^^^^^^^^^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[derive(yew_router::Routable, Debug, Clone, PartialEq)]
enum Routes {
#[at("/")]
Home,
#[at("/settings/{*_rest}")]
Settings,
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: route parameter `_rest` does not have a corresponding field in variant `Settings`
--> tests/routable_derive/param-without-field-fail.rs:5:10
|
5 | #[at("/settings/{*_rest}")]
| ^^^^^^^^^^^^^^^^^^^^
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#[derive(yew_router::Routable)]
enum Routes {
#[at("/one/:two")]
#[at("/one/{two}")]
One(u32),
}

Expand Down
8 changes: 4 additions & 4 deletions packages/yew-router-macro/tests/routable_derive/valid-pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
enum Routes {
#[at("/")]
One,
#[at("/two/:id")]
#[at("/two/{id}")]
Two { id: u32 },
#[at("/:a/:b/*rest")]
#[at("/{a}/{b}/{*rest}")]
Three { a: u32, b: u32, rest: ::std::string::String },
#[at("/404")]
#[not_found]
Expand All @@ -15,9 +15,9 @@ enum Routes {

#[derive(Debug, PartialEq, Clone, ::yew_router::Routable)]
enum MoreRoutes {
#[at("/subpath/*rest")]
#[at("/subpath/{*rest}")]
Subpath { rest: ::std::string::String },
#[at("/*all")]
#[at("/{*all}")]
CatchAll { all: ::std::string::String },
}

Expand Down
2 changes: 1 addition & 1 deletion packages/yew-router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ yew-router-macro = { version = "0.20.0", path = "../yew-router-macro" }
wasm-bindgen.workspace = true
js-sys.workspace = true
gloo = { workspace = true, features = ["futures"] }
route-recognizer = "0.3"
matchit = "0.9"
serde.workspace = true
serde_urlencoded = "0.7.1"
tracing = "0.1.44"
Expand Down
20 changes: 13 additions & 7 deletions packages/yew-router/src/macro_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,37 @@ pub fn encode_path_for_url(path: &str) -> String {
.join("/")
}

use std::collections::HashMap;

use crate::utils::strip_slash_suffix;
use crate::Routable;

// re-export Router because the macro needs to access it
pub type Router = route_recognizer::Router<String>;
pub type Router = matchit::Router<String>;

/// Build a `route_recognizer::Router` from a `Routable` type.
/// Build a `matchit::Router` from a `Routable` type.
pub fn build_router<R: Routable>() -> Router {
let mut router = Router::new();
R::routes().iter().for_each(|path| {
let stripped_route = strip_slash_suffix(path);
router.add(stripped_route, path.to_string());
router
.insert(stripped_route, path.to_string())
.unwrap_or_else(|e| panic!("failed to insert route {stripped_route:?}: {e}"));
});

router
}

/// Use a `route_recognizer::Router` to build the route of a `Routable`
/// Use a `matchit::Router` to match the route of a `Routable`
pub fn recognize_with_router<R: Routable>(router: &Router, pathname: &str) -> Option<R> {
let pathname = strip_slash_suffix(pathname);
let matched = router.recognize(pathname);
let matched = router.at(pathname);

match matched {
Ok(matched) => R::from_path(matched.handler(), &matched.params().into_iter().collect())
.or_else(R::not_found_route),
Ok(matched) => {
let params: HashMap<&str, &str> = matched.params.iter().collect();
R::from_path(matched.value, &params).or_else(R::not_found_route)
}
Err(_) => R::not_found_route(),
}
}
2 changes: 1 addition & 1 deletion packages/yew-router/src/routable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl Routable for AnyRoute {
}

fn routes() -> Vec<&'static str> {
vec!["/*path"]
vec!["/{*path}"]
}

fn not_found_route() -> Option<Self> {
Expand Down
2 changes: 1 addition & 1 deletion packages/yew-router/tests/basename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct Query {
enum Routes {
#[at("/")]
Home,
#[at("/no/:id")]
#[at("/no/{id}")]
No { id: u32 },
#[at("/404")]
NotFound,
Expand Down
2 changes: 1 addition & 1 deletion packages/yew-router/tests/browser_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct Query {
enum Routes {
#[at("/")]
Home,
#[at("/no/:id")]
#[at("/no/{id}")]
No { id: u32 },
#[at("/404")]
NotFound,
Expand Down
2 changes: 1 addition & 1 deletion packages/yew-router/tests/hash_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct Query {
enum Routes {
#[at("/")]
Home,
#[at("/no/:id")]
#[at("/no/{id}")]
No { id: u32 },
#[at("/404")]
NotFound,
Expand Down
Loading
Loading