···6363 return Ok(RouteArgs { path, locales });
6464 }
65656666- // Try to parse the first argument
6767- let lookahead = input.lookahead1();
6666+ // First argument: either a path expression or a named argument like locales(...)
6767+ if input.peek(Ident) && input.peek2(syn::token::Paren) {
6868+ // First argument is a named argument (e.g., locales(...))
6969+ // This means it's a variant-only route with no base path
7070+ let ident: Ident = input.parse()?;
7171+ let ident_str = ident.to_string();
68726969- // Check if it's "locales(...)"
7070- if lookahead.peek(Ident) {
7171- let ident: Ident = input.fork().parse()?;
7272- if ident == "locales" {
7373- // Parse locales(...) argument
7474- input.parse::<Ident>()?; // consume "locales"
7373+ if ident_str == "locales" {
7574 let content;
7675 syn::parenthesized!(content in input);
7777-7876 let variants = Punctuated::<LocaleVariant, Token![,]>::parse_terminated(&content)?;
7977 locales = variants.into_iter().collect();
8080-8181- // Check for duplicate locales
8282- Self::check_duplicate_locales(&locales)?;
8383-8484- return Ok(RouteArgs { path, locales });
7878+ } else {
7979+ return Err(syn::Error::new_spanned(
8080+ ident,
8181+ format!("unknown argument '{}', expected 'locales'", ident_str),
8282+ ));
8583 }
8686- }
8787-8888- // Otherwise, try to parse as path expression
8989- if !input.is_empty() {
8484+ } else {
8585+ // First argument is a path expression
9086 path = Some(input.parse::<Expr>()?);
9187 }
92889393- // Check if there's a comma and more args
9494- if !input.is_empty() {
8989+ // Parse remaining named arguments (locales, middleware, etc.)
9090+ while !input.is_empty() {
9591 input.parse::<Token![,]>()?;
96929797- // Check for locales(...) as second argument
9898- if input.peek(Ident) {
9393+ if input.is_empty() {
9494+ break;
9595+ }
9696+9797+ // All subsequent arguments must be named (e.g., locales(...), middleware(...))
9898+ if input.peek(Ident) && input.peek2(syn::token::Paren) {
9999 let ident: Ident = input.parse()?;
100100- if ident == "locales" {
100100+ let ident_str = ident.to_string();
101101+102102+ if ident_str == "locales" {
103103+ if !locales.is_empty() {
104104+ return Err(syn::Error::new_spanned(
105105+ ident,
106106+ "locales specified multiple times",
107107+ ));
108108+ }
101109 let content;
102110 syn::parenthesized!(content in input);
103103-104111 let variants =
105112 Punctuated::<LocaleVariant, Token![,]>::parse_terminated(&content)?;
106113 locales = variants.into_iter().collect();
107107-108108- // Check for duplicate locales
109109- Self::check_duplicate_locales(&locales)?;
114114+ } else {
115115+ return Err(syn::Error::new_spanned(
116116+ ident,
117117+ format!("unknown argument '{}'", ident_str),
118118+ ));
110119 }
120120+ } else {
121121+ return Err(syn::Error::new(
122122+ input.span(),
123123+ "expected named argument (e.g., locales(...)), path must be first argument",
124124+ ));
111125 }
112126 }
127127+128128+ // Check for duplicate locales
129129+ Self::check_duplicate_locales(&locales)?;
113130114131 Ok(RouteArgs { path, locales })
115132 }
+15
examples/i18n/src/routes/wrong_order.rs.test
···11+use crate::layout::layout;
22+use maud::html;
33+use maudit::route::prelude::*;
44+55+// This should produce a compile error because path comes after locales
66+#[route(locales(en = "/en"), "/about")]
77+pub struct WrongOrder;
88+99+impl Route for WrongOrder {
1010+ fn render(&self, _ctx: &mut PageContext) -> impl Into<RenderResult> {
1111+ layout(html! {
1212+ h1 { "This should not compile!" }
1313+ })
1414+ }
1515+}
+26
examples/i18n/test_errors.txt
···11+Test cases for error handling:
22+33+1. Duplicate locales error:
44+#[route("/about", locales(en = "/en"), locales(sv = "/sv"))]
55+Expected error: "locales specified multiple times"
66+77+2. Duplicate locale within locales:
88+#[route("/about", locales(en = "/en", en = "/english"))]
99+Expected error: "duplicate locale 'en' specified"
1010+1111+3. Unknown argument:
1212+#[route("/about", something(foo = "bar"))]
1313+Expected error: "unknown argument 'something'"
1414+1515+4. Prefix without base path:
1616+#[route(locales(en(prefix = "/en")))]
1717+Expected error: "Cannot use locale prefix without a base route path"
1818+1919+5. Path in wrong position:
2020+#[route(locales(en = "/en"), "/about")]
2121+Expected error: "expected named argument (e.g., locales(...)), path must be first argument"
2222+2323+All these cases should produce compile-time errors with clear messages.
2424+The refactored parser enforces that the path must be the first argument (if present),
2525+followed by named arguments like locales(...). This is flexible enough to support
2626+future named arguments while maintaining clear syntax rules.