Skip to main content

openstranded_common_wasmcontract/
registry.rs

1use std::collections::HashMap;
2
3use crate::ServiceError;
4
5#[cfg(feature = "parse")]
6use crate::Value;
7#[cfg(feature = "parse")]
8use ron::value::Value as RonValue;
9
10// ── RegistryEntry (DTO) ────────────────────────────────────────────
11
12/// A single file from a Content Pack, indexed in the Registry.
13///
14/// This is a data-transfer object that crosses the WASM boundary.
15#[derive(Clone, Debug)]
16pub struct RegistryEntry {
17    /// Filename (e.g. "`items_edible.ron`").
18    pub filename: String,
19
20    /// Raw file bytes.
21    pub data: Vec<u8>,
22}
23
24// ── Registry (full data store) ─────────────────────────────────────
25
26/// All data from a Content Pack, grouped by domain (from manifest.toml).
27///
28/// Registry is an "in-memory filesystem". It does not know the structure of
29/// the data inside files. Each plugin decides how to parse the bytes it receives.
30///
31/// # Structure
32///
33/// ```text
34/// domains: {
35///     "items"   → { "items_edible.ron": [...bytes...], "items_material.ron": [...bytes...] },
36///     "recipes" → { "combinations.ron": [...bytes...] },
37/// }
38/// ```
39#[derive(Clone, Debug, Default)]
40pub struct Registry {
41    /// Domain → { filename → raw bytes }
42    pub domains: HashMap<String, HashMap<String, Vec<u8>>>,
43}
44
45impl Registry {
46    /// Create an empty Registry.
47    #[must_use]
48    pub fn new() -> Self {
49        Self {
50            domains: HashMap::new(),
51        }
52    }
53
54    /// Get raw bytes of a specific file within a domain.
55    ///
56    /// Returns an error message string if the domain or file is not found.
57    pub fn file(&self, domain: &str, filename: &str) -> Result<&[u8], String> {
58        self.domains
59            .get(domain)
60            .and_then(|files| files.get(filename))
61            .map(std::vec::Vec::as_slice)
62            .ok_or_else(|| format!("file '{domain}/{filename}' not found in registry"))
63    }
64
65    /// Check whether a domain exists.
66    #[must_use]
67    pub fn has_domain(&self, name: &str) -> bool {
68        self.domains.contains_key(name)
69    }
70
71    /// Whether the Registry is empty.
72    #[must_use]
73    pub fn is_empty(&self) -> bool {
74        self.domains.is_empty()
75    }
76
77    /// Get all files in a domain.
78    ///
79    /// Returns an error message string if the domain does not exist.
80    pub fn domain(&self, name: &str) -> Result<&HashMap<String, Vec<u8>>, String> {
81        self.domains
82            .get(name)
83            .ok_or_else(|| format!("registry domain '{name}' not found"))
84    }
85
86    /// Add a file to a domain.
87    pub fn add_file(&mut self, domain: &str, filename: &str, data: Vec<u8>) {
88        self.domains
89            .entry(domain.to_owned())
90            .or_default()
91            .insert(filename.to_owned(), data);
92    }
93
94    /// Iterate over all domain names.
95    pub fn domain_names(&self) -> impl Iterator<Item = &str> {
96        self.domains.keys().map(std::string::String::as_str)
97    }
98
99    /// Number of domains.
100    #[must_use]
101    pub fn len(&self) -> usize {
102        self.domains.len()
103    }
104}
105
106// ── Registry data parsing ──────────────────────────────────────────
107
108/// Parse registry data bytes into a Value tree.
109///
110/// The parsing backend is version-dependent (currently RON).
111/// This function is the single point of parsing: plugins that use it
112/// automatically get support for new data formats when the crate
113/// is upgraded.
114///
115/// # Examples
116///
117/// ```rust
118/// use openstranded_common_wasmcontract::parse_registry_data;
119///
120/// // Named struct (common in data files)
121/// let bytes = b"[ItemDef(id: 1, name: \"Wood\")]";
122/// let data = parse_registry_data(bytes).unwrap();
123/// ```
124#[cfg(feature = "parse")]
125pub fn parse_registry_data(bytes: &[u8]) -> Result<Value, ServiceError> {
126    let s = std::str::from_utf8(bytes).map_err(|e| {
127        ServiceError::ParseError(format!("registry data is not valid UTF-8: {e}"))
128    })?;
129    let ron_value: RonValue = ron::from_str(s).map_err(|e| {
130        ServiceError::ParseError(format!("failed to parse registry data: {e}"))
131    })?;
132    Ok(convert_ron_value(ron_value))
133}
134
135/// Convenience wrapper: parse bytes as an array and return elements.
136///
137/// Errors if the root value is not an array.
138///
139/// # Examples
140///
141/// ```rust
142/// use openstranded_common_wasmcontract::parse_registry_list;
143///
144/// let bytes = b"[ItemDef(id: 1), ItemDef(id: 2)]";
145/// let items = parse_registry_list(bytes).unwrap();
146/// assert_eq!(items.len(), 2);
147/// ```
148#[cfg(feature = "parse")]
149pub fn parse_registry_list(bytes: &[u8]) -> Result<Vec<Value>, ServiceError> {
150    let root = parse_registry_data(bytes)?;
151    match root {
152        Value::Array(arr) => Ok(arr),
153        other => Err(ServiceError::TypeMismatch {
154            expected: "array".into(),
155            found: format!("{other:?}"),
156        }),
157    }
158}
159
160// ── RON value conversion ───────────────────────────────────────────
161
162/// Convert a RON dynamic value to our Value.
163#[cfg(feature = "parse")]
164fn convert_ron_value(v: RonValue) -> Value {
165    match v {
166        RonValue::Unit => Value::Null,
167        RonValue::Bool(b) => Value::Bool(b),
168        RonValue::Number(n) => {
169            match n {
170                ron::value::Number::I32(i) => Value::I32(i),
171                ron::value::Number::I64(i) => {
172                    if let Ok(n) = i32::try_from(i) {
173                        Value::I32(n)
174                    } else {
175                        Value::F64(i as f64)
176                    }
177                }
178                ron::value::Number::F32(f) => Value::F64(f64::from(f.0)),
179                ron::value::Number::F64(f) => Value::F64(f.0),
180                ron::value::Number::U32(u) => Value::U32(u),
181                ron::value::Number::U64(u) => {
182                    if let Ok(n) = u32::try_from(u) {
183                        Value::U32(n)
184                    } else {
185                        Value::F64(u as f64)
186                    }
187                }
188                // Handle remaining integer types
189                ron::value::Number::I8(i) => Value::I32(i32::from(i)),
190                ron::value::Number::I16(i) => Value::I32(i32::from(i)),
191                ron::value::Number::U8(u) => Value::U32(u32::from(u)),
192                ron::value::Number::U16(u) => Value::U32(u32::from(u)),
193                #[allow(unreachable_patterns)]
194                _ => Value::Null,
195            }
196        }
197        RonValue::Char(c) => Value::String(c.to_string()),
198        RonValue::String(s) => Value::String(s),
199        RonValue::Bytes(b) => Value::Bytes(b),
200        RonValue::Seq(arr) => {
201            Value::Array(arr.into_iter().map(convert_ron_value).collect())
202        }
203        RonValue::Map(map) => {
204            Value::Map(
205                map.into_iter()
206                    .map(|(k, v)| (convert_ron_key(k), convert_ron_value(v)))
207                    .collect(),
208            )
209        }
210        RonValue::Option(Some(v)) => convert_ron_value(*v),
211        RonValue::Option(None) => Value::Null,
212    }
213}
214
215/// Convert a RON map key (which can be any Value) to a String.
216#[cfg(feature = "parse")]
217fn convert_ron_key(k: RonValue) -> String {
218    match k {
219        RonValue::String(s) => s,
220        RonValue::Char(c) => c.to_string(),
221        other => format!("{other:?}"),
222    }
223}
224
225// ── Tests ──────────────────────────────────────────────────────────
226
227#[cfg(test)]
228#[cfg(feature = "parse")]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_parse_registry_data_object() {
234        let ron = br#"ItemDef(id: 1, name: "Wood")"#;
235        let value = parse_registry_data(ron).unwrap();
236        assert_eq!(value.get("id").unwrap().as_u32().unwrap(), 1);
237        assert_eq!(value.get("name").unwrap().as_str().unwrap(), "Wood");
238    }
239
240    #[test]
241    fn test_parse_registry_data_array() {
242        let ron = br#"[1, 2, 3]"#;
243        let value = parse_registry_data(ron).unwrap();
244        assert_eq!(value.as_array().unwrap().len(), 3);
245    }
246
247    #[test]
248    fn test_parse_registry_list_ok() {
249        let ron = br#"[ItemDef(id: 1), ItemDef(id: 2)]"#;
250        let items = parse_registry_list(ron).unwrap();
251        assert_eq!(items.len(), 2);
252        assert_eq!(items[0].get("id").unwrap().as_u32().unwrap(), 1);
253    }
254
255    #[test]
256    fn test_parse_registry_list_not_array() {
257        let ron = br#"ItemDef(id: 1)"#;
258        let err = parse_registry_list(ron).unwrap_err();
259        assert!(matches!(err, ServiceError::TypeMismatch { .. }));
260    }
261
262    #[test]
263    fn test_parse_invalid_ron() {
264        let ron = b"not valid ron {{{";
265        assert!(parse_registry_data(ron).is_err());
266    }
267
268    #[test]
269    fn test_registry_add_file() {
270        let mut reg = Registry::new();
271        reg.add_file("items", "wood.ron", b"hello".to_vec());
272        assert!(reg.has_domain("items"));
273        assert!(!reg.is_empty());
274        assert_eq!(reg.len(), 1);
275    }
276
277    #[test]
278    fn test_registry_file_ok() {
279        let mut reg = Registry::new();
280        reg.add_file("items", "wood.ron", b"hello".to_vec());
281        let data = reg.file("items", "wood.ron").unwrap();
282        assert_eq!(data, b"hello");
283    }
284
285    #[test]
286    fn test_registry_file_not_found() {
287        let reg = Registry::new();
288        assert!(reg.file("nope", "x.ron").is_err());
289    }
290
291    #[test]
292    fn test_registry_domain() {
293        let mut reg = Registry::new();
294        reg.add_file("items", "a.ron", vec![]);
295        let files = reg.domain("items").unwrap();
296        assert_eq!(files.len(), 1);
297        assert!(reg.domain("nope").is_err());
298    }
299
300    #[test]
301    fn test_registry_domain_names() {
302        let mut reg = Registry::new();
303        reg.add_file("items", "a.ron", vec![]);
304        reg.add_file("recipes", "b.ron", vec![]);
305        let names: Vec<&str> = reg.domain_names().collect();
306        assert_eq!(names.len(), 2);
307        assert!(names.contains(&"items"));
308        assert!(names.contains(&"recipes"));
309    }
310
311    #[test]
312    fn test_registry_default_empty() {
313        let reg = Registry::default();
314        assert!(reg.is_empty());
315    }
316
317    #[test]
318    fn test_registry_clone() {
319        let mut reg = Registry::new();
320        reg.add_file("test", "f.ron", vec![1, 2, 3]);
321        let cloned = reg.clone();
322        assert_eq!(cloned.file("test", "f.ron").unwrap(), &[1, 2, 3]);
323    }
324}