Skip to main content

cpex_core/extensions/
guarded.rs

1// Location: ./crates/cpex-core/src/extensions/guarded.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// Guarded<T> — capability-gated write access.
7//
8// A value that requires a WriteToken for mutable access. Read access
9// is always available (if the plugin can see the extension at all).
10// Write access requires the framework to issue a WriteToken based on
11// the plugin's declared capabilities.
12//
13// Mirrors the spec in rust-implementation-spec.md §2.3.
14
15use serde::{Deserialize, Serialize};
16
17/// A value that requires a WriteToken for mutable access.
18///
19/// Read access via `.read()` is always available. Write access via
20/// `.write(token)` requires a `WriteToken` proving the caller has
21/// the capability.
22///
23/// The framework issues write tokens only to plugins that declared
24/// the corresponding write capability (e.g., `write_headers`).
25/// Plugin code without a token cannot call `.write()`.
26#[derive(Clone, Debug, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct Guarded<T> {
29    inner: T,
30}
31
32impl<T> Guarded<T> {
33    /// Wrap a value in a guard.
34    pub fn new(value: T) -> Self {
35        Self { inner: value }
36    }
37
38    /// Read access — always available if the plugin can see this extension.
39    pub fn read(&self) -> &T {
40        &self.inner
41    }
42
43    /// Write access — requires a WriteToken proving the caller has capability.
44    ///
45    /// The framework issues WriteTokens only to plugins that declared
46    /// the write capability in their config. Without the token, this
47    /// method is uncallable — the plugin can read but not write.
48    pub fn write(&mut self, _token: &WriteToken) -> &mut T {
49        &mut self.inner
50    }
51
52    /// Consume the guard, returning the inner value.
53    pub fn into_inner(self) -> T {
54        self.inner
55    }
56}
57
58impl<T: Default> Default for Guarded<T> {
59    fn default() -> Self {
60        Self {
61            inner: T::default(),
62        }
63    }
64}
65
66/// Opaque token for write access — only the framework can create one.
67///
68/// `pub(crate)` constructor means plugin crates cannot mint tokens.
69/// The executor creates tokens based on the plugin's declared
70/// capabilities from `PluginConfig`.
71pub struct WriteToken {
72    _private: (),
73}
74
75impl WriteToken {
76    /// Only callable by the framework (pub(crate)).
77    /// Plugin crates cannot construct this.
78    pub(crate) fn new() -> Self {
79        Self { _private: () }
80    }
81}
82
83// WriteToken is not Clone, not Copy — each plugin gets its own from the executor.
84// It's also not Send/Sync by default (no auto-traits on zero-sized private fields).
85// We explicitly mark it safe since it's just a capability proof with no data.
86unsafe impl Send for WriteToken {}
87unsafe impl Sync for WriteToken {}
88
89impl std::fmt::Debug for WriteToken {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.write_str("WriteToken")
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_guarded_read_without_token() {
101        let guarded = Guarded::new(42);
102        assert_eq!(*guarded.read(), 42);
103    }
104
105    #[test]
106    fn test_guarded_write_with_token() {
107        let mut guarded = Guarded::new(42);
108        let token = WriteToken::new();
109        *guarded.write(&token) = 100;
110        assert_eq!(*guarded.read(), 100);
111    }
112
113    #[test]
114    fn test_guarded_serde_transparent() {
115        let guarded = Guarded::new("hello".to_string());
116        let json = serde_json::to_string(&guarded).unwrap();
117        assert_eq!(json, "\"hello\"");
118        let deserialized: Guarded<String> = serde_json::from_str(&json).unwrap();
119        assert_eq!(*deserialized.read(), "hello");
120    }
121
122    #[test]
123    fn test_guarded_with_struct() {
124        use std::collections::HashMap;
125
126        #[derive(Clone, Debug, Default, Serialize, Deserialize)]
127        struct Headers {
128            map: HashMap<String, String>,
129        }
130
131        let mut guarded = Guarded::new(Headers::default());
132        let token = WriteToken::new();
133
134        // Read — no token needed
135        assert!(guarded.read().map.is_empty());
136
137        // Write — token required
138        guarded
139            .write(&token)
140            .map
141            .insert("X-Auth".into(), "Bearer tok".into());
142        assert_eq!(guarded.read().map.get("X-Auth").unwrap(), "Bearer tok");
143    }
144}