coil-wasm 0.1.0

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

use crate::grants::StorageClassGrant;
use crate::host_api::HostServiceRequest;
use crate::invocation::InvocationContext;

use super::auth::{AuthServiceExecution, auth_details_for_request};
use super::cache::{CacheIntentExecution, cache_key_for_request};
use super::data::{DataServiceExecution, data_summary_for_request};
use super::journal::HostServiceExecutor;
use super::render::{RenderServiceExecution, render_fragment_for_request, render_fragment_key};
use super::storage::{
    StorageServiceExecution, storage_description_for_request, storage_total_for_request,
};
use super::types::{
    HostServiceExecution, HostServiceResult, JobExecution, MetadataExecution, NetworkExecution,
    SecretExecution,
};

#[derive(Debug, Clone)]
pub struct SyntheticHostServiceExecutor {
    state: Arc<Mutex<SyntheticHostServiceState>>,
}

impl HostServiceExecutor for SyntheticHostServiceExecutor {
    fn execute(
        &self,
        call: &crate::host_api::HostServiceCall,
        context: &InvocationContext,
    ) -> Result<HostServiceExecution, crate::error::WasmModelError> {
        let mut state = self
            .state
            .lock()
            .expect("synthetic host service state poisoned");
        Ok(state.execute(call.clone(), context))
    }
}

impl Default for SyntheticHostServiceExecutor {
    fn default() -> Self {
        Self {
            state: Arc::new(Mutex::new(SyntheticHostServiceState::default())),
        }
    }
}

#[derive(Debug, Default)]
struct SyntheticHostServiceState {
    auth_checks: u32,
    data_sequences: u64,
    storage_bytes_by_class: std::collections::BTreeMap<StorageClassGrant, u64>,
    render_fragments: std::collections::BTreeMap<String, String>,
    cache_hints: Vec<String>,
    metadata_writes: usize,
    job_sequences: u64,
}

impl SyntheticHostServiceState {
    fn execute(
        &mut self,
        call: crate::host_api::HostServiceCall,
        context: &InvocationContext,
    ) -> HostServiceExecution {
        let result = match &call.request {
            HostServiceRequest::Auth(request) => {
                self.auth_checks = self.auth_checks.saturating_add(1);
                let principal_id = context.principal.id.clone();
                let details = auth_details_for_request(request, context, self.auth_checks);
                HostServiceResult::Auth(AuthServiceExecution {
                    request: request.clone(),
                    allowed: true,
                    checks_seen: self.auth_checks,
                    principal_id,
                    details,
                })
            }
            HostServiceRequest::Data(request) => {
                self.data_sequences = self.data_sequences.saturating_add(1);
                HostServiceResult::Data(DataServiceExecution {
                    request: request.clone(),
                    summary: data_summary_for_request(request, context, self.data_sequences),
                    sequence: self.data_sequences,
                })
            }
            HostServiceRequest::Storage(request) => {
                let total_bytes =
                    storage_total_for_request(request, &mut self.storage_bytes_by_class, context);
                HostServiceResult::Storage(StorageServiceExecution {
                    request: request.clone(),
                    description: storage_description_for_request(request, context, total_bytes),
                    total_bytes,
                })
            }
            HostServiceRequest::Render(request) => {
                let fragment = render_fragment_for_request(request, context);
                self.render_fragments
                    .insert(render_fragment_key(request, context), fragment.clone());
                HostServiceResult::Render(RenderServiceExecution {
                    request: request.clone(),
                    fragment,
                })
            }
            HostServiceRequest::CacheIntent(request) => {
                let cache_key = cache_key_for_request(request, context, self.cache_hints.len());
                self.cache_hints.push(cache_key.clone());
                HostServiceResult::CacheIntent(CacheIntentExecution {
                    request: request.clone(),
                    cache_key,
                    applied: true,
                })
            }
            HostServiceRequest::OutboundHttp {
                integration,
                response_bytes,
            } => HostServiceResult::Network(NetworkExecution {
                integration: integration.clone(),
                endpoint: format!("synthetic://{integration}"),
                status: 200,
                response_bytes: *response_bytes,
            }),
            HostServiceRequest::SecretRead { secret } => {
                HostServiceResult::Secret(SecretExecution {
                    secret: secret.clone(),
                    source: "synthetic".to_string(),
                    value_bytes: secret.len(),
                })
            }
            HostServiceRequest::EnqueueJob { queue } => {
                self.job_sequences = self.job_sequences.saturating_add(1);
                HostServiceResult::Job(JobExecution {
                    queue: queue.clone(),
                    job_id: format!("synthetic:{queue}:{}", self.job_sequences),
                    enqueued_at_unix_seconds: self.job_sequences,
                })
            }
            HostServiceRequest::MetadataWrite { kind } => {
                self.metadata_writes = self.metadata_writes.saturating_add(1);
                HostServiceResult::Metadata(MetadataExecution {
                    kind: *kind,
                    recorded: true,
                    journal_entries: self.metadata_writes,
                })
            }
        };

        HostServiceExecution { call, result }
    }
}

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

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

    #[test]
    fn synthetic_executor_materializes_auth_results() {
        let executor = SyntheticHostServiceExecutor::default();
        let call = HostServiceCall::new(
            HostCapabilityGrant::AuthCheck,
            HostServiceRequest::Auth(AuthServiceRequest::Check),
        );
        let execution = executor.execute(&call, &context()).unwrap();
        match execution.result {
            HostServiceResult::Auth(result) => assert!(result.allowed),
            other => panic!("unexpected result: {other:?}"),
        }
    }
}