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)
            }
        }
    };
}