cardinal_zip/
generate.rs

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}