perseus/router/match_route.rs
1use super::{RouteInfo, RouteVerdict};
2use crate::i18n::Locales;
3use crate::path::*;
4use crate::template::{Entity, EntityMap, Forever};
5use std::collections::HashMap;
6use sycamore::web::Html;
7
8/// Determines the template to use for the given path by checking against the
9/// render configuration, also returning whether we matched a simple page or an
10/// incrementally-generated one (`true` for incrementally generated). Note that
11/// simple pages include those on incrementally-generated templates that we
12/// pre-rendered with *build paths* at build-time (and are hence in an immutable
13/// store rather than a mutable store).
14///
15/// This houses the central routing algorithm of Perseus, which is based fully
16/// on the fact that we know about every single page except those rendered with
17/// ISR, and we can infer about them based on template root path domains. If
18/// that domain system is violated, this routing algorithm will not behave as
19/// expected whatsoever (as far as routing goes, it's undefined behavior)!
20fn get_template_for_path<'a, G: Html>(
21 path: &str,
22 render_cfg: &HashMap<String, String>,
23 entities: &'a EntityMap<G>,
24) -> (Option<&'a Forever<Entity<G>>>, bool) {
25 let mut was_incremental_match = false;
26 // Match the path to one of the entities
27 let mut entity_name = None;
28 // We'll try a direct match first
29 if let Some(entity_root_path) = render_cfg.get(path) {
30 entity_name = Some(entity_root_path.to_string());
31 }
32 // Next, an ISR match (more complex), which we only want to run if we didn't get
33 // an exact match above
34 if entity_name.is_none() {
35 // We progressively look for more and more specificity of the path, adding each
36 // segment. That way, we're searching forwards rather than backwards,
37 // which is more efficient.
38 let path_segments: Vec<&str> = path.split('/').collect();
39 for (idx, _) in path_segments.iter().enumerate() {
40 // Make a path out of this and all the previous segments
41 let path_to_try = path_segments[0..(idx + 1)].join("/") + "/*";
42
43 // If we find something, keep going until we don't (maximize specificity)
44 if let Some(entity_root_path) = render_cfg.get(&path_to_try) {
45 was_incremental_match = true;
46 entity_name = Some(entity_root_path.to_string());
47 }
48 }
49 }
50 // If we still have nothing, then the page doesn't exist, *unless* there's
51 // incremental generation on the index template, in which case it does
52 // (this doesn't break priorities because, above, we go for the most specific,
53 // and this is the least, meaning there is nothing more specific)
54 if let Some(entity_name) = entity_name {
55 (entities.get(&entity_name), was_incremental_match)
56 } else if render_cfg.contains_key("/*") {
57 (entities.get(""), true)
58 } else {
59 (None, was_incremental_match)
60 }
61}
62
63/// Matches the given path to a `RouteVerdict`. This takes a `TemplateMap` to
64/// match against, the render configuration to index, and it needs to know if
65/// i18n is being used. The path this takes should be raw, it may or may not
66/// have a locale, but should be split into segments by `/`, with empty ones
67/// having been removed.
68pub(crate) fn match_route<G: Html>(
69 path_slice: &[&str],
70 render_cfg: &HashMap<String, String>,
71 entities: &EntityMap<G>,
72 locales: &Locales,
73) -> RouteVerdict {
74 let path_vec = path_slice.to_vec();
75 let path_joined = PathMaybeWithLocale(path_vec.join("/")); // This should not have a leading forward slash, it's used for asset fetching by
76 // the app shell
77
78 // There are different logic chains if we're using i18n, so we fork out early
79 if locales.using_i18n && !path_slice.is_empty() {
80 let locale = path_slice[0];
81 // Check if the 'locale' is supported (otherwise it may be the first section of
82 // an uni18ned route)
83 if locales.is_supported(locale) {
84 // We'll assume this has already been i18ned (if one of your routes has the same
85 // name as a supported locale, ffs)
86 let path_without_locale = PathWithoutLocale(path_slice[1..].to_vec().join("/"));
87 // Get the template to use
88 let (entity, was_incremental_match) =
89 get_template_for_path(&path_without_locale, render_cfg, entities);
90 match entity {
91 Some(entity) => RouteVerdict::Found(RouteInfo {
92 locale: locale.to_string(),
93 // This will be used in asset fetching from the server
94 path: path_without_locale,
95 // The user can get the full entity again if they want to, we just use it to
96 // make sure the path exists
97 entity_name: entity.get_path(),
98 was_incremental_match,
99 }),
100 None => RouteVerdict::NotFound {
101 locale: locale.to_string(),
102 },
103 }
104 } else {
105 // If the locale isn't supported, we assume that it's part of a route that still
106 // needs a locale (we'll detect the user's preferred)
107 // This will result in a redirect, and the actual template to use will be
108 // determined after that We'll just pass through the path to be
109 // redirected to (after it's had a locale placed in front)
110 let path_joined = PathWithoutLocale(path_joined.0);
111 RouteVerdict::LocaleDetection(path_joined)
112 }
113 } else if locales.using_i18n {
114 // If we're here, then we're using i18n, but we're at the root path, which is a
115 // locale detection point
116 let path_joined = PathWithoutLocale(path_joined.0);
117 RouteVerdict::LocaleDetection(path_joined)
118 } else {
119 // We're not using i18n
120 let path_joined = PathWithoutLocale(path_joined.0);
121 // Get the template to use
122 let (entity, was_incremental_match) =
123 get_template_for_path(&path_joined, render_cfg, entities);
124 match entity {
125 Some(entity) => RouteVerdict::Found(RouteInfo {
126 locale: locales.default.to_string(),
127 // This will be used in asset fetching from the server
128 path: path_joined,
129 // The user can get the full entity again if they want to, we just use it to make
130 // sure the path exists
131 entity_name: entity.get_path(),
132 was_incremental_match,
133 }),
134 None => RouteVerdict::NotFound {
135 locale: "xx-XX".to_string(),
136 },
137 }
138 }
139}