coil-wasm 0.1.0

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

use crate::error::WasmModelError;
use crate::grants::HostCapabilityGrant;
use crate::ids::{ExtensionPointKind, HttpMethod};
use crate::validation::{validate_route, validate_token};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageExtensionPoint {
    pub route: String,
    pub methods: BTreeSet<HttpMethod>,
}

impl PageExtensionPoint {
    pub fn new(
        route: impl Into<String>,
        methods: impl IntoIterator<Item = HttpMethod>,
    ) -> Result<Self, WasmModelError> {
        let route = validate_route("page_route", route.into())?;
        let methods = methods.into_iter().collect::<BTreeSet<_>>();

        if methods.is_empty() {
            return Err(WasmModelError::EmptyField {
                field: "page_methods",
            });
        }

        for method in &methods {
            if !matches!(
                method,
                HttpMethod::Get | HttpMethod::Head | HttpMethod::Post
            ) {
                return Err(WasmModelError::UnsupportedPageMethod { method: *method });
            }
        }

        Ok(Self { route, methods })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiExtensionPoint {
    pub route: String,
    pub methods: BTreeSet<HttpMethod>,
}

impl ApiExtensionPoint {
    pub fn new(
        route: impl Into<String>,
        methods: impl IntoIterator<Item = HttpMethod>,
    ) -> Result<Self, WasmModelError> {
        let route = validate_route("api_route", route.into())?;
        let methods = methods.into_iter().collect::<BTreeSet<_>>();

        if methods.is_empty() {
            return Err(WasmModelError::EmptyField {
                field: "api_methods",
            });
        }

        Ok(Self { route, methods })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JobExtensionPoint {
    pub job_name: String,
    pub queue: String,
}

impl JobExtensionPoint {
    pub fn new(
        job_name: impl Into<String>,
        queue: impl Into<String>,
    ) -> Result<Self, WasmModelError> {
        Ok(Self {
            job_name: validate_token("job_name", job_name.into())?,
            queue: validate_token("queue", queue.into())?,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduledJobExtensionPoint {
    pub job_name: String,
    pub schedule: String,
}

impl ScheduledJobExtensionPoint {
    pub fn new(
        job_name: impl Into<String>,
        schedule: impl Into<String>,
    ) -> Result<Self, WasmModelError> {
        Ok(Self {
            job_name: validate_token("scheduled_job_name", job_name.into())?,
            schedule: crate::validation::require_non_empty("schedule", schedule.into())?,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebhookExtensionPoint {
    pub source: String,
    pub event: String,
}

impl WebhookExtensionPoint {
    pub fn new(
        source: impl Into<String>,
        event: impl Into<String>,
    ) -> Result<Self, WasmModelError> {
        Ok(Self {
            source: validate_token("webhook_source", source.into())?,
            event: validate_token("webhook_event", event.into())?,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdminWidgetExtensionPoint {
    pub slot: String,
}

impl AdminWidgetExtensionPoint {
    pub fn new(slot: impl Into<String>) -> Result<Self, WasmModelError> {
        Ok(Self {
            slot: validate_token("admin_widget_slot", slot.into())?,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderHookExtensionPoint {
    pub slot: String,
}

impl RenderHookExtensionPoint {
    pub fn new(slot: impl Into<String>) -> Result<Self, WasmModelError> {
        Ok(Self {
            slot: validate_token("render_hook_slot", slot.into())?,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtensionPoint {
    Page(PageExtensionPoint),
    Api(ApiExtensionPoint),
    Job(JobExtensionPoint),
    ScheduledJob(ScheduledJobExtensionPoint),
    Webhook(WebhookExtensionPoint),
    AdminWidget(AdminWidgetExtensionPoint),
    RenderHook(RenderHookExtensionPoint),
}

impl ExtensionPoint {
    pub fn kind(&self) -> ExtensionPointKind {
        match self {
            Self::Page(_) => ExtensionPointKind::Page,
            Self::Api(_) => ExtensionPointKind::Api,
            Self::Job(_) => ExtensionPointKind::Job,
            Self::ScheduledJob(_) => ExtensionPointKind::ScheduledJob,
            Self::Webhook(_) => ExtensionPointKind::Webhook,
            Self::AdminWidget(_) => ExtensionPointKind::AdminWidget,
            Self::RenderHook(_) => ExtensionPointKind::RenderHook,
        }
    }

    pub(crate) fn supports_grant(&self, grant: &HostCapabilityGrant) -> bool {
        match grant {
            HostCapabilityGrant::RenderFragment { .. } => {
                matches!(
                    self,
                    Self::Page(_) | Self::AdminWidget(_) | Self::RenderHook(_)
                )
            }
            HostCapabilityGrant::MetadataWrite { .. } | HostCapabilityGrant::CacheHintWrite => {
                matches!(
                    self,
                    Self::Page(_) | Self::Api(_) | Self::AdminWidget(_) | Self::RenderHook(_)
                )
            }
            _ => true,
        }
    }
}