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
//! The internals of Perseus' state generation platform. This is not responsible
//! for the reactivity of state, or any other browser-side work. This is
//! responsible for the actual *generation* of state on the engine-side, at both
//! build-time and request-time.
//!
//! If you wanted to isolate the core of engine-side Perseus, it would be this
//! module.

mod build;
mod build_error_page;
mod export;
mod export_error_page;
mod serve;
/// This has the actual API endpoints.
mod server;
mod tinker;

pub use server::{ApiResponse, SubsequentLoadQueryParams};

use crate::{
    error_views::ErrorViews,
    errors::*,
    i18n::{Locales, TranslationsManager},
    init::{PerseusAppBase, Tm},
    plugins::Plugins,
    server::HtmlShell,
    state::{GlobalStateCreator, TemplateState},
    stores::{ImmutableStore, MutableStore},
    template::EntityMap,
};
use futures::executor::block_on;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use sycamore::web::SsrNode;

/// The Perseus state generator.
#[derive(Debug)]
pub struct Turbine<M: MutableStore, T: TranslationsManager> {
    /// All the templates and capsules in the app.
    entities: EntityMap<SsrNode>,
    /// The app's error views.
    error_views: Arc<ErrorViews<SsrNode>>,
    /// The app's locales data.
    locales: Locales,
    /// An immutable store.
    immutable_store: ImmutableStore,
    /// A mutable store.
    mutable_store: M,
    /// A translations manager.
    translations_manager: T,
    /// The global state creator.
    global_state_creator: Arc<GlobalStateCreator>,
    plugins: Arc<Plugins>,
    index_view_str: String,
    root_id: String,
    /// This is stored as a `PathBuf` so we can easily check whether or not it
    /// exists.
    pub static_dir: PathBuf,
    /// The app's static aliases.
    pub static_aliases: HashMap<String, String>,
    // --- These may not be populated at creation ---
    /// The app's render configuration, a map of paths in the app to the names
    /// of the templates that generated them. (Since templates can have
    /// multiple `/` delimiters in their names.)
    ///
    /// Since the paths are not actually valid paths, we leave them typed as
    /// `String`s, but these keys are in effect `PathWithoutLocale` instances.
    render_cfg: HashMap<String, String>,
    /// The app's global state, kept cached throughout the build process because
    /// every template we build will need access to it through context.
    global_state: TemplateState,
    /// The HTML shell that can be used for constructing the full pages this app
    /// returns.
    html_shell: Option<HtmlShell>,
}

// We want to be able to create a turbine straight from an app base
impl<M: MutableStore, T: TranslationsManager> TryFrom<PerseusAppBase<SsrNode, M, T>>
    for Turbine<M, T>
{
    type Error = PluginError;

    fn try_from(app: PerseusAppBase<SsrNode, M, T>) -> Result<Self, Self::Error> {
        let locales = app.get_locales()?;
        let immutable_store = app.get_immutable_store()?;
        let index_view_str = app.get_index_view_str();
        let root_id = app.get_root()?;
        let static_aliases = app.get_static_aliases()?;

        Ok(Self {
            entities: app.entities,
            locales,
            immutable_store,
            mutable_store: app.mutable_store,
            global_state_creator: app.global_state_creator,
            plugins: app.plugins,
            index_view_str,
            root_id,
            static_dir: PathBuf::from(&app.static_dir),
            static_aliases,
            #[cfg(debug_assertions)]
            error_views: app.error_views.unwrap_or_default(),
            #[cfg(not(debug_assertions))]
            error_views: app
                .error_views
                .expect("you must provide your own error pages in production"),
            // This consumes the app
            // Note that we can't do anything in parallel with this anyway
            translations_manager: match app.translations_manager {
                Tm::Dummy(tm) => tm,
                Tm::Full(tm) => block_on(tm),
            },

            // If we're going from a `PerseusApp`, these will be filled in later
            render_cfg: HashMap::new(),
            // This will be immediately overriden
            global_state: TemplateState::empty(),
            html_shell: None,
        })
    }
}

impl<M: MutableStore, T: TranslationsManager> Turbine<M, T> {
    /// Updates some internal fields of the turbine by assuming the app has been
    /// built in the past. This expects a number of things to exist in the
    /// filesystem. Note that calling `.build()` will automatically perform
    /// this population.
    pub async fn populate_after_build(&mut self) -> Result<(), ServerError> {
        // Get the render config
        let render_cfg_str = self.immutable_store.read("render_conf.json").await?;
        let render_cfg = serde_json::from_str::<HashMap<String, String>>(&render_cfg_str)
            .map_err(|err| ServerError::BuildError(BuildError::RenderCfgInvalid { source: err }))?;
        self.render_cfg = render_cfg;

        // Get the global state
        let global_state = self.immutable_store.read("static/global_state.json").await;
        self.global_state = match global_state {
            Ok(state) => TemplateState::from_str(&state)
                .map_err(|err| ServerError::InvalidPageState { source: err })?,
            Err(StoreError::NotFound { .. }) => TemplateState::empty(),
            Err(err) => return Err(err.into()),
        };

        let html_shell = PerseusAppBase::<SsrNode, M, T>::get_html_shell(
            self.index_view_str.to_string(),
            &self.root_id,
            &self.render_cfg,
            &self.plugins,
        )
        .await?;
        self.html_shell = Some(html_shell);

        Ok(())
    }
}