1use std::collections::HashMap;
2
3use toml::Value;
4
5use crate::{CZip, CZipV1};
6
7#[derive(Debug, Clone)]
8pub struct LatestCzip {
9 config: Value,
10 plugins: HashMap<String, Vec<u8>>,
11}
12
13impl LatestCzip {
14 pub fn new(config: Value, plugins: HashMap<String, Vec<u8>>) -> Self {
15 Self { config, plugins }
16 }
17
18 pub fn config(&self) -> &Value {
19 &self.config
20 }
21
22 pub fn plugins(&self) -> &HashMap<String, Vec<u8>> {
23 &self.plugins
24 }
25
26 pub fn into_parts(self) -> (Value, HashMap<String, Vec<u8>>) {
27 (self.config, self.plugins)
28 }
29}
30
31pub fn generate_latest(opts: LatestCzip) -> CZip {
32 let (config, plugins) = opts.into_parts();
33 CZip::V1(CZipV1::with_plugins(config, plugins))
34}
35
36pub fn generate_latest_bin(opts: LatestCzip) -> Vec<u8> {
37 generate_latest(opts).into()
38}
39
40#[cfg(target_arch = "wasm32")]
41mod wasm {
42 use super::{generate_latest_bin, LatestCzip};
43 use std::collections::HashMap;
44
45 use js_sys::{Array, Object, Reflect, Uint8Array};
46 use toml::Value;
47 use wasm_bindgen::prelude::*;
48 use wasm_bindgen::JsCast;
49
50 #[wasm_bindgen(js_name = generateLatestCzip)]
51 pub fn generate_latest_czip(config_toml: String, plugins: JsValue) -> Result<Vec<u8>, JsValue> {
52 let config = toml::from_str::<Value>(&config_toml)
53 .map_err(|err| JsValue::from_str(&format!("invalid CZIP configuration: {err}")))?;
54 let plugin_map = parse_plugins(plugins)?;
55 Ok(generate_latest_bin(LatestCzip::new(config, plugin_map)))
56 }
57
58 fn parse_plugins(raw: JsValue) -> Result<HashMap<String, Vec<u8>>, JsValue> {
59 if raw.is_null() || raw.is_undefined() {
60 return Ok(HashMap::new());
61 }
62
63 let object = raw
64 .dyn_into::<Object>()
65 .map_err(|_| JsValue::from_str("plugins must be a plain object"))?;
66 let entries = Object::entries(&object);
67 let mut plugins = HashMap::with_capacity(entries.length() as usize);
68
69 for idx in 0..entries.length() {
70 let entry = entries.get(idx);
71 let entry = Array::from(&entry);
72 let name = entry
73 .get(0)
74 .as_string()
75 .ok_or_else(|| JsValue::from_str("plugin names must be strings"))?;
76 let value = entry.get(1);
77 let bytes = Uint8Array::new(&value).to_vec();
78 plugins.insert(name, bytes);
79 }
80
81 Ok(plugins)
82 }
83
84 #[cfg(test)]
85 mod tests {
86 use super::generate_latest_czip;
87 use crate::{generate_latest_bin, CZip, LatestCzip};
88 use js_sys::{Object, Reflect, Uint8Array};
89 use std::collections::HashMap;
90 use wasm_bindgen::JsValue;
91 use wasm_bindgen_test::*;
92
93 #[wasm_bindgen_test]
94 fn generates_archive_from_js_inputs() {
95 let config = r#"
96 [gateway]
97 version = "1.0"
98 "#
99 .to_string();
100
101 let plugins_js = Object::new();
102 let payload = Uint8Array::from(&[0xAA, 0xBB, 0xCC][..]);
103 Reflect::set(&plugins_js, &JsValue::from_str("logger"), &payload.into())
104 .expect("should set plugin payload");
105
106 let bytes = generate_latest_czip(config.clone(), JsValue::from(plugins_js))
107 .expect("wasm binding should succeed");
108
109 let archive = CZip::try_from(bytes.as_slice()).expect("archive should deserialize");
110
111 let mut expected_plugins = HashMap::new();
112 expected_plugins.insert("logger".to_string(), vec![0xAA, 0xBB, 0xCC]);
113
114 match archive {
115 CZip::V1(inner) => {
116 let expected_config: toml::Value =
117 toml::from_str(&config).expect("config should parse for comparison");
118 assert_eq!(inner.config(), &expected_config);
119 assert_eq!(inner.plugins(), &expected_plugins);
120 }
121 }
122 }
123
124 #[wasm_bindgen_test]
125 fn js_generation_matches_rust() {
126 let config_src = r#"
127 [gateway]
128 version = "1.0"
129 "#;
130 let config_value: toml::Value =
131 toml::from_str(config_src).expect("config should parse for native generation");
132
133 let mut native_plugins = HashMap::new();
134 native_plugins.insert("logger".to_string(), vec![0x01, 0x02, 0x03]);
135 native_plugins.insert("metrics".to_string(), vec![0xAA, 0xBB]);
136
137 let native_bytes = generate_latest_bin(LatestCzip::new(
138 config_value.clone(),
139 native_plugins.clone(),
140 ));
141
142 let plugins_js = Object::new();
143 for (name, payload) in native_plugins.iter() {
144 let array = Uint8Array::from(payload.as_slice());
145 Reflect::set(&plugins_js, &JsValue::from_str(name), &array.into())
146 .expect("should map plugin payload into JS object");
147 }
148
149 let wasm_bytes =
150 generate_latest_czip(config_src.to_string(), JsValue::from(plugins_js))
151 .expect("wasm binding should succeed");
152
153 assert_eq!(
154 wasm_bytes, native_bytes,
155 "WASM output should match native payload"
156 );
157
158 let archive =
159 CZip::try_from(wasm_bytes.as_slice()).expect("archive should deserialize");
160 match archive {
161 CZip::V1(inner) => {
162 assert_eq!(inner.config(), &config_value);
163 assert_eq!(inner.plugins(), &native_plugins);
164 }
165 }
166 }
167 }
168}
169
170#[cfg(target_arch = "wasm32")]
171pub use wasm::generate_latest_czip;
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use std::collections::HashMap;
177 use toml::Value;
178
179 #[test]
180 fn generates_czip_payload() {
181 let config: Value = toml::from_str(
182 r#"
183 [gateway]
184 version = "1.0"
185 "#,
186 )
187 .expect("config should parse");
188
189 let mut plugins = HashMap::new();
190 plugins.insert("logger".to_string(), vec![1, 2, 3]);
191
192 let bytes = generate_latest_bin(LatestCzip::new(config.clone(), plugins.clone()));
193 let archive = crate::CZip::try_from(bytes.as_slice()).expect("archive should deserialize");
194
195 match archive {
196 crate::CZip::V1(inner) => {
197 assert_eq!(inner.config(), &config);
198 assert_eq!(inner.plugins(), &plugins);
199 }
200 }
201 }
202}