1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
use crate::locales::Locales;
use crate::template::TemplateMap;
use crate::templates::ArcTemplateMap;
use crate::Html;
use crate::Template;
use std::collections::HashMap;
use std::rc::Rc;
/// The backend for `get_template_for_path` to avoid code duplication for the `Arc` and `Rc` versions.
macro_rules! get_template_for_path {
($raw_path:expr, $render_cfg:expr, $templates:expr) => {{
let mut path = $raw_path;
// If the path is empty, we're looking for the special `index` page
if path.is_empty() {
path = "index";
}
let mut was_incremental_match = false;
// Match the path to one of the templates
let mut template_name = String::new();
// We'll try a direct match first
if let Some(template_root_path) = $render_cfg.get(path) {
template_name = template_root_path.to_string();
}
// Next, an ISR match (more complex), which we only want to run if we didn't get an exact match above
if template_name.is_empty() {
// We progressively look for more and more specificity of the path, adding each segment
// That way, we're searching forwards rather than backwards, which is more efficient
let path_segments: Vec<&str> = path.split('/').collect();
for (idx, _) in path_segments.iter().enumerate() {
// Make a path out of this and all the previous segments
let path_to_try = path_segments[0..(idx + 1)].join("/") + "/*";
// If we find something, keep going until we don't (maximise specificity)
if let Some(template_root_path) = $render_cfg.get(&path_to_try) {
was_incremental_match = true;
template_name = template_root_path.to_string();
} else {
break;
}
}
}
// If we still have nothing, then the page doesn't exist
if template_name.is_empty() {
return (None, was_incremental_match);
}
// Return the necessary info for the caller to get the template in a form it wants (might be an `Rc` of a reference)
(template_name, was_incremental_match)
}};
}
/// Determines the template to use for the given path by checking against the render configuration., also returning whether we matched
/// a simple page or an incrementally-generated one (`true` for incrementally generated). Note that simple pages include those on
/// incrementally-generated templates that we pre-rendered with *build paths* at build-time (and are hence in an immutable store rather
/// than a mutable store).
///
/// This houses the central routing algorithm of Perseus, which is based fully on the fact that we know about every single page except
/// those rendered with ISR, and we can infer about them based on template root path domains. If that domain system is violated, this
/// routing algorithm will not behave as expected whatsoever (as far as routing goes, it's undefined behaviour)!
pub fn get_template_for_path<G: Html>(
raw_path: &str,
render_cfg: &HashMap<String, String>,
templates: &TemplateMap<G>,
) -> (Option<Rc<Template<G>>>, bool) {
let (template_name, was_incremental_match) =
get_template_for_path!(raw_path, render_cfg, templates);
(
templates.get(&template_name).cloned(),
was_incremental_match,
)
}
/// A version of `get_template_for_path` that accepts an `ArcTemplateMap<G>`. This is used by `match_route_atomic`, which should be used in scenarios in which the
/// template map needs to be passed betgween threads.
///
/// Warning: this returns a `&Template<G>` rather than a `Rc<Template<G>>`, and thus should only be used independently of the rest of Perseus (through `match_route_atomic`).
pub fn get_template_for_path_atomic<'a, G: Html>(
raw_path: &str,
render_cfg: &HashMap<String, String>,
templates: &'a ArcTemplateMap<G>,
) -> (Option<&'a Template<G>>, bool) {
let (template_name, was_incremental_match) =
get_template_for_path!(raw_path, render_cfg, templates);
(
templates
.get(&template_name)
.map(|pointer| pointer.as_ref()),
was_incremental_match,
)
}
/// Matches the given path to a `RouteVerdict`. This takes a `TemplateMap` to match against, the render configuration to index, and it
/// needs to know if i18n is being used. The path this takes should be raw, it may or may not have a locale, but should be split into
/// segments by `/`, with empty ones having been removed.
pub fn match_route<G: Html>(
path_slice: &[&str],
render_cfg: &HashMap<String, String>,
templates: &TemplateMap<G>,
locales: &Locales,
) -> RouteVerdict<G> {
let path_vec: Vec<&str> = path_slice.to_vec();
let path_joined = path_vec.join("/"); // This should not have a leading forward slash, it's used for asset fetching by the app shell
let verdict;
// There are different logic chains if we're using i18n, so we fork out early
if locales.using_i18n && !path_slice.is_empty() {
let locale = path_slice[0];
// Check if the 'locale' is supported (otherwise it may be the first section of an uni18ned route)
if locales.is_supported(locale) {
// We'll assume this has already been i18ned (if one of your routes has the same name as a supported locale, ffs)
let path_without_locale = path_slice[1..].to_vec().join("/");
// Get the template to use
let (template, was_incremental_match) =
get_template_for_path(&path_without_locale, render_cfg, templates);
verdict = match template {
Some(template) => RouteVerdict::Found(RouteInfo {
locale: locale.to_string(),
// This will be used in asset fetching from the server
path: path_without_locale,
template,
was_incremental_match,
}),
None => RouteVerdict::NotFound,
};
} else {
// If the locale isn't supported, we assume that it's part of a route that still needs a locale (we'll detect the user's preferred)
// This will result in a redirect, and the actual template to use will be determined after that
// We'll just pass through the path to be redirected to (after it's had a locale placed in front)
verdict = RouteVerdict::LocaleDetection(path_joined)
}
} else if locales.using_i18n {
// If we're here, then we're using i18n, but we're at the root path, which is a locale detection point
verdict = RouteVerdict::LocaleDetection(path_joined);
} else {
// Get the template to use
let (template, was_incremental_match) =
get_template_for_path(&path_joined, render_cfg, templates);
verdict = match template {
Some(template) => RouteVerdict::Found(RouteInfo {
locale: locales.default.to_string(),
// This will be used in asset fetching from the server
path: path_joined,
template,
was_incremental_match,
}),
None => RouteVerdict::NotFound,
};
}
verdict
}
/// A version of `match_route` that accepts an `ArcTemplateMap<G>`. This should be used in multithreaded situations, like on the server.
pub fn match_route_atomic<'a, G: Html>(
path_slice: &[&str],
render_cfg: &HashMap<String, String>,
templates: &'a ArcTemplateMap<G>,
locales: &Locales,
) -> RouteVerdictAtomic<'a, G> {
let path_vec: Vec<&str> = path_slice.to_vec();
let path_joined = path_vec.join("/"); // This should not have a leading forward slash, it's used for asset fetching by the app shell
let verdict;
// There are different logic chains if we're using i18n, so we fork out early
if locales.using_i18n && !path_slice.is_empty() {
let locale = path_slice[0];
// Check if the 'locale' is supported (otherwise it may be the first section of an uni18ned route)
if locales.is_supported(locale) {
// We'll assume this has already been i18ned (if one of your routes has the same name as a supported locale, ffs)
let path_without_locale = path_slice[1..].to_vec().join("/");
// Get the template to use
let (template, was_incremental_match) =
get_template_for_path_atomic(&path_without_locale, render_cfg, templates);
verdict = match template {
Some(template) => RouteVerdictAtomic::Found(RouteInfoAtomic {
locale: locale.to_string(),
// This will be used in asset fetching from the server
path: path_without_locale,
template,
was_incremental_match,
}),
None => RouteVerdictAtomic::NotFound,
};
} else {
// If the locale isn't supported, we assume that it's part of a route that still needs a locale (we'll detect the user's preferred)
// This will result in a redirect, and the actual template to use will be determined after that
// We'll just pass through the path to be redirected to (after it's had a locale placed in front)
verdict = RouteVerdictAtomic::LocaleDetection(path_joined)
}
} else if locales.using_i18n {
// If we're here, then we're using i18n, but we're at the root path, which is a locale detection point
verdict = RouteVerdictAtomic::LocaleDetection(path_joined);
} else {
// Get the template to use
let (template, was_incremental_match) =
get_template_for_path_atomic(&path_joined, render_cfg, templates);
verdict = match template {
Some(template) => RouteVerdictAtomic::Found(RouteInfoAtomic {
locale: locales.default.to_string(),
// This will be used in asset fetching from the server
path: path_joined,
template,
was_incremental_match,
}),
None => RouteVerdictAtomic::NotFound,
};
}
verdict
}
/// Information about a route, which, combined with error pages and a client-side translations manager, allows the initialization of
/// the app shell and the rendering of a page.
pub struct RouteInfo<G: Html> {
/// The actual path of the route.
pub path: String,
/// The template that will be used. The app shell will derive props and a translator to pass to the template function.
pub template: Rc<Template<G>>,
/// Whether or not the matched page was incrementally-generated at runtime (if it has been yet). If this is `true`, the server will
/// use a mutable store rather than an immutable one. See the book for more details.
pub was_incremental_match: bool,
/// The locale for the template to be rendered in.
pub locale: String,
}
/// The possible outcomes of matching a route. This is an alternative implementation of Sycamore's `Route` trait to enable greater
/// control and tighter integration of routing with templates. This can only be used if `Routes` has been defined in context (done
/// automatically by the CLI).
pub enum RouteVerdict<G: Html> {
/// The given route was found, and route information is attached.
Found(RouteInfo<G>),
/// The given route was not found, and a `404 Not Found` page should be shown.
NotFound,
/// The given route maps to the locale detector, which will redirect the user to the attached path (in the appropriate locale).
LocaleDetection(String),
}
/// Information about a route, which, combined with error pages and a client-side translations manager, allows the initialization of
/// the app shell and the rendering of a page.
///
/// This version is designed for multithreaded scenarios, and stores a reference to a template rather than an `Rc<Template<G>>`. That means this is not compatible
/// with Perseus on the client-side, only on the server-side.
pub struct RouteInfoAtomic<'a, G: Html> {
/// The actual path of the route.
pub path: String,
/// The template that will be used. The app shell will derive props and a translator to pass to the template function.
pub template: &'a Template<G>,
/// Whether or not the matched page was incrementally-generated at runtime (if it has been yet). If this is `true`, the server will
/// use a mutable store rather than an immutable one. See the book for more details.
pub was_incremental_match: bool,
/// The locale for the template to be rendered in.
pub locale: String,
}
/// The possible outcomes of matching a route. This is an alternative implementation of Sycamore's `Route` trait to enable greater
/// control and tighter integration of routing with templates. This can only be used if `Routes` has been defined in context (done
/// automatically by the CLI).
///
/// This version uses `RouteInfoAtomic`, and is designed for multithreaded scenarios (i.e. on the server).
pub enum RouteVerdictAtomic<'a, G: Html> {
/// The given route was found, and route information is attached.
Found(RouteInfoAtomic<'a, G>),
/// The given route was not found, and a `404 Not Found` page should be shown.
NotFound,
/// The given route maps to the locale detector, which will redirect the user to the attached path (in the appropriate locale).
LocaleDetection(String),
}
/// Creates an app-specific routing `struct`. Sycamore expects an `enum` to do this, so we create a `struct` that behaves similarly. If
/// we don't do this, we can't get the information necessary for routing into the `enum` at all (context and global variables don't suit
/// this particular case).
#[macro_export]
macro_rules! create_app_route {
{
name => $name:ident,
render_cfg => $render_cfg:expr,
templates => $templates:expr,
locales => $locales:expr
} => {
/// The route type for the app, with all routing logic inbuilt through the generation macro.
struct $name<G: $crate::Html>($crate::internal::router::RouteVerdict<G>);
impl<G: $crate::Html> ::sycamore_router::Route for $name<G> {
fn match_route(path: &[&str]) -> Self {
let verdict = $crate::internal::router::match_route(path, $render_cfg, $templates, $locales);
Self(verdict)
}
}
};
}