leptos_routes_macro/lib.rs
1mod expr_wrapper;
2mod generate;
3mod module_path;
4mod path;
5mod route_def;
6mod route_macro_args;
7mod util;
8
9use crate::expr_wrapper::ExprWrapper;
10use crate::module_path::ModulePath;
11use crate::route_def::{collect_route_definitions, RouteDef};
12use darling::ast::NestedMeta;
13use darling::FromMeta;
14use proc_macro::TokenStream;
15use proc_macro_error2::{abort, proc_macro_error};
16use quote::quote;
17use syn::{parse_macro_input, Item, ItemMod};
18
19#[proc_macro_attribute]
20#[proc_macro_error]
21pub fn route(_attr: TokenStream, input: TokenStream) -> TokenStream {
22 input
23}
24
25#[derive(Debug, FromMeta)]
26struct RoutesMacroArgs {
27 #[darling(default)]
28 with_views: bool,
29
30 #[darling(default)]
31 fallback: Option<ExprWrapper>,
32}
33
34/// This is the entry point for route-declarations. Put it on a module. Declare your routes using
35/// the `route` attribute on nested modules. You can freely nest your routes.
36///
37/// ```
38/// use leptos_routes::routes;
39///
40/// #[routes]
41/// pub mod routes {
42///
43/// #[route("/users")]
44/// pub mod users {
45///
46/// #[route("/:id")]
47/// pub mod user {
48///
49/// #[route("/details")]
50/// pub mod details {}
51/// }
52/// }
53/// }
54/// ```
55///
56/// You can also define the view for each route on the route declaration and simply let `leptos-routes` generate
57/// your router implementation.
58///
59/// ```
60/// use assertr::assert_that;
61/// use assertr::prelude::PartialEqAssertions;
62/// use leptos::prelude::*;
63/// use leptos_router::components::{Outlet, Router};
64/// use leptos_router::location::RequestUrl;
65/// use leptos_routes::routes;
66///
67/// #[routes(with_views, fallback = "|| view! { <Err404/> }")]
68/// pub mod routes {
69///
70/// #[route("/", layout = "MainLayout", fallback = "Dashboard")]
71/// pub mod root {
72///
73/// #[route("/welcome", view = "Welcome")]
74/// pub mod welcome {}
75///
76/// #[route("/users", layout = "UsersLayout", fallback = "NoUser")]
77/// pub mod users {
78///
79/// #[route("/:id", layout = "UserLayout", fallback="User")]
80/// pub mod user {
81///
82/// #[route("/details", view = "UserDetails")]
83/// pub mod details {}
84/// }
85/// }
86/// }
87/// }
88///
89/// #[component]
90/// fn Err404() -> impl IntoView { view! { "Err404" } }
91/// #[component]
92/// fn MainLayout() -> impl IntoView { view! { <div id="main-layout"> <Outlet/> </div> } }
93/// #[component]
94/// fn UsersLayout() -> impl IntoView { view! { <div id="users-layout"> <Outlet/> </div> } }
95/// #[component]
96/// fn UserLayout() -> impl IntoView { view! { <div id="user-layout"> <Outlet/> </div> } }
97/// #[component]
98/// fn Dashboard() -> impl IntoView { view! { "Dashboard" } }
99/// #[component]
100/// fn Welcome() -> impl IntoView { view! { "Welcome" } }
101/// #[component]
102/// fn NoUser() -> impl IntoView { view! { "NoUser" } }
103/// #[component]
104/// fn User() -> impl IntoView { view! {"User" } }
105/// #[component]
106/// fn UserDetails() -> impl IntoView { view! { "UserDetails" } }
107///
108/// fn main() {
109/// fn app() -> impl IntoView {
110/// view! {
111/// <Router>
112/// { routes::generated_routes() }
113/// </Router>
114/// }
115/// }
116///
117/// let _ = Owner::new_root(None);
118///
119/// provide_context::<RequestUrl>(RequestUrl::new(
120/// routes::root::users::user::Details
121/// .materialize("42")
122/// .as_str(),
123/// ));
124/// assert_that(app().to_html()).is_equal_to(r#"<div id="main-layout"><div id="users-layout"><div id="user-layout">UserDetails</div></div></div>"#);
125/// }
126/// ```
127#[proc_macro_attribute]
128#[proc_macro_error]
129pub fn routes(args: TokenStream, input: TokenStream) -> TokenStream {
130 let attr_args = match NestedMeta::parse_meta_list(args.into()) {
131 Ok(v) => v,
132 Err(e) => {
133 return TokenStream::from(darling::Error::from(e).write_errors());
134 }
135 };
136 let args = match RoutesMacroArgs::from_list(&attr_args) {
137 Ok(v) => v,
138 Err(e) => {
139 return TokenStream::from(e.write_errors());
140 }
141 };
142
143 let mut root_mod: ItemMod = parse_macro_input!(input as ItemMod);
144
145 // Make sure we have module contents to work with.
146 let (_brace, ref mut content) = match root_mod.content {
147 Some((brace, ref mut content)) => (brace, content),
148 None => {
149 abort!(root_mod.ident, "routes macro requires a module with a body");
150 }
151 };
152
153 // Add the route import at the start of the module.
154 let route_import: Item = syn::parse_quote! {
155 use ::leptos_routes::route;
156 };
157 content.insert(0, route_import);
158
159 let mut route_defs: Vec<RouteDef> = Vec::new();
160 for item in content.iter_mut() {
161 if let Item::Mod(child_module) = item {
162 add_additional_imports_to_modules(child_module);
163
164 collect_route_definitions(
165 child_module,
166 None,
167 None,
168 &mut route_defs,
169 ModulePath::root(root_mod.ident.clone()),
170 );
171 }
172 }
173
174 generate::impls(&mut root_mod, args, route_defs);
175
176 let (brace, ref mut content) = match root_mod.content {
177 Some((brace, ref mut content)) => (brace, content),
178 None => unreachable!("Already checked for empty module"),
179 };
180
181 // Reconstruct the module with all additions.
182 root_mod.content = Some((brace, content.to_vec()));
183
184 Into::into(quote! { #root_mod })
185}
186
187fn add_additional_imports_to_modules(module: &mut ItemMod) {
188 if let Some((_, items)) = &mut module.content {
189 let imports: Item = syn::parse_quote! {
190 use ::leptos_routes::route;
191 };
192 items.insert(0, imports);
193
194 for item in items.iter_mut() {
195 if let Item::Mod(child_module) = item {
196 add_additional_imports_to_modules(child_module);
197 }
198 }
199 }
200}