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