Skip to content

Commit a51aa1c

Browse files
committed
fix(yew-router-macro): reject route params without corresponding fields
A unit variant like `Settings` with `#[at("/settings/{*_rest}")]` compiled but produced a broken `to_path()` that returned the literal pattern string. This was a pre-existing bug (same with the old `*rest` syntax) now caught at compile time. Also fixes the nested-router docs examples to use named fields.
1 parent abf8d01 commit a51aa1c

7 files changed

Lines changed: 67 additions & 8 deletions

File tree

packages/yew-router-macro/src/routable_derive.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
88
const AT_ATTR_IDENT: &str = "at";
99
const NOT_FOUND_ATTR_IDENT: &str = "not_found";
1010

11+
/// Extract parameter names from a matchit-style route pattern.
12+
/// E.g. `"/posts/{id}"` → `["id"]`, `"/files/{*path}"` → `["path"]`.
13+
fn extract_route_params(route: &str) -> Vec<String> {
14+
let mut params = Vec::new();
15+
let mut chars = route.chars().peekable();
16+
while let Some(c) = chars.next() {
17+
if c == '{' {
18+
if chars.peek() == Some(&'*') {
19+
chars.next();
20+
}
21+
let name: String = chars.by_ref().take_while(|&c| c != '}').collect();
22+
if !name.is_empty() {
23+
params.push(name);
24+
}
25+
}
26+
}
27+
params
28+
}
29+
1130
pub struct Routable {
1231
ident: Ident,
1332
ats: Vec<LitStr>,
@@ -101,6 +120,32 @@ fn parse_variants_attributes(
101120
));
102121
}
103122

123+
let route_params = extract_route_params(&val);
124+
if !route_params.is_empty() {
125+
let field_names: std::collections::HashSet<String> = match &variant.fields {
126+
Fields::Named(fields) => fields
127+
.named
128+
.iter()
129+
.filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
130+
.collect(),
131+
Fields::Unit => std::collections::HashSet::new(),
132+
Fields::Unnamed(_) => unreachable!(),
133+
};
134+
135+
for param in &route_params {
136+
if !field_names.contains(param) {
137+
return Err(syn::Error::new_spanned(
138+
&lit,
139+
format!(
140+
"route parameter `{param}` does not have a corresponding field in \
141+
variant `{}`",
142+
variant.ident
143+
),
144+
));
145+
}
146+
}
147+
}
148+
104149
ats.push(lit);
105150

106151
for attr in attrs.iter() {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#[derive(yew_router::Routable, Debug, Clone, PartialEq)]
2+
enum Routes {
3+
#[at("/")]
4+
Home,
5+
#[at("/settings/{*_rest}")]
6+
Settings,
7+
}
8+
9+
fn main() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: route parameter `_rest` does not have a corresponding field in variant `Settings`
2+
--> tests/routable_derive/param-without-field-fail.rs:5:10
3+
|
4+
5 | #[at("/settings/{*_rest}")]
5+
| ^^^^^^^^^^^^^^^^^^^^

website/docs/concepts/router.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ enum MainRoute {
424424
#[at("/settings")]
425425
SettingsRoot,
426426
#[at("/settings/{*_rest}")]
427-
Settings,
427+
Settings { _rest: String },
428428
#[not_found]
429429
#[at("/404")]
430430
NotFound,
@@ -448,7 +448,7 @@ fn switch_main(route: MainRoute) -> Html {
448448
MainRoute::Home => html! {<h1>{"Home"}</h1>},
449449
MainRoute::News => html! {<h1>{"News"}</h1>},
450450
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
451-
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
451+
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
452452
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
453453
}
454454
}

website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/router.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ enum MainRoute {
397397
#[at("/settings")]
398398
SettingsRoot,
399399
#[at("/settings/{*_rest}")]
400-
Settings,
400+
Settings { _rest: String },
401401
#[not_found]
402402
#[at("/404")]
403403
NotFound,
@@ -421,7 +421,7 @@ fn switch_main(route: MainRoute) -> Html {
421421
MainRoute::Home => html! {<h1>{"Home"}</h1>},
422422
MainRoute::News => html! {<h1>{"News"}</h1>},
423423
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
424-
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
424+
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
425425
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
426426
}
427427
}

website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/router.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ enum MainRoute {
397397
#[at("/settings")]
398398
SettingsRoot,
399399
#[at("/settings/{*_rest}")]
400-
Settings,
400+
Settings { _rest: String },
401401
#[not_found]
402402
#[at("/404")]
403403
NotFound,
@@ -421,7 +421,7 @@ fn switch_main(route: MainRoute) -> Html {
421421
MainRoute::Home => html! {<h1>{"Home"}</h1>},
422422
MainRoute::News => html! {<h1>{"News"}</h1>},
423423
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
424-
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
424+
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
425425
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
426426
}
427427
}

website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/router.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ enum MainRoute {
397397
#[at("/settings")]
398398
SettingsRoot,
399399
#[at("/settings/{*_rest}")]
400-
Settings,
400+
Settings { _rest: String },
401401
#[not_found]
402402
#[at("/404")]
403403
NotFound,
@@ -421,7 +421,7 @@ fn switch_main(route: MainRoute) -> Html {
421421
MainRoute::Home => html! {<h1>{"Home"}</h1>},
422422
MainRoute::News => html! {<h1>{"News"}</h1>},
423423
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
424-
MainRoute::SettingsRoot | MainRoute::Settings => html! { <Switch<SettingsRoute> render={switch_settings} /> },
424+
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
425425
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
426426
}
427427
}

0 commit comments

Comments
 (0)