Skip to main content

camel_wit/
lib.rs

1/// WIT source for the `plugin` world (standalone file).
2pub const PLUGIN_WIT: &str = include_str!("../wit/camel-plugin.wit");
3
4/// WIT source for the `bean` world (standalone file, same package as PLUGIN_WIT).
5pub const BEAN_WIT: &str = include_str!("../wit/camel-bean.wit");
6
7/// WIT source for the `source` world (standalone file, same package as PLUGIN_WIT).
8pub const SOURCE_WIT: &str = include_str!("../wit/camel-source.wit");
9
10/// Combined WIT package with both `plugin` and `bean` worlds in a single document.
11pub const FULL_WIT: &str = include_str!("../wit/camel-all.wit");
12
13// TODO(WIT-006): WIT interface versioning strategy is not yet defined.
14// Consider using `@since(version = X.Y.Z)` WIT annotations when supported by
15// the wit-bindgen / wasm-component-ld toolchain to enable compatibility checks.
16
17// ── Common content type constants ────────────────────────────────────────
18
19/// MIME type for JSON data.
20pub const APPLICATION_JSON: &str = "application/json";
21
22/// MIME type for plain text.
23pub const TEXT_PLAIN: &str = "text/plain";
24
25/// MIME type for arbitrary binary data.
26pub const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
27
28/// MIME type for HTML documents.
29pub const TEXT_HTML: &str = "text/html";
30
31/// MIME type for XML documents.
32pub const APPLICATION_XML: &str = "application/xml";
33
34/// MIME type for URL-encoded form data.
35pub const APPLICATION_FORM_URLENCODED: &str = "application/x-www-form-urlencoded";
36
37/// Absolute path to the `wit/` directory bundled with this crate.
38///
39/// Returns the `wit/` subdirectory under the crate's manifest directory,
40/// resolved at **compile time** via `CARGO_MANIFEST_DIR`. This points to the
41/// `camel-wit` source directory (local path dep or registry unpack location).
42///
43/// # Stability
44///
45/// This path is stable during builds and in development tooling, but is
46/// **not a reliable runtime path in redistributed binaries** — after
47/// `cargo install`, the source tree is no longer available and callers may
48/// see a missing-directory warning.
49///
50/// # When to use
51///
52/// Prefer the `*_WIT` string constants (`PLUGIN_WIT`, `BEAN_WIT`, `FULL_WIT`)
53/// for embedding WIT content robustly. Use this function only for CLI tooling
54/// that needs filesystem access at build/dev time (e.g. `wasm-tools`,
55/// `wit-bindgen` CLI invoked from a build script).
56pub fn wit_dir() -> &'static std::path::Path {
57    std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/wit"))
58}
59
60use std::sync::atomic::{AtomicUsize, Ordering};
61
62use camel_api::CamelError;
63
64/// Default maximum number of resources a WIT host may allocate.
65const DEFAULT_MAX_RESOURCES: usize = 1000;
66
67/// Host-side resource tracker for WIT-based WASM plugins.
68///
69/// Enforces a configurable upper bound on the number of concurrently
70/// allocated resources to prevent unbounded memory growth in the host.
71///
72/// Thread-safe: uses a CAS loop on an atomic counter so concurrent
73/// `allocate` calls cannot race past the limit.
74#[derive(Debug)]
75pub struct WitHost {
76    max_resources: usize,
77    allocation_count: AtomicUsize,
78}
79
80impl WitHost {
81    /// Creates a new `WitHost` with the default resource limit (1000).
82    pub fn new() -> Self {
83        Self::with_max_resources(DEFAULT_MAX_RESOURCES)
84    }
85
86    /// Creates a new `WitHost` with an explicit maximum resource count.
87    pub fn with_max_resources(max: usize) -> Self {
88        Self {
89            max_resources: max,
90            allocation_count: AtomicUsize::new(0),
91        }
92    }
93
94    /// Allocates a resource slot.
95    ///
96    /// Returns `Err(CamelError::ProcessorError)` if the resource limit
97    /// would be exceeded. Uses a CAS loop to avoid TOCTOU races when
98    /// called concurrently from multiple threads.
99    pub fn allocate(&self, _name: &str) -> Result<(), CamelError> {
100        let mut current = self.allocation_count.load(Ordering::Relaxed);
101        loop {
102            if current >= self.max_resources {
103                return Err(CamelError::ProcessorError("resource limit exceeded".into()));
104            }
105            match self.allocation_count.compare_exchange_weak(
106                current,
107                current + 1,
108                Ordering::AcqRel,
109                Ordering::Relaxed,
110            ) {
111                Ok(_) => return Ok(()),
112                Err(actual) => current = actual,
113            }
114        }
115    }
116
117    /// Deallocates a resource slot, freeing capacity for future allocations.
118    pub fn deallocate(&self, _name: &str) {
119        self.allocation_count.fetch_sub(1, Ordering::AcqRel);
120    }
121
122    /// Returns the current number of allocated resources.
123    pub fn resource_count(&self) -> usize {
124        self.allocation_count.load(Ordering::Acquire)
125    }
126
127    /// Returns the configured maximum resource limit.
128    pub fn max_resources(&self) -> usize {
129        self.max_resources
130    }
131}
132
133impl Default for WitHost {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_wit_host_rejects_beyond_max_resources() {
145        let host = WitHost::with_max_resources(3);
146        host.allocate("a").unwrap();
147        host.allocate("b").unwrap();
148        host.allocate("c").unwrap();
149        let result = host.allocate("d"); // must fail
150        assert!(result.is_err());
151    }
152
153    #[test]
154    fn test_wit_host_default_limit_is_1000() {
155        let host = WitHost::new();
156        assert_eq!(host.max_resources(), 1000);
157    }
158
159    #[test]
160    fn test_wit_host_allows_up_to_limit() {
161        let host = WitHost::with_max_resources(2);
162        assert!(host.allocate("x").is_ok());
163        assert!(host.allocate("y").is_ok());
164        assert!(host.allocate("z").is_err());
165    }
166
167    #[test]
168    fn test_wit_host_deallocate_frees_slot() {
169        let host = WitHost::with_max_resources(1);
170        host.allocate("a").unwrap();
171        assert!(host.allocate("b").is_err());
172        host.deallocate("a");
173        assert!(host.allocate("b").is_ok());
174    }
175
176    #[test]
177    fn test_wit_host_resource_count_tracks_allocations() {
178        let host = WitHost::new();
179        assert_eq!(host.resource_count(), 0);
180        host.allocate("a").unwrap();
181        host.allocate("b").unwrap();
182        assert_eq!(host.resource_count(), 2);
183        host.deallocate("a");
184        assert_eq!(host.resource_count(), 1);
185    }
186
187    #[test]
188    fn test_wit_host_error_is_processor_error() {
189        let host = WitHost::with_max_resources(1);
190        host.allocate("a").unwrap();
191        let err = host.allocate("b").unwrap_err();
192        assert!(matches!(err, CamelError::ProcessorError(_)));
193        assert!(err.to_string().contains("resource limit exceeded"));
194    }
195
196    // ── WIT-002: Tests for WIT definitions ──────────────────────────────────
197
198    #[test]
199    fn test_wit_dir_exists() {
200        let dir = wit_dir();
201        assert!(
202            dir.exists(),
203            "wit_dir() should point to an existing directory"
204        );
205        assert!(dir.is_dir(), "wit_dir() should be a directory");
206    }
207
208    #[test]
209    fn test_wit_dir_contains_expected_files() {
210        let dir = wit_dir();
211        assert!(
212            dir.join("camel-plugin.wit").exists(),
213            "camel-plugin.wit missing"
214        );
215        assert!(
216            dir.join("camel-bean.wit").exists(),
217            "camel-bean.wit missing"
218        );
219        assert!(dir.join("camel-all.wit").exists(), "camel-all.wit missing");
220    }
221
222    #[test]
223    fn test_plugin_wit_is_non_empty() {
224        assert!(!PLUGIN_WIT.is_empty(), "PLUGIN_WIT should not be empty");
225    }
226
227    #[test]
228    fn test_bean_wit_is_non_empty() {
229        assert!(!BEAN_WIT.is_empty(), "BEAN_WIT should not be empty");
230    }
231
232    #[test]
233    fn test_full_wit_is_non_empty() {
234        assert!(!FULL_WIT.is_empty(), "FULL_WIT should not be empty");
235    }
236
237    #[test]
238    fn test_wit_constants_contain_package_declaration() {
239        assert!(PLUGIN_WIT.contains("package camel:plugin"));
240        assert!(BEAN_WIT.contains("package camel:plugin"));
241        assert!(FULL_WIT.contains("package camel:plugin"));
242    }
243
244    #[test]
245    fn test_wit_exchange_has_route_and_message_id_fields() {
246        // WIT-005: verify route-id and message-id fields are present
247        assert!(
248            FULL_WIT.contains("route-id"),
249            "wasm-exchange should contain route-id field"
250        );
251        assert!(
252            FULL_WIT.contains("message-id"),
253            "wasm-exchange should contain message-id field"
254        );
255        assert!(
256            PLUGIN_WIT.contains("route-id"),
257            "plugin WIT should contain route-id field"
258        );
259        assert!(
260            PLUGIN_WIT.contains("message-id"),
261            "plugin WIT should contain message-id field"
262        );
263    }
264
265    #[test]
266    fn test_plugin_wit_contains_authorization_policy_world() {
267        assert!(
268            PLUGIN_WIT.contains("world authorization-policy"),
269            "PLUGIN_WIT should contain 'world authorization-policy'"
270        );
271    }
272
273    #[test]
274    fn test_plugin_wit_authorization_policy_has_evaluate() {
275        assert!(
276            PLUGIN_WIT.contains("export evaluate: func(exchange: wasm-exchange) -> result<option<string>, wasm-error>"),
277            "PLUGIN_WIT should contain evaluate export"
278        );
279    }
280
281    #[test]
282    fn test_plugin_wit_authorization_policy_has_init_with_config() {
283        assert!(
284            PLUGIN_WIT.contains(
285                "export init: func(config: list<tuple<string, string>>) -> result<_, string>"
286            ),
287            "PLUGIN_WIT should contain init with config parameter"
288        );
289    }
290
291    #[test]
292    fn test_full_wit_contains_authorization_policy_world() {
293        assert!(
294            FULL_WIT.contains("world authorization-policy"),
295            "FULL_WIT should contain 'world authorization-policy'"
296        );
297    }
298
299    fn strip_comments(wit: &str) -> String {
300        wit.lines()
301            .filter(|l| !l.trim_start().starts_with("//"))
302            .collect::<Vec<_>>()
303            .join("\n")
304            .trim()
305            .to_string()
306    }
307
308    #[test]
309    fn test_example_bean_wit_matches_canonical() {
310        let example_dir = std::path::Path::new(concat!(
311            env!("CARGO_MANIFEST_DIR"),
312            "/../../examples/wasm-bean-example/wit"
313        ));
314        if !example_dir.exists() {
315            return;
316        }
317        let example_bean = std::fs::read_to_string(example_dir.join("camel-bean.wit"))
318            .expect("read example bean wit");
319        let canonical_stripped = strip_comments(BEAN_WIT);
320        let example_stripped = strip_comments(&example_bean);
321        assert_eq!(
322            canonical_stripped, example_stripped,
323            "examples/wasm-bean-example/wit/camel-bean.wit must match canonical without comments"
324        );
325    }
326
327    #[test]
328    fn test_example_plugin_wit_has_route_id_and_message_id() {
329        let example_dir = std::path::Path::new(concat!(
330            env!("CARGO_MANIFEST_DIR"),
331            "/../../examples/wasm-bean-example/wit"
332        ));
333        if !example_dir.exists() {
334            return;
335        }
336        let example_plugin = std::fs::read_to_string(example_dir.join("camel-plugin.wit"))
337            .expect("read example plugin wit");
338        assert!(
339            example_plugin.contains("route-id"),
340            "example camel-plugin.wit must contain route-id"
341        );
342        assert!(
343            example_plugin.contains("message-id"),
344            "example camel-plugin.wit must contain message-id"
345        );
346        assert!(
347            example_plugin.contains("world authorization-policy"),
348            "example camel-plugin.wit must contain authorization-policy world"
349        );
350        assert!(
351            example_plugin.contains(
352                "export init: func(config: list<tuple<string, string>>) -> result<_, string>"
353            ),
354            "example camel-plugin.wit must contain init(config) in bean world"
355        );
356    }
357
358    #[test]
359    fn test_full_wit_has_all_worlds() {
360        // FULL_WIT (canonical camel-all.wit) is the merged reference document
361        // containing every world. The example wit dir intentionally ships only
362        // camel-plugin.wit + camel-bean.wit (compile-ready subset, no world
363        // overlap) because wit-bindgen 0.58 rejects duplicate world
364        // declarations across files in the same package.
365        assert!(
366            FULL_WIT.contains("world plugin"),
367            "FULL_WIT must contain plugin world"
368        );
369        assert!(
370            FULL_WIT.contains("world bean"),
371            "FULL_WIT must contain bean world"
372        );
373        assert!(
374            FULL_WIT.contains("world authorization-policy"),
375            "FULL_WIT must contain authorization-policy world"
376        );
377    }
378
379    #[test]
380    fn test_host_wit_matches_canonical() {
381        let host_wit_dir = std::path::Path::new(concat!(
382            env!("CARGO_MANIFEST_DIR"),
383            "/../components/camel-component-wasm/wit"
384        ));
385        if !host_wit_dir.exists() {
386            return;
387        }
388        let host_plugin = std::fs::read_to_string(host_wit_dir.join("camel-plugin.wit"))
389            .expect("read host plugin wit");
390        let canonical_stripped = strip_comments(PLUGIN_WIT);
391        let host_stripped = strip_comments(&host_plugin);
392        assert_eq!(
393            canonical_stripped, host_stripped,
394            "camel-component-wasm/wit/camel-plugin.wit must match canonical without comments"
395        );
396    }
397
398    #[test]
399    fn test_content_type_constants_compile() {
400        // Verifies the exported constants are accessible and have expected values.
401        assert_eq!(APPLICATION_JSON, "application/json");
402        assert_eq!(TEXT_PLAIN, "text/plain");
403        assert_eq!(APPLICATION_OCTET_STREAM, "application/octet-stream");
404        assert_eq!(TEXT_HTML, "text/html");
405        assert_eq!(APPLICATION_XML, "application/xml");
406        assert_eq!(
407            APPLICATION_FORM_URLENCODED,
408            "application/x-www-form-urlencoded"
409        );
410    }
411}