coil-wasm 0.1.0

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

use crate::error::WasmModelError;
use crate::host_api::HostServiceCall;
use crate::invocation::InvocationContext;

use super::types::HostServiceExecution;

pub trait HostServiceExecutor: std::fmt::Debug + Send + Sync {
    fn execute(
        &self,
        call: &HostServiceCall,
        context: &InvocationContext,
    ) -> Result<HostServiceExecution, WasmModelError>;
}

#[derive(Debug, Clone, Default)]
pub struct DeniedHostServiceExecutor;

impl HostServiceExecutor for DeniedHostServiceExecutor {
    fn execute(
        &self,
        call: &HostServiceCall,
        _context: &InvocationContext,
    ) -> Result<HostServiceExecution, WasmModelError> {
        Err(WasmModelError::HostServiceUnavailable {
            handler_id: "unknown".to_string(),
            domain: call.domain(),
            reason: "no host service executor configured for this session".to_string(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct HostServiceJournal {
    executor: Arc<dyn HostServiceExecutor>,
    executions: Vec<HostServiceExecution>,
}

impl HostServiceJournal {
    pub fn new() -> Self {
        Self::with_executor(Arc::new(DeniedHostServiceExecutor::default()))
    }

    pub fn with_executor(executor: Arc<dyn HostServiceExecutor>) -> Self {
        Self {
            executor,
            executions: Vec::new(),
        }
    }

    pub fn executions(&self) -> &[HostServiceExecution] {
        &self.executions
    }

    pub fn execute(
        &mut self,
        call: HostServiceCall,
        context: &InvocationContext,
    ) -> Result<HostServiceExecution, WasmModelError> {
        let execution = self.executor.execute(&call, context)?;
        self.executions.push(execution.clone());
        Ok(execution)
    }
}

impl Default for HostServiceJournal {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::grants::HostCapabilityGrant;
    use crate::host_api::{AuthServiceRequest, HostServiceRequest};
    use crate::invocation::{
        ApiInvocation, CustomerAppContext, InvocationContext, InvocationInput, PrincipalRef,
        TraceContext,
    };

    fn context() -> InvocationContext {
        InvocationContext::new(
            CustomerAppContext::new("journal-app")
                .unwrap()
                .with_tenant_id("101")
                .unwrap()
                .with_locale("en-GB")
                .unwrap(),
            PrincipalRef::user("alice").unwrap(),
            TraceContext::new("trace-journal").unwrap(),
            InvocationInput::Api(
                ApiInvocation::new("/journal", crate::ids::HttpMethod::Get).unwrap(),
            ),
        )
    }

    #[test]
    fn denied_executor_rejects_execution() {
        let executor = DeniedHostServiceExecutor;
        let call = crate::host_api::HostServiceCall::new(
            HostCapabilityGrant::AuthCheck,
            HostServiceRequest::Auth(AuthServiceRequest::Check),
        );
        let error = executor.execute(&call, &context()).unwrap_err();
        assert!(matches!(
            error,
            WasmModelError::HostServiceUnavailable { .. }
        ));
    }
}