coil-wasm 0.1.1

WASM extension runtime and host APIs for the Coil framework.
Documentation
use std::collections::BTreeSet;
use std::fmt;
use std::time::Duration;

use crate::error::WasmModelError;
use crate::ids::ExtensionPointKind;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StorageClassGrant {
    PublicUpload,
    PrivateShared,
    LocalOnlySensitive,
    PublicAsset,
}

impl fmt::Display for StorageClassGrant {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::PublicUpload => f.write_str("public_upload"),
            Self::PrivateShared => f.write_str("private_shared"),
            Self::LocalOnlySensitive => f.write_str("local_only_sensitive"),
            Self::PublicAsset => f.write_str("public_asset"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MetadataGrant {
    JsonLd,
    SitemapEntry,
    Translation,
    SeoHead,
}

impl fmt::Display for MetadataGrant {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::JsonLd => f.write_str("json_ld"),
            Self::SitemapEntry => f.write_str("sitemap_entry"),
            Self::Translation => f.write_str("translation"),
            Self::SeoHead => f.write_str("seo_head"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum HostCapabilityGrant {
    DataRead { resource: String },
    DataWrite { resource: String },
    AuthCheck,
    AuthList,
    AuthLookup,
    AuthTupleWrite,
    StorageRead { class: StorageClassGrant },
    StorageWrite { class: StorageClassGrant },
    RenderFragment { slot: String },
    MetadataWrite { kind: MetadataGrant },
    CacheHintWrite,
    OutboundHttp { integration: String },
    SecretRead { secret: String },
    EnqueueJob { queue: String },
}

impl fmt::Display for HostCapabilityGrant {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DataRead { resource } => write!(f, "data.read:{resource}"),
            Self::DataWrite { resource } => write!(f, "data.write:{resource}"),
            Self::AuthCheck => f.write_str("auth.check"),
            Self::AuthList => f.write_str("auth.list"),
            Self::AuthLookup => f.write_str("auth.lookup"),
            Self::AuthTupleWrite => f.write_str("auth.tuple_write"),
            Self::StorageRead { class } => write!(f, "storage.read:{class}"),
            Self::StorageWrite { class } => write!(f, "storage.write:{class}"),
            Self::RenderFragment { slot } => write!(f, "render.fragment:{slot}"),
            Self::MetadataWrite { kind } => write!(f, "metadata.write:{kind}"),
            Self::CacheHintWrite => f.write_str("cache.hint.write"),
            Self::OutboundHttp { integration } => write!(f, "http.outbound:{integration}"),
            Self::SecretRead { secret } => write!(f, "secret.read:{secret}"),
            Self::EnqueueJob { queue } => write!(f, "job.enqueue:{queue}"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HostGrantSet {
    grants: BTreeSet<HostCapabilityGrant>,
}

impl HostGrantSet {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn from_grants(grants: impl IntoIterator<Item = HostCapabilityGrant>) -> Self {
        let mut set = Self::new();
        for grant in grants {
            set.insert(grant);
        }
        set
    }

    pub fn insert(&mut self, grant: HostCapabilityGrant) {
        self.grants.insert(grant);
    }

    pub fn contains(&self, grant: &HostCapabilityGrant) -> bool {
        self.grants.contains(grant)
    }

    pub fn is_subset_of(&self, other: &Self) -> bool {
        self.grants.iter().all(|grant| other.contains(grant))
    }

    pub fn len(&self) -> usize {
        self.grants.len()
    }

    pub fn iter(&self) -> impl Iterator<Item = &HostCapabilityGrant> {
        self.grants.iter()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceLimits {
    pub max_runtime: Duration,
    pub max_memory_bytes: u64,
    pub max_outbound_requests: u32,
    pub max_outbound_response_bytes: u64,
    pub max_storage_writes: u32,
    pub max_storage_bytes: u64,
    pub max_concurrency: u16,
}

impl ResourceLimits {
    pub const fn new(
        max_runtime: Duration,
        max_memory_bytes: u64,
        max_outbound_requests: u32,
        max_outbound_response_bytes: u64,
        max_storage_writes: u32,
        max_storage_bytes: u64,
        max_concurrency: u16,
    ) -> Self {
        Self {
            max_runtime,
            max_memory_bytes,
            max_outbound_requests,
            max_outbound_response_bytes,
            max_storage_writes,
            max_storage_bytes,
            max_concurrency,
        }
    }

    pub fn baseline_for(point: ExtensionPointKind) -> Self {
        match point {
            ExtensionPointKind::Page
            | ExtensionPointKind::Api
            | ExtensionPointKind::AdminWidget
            | ExtensionPointKind::RenderHook => Self::new(
                Duration::from_secs(2),
                64 * 1024 * 1024,
                4,
                4 * 1024 * 1024,
                2,
                8 * 1024 * 1024,
                32,
            ),
            ExtensionPointKind::Webhook => Self::new(
                Duration::from_secs(5),
                64 * 1024 * 1024,
                6,
                8 * 1024 * 1024,
                2,
                8 * 1024 * 1024,
                16,
            ),
            ExtensionPointKind::Job | ExtensionPointKind::ScheduledJob => Self::new(
                Duration::from_secs(30),
                128 * 1024 * 1024,
                20,
                16 * 1024 * 1024,
                16,
                64 * 1024 * 1024,
                4,
            ),
        }
    }

    pub fn validate(&self) -> Result<(), WasmModelError> {
        if self.max_runtime.is_zero() {
            return Err(WasmModelError::ZeroLimit {
                field: "max_runtime",
            });
        }
        if self.max_memory_bytes == 0 {
            return Err(WasmModelError::ZeroLimit {
                field: "max_memory_bytes",
            });
        }
        if self.max_outbound_requests == 0 {
            return Err(WasmModelError::ZeroLimit {
                field: "max_outbound_requests",
            });
        }
        if self.max_outbound_response_bytes == 0 {
            return Err(WasmModelError::ZeroLimit {
                field: "max_outbound_response_bytes",
            });
        }
        if self.max_storage_writes == 0 {
            return Err(WasmModelError::ZeroLimit {
                field: "max_storage_writes",
            });
        }
        if self.max_storage_bytes == 0 {
            return Err(WasmModelError::ZeroLimit {
                field: "max_storage_bytes",
            });
        }
        if self.max_concurrency == 0 {
            return Err(WasmModelError::ZeroLimit {
                field: "max_concurrency",
            });
        }

        Ok(())
    }

    pub(crate) fn ensure_no_looser_than(
        &self,
        declared: &Self,
        handler_id: &crate::ids::HandlerId,
    ) -> Result<(), WasmModelError> {
        let checks = [
            (self.max_runtime <= declared.max_runtime, "max_runtime"),
            (
                self.max_memory_bytes <= declared.max_memory_bytes,
                "max_memory_bytes",
            ),
            (
                self.max_outbound_requests <= declared.max_outbound_requests,
                "max_outbound_requests",
            ),
            (
                self.max_outbound_response_bytes <= declared.max_outbound_response_bytes,
                "max_outbound_response_bytes",
            ),
            (
                self.max_storage_writes <= declared.max_storage_writes,
                "max_storage_writes",
            ),
            (
                self.max_storage_bytes <= declared.max_storage_bytes,
                "max_storage_bytes",
            ),
            (
                self.max_concurrency <= declared.max_concurrency,
                "max_concurrency",
            ),
        ];

        for (passes, field) in checks {
            if !passes {
                return Err(WasmModelError::LimitOverrideExceedsDeclared {
                    handler_id: handler_id.to_string(),
                    field,
                });
            }
        }

        Ok(())
    }
}