Skip to main content

cc_lb_plugin_wire/
schema.rs

1//! Wire versioning contract shared between host and plugin authors.
2//!
3//! The host and each plugin agree on a `WireVersion` per hook. Plugin
4//! authors declare their hook's version in `#[hook(...)]` metadata, and
5//! the host verifies at admission time that the declared version is in
6//! the host's supported list AND that the wasm's embedded layout
7//! fingerprint matches the host's computed fingerprint.
8
9use core::fmt;
10
11/// Wire schema version. Distinct enum values indicate incompatible
12/// struct layouts. Bump when adding/removing/reordering fields of ANY
13/// hook's wire type. Once V2 is added, V1 stays as a supported legacy
14/// version until deprecated by explicit host-list removal.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[repr(u8)]
17pub enum WireVersion {
18    V1 = 1,
19    V2 = 2,
20}
21
22impl WireVersion {
23    pub const fn as_u8(self) -> u8 {
24        self as u8
25    }
26
27    pub const fn from_u8(v: u8) -> Option<Self> {
28        match v {
29            1 => Some(Self::V1),
30            2 => Some(Self::V2),
31            _ => None,
32        }
33    }
34
35    pub const fn as_str(self) -> &'static str {
36        match self {
37            Self::V1 => "v1",
38            Self::V2 => "v2",
39        }
40    }
41}
42
43impl fmt::Display for WireVersion {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.write_str(self.as_str())
46    }
47}
48
49/// Plugin hook kind. Each hook is a distinct wire contract.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub enum HookKind {
52    Filter,
53    Shape,
54    Observe,
55    TransformResponse,
56    TransformSseEvent,
57}
58
59impl HookKind {
60    pub const fn as_str(self) -> &'static str {
61        match self {
62            Self::Filter => "filter",
63            Self::Shape => "shape",
64            Self::Observe => "observe",
65            Self::TransformResponse => "transform_response",
66            Self::TransformSseEvent => "transform_sse_event",
67        }
68    }
69
70    /// Wasm function export name for this hook (guest-side).
71    pub const fn export_name(self) -> &'static str {
72        match self {
73            Self::Filter => "cc_lb_filter",
74            Self::Shape => "cc_lb_shape",
75            Self::Observe => "cc_lb_observe",
76            Self::TransformResponse => "cc_lb_transform_response",
77            Self::TransformSseEvent => "cc_lb_transform_sse_event",
78        }
79    }
80
81    /// Custom section name prefix. The full section name for a specific
82    /// version is `{prefix}.{version_str}` (e.g., `cc_lb.schema.filter.v1`).
83    pub const fn section_prefix(self) -> &'static str {
84        match self {
85            Self::Filter => "cc_lb.schema.filter",
86            Self::Shape => "cc_lb.schema.shape",
87            Self::Observe => "cc_lb.schema.observe",
88            Self::TransformResponse => "cc_lb.schema.transform_response",
89            Self::TransformSseEvent => "cc_lb.schema.transform_sse_event",
90        }
91    }
92
93    /// All hook kinds — for iteration.
94    pub const ALL: &'static [HookKind] = &[
95        Self::Filter,
96        Self::Shape,
97        Self::Observe,
98        Self::TransformResponse,
99        Self::TransformSseEvent,
100    ];
101
102    pub fn parse(s: &str) -> Option<Self> {
103        match s {
104            "filter" => Some(Self::Filter),
105            "shape" => Some(Self::Shape),
106            "observe" => Some(Self::Observe),
107            "transform_response" => Some(Self::TransformResponse),
108            "transform_sse_event" => Some(Self::TransformSseEvent),
109            _ => None,
110        }
111    }
112}
113
114impl fmt::Display for HookKind {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.write_str(self.as_str())
117    }
118}
119
120/// Host-supported versions per hook. Update when adding new versions.
121pub const HOST_SUPPORTED_FILTER_VERSIONS: &[WireVersion] = &[WireVersion::V1, WireVersion::V2];
122pub const HOST_SUPPORTED_SHAPE_VERSIONS: &[WireVersion] = &[WireVersion::V1];
123pub const HOST_SUPPORTED_OBSERVE_VERSIONS: &[WireVersion] = &[WireVersion::V1];
124pub const HOST_SUPPORTED_TRANSFORM_RESPONSE_VERSIONS: &[WireVersion] = &[WireVersion::V1];
125pub const HOST_SUPPORTED_TRANSFORM_SSE_EVENT_VERSIONS: &[WireVersion] = &[WireVersion::V1];
126
127pub fn host_supported_versions(hook: HookKind) -> &'static [WireVersion] {
128    match hook {
129        HookKind::Filter => HOST_SUPPORTED_FILTER_VERSIONS,
130        HookKind::Shape => HOST_SUPPORTED_SHAPE_VERSIONS,
131        HookKind::Observe => HOST_SUPPORTED_OBSERVE_VERSIONS,
132        HookKind::TransformResponse => HOST_SUPPORTED_TRANSFORM_RESPONSE_VERSIONS,
133        HookKind::TransformSseEvent => HOST_SUPPORTED_TRANSFORM_SSE_EVENT_VERSIONS,
134    }
135}
136
137pub fn host_supports(hook: HookKind, version: WireVersion) -> bool {
138    host_supported_versions(hook).contains(&version)
139}
140
141/// Layout fingerprint published by each wire struct via the
142/// `#[derive(WireSchema)]` proc-macro (defined in
143/// `cc-lb-pdk-wasmtime-macros`). The macro walks the struct AST,
144/// serialises fields to a canonical descriptor string, then embeds
145/// `BLAKE3(descriptor)` as a 32-byte const.
146pub trait WireSchema {
147    const FINGERPRINT: [u8; 32];
148    const DESCRIPTOR: &'static str;
149}
150
151/// Packed `(out_ptr, out_len)` return value used by every guest hook export.
152pub const fn pack_ret(ptr: u32, len: u32) -> u64 {
153    ((ptr as u64) << 32) | (len as u64)
154}
155
156/// Inverse of [`pack_ret`].
157pub const fn unpack_ret(packed: u64) -> (u32, u32) {
158    let ptr = (packed >> 32) as u32;
159    let len = (packed & 0xFFFF_FFFF) as u32;
160    (ptr, len)
161}