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}