standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! Scripted HTTP for daemon plugins under test: `daemon::http` requests go
//! to the [`MockHost`], which checks the `fetch:` grants exactly as
//! `standardd` does, records every request it lets through, and answers
//! each from the first queued response whose method and URL prefix match.

use alloc::collections::VecDeque;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use super::MockHost;
use crate::daemon::http::{Request, Response, Target, too_long};
use crate::error::{Error, Result};

/// One queued answer: for requests of `method` (any when `*`) whose URL
/// starts with `prefix`.
#[derive(Clone, Debug)]
struct Scripted {
    method: String,
    prefix: String,
    answer: Result<Response>,
}

/// The HTTP state of a [`MockHost`].
#[derive(Clone, Debug, Default)]
pub(crate) struct MockHttp {
    queue: VecDeque<Scripted>,
    requests: Vec<Request>,
}

impl MockHost {
    /// Queues `response` for the next request of `method` (`GET`, `POST`,
    /// ...; `*` for any) whose URL starts with `url_prefix`. Each queued
    /// answer is used once, in the order queued; a request nothing matches
    /// fails with `Error::Unavailable`.
    pub fn http(mut self, method: &str, url_prefix: &str, response: Response) -> Self {
        self.queue_http(method, url_prefix, Ok(response));
        self
    }

    /// Queues an answer, a failure included (`Error::Unavailable("timeout")`
    /// is what a request past its timeout gets), on a running host:
    /// `harness.host(|host| host.queue_http(...))`.
    pub fn queue_http(&mut self, method: &str, url_prefix: &str, answer: Result<Response>) {
        self.http.queue.push_back(Scripted {
            method: method.to_ascii_uppercase(),
            prefix: url_prefix.to_string(),
            answer,
        });
    }

    /// Every request the grants let through, in order, as the plugin built
    /// it (method, URL, headers, body, timeout, body cap).
    pub fn http_requests(&self) -> &[Request] {
        &self.http.requests
    }

    /// Answers queued and not used yet.
    pub fn http_pending(&self) -> usize {
        self.http.queue.len()
    }

    fn send_http(&mut self, request: &Request, target: &Target<'_>) -> Result<Response> {
        if !self
            .grants
            .allows_fetch(target.scheme, &target.host(), target.port())
        {
            let Error::GrantDenied { grant } = target.denied() else {
                unreachable!("a fetch refusal names its grant")
            };
            self.denials.push(grant.clone());
            return Err(Error::GrantDenied { grant });
        }
        self.http.requests.push(request.clone());
        let method = request.method.to_ascii_uppercase();
        let index = self.http.queue.iter().position(|scripted| {
            (scripted.method == "*" || scripted.method == method)
                && request.url.starts_with(&scripted.prefix)
        });
        let Some(scripted) = index.and_then(|index| self.http.queue.remove(index)) else {
            return Err(Error::Unavailable(alloc::format!(
                "no response queued for {method} {}",
                request.url
            )));
        };
        let response = scripted.answer?;
        if response.body.len() > request.max_body {
            return Err(too_long(request.max_body));
        }
        Ok(response)
    }
}

/// `daemon::http`'s transport natively.
pub(crate) fn send(request: &Request, target: &Target<'_>) -> Result<Response> {
    super::with_host(|host| host.send_http(request, target))
}