Skip to content

Commit 25b9808

Browse files
committed
refactor: moved showcase example into state generation example
This makes things clearer and easier to test.
1 parent d58dd29 commit 25b9808

31 files changed

+374
-431
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
/target
2-
31
.perseus/
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "perseus-example-base"
3+
version = "0.3.2"
4+
edition = "2018"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
10+
sycamore = "0.7"
11+
serde = { version = "1", features = ["derive"] }
12+
serde_json = "1"
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# State Generation Example
2+
3+
This examples shows all the ways of generating template state in Perseus, with each file representing a different way of generating state. Though this example shows many concepts, it's practical to group them all together due to their fundamental connectedness.

examples/showcase/index.html renamed to examples/core/state_generation/index.html

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Perseus Example – Showcase</title>
76
</head>
87
<body>
98
<div id="root"></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use perseus::{ErrorPages, Html};
2+
use sycamore::view;
3+
4+
pub fn get_error_pages<G: Html>() -> ErrorPages<G> {
5+
let mut error_pages = ErrorPages::new(|url, status, err, _| {
6+
view! {
7+
p { (format!("An error with HTTP code {} occurred at '{}': '{}'.", status, url, err)) }
8+
}
9+
});
10+
error_pages.add_page(404, |_, _, _, _| {
11+
view! {
12+
p { "Page not found." }
13+
}
14+
});
15+
16+
error_pages
17+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
mod error_pages;
2+
mod templates;
3+
4+
use perseus::define_app;
5+
define_app! {
6+
templates: [
7+
crate::templates::build_state::get_template::<G>(),
8+
crate::templates::build_paths::get_template::<G>(),
9+
crate::templates::request_state::get_template::<G>(),
10+
crate::templates::incremental_generation::get_template::<G>(),
11+
crate::templates::revalidation::get_template::<G>(),
12+
crate::templates::revalidation_and_incremental_generation::get_template::<G>(),
13+
crate::templates::amalgamation::get_template::<G>()
14+
],
15+
error_pages: crate::error_pages::get_error_pages()
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use perseus::{RenderFnResultWithCause, Request, States, Template};
2+
use sycamore::prelude::{view, Html, View};
3+
4+
#[perseus::make_rx(PageStateRx)]
5+
pub struct PageState {
6+
pub message: String,
7+
}
8+
9+
#[perseus::template_rx(AmalgamationPage)]
10+
pub fn amalgamation_page(state: PageStateRx) -> View<G> {
11+
view! {
12+
p { (format!("The message is: '{}'", state.message.get())) }
13+
}
14+
}
15+
16+
pub fn get_template<G: Html>() -> Template<G> {
17+
Template::new("amalgamation")
18+
// We'll generate some state at build time and some more at request time
19+
.build_state_fn(get_build_state)
20+
.request_state_fn(get_request_state)
21+
// But Perseus doesn't know which one to use, so we provide a function to unify them
22+
.amalgamate_states_fn(amalgamate_states)
23+
.template(amalgamation_page)
24+
}
25+
26+
#[perseus::autoserde(amalgamate_states)]
27+
pub fn amalgamate_states(states: States) -> RenderFnResultWithCause<Option<PageState>> {
28+
// We know they'll both be defined, and Perseus currently has to provide both as serialized strings
29+
let build_state = serde_json::from_str::<PageState>(&states.build_state.unwrap())?;
30+
let req_state = serde_json::from_str::<PageState>(&states.request_state.unwrap())?;
31+
32+
Ok(Some(PageState {
33+
message: format!(
34+
"Hello from the amalgamation! (Build says: '{}', server says: '{}'.)",
35+
build_state.message, req_state.message
36+
),
37+
}))
38+
}
39+
40+
#[perseus::autoserde(build_state)]
41+
pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
42+
Ok(PageState {
43+
message: "Hello from the build process!".to_string(),
44+
})
45+
}
46+
47+
#[perseus::autoserde(request_state)]
48+
pub async fn get_request_state(
49+
_path: String,
50+
_locale: String,
51+
_req: Request,
52+
) -> RenderFnResultWithCause<PageState> {
53+
Ok(PageState {
54+
message: "Hello from the server!".to_string(),
55+
})
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use perseus::{RenderFnResult, RenderFnResultWithCause, Template};
2+
use sycamore::prelude::{view, Html, View};
3+
4+
#[perseus::make_rx(PageStateRx)]
5+
pub struct PageState {
6+
title: String,
7+
content: String,
8+
}
9+
10+
#[perseus::template_rx(BuildPathsPage)]
11+
pub fn build_paths_page(state: PageStateRx) -> View<G> {
12+
let title = state.title;
13+
let content = state.content;
14+
view! {
15+
h1 {
16+
(title.get())
17+
}
18+
p {
19+
(content.get())
20+
}
21+
}
22+
}
23+
24+
pub fn get_template<G: Html>() -> Template<G> {
25+
Template::new("build_paths")
26+
.build_paths_fn(get_build_paths)
27+
.build_state_fn(get_build_state)
28+
.template(build_paths_page)
29+
}
30+
31+
// We'll take in the path here, which will consist of the template name `build_paths` followed by the spcific path we're building for (as exported from `get_build_paths`)
32+
#[perseus::autoserde(build_state)]
33+
pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
34+
let title = path.clone();
35+
let content = format!(
36+
"This is a post entitled '{}'. Its original slug was '{}'.",
37+
&title, &path
38+
);
39+
40+
Ok(PageState { title, content })
41+
}
42+
43+
// This just returns a vector of all the paths we want to generate for underneath `build_paths` (the template's name and root path)
44+
// Like for build state, this function is asynchronous, so you could fetch these paths from a database or the like
45+
// Note that everything you export from here will be prefixed with `<template-name>/` when it becomes a URL in your app
46+
//
47+
// Note also that there's almost no point in using build paths without build state, as every page would come out exactly the same (unless you differentiated them on the client...)
48+
pub async fn get_build_paths() -> RenderFnResult<Vec<String>> {
49+
Ok(vec!["test".to_string(), "blah/test/blah".to_string()])
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use perseus::{RenderFnResultWithCause, Template};
2+
use sycamore::prelude::{view, Html, View};
3+
4+
#[perseus::make_rx(PageStateRx)]
5+
pub struct PageState {
6+
pub greeting: String,
7+
}
8+
9+
#[perseus::template_rx(BuildStatePage)]
10+
pub fn build_state_page(state: PageStateRx) -> View<G> {
11+
view! {
12+
p { (state.greeting.get()) }
13+
}
14+
}
15+
16+
pub fn get_template<G: Html>() -> Template<G> {
17+
Template::new("build_state")
18+
.build_state_fn(get_build_state)
19+
.template(build_state_page)
20+
}
21+
22+
// We're told the path we're generating for (useless unless we're using build paths as well) and the locale (which will be `xx-XX` if we're not using i18n)
23+
// Note that this function is asynchronous, so we can do work like fetching from a server or the like here (see the `demo/fetching` example)
24+
#[perseus::autoserde(build_state)]
25+
pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
26+
Ok(PageState {
27+
greeting: "Hello World!".to_string(),
28+
})
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// This is exactly the same as the build paths example except for a few lines and some names
2+
3+
use perseus::{blame_err, RenderFnResult, RenderFnResultWithCause, Template};
4+
use sycamore::prelude::{view, Html, View};
5+
6+
#[perseus::make_rx(PageStateRx)]
7+
pub struct PageState {
8+
title: String,
9+
content: String,
10+
}
11+
12+
#[perseus::template_rx(IncrementalGenerationPage)]
13+
pub fn incremental_generation_page(state: PageStateRx) -> View<G> {
14+
let title = state.title;
15+
let content = state.content;
16+
view! {
17+
h1 {
18+
(title.get())
19+
}
20+
p {
21+
(content.get())
22+
}
23+
}
24+
}
25+
26+
pub fn get_template<G: Html>() -> Template<G> {
27+
Template::new("incremental_generation")
28+
.build_paths_fn(get_build_paths)
29+
.build_state_fn(get_build_state)
30+
// This line makes Perseus try to render any given path under the template's root path (`incremental_generation`) by putting it through `get_build_state`
31+
// If you want to filter the path because some are invalid (e.g. entries that aren't in some database), we can filter them out at the state of the build state function
32+
.incremental_generation()
33+
.template(incremental_generation_page)
34+
}
35+
36+
// We'll take in the path here, which will consist of the template name `incremental_generation` followed by the spcific path we're building for (as exported from `get_build_paths`)
37+
#[perseus::autoserde(build_state)]
38+
pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
39+
// This path is illegal, and can't be rendered
40+
// Because we're using incremental generation, we could gte literally anything as the `path`
41+
if path == "post/tests" {
42+
// This tells Perseus to return an error that's the client's fault, with the HTTP status code 404 (not found) and the message 'illegal page'
43+
// You could return this error manually, but this is more convenient
44+
blame_err!(client, 404, "illegal page");
45+
}
46+
let title = path.clone();
47+
let content = format!(
48+
"This is a post entitled '{}'. Its original slug was '{}'.",
49+
&title, &path
50+
);
51+
52+
Ok(PageState { title, content })
53+
}
54+
55+
// This just returns a vector of all the paths we want to generate for underneath `incremental_generation` (the template's name and root path)
56+
// Like for build state, this function is asynchronous, so you could fetch these paths from a database or the like
57+
// Note that everything you export from here will be prefixed with `<template-name>/` when it becomes a URL in your app
58+
//
59+
// Note also that there's almost no point in using build paths without build state, as every page would come out exactly the same (unless you differentiated them on the client...)
60+
pub async fn get_build_paths() -> RenderFnResult<Vec<String>> {
61+
Ok(vec!["test".to_string(), "blah/test/blah".to_string()])
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub mod amalgamation;
2+
pub mod build_paths;
3+
pub mod build_state;
4+
pub mod incremental_generation;
5+
pub mod request_state;
6+
pub mod revalidation;
7+
pub mod revalidation_and_incremental_generation;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use perseus::{RenderFnResultWithCause, Request, Template};
2+
use sycamore::prelude::{view, Html, View};
3+
4+
#[perseus::make_rx(PageStateRx)]
5+
pub struct PageState {
6+
ip: String,
7+
}
8+
9+
#[perseus::template_rx(RequestStatePage)]
10+
pub fn request_state_page(state: PageStateRx) -> View<G> {
11+
view! {
12+
p {
13+
(
14+
format!("Your IP address is {}.", state.ip.get())
15+
)
16+
}
17+
}
18+
}
19+
20+
pub fn get_template<G: Html>() -> Template<G> {
21+
Template::new("request_state")
22+
.request_state_fn(get_request_state)
23+
.template(request_state_page)
24+
}
25+
26+
#[perseus::autoserde(request_state)]
27+
pub async fn get_request_state(
28+
_path: String,
29+
_locale: String,
30+
// Unlike in build state, in request state we get access to the information that the user sent with their HTTP request
31+
// IN this example, we extract the browser's reporting of their IP address and display it to them
32+
req: Request,
33+
) -> RenderFnResultWithCause<PageState> {
34+
Ok(PageState {
35+
ip: format!(
36+
"{:?}",
37+
req.headers()
38+
// NOTE: This header can be trivially spoofed, and may well not be the user's actual IP address
39+
.get("X-Forwarded-For")
40+
.unwrap_or(&perseus::http::HeaderValue::from_str("hidden from view!").unwrap())
41+
),
42+
})
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use perseus::{RenderFnResultWithCause, Template};
2+
use sycamore::prelude::{view, Html, View};
3+
4+
#[perseus::make_rx(PageStateRx)]
5+
pub struct PageState {
6+
pub time: String,
7+
}
8+
9+
#[perseus::template_rx(RevalidationPage)]
10+
pub fn revalidation_page(state: PageStateRx) -> View<G> {
11+
view! {
12+
p { (format!("The time when this page was last rendered was '{}'.", state.time.get())) }
13+
}
14+
}
15+
16+
pub fn get_template<G: Html>() -> Template<G> {
17+
Template::new("revalidation")
18+
.template(revalidation_page)
19+
// This page will revalidate every five seconds (and so the time displayed will be updated)
20+
.revalidate_after("5s".to_string())
21+
// This is an alternative method of revalidation that uses logic, which will be executed every itme a user tries to
22+
// load this page. For that reason, this should NOT do long-running work, as requests will be delayed. If both this
23+
// and `revaldiate_after()` are provided, this logic will only run when `revalidate_after()` tells Perseus
24+
// that it should revalidate.
25+
.should_revalidate_fn(|| async { Ok(true) })
26+
.build_state_fn(get_build_state)
27+
}
28+
29+
// This will get the system time when the app was built
30+
#[perseus::autoserde(build_state)]
31+
pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
32+
Ok(PageState {
33+
time: format!("{:?}", std::time::SystemTime::now()),
34+
})
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// This page exists mostly for testing revalidation together with incremental generation (because the two work in complex
2+
// ways together)
3+
4+
use perseus::{RenderFnResult, RenderFnResultWithCause, Template};
5+
use sycamore::prelude::{view, Html, View};
6+
7+
#[perseus::make_rx(PageStateRx)]
8+
pub struct PageState {
9+
pub time: String,
10+
}
11+
12+
#[perseus::template_rx(RevalidationPage)]
13+
pub fn revalidation_and_incremental_generation_page(state: PageStateRx) -> View<G> {
14+
view! {
15+
p { (format!("The time when this page was last rendered was '{}'.", state.time.get())) }
16+
}
17+
}
18+
19+
pub fn get_template<G: Html>() -> Template<G> {
20+
Template::new("revalidation_and_incremental_generation")
21+
.template(revalidation_and_incremental_generation_page)
22+
// This page will revalidate every five seconds (and so the time displayed will be updated)
23+
.revalidate_after("5s".to_string())
24+
// This is an alternative method of revalidation that uses logic, which will be executed every itme a user tries to
25+
// load this page. For that reason, this should NOT do long-running work, as requests will be delayed. If both this
26+
// and `revaldiate_after()` are provided, this logic will only run when `revalidate_after()` tells Perseus
27+
// that it should revalidate.
28+
.should_revalidate_fn(|| async { Ok(true) })
29+
.build_state_fn(get_build_state)
30+
.build_paths_fn(get_build_paths)
31+
.incremental_generation()
32+
}
33+
34+
// This will get the system time when the app was built
35+
#[perseus::autoserde(build_state)]
36+
pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause<PageState> {
37+
Ok(PageState {
38+
time: format!("{:?}", std::time::SystemTime::now()),
39+
})
40+
}
41+
42+
pub async fn get_build_paths() -> RenderFnResult<Vec<String>> {
43+
Ok(vec!["test".to_string(), "blah/test/blah".to_string()])
44+
}

0 commit comments

Comments
 (0)