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
use crate::errors::PluginError;
use crate::plugins::*;
use std::any::Any;
use std::collections::HashMap;
/// A control action, which can only be taken by one plugin. When run, control
/// actions will return an `Option<R>` on what their runners return, which will
/// be `None` if no runner is set.
pub struct ControlPluginAction<A, R> {
/// The name of the plugin that controls this action. As this is a control
/// action, only one plugin can manage a single action.
controller_name: String,
/// The single runner function for this action. This may not be defined if
/// no plugin takes this action.
runner: Option<Runner<A, R>>,
}
impl<A, R> PluginAction<A, R, Option<R>> for ControlPluginAction<A, R> {
/// Runs the single registered runner for the action.
fn run(
&self,
action_data: A,
plugin_data: &HashMap<String, Box<dyn Any + Send + Sync>>,
) -> Result<Option<R>, PluginError> {
// If no runner is defined, this won't have any effect (same as functional
// actions with no registered runners)
self.runner
.as_ref()
.map(|runner| {
runner(
&action_data,
// We must have data registered for every active plugin (even if it's empty)
&**plugin_data.get(&self.controller_name).unwrap_or_else(|| {
panic!(
"no plugin data for registered plugin {}",
&self.controller_name
)
}),
)
.map_err(|err| PluginError {
name: self.controller_name.to_string(),
source: err,
})
})
// Turn `Option<Result<T, E>>` -> `Result<Option<T>, E>`
.transpose()
}
fn register_plugin(
&mut self,
name: &str,
runner: impl Fn(&A, &(dyn Any + Send + Sync)) -> Result<R, Box<dyn std::error::Error + Send + Sync>>
+ Send
+ Sync
+ 'static,
) {
self.register_plugin_box(name, Box::new(runner))
}
fn register_plugin_box(&mut self, name: &str, runner: Runner<A, R>) {
// Check if the action has already been taken by another plugin
if self.runner.is_some() {
// We panic here because an explicitly requested plugin couldn't be loaded, so
// we have to assume that any further behavior in the engine is unwanted
// Therefore, a graceful error would be inappropriate, this is critical in every
// sense
panic!("attempted to register runner from plugin '{}' for control action that already had a registered runner from plugin '{}' (these plugins conflict, see the book for further details)", name, self.controller_name);
}
self.controller_name = name.to_string();
self.runner = Some(runner);
}
}
// Using a default implementation allows us to avoid the action data having to
// implement `Default` as well, which is frequently infeasible
impl<A, R> Default for ControlPluginAction<A, R> {
fn default() -> Self {
Self {
controller_name: String::default(),
runner: None,
}
}
}
impl<A, R> std::fmt::Debug for ControlPluginAction<A, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlPluginAction")
.field("controller_name", &self.controller_name)
.finish_non_exhaustive()
}
}
/// All the control actions that a plugin can take.
#[derive(Default, Debug)]
pub struct ControlPluginActions {
/// Actions pertaining to the modification of settings created with
/// `PerseusApp`.
pub settings_actions: ControlPluginSettingsActions,
/// Actions pertaining to the build process.
pub build_actions: ControlPluginBuildActions,
/// Actions pertaining to the export process.
pub export_actions: ControlPluginExportActions,
/// Actions pertaining to the server.
pub server_actions: ControlPluginServerActions,
/// Actions pertaining to the client-side code.
pub client_actions: ControlPluginClientActions,
}
/// Control actions that pertain to altering settings from `PerseusApp`.
#[derive(Default, Debug)]
pub struct ControlPluginSettingsActions {
/// Sets an immutable store to be used everywhere. This will provided the
/// current immutable store for reference.
pub set_immutable_store:
ControlPluginAction<crate::stores::ImmutableStore, crate::stores::ImmutableStore>,
/// Sets the locales to be used everywhere, providing the current ones for
/// reference.
pub set_locales: ControlPluginAction<crate::i18n::Locales, crate::i18n::Locales>,
/// Sets the app root to be used everywhere. This must correspond to the ID
/// of an empty HTML `div`.
pub set_app_root: ControlPluginAction<(), String>,
/// Actions pertaining to the HTML shell, partitioned away for deliberate
/// inconvenience (you should almost never use these).
pub html_shell_actions: ControlPluginHtmlShellActions,
}
/// Control actions that pertain to the HTML shell. Note that these actions
/// should be used extremely sparingly, as they are very rarely needed (see the
/// available functional actions for the HTML shell), and they can have
/// confusing side effects for CSS hierarchies, as well as potentially
/// interrupting Perseus' interpolation processes. Changing certain things
/// with these may break Perseus completely in certain cases!
#[derive(Default, Debug)]
pub struct ControlPluginHtmlShellActions {
/// Overrides whatever the user provided as their HTML shell completely.
/// Whatever you provide here MUST contain a `<head>` and a `<body>` at
/// least, or Perseus will completely fail.
pub set_shell: ControlPluginAction<(), String>,
}
/// Control actions that pertain to the build process.
#[derive(Default, Debug)]
pub struct ControlPluginBuildActions {}
/// Control actions that pertain to the export process.
#[derive(Default, Debug)]
pub struct ControlPluginExportActions {}
/// Control actions that pertain to the server.
#[derive(Default, Debug)]
pub struct ControlPluginServerActions {}
/// Control actions that pertain to the client-side code. As yet, there are none
/// of these.
#[derive(Default, Debug)]
pub struct ControlPluginClientActions {}