Skip to content

Commit

Permalink
feat(routing): ✨ switched to template-based routing
Browse files Browse the repository at this point in the history
This simplifies routing significantly and couples it fully to the templates system.

BREAKING CHANGE: `define_app!` no longer takes routing paths, just templates
Closes #12.
  • Loading branch information
arctic-hen7 committed Sep 17, 2021
1 parent f7ec1aa commit 78688c1
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 220 deletions.
2 changes: 1 addition & 1 deletion examples/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Starter App</title>
<!-- Importing this runs Perseus -->
<script src="/.perseus/bundle.js" defer></script>
<script type="module" src="/.perseus/main.js" defer></script>
</head>
<body>
<div id="root"></div>
Expand Down
70 changes: 35 additions & 35 deletions examples/cli/.perseus/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use app::{get_error_pages, get_locales, get_routes, APP_ROUTE};
use app::{get_error_pages, get_locales, get_templates_map, APP_ROOT};
use perseus::router::{RouteInfo, RouteVerdict};
use perseus::{app_shell, detect_locale, ClientTranslationsManager, DomNode};
use perseus::shell::get_render_cfg;
use perseus::{app_shell, create_app_route, detect_locale, ClientTranslationsManager, DomNode};
use std::cell::RefCell;
use std::rc::Rc;
use sycamore::context::{ContextProvider, ContextProviderProps};
use sycamore::prelude::{template, StateHandle};
use sycamore_router::{HistoryIntegration, Router, RouterProps};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
Expand All @@ -18,7 +18,7 @@ pub fn run() -> Result<(), JsValue> {
.unwrap()
.document()
.unwrap()
.query_selector(APP_ROUTE)
.query_selector(APP_ROOT)
.unwrap()
.unwrap();

Expand All @@ -27,42 +27,42 @@ pub fn run() -> Result<(), JsValue> {
Rc::new(RefCell::new(ClientTranslationsManager::new(&get_locales())));
// Get the error pages in an `Rc` so we aren't creating hundreds of them
let error_pages = Rc::new(get_error_pages());
// Get the routes in an `Rc` as well
let routes = Rc::new(get_routes::<DomNode>());

// Create the router we'll use for this app, based on the user's app definition
create_app_route! {
name => AppRoute,
// The render configuration is injected verbatim into the HTML shell, so it certainly should be present
render_cfg => get_render_cfg().expect("render configuration invalid or not injected"),
templates => get_templates_map(),
locales => get_locales()
}

sycamore::render_to(
|| {
template! {
// We provide the routes in context (can't provide them directly because of Sycamore trait constraints)
// BUG: context doesn't exist when link clicked first time, works second time...
ContextProvider(ContextProviderProps {
value: Rc::clone(&routes),
children: || template! {
Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle<RouteVerdict<DomNode>>| {
match route.get().as_ref() {
// Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell!
RouteVerdict::Found(RouteInfo {
path,
template_fn,
locale
}) => app_shell(
path.clone(),
template_fn.clone(),
locale.clone(),
// We give the app shell a translations manager and let it get the `Rc<Translator>` itself (because it can do async safely)
Rc::clone(&translations_manager),
Rc::clone(&error_pages)
),
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
// Those all go to the same system that redirects to the appropriate locale
RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()),
// We handle the 404 for the user for convenience
// To get a translator here, we'd have to go async and dangerously check the URL
RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None),
}
}))
Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle<AppRoute<DomNode>>| {
match &route.get().as_ref().0 {
// Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell!
RouteVerdict::Found(RouteInfo {
path,
template,
locale
}) => app_shell(
path.clone(),
template.clone(),
locale.clone(),
// We give the app shell a translations manager and let it get the `Rc<Translator>` itself (because it can do async safely)
Rc::clone(&translations_manager),
Rc::clone(&error_pages)
),
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
// Those all go to the same system that redirects to the appropriate locale
RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()),
// We handle the 404 for the user for convenience
// To get a translator here, we'd have to go async and dangerously check the URL
RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None),
}
})
}))
}
},
&root,
Expand Down
2 changes: 1 addition & 1 deletion examples/cli/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Starter App</title>
<!-- Importing this runs Perseus -->
<script src="/.perseus/bundle.js" defer></script>
<script type="module" src="/.perseus/main.js" defer></script>
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 2 additions & 2 deletions examples/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ define_app! {
root: "#root",
error_pages: crate::error_pages::get_error_pages(),
templates: [
"/" => crate::pages::index::get_page::<G>(),
"/about" => crate::pages::about::get_page::<G>()
crate::pages::index::get_page::<G>(),
crate::pages::about::get_page::<G>()
],
locales: {
default: "en-US",
Expand Down
5 changes: 2 additions & 3 deletions examples/i18n/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ define_app! {
root: "#root",
error_pages: crate::error_pages::get_error_pages(),
templates: [
"/about" => crate::templates::about::get_template::<G>(),
// Note that the index page comes last, otherwise locale detection for `/about` matches `about` as a locale
"/" => crate::templates::index::get_template::<G>()
crate::templates::about::get_template::<G>(),
crate::templates::index::get_template::<G>()
],
locales: {
default: "en-US",
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Showcase App</title>
<!-- Importing this runs Perseus -->
<script src="/.perseus/bundle.js" defer></script>
<script type="module" src="/.perseus/main.js" defer></script>
</head>
<body>
<div id="root"></div>
Expand Down
16 changes: 8 additions & 8 deletions examples/showcase/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ define_app! {
root: "#root",
error_pages: crate::error_pages::get_error_pages(),
templates: [
"/" => crate::templates::index::get_template::<G>(),
"/about" => crate::templates::about::get_template::<G>(),
"/post/new" => crate::templates::new_post::get_template::<G>(),
crate::templates::index::get_template::<G>(),
crate::templates::about::get_template::<G>(),
crate::templates::new_post::get_template::<G>(),
// BUG: Sycamore doesn't support dynamic paths before dynamic segments (https://github.com/sycamore-rs/sycamore/issues/228)
"/post/<slug..>" => crate::templates::post::get_template::<G>(),
"/ip" => crate::templates::ip::get_template::<G>(),
"/time" => crate::templates::time_root::get_template::<G>(),
"/timeisr/<slug>" => crate::templates::time::get_template::<G>(),
"/amalgamation" => crate::templates::amalgamation::get_template::<G>()
crate::templates::post::get_template::<G>(),
crate::templates::ip::get_template::<G>(),
crate::templates::time_root::get_template::<G>(),
crate::templates::time::get_template::<G>(),
crate::templates::amalgamation::get_template::<G>()
],
locales: {
default: "en-US",
Expand Down
1 change: 1 addition & 0 deletions packages/perseus-actix-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ perseus = { path = "../perseus", version = "0.1.4" }
actix-web = "3.3"
actix-files = "0.5"
urlencoding = "2.1"
serde = "1"
serde_json = "1"
error-chain = "0.12"
futures = "0.3"
Expand Down
33 changes: 29 additions & 4 deletions packages/perseus-actix-web/src/configurer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::page_data::page_data;
use crate::translations::translations;
use actix_files::NamedFile;
use actix_web::web;
use actix_web::{web, HttpResponse};
use perseus::{get_render_cfg, ConfigManager, Locales, SsrNode, TemplateMap, TranslationsManager};
use std::collections::HashMap;
use std::fs;

/// The options for setting up the Actix Web integration. This should be literally constructed, as nothing is optional.
#[derive(Clone)]
Expand All @@ -22,6 +24,17 @@ pub struct Options {
pub locales: Locales,
}

async fn render_conf(
render_conf: web::Data<HashMap<String, String>>,
) -> web::Json<HashMap<String, String>> {
web::Json(render_conf.get_ref().clone())
}
/// This returns the HTML index file with the render configuration injected as a JS global variable.
async fn index(index_with_render_cfg: web::Data<String>) -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html")
.body(index_with_render_cfg.get_ref())
}
async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.js_bundle)
}
Expand All @@ -31,9 +44,6 @@ async fn js_init(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
async fn wasm_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.wasm_bundle)
}
async fn index(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
NamedFile::open(&opts.index)
}

/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments.
pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'static>(
Expand All @@ -44,21 +54,36 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
let render_cfg = get_render_cfg(&config_manager)
.await
.expect("Couldn't get render configuration!");
// Get the index file and inject the render configuration into ahead of time
// We do this by injecting a script that defines the render config as a global variable, which we put just before the close of the head
let index_file = fs::read_to_string(&opts.index).expect("Couldn't get HTML index file!");
let index_with_render_cfg = index_file.replace(
"</head>",
// It's safe to assume that something we just deserialized will serialize again in this case
&format!(
"<script>window.__PERSEUS_RENDER_CFG = '{}';</script>\n</head>",
serde_json::to_string(&render_cfg).unwrap()
),
);

move |cfg: &mut web::ServiceConfig| {
cfg
// We implant the render config in the app data for better performance, it's needed on every request
.data(render_cfg.clone())
.data(config_manager.clone())
.data(translations_manager.clone())
.data(opts.clone())
.data(index_with_render_cfg.clone())
// TODO chunk JS and Wasm bundles
// These allow getting the basic app code (not including the static data)
// This contains everything in the spirit of a pseudo-SPA
.route("/.perseus/main.js", web::get().to(js_init))
.route("/.perseus/bundle.js", web::get().to(js_bundle))
.route("/.perseus/bundle.wasm", web::get().to(wasm_bundle))
.route("/.perseus/render_conf.json", web::get().to(render_conf))
// This allows getting the static HTML/JSON of a page
// We stream both together in a single JSON object so SSR works (otherwise we'd have request IDs and weird caching...)
// A request to this should also provide the template name (routing should only be done once on the client) as a query parameter
.route(
"/.perseus/page/{locale}/{filename:.*}",
web::get().to(page_data::<C, T>),
Expand Down
13 changes: 10 additions & 3 deletions packages/perseus-actix-web/src/page_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ use crate::conv_req::convert_req;
use crate::Options;
use actix_web::{http::StatusCode, web, HttpRequest, HttpResponse};
use perseus::{err_to_status_code, get_page, ConfigManager, TranslationsManager};
use std::collections::HashMap;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct PageDataReq {
pub template_name: String,
}

/// The handler for calls to `.perseus/page/*`. This will manage returning errors and the like.
pub async fn page_data<C: ConfigManager, T: TranslationsManager>(
req: HttpRequest,
opts: web::Data<Options>,
render_cfg: web::Data<HashMap<String, String>>,
config_manager: web::Data<C>,
translations_manager: web::Data<T>,
web::Query(query_params): web::Query<PageDataReq>,
) -> HttpResponse {
let templates = &opts.templates_map;
let locale = req.match_info().query("locale");
// TODO get the template name from the query
let template_name = query_params.template_name;
// Check if the locale is supported
if opts.locales.is_supported(locale) {
let path = req.match_info().query("filename");
Expand All @@ -30,8 +37,8 @@ pub async fn page_data<C: ConfigManager, T: TranslationsManager>(
let page_data = get_page(
path,
locale,
&template_name,
http_req,
&render_cfg,
templates,
config_manager.get_ref(),
translations_manager.get_ref(),
Expand Down
1 change: 1 addition & 0 deletions packages/perseus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ http = "0.2"
async-trait = "0.1"
fluent-bundle = { version = "0.15", optional = true }
unic-langid = { version = "0.9", optional = true }
js-sys = "0.3"

[features]
default = ["translator-fluent", "translator-dflt-fluent"]
Expand Down
17 changes: 2 additions & 15 deletions packages/perseus/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ macro_rules! define_app {
root: $root_selector:literal,
error_pages: $error_pages:expr,
templates: [
$($router_path:literal => $template:expr),+
$($template:expr),+
],
// This deliberately enforces verbose i18n definition, and forces developers to consider i18n as integral
locales: {
Expand All @@ -117,20 +117,7 @@ macro_rules! define_app {
$(,translations_manager: $translations_manager:expr)?
} => {
/// The CSS selector that will find the app root to render Perseus in.
pub const APP_ROUTE: &str = $root_selector;

/// Gets the routes for the app in Perseus' custom abstraction over Sycamore's routing logic. This enables tight coupling of
/// the templates and the routing system. This can be used on the client or server side.
pub fn get_routes<G: $crate::GenericNode>() -> $crate::router::Routes<G> {
$crate::router::Routes::new(
vec![
$(
($router_path.to_string(), $template)
),+
],
get_locales()
)
}
pub const APP_ROOT: &str = $root_selector;

/// Gets the config manager to use. This allows the user to conveniently test production managers in development. If nothing is
/// given, the filesystem will be used.
Expand Down
Loading

0 comments on commit 78688c1

Please sign in to comment.