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