holger-plugin-abi 0.1.0

holger package-handler plugin ABI: the wire contract a handler speaks whether it is linked in or loaded from a .wasm
Documentation
//! The values that cross the boundary, and their encoders.
//!
//! These deliberately mirror `holger_traits::{ArtifactId, ArtifactEntry}` rather
//! than reusing them: `holger-traits` depends on `znippy-common`, which carries
//! arrow, io_uring and the gatling engine, so it cannot be compiled for
//! `wasm32-unknown-unknown`. The mapping between the two lives in exactly one
//! place — `holger_plugin_host::convert` — so a guest and a linked backend are
//! projected into the router through the same code.

use crate::codec::{read_result, write_result, DecodeError, Reader, Writer};

/// Bumped when the layout of anything in this module changes. The host refuses a
/// module that reports a different number: a silently mismatched ABI decodes
/// garbage into real fields, which is worse than not loading at all.
pub const ABI_VERSION: u32 = 1;

/// A package coordinate. Mirror of `holger_traits::ArtifactId`.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WireArtifactId {
    pub namespace: Option<String>,
    pub name: String,
    pub version: String,
}

impl WireArtifactId {
    pub fn encode(&self, w: &mut Writer) {
        w.opt_str(self.namespace.as_deref()).str(&self.name).str(&self.version);
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            namespace: r.opt_str("ArtifactId.namespace")?,
            name: r.str("ArtifactId.name")?,
            version: r.str("ArtifactId.version")?,
        })
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        self.encode(&mut w);
        w.finish()
    }

    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self::decode(&mut r)?;
        r.expect_end("ArtifactId")?;
        Ok(v)
    }
}

/// One listed artifact. Mirror of `holger_traits::ArtifactEntry`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireArtifactEntry {
    pub id: WireArtifactId,
    pub size_bytes: i64,
    pub content_type: String,
}

impl WireArtifactEntry {
    pub fn encode(&self, w: &mut Writer) {
        self.id.encode(w);
        w.i64(self.size_bytes).str(&self.content_type);
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            id: WireArtifactId::decode(r)?,
            size_bytes: r.i64("ArtifactEntry.size_bytes")?,
            content_type: r.str("ArtifactEntry.content_type")?,
        })
    }
}

/// What a module says about itself at load time, read once from
/// `plugin_manifest`. This is why registration can read a *directory* rather
/// than a config file: the module names itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireManifest {
    /// Must equal [`ABI_VERSION`] or the host refuses the module.
    pub abi_version: u32,
    /// The handler's own name — `skidbladnir`, `rust-toolchain`, …
    pub handler: String,
    /// The `holger_traits::ArtifactFormat` this handler reports, as its
    /// serde-lowercase spelling (`znippy`, `maven3`, …). The host parses it and
    /// **refuses an unknown one by name** rather than falling back to `raw`.
    pub format: String,
    /// Whether `put` is expected to succeed.
    pub writable: bool,
}

impl WireManifest {
    pub fn encode(&self, w: &mut Writer) {
        w.u32(self.abi_version).str(&self.handler).str(&self.format).bool(self.writable);
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            abi_version: r.u32("Manifest.abi_version")?,
            handler: r.str("Manifest.handler")?,
            format: r.str("Manifest.format")?,
            writable: r.bool("Manifest.writable")?,
        })
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        self.encode(&mut w);
        w.finish()
    }

    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self::decode(&mut r)?;
        r.expect_end("Manifest")?;
        Ok(v)
    }
}

/// `(status, headers, body)` — the established HTTP-door contract across
/// holger's backends, carried as one value.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireHttpResponse {
    pub status: u16,
    pub headers: Vec<(String, String)>,
    pub body: Vec<u8>,
}

impl WireHttpResponse {
    pub fn encode(&self, w: &mut Writer) {
        w.u16(self.status).u32(self.headers.len() as u32);
        for (k, v) in &self.headers {
            w.str(k).str(v);
        }
        w.bytes(&self.body);
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let status = r.u16("HttpResponse.status")?;
        // Each header costs at least 8 bytes on the wire, so a count is checked
        // against the bytes that remain before anything is allocated.
        let n = r.count("HttpResponse.headers")?;
        let mut headers = Vec::with_capacity(n.min(1024));
        for _ in 0..n {
            let k = r.str("HttpResponse.header.name")?;
            let v = r.str("HttpResponse.header.value")?;
            headers.push((k, v));
        }
        Ok(Self { status, headers, body: r.bytes("HttpResponse.body")? })
    }
}

/// The request half of the HTTP door.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireHttpRequest {
    pub method: String,
    pub suburl: String,
    pub body: Vec<u8>,
}

impl WireHttpRequest {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        w.str(&self.method).str(&self.suburl).bytes(&self.body);
        w.finish()
    }

    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self {
            method: r.str("HttpRequest.method")?,
            suburl: r.str("HttpRequest.suburl")?,
            body: r.bytes("HttpRequest.body")?,
        };
        r.expect_end("HttpRequest")?;
        Ok(v)
    }
}

/// Arguments to `plugin_put`.
pub struct WirePutRequest {
    pub id: WireArtifactId,
    pub data: Vec<u8>,
}

impl WirePutRequest {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        self.id.encode(&mut w);
        w.bytes(&self.data);
        w.finish()
    }

    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self { id: WireArtifactId::decode(&mut r)?, data: r.bytes("PutRequest.data")? };
        r.expect_end("PutRequest")?;
        Ok(v)
    }
}

/// Arguments to `plugin_list`.
pub struct WireListRequest {
    pub name_filter: Option<String>,
    pub limit: u32,
}

impl WireListRequest {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        w.opt_str(self.name_filter.as_deref()).u32(self.limit);
        w.finish()
    }

    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self {
            name_filter: r.opt_str("ListRequest.name_filter")?,
            limit: r.u32("ListRequest.limit")?,
        };
        r.expect_end("ListRequest")?;
        Ok(v)
    }
}

// ─── Response encoders ───────────────────────────────────────────────
//
// One function per response shape, called by BOTH the guest (to write) and the
// host (to read). A verb's two halves cannot drift apart because there is only
// one of each.

pub fn encode_fetch_response(v: &Result<Option<Vec<u8>>, String>) -> Vec<u8> {
    let mut w = Writer::new();
    write_result(&mut w, v, |w, body| {
        w.opt_bytes(body.as_deref());
    });
    w.finish()
}

pub fn decode_fetch_response(b: &[u8]) -> Result<Result<Option<Vec<u8>>, String>, DecodeError> {
    let mut r = Reader::new(b);
    let v = read_result(&mut r, "fetch", |r| r.opt_bytes("fetch.body"))?;
    r.expect_end("fetch")?;
    Ok(v)
}

pub fn encode_unit_response(v: &Result<(), String>) -> Vec<u8> {
    let mut w = Writer::new();
    write_result(&mut w, v, |_, _| {});
    w.finish()
}

pub fn decode_unit_response(b: &[u8]) -> Result<Result<(), String>, DecodeError> {
    let mut r = Reader::new(b);
    let v = read_result(&mut r, "unit", |_| Ok(()))?;
    r.expect_end("unit")?;
    Ok(v)
}

pub fn encode_list_response(v: &Result<Vec<WireArtifactEntry>, String>) -> Vec<u8> {
    let mut w = Writer::new();
    write_result(&mut w, v, |w, entries| {
        w.u32(entries.len() as u32);
        for e in entries {
            e.encode(w);
        }
    });
    w.finish()
}

pub fn decode_list_response(b: &[u8]) -> Result<Result<Vec<WireArtifactEntry>, String>, DecodeError>
{
    let mut r = Reader::new(b);
    let v = read_result(&mut r, "list", |r| {
        let n = r.count("list.count")?;
        let mut out = Vec::with_capacity(n.min(4096));
        for _ in 0..n {
            out.push(WireArtifactEntry::decode(r)?);
        }
        Ok(out)
    })?;
    r.expect_end("list")?;
    Ok(v)
}

pub fn encode_coordinate_response(v: &Option<WireArtifactId>) -> Vec<u8> {
    let mut w = Writer::new();
    match v {
        None => {
            w.u8(0);
        }
        Some(id) => {
            w.u8(1);
            id.encode(&mut w);
        }
    }
    w.finish()
}

pub fn decode_coordinate_response(b: &[u8]) -> Result<Option<WireArtifactId>, DecodeError> {
    let mut r = Reader::new(b);
    let v = match r.u8("coordinate.tag")? {
        0 => None,
        1 => Some(WireArtifactId::decode(&mut r)?),
        tag => return Err(DecodeError::BadTag { field: "coordinate.tag", tag }),
    };
    r.expect_end("coordinate")?;
    Ok(v)
}

pub fn encode_http_response(v: &Result<WireHttpResponse, String>) -> Vec<u8> {
    let mut w = Writer::new();
    write_result(&mut w, v, |w, resp| resp.encode(w));
    w.finish()
}

pub fn decode_http_response(b: &[u8]) -> Result<Result<WireHttpResponse, String>, DecodeError> {
    let mut r = Reader::new(b);
    let v = read_result(&mut r, "http", WireHttpResponse::decode)?;
    r.expect_end("http")?;
    Ok(v)
}

/// A `(key, size)` listing row from [`crate::BlobStore::list`].
pub fn encode_store_listing(rows: &[(String, u64)]) -> Vec<u8> {
    let mut w = Writer::new();
    w.u32(rows.len() as u32);
    for (k, n) in rows {
        w.str(k).u64(*n);
    }
    w.finish()
}

pub fn decode_store_listing(b: &[u8]) -> Result<Vec<(String, u64)>, DecodeError> {
    let mut r = Reader::new(b);
    let n = r.count("store_listing.count")?;
    let mut out = Vec::with_capacity(n.min(4096));
    for _ in 0..n {
        let k = r.str("store_listing.key")?;
        out.push((k, r.u64("store_listing.size")?));
    }
    r.expect_end("store_listing")?;
    Ok(out)
}

pub fn encode_store_get(v: &Option<Vec<u8>>) -> Vec<u8> {
    let mut w = Writer::new();
    w.opt_bytes(v.as_deref());
    w.finish()
}

pub fn decode_store_get(b: &[u8]) -> Result<Option<Vec<u8>>, DecodeError> {
    let mut r = Reader::new(b);
    let v = r.opt_bytes("store_get.body")?;
    r.expect_end("store_get")?;
    Ok(v)
}