#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::sync::Arc;
use super::manifest::SandboxCapability;
use super::plugin::SandboxedPlugin;
#[derive(Clone, Default)]
pub struct RenderSlots {
declared: Vec<String>,
plugins: Vec<Arc<SandboxedPlugin>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SlotError {
UndeclaredSlot {
plugin: String,
slot: String,
},
}
impl std::fmt::Display for SlotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UndeclaredSlot { plugin, slot } => write!(
f,
"sandboxed plugin `{plugin}` was granted the render slot {slot:?}, which this \
application does not declare; declare it or do not install this plugin"
),
}
}
}
impl std::error::Error for SlotError {}
impl std::fmt::Debug for RenderSlots {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RenderSlots")
.field("declared", &self.declared)
.field(
"plugins",
&self
.plugins
.iter()
.map(|plugin| plugin.manifest().name.as_str())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl RenderSlots {
#[must_use]
pub fn declaring(slots: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
declared: slots.into_iter().map(Into::into).collect(),
plugins: Vec::new(),
}
}
#[must_use]
pub fn declared(&self) -> &[String] {
&self.declared
}
pub fn with(mut self, plugin: Arc<SandboxedPlugin>) -> Result<Self, SlotError> {
let manifest = plugin.manifest();
if manifest.is_granted(SandboxCapability::Render) {
for slot in &manifest.grants.slots {
if !self.declared.iter().any(|declared| declared == slot) {
return Err(SlotError::UndeclaredSlot {
plugin: manifest.name.clone(),
slot: slot.clone(),
});
}
}
}
self.plugins.push(plugin);
Ok(self)
}
pub async fn render(&self, slot: &str, context: &[(String, String)]) -> String {
if !self.declared.iter().any(|declared| declared == slot) {
tracing::warn!(
slot,
"a handler asked for a render slot this application does not declare"
);
return String::new();
}
let mut out = String::new();
for plugin in &self.plugins {
let manifest = plugin.manifest();
if !manifest.is_granted(SandboxCapability::Render)
|| !manifest.grants.allows(SandboxCapability::Render, slot)
{
continue;
}
if let Some(fragment) = plugin.render_slot(slot, context).await {
out.push_str(&fragment);
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin_sandbox::{SandboxArtifact, SandboxManifest};
fn manifest_toml(slots: &str) -> String {
format!(
r#"
name = "shop"
version = "0.1.0"
wire_version = 1
prefix = "/shop"
capabilities = ["http-request", "render"]
sha256 = "{digest}"
[[routes]]
method = "GET"
path = "/shop/panel"
[grants]
slots = [{slots}]
"#,
digest = "a".repeat(64)
)
}
fn plugin(slots: &str) -> Arc<SandboxedPlugin> {
let manifest = SandboxManifest::parse(&manifest_toml(slots)).expect("valid");
let module = wat::parse_str(super::super::test_guests::RENDER_CLIENT).expect("valid WAT");
let artifact = SandboxArtifact::seal(manifest, module).expect("seals");
Arc::new(SandboxedPlugin::from_artifact(&artifact).expect("loads"))
}
#[test]
fn a_plugin_naming_a_slot_the_app_never_declared_is_refused_at_boot() {
let slots = RenderSlots::declaring(["order-summary"]);
assert_eq!(
slots.with(plugin("\"checkout-total\"")).err(),
Some(SlotError::UndeclaredSlot {
plugin: "shop".to_owned(),
slot: "checkout-total".to_owned(),
})
);
}
#[tokio::test]
async fn a_slot_neither_half_agreed_on_renders_nothing() {
let slots = RenderSlots::declaring(["order-summary", "sidebar"])
.with(plugin("\"order-summary\""))
.expect("registers");
assert!(!slots.render("order-summary", &[]).await.is_empty());
assert!(slots.render("sidebar", &[]).await.is_empty());
assert!(slots.render("checkout-total", &[]).await.is_empty());
}
#[tokio::test]
async fn a_failing_hook_contributes_nothing_and_the_page_still_renders() {
let slots = RenderSlots::declaring(["order-summary", "unsafe-tag"])
.with(plugin("\"order-summary\", \"unsafe-tag\""))
.expect("registers");
assert!(slots.render("unsafe-tag", &[]).await.is_empty());
assert!(!slots.render("order-summary", &[]).await.is_empty());
}
}