use std::collections::HashMap;
use crate::ServiceError;
#[cfg(feature = "parse")]
use crate::Value;
#[cfg(feature = "parse")]
use ron::value::Value as RonValue;
#[derive(Clone, Debug)]
pub struct RegistryEntry {
pub filename: String,
pub data: Vec<u8>,
}
#[derive(Clone, Debug, Default)]
pub struct Registry {
pub domains: HashMap<String, HashMap<String, Vec<u8>>>,
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self {
domains: HashMap::new(),
}
}
pub fn file(&self, domain: &str, filename: &str) -> Result<&[u8], String> {
self.domains
.get(domain)
.and_then(|files| files.get(filename))
.map(std::vec::Vec::as_slice)
.ok_or_else(|| format!("file '{domain}/{filename}' not found in registry"))
}
#[must_use]
pub fn has_domain(&self, name: &str) -> bool {
self.domains.contains_key(name)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.domains.is_empty()
}
pub fn domain(&self, name: &str) -> Result<&HashMap<String, Vec<u8>>, String> {
self.domains
.get(name)
.ok_or_else(|| format!("registry domain '{name}' not found"))
}
pub fn add_file(&mut self, domain: &str, filename: &str, data: Vec<u8>) {
self.domains
.entry(domain.to_owned())
.or_default()
.insert(filename.to_owned(), data);
}
pub fn domain_names(&self) -> impl Iterator<Item = &str> {
self.domains.keys().map(std::string::String::as_str)
}
#[must_use]
pub fn len(&self) -> usize {
self.domains.len()
}
}
#[cfg(feature = "parse")]
pub fn parse_registry_data(bytes: &[u8]) -> Result<Value, ServiceError> {
let s = std::str::from_utf8(bytes).map_err(|e| {
ServiceError::ParseError(format!("registry data is not valid UTF-8: {e}"))
})?;
let ron_value: RonValue = ron::from_str(s).map_err(|e| {
ServiceError::ParseError(format!("failed to parse registry data: {e}"))
})?;
Ok(convert_ron_value(ron_value))
}
#[cfg(feature = "parse")]
pub fn parse_registry_list(bytes: &[u8]) -> Result<Vec<Value>, ServiceError> {
let root = parse_registry_data(bytes)?;
match root {
Value::Array(arr) => Ok(arr),
other => Err(ServiceError::TypeMismatch {
expected: "array".into(),
found: format!("{other:?}"),
}),
}
}
#[cfg(feature = "parse")]
fn convert_ron_value(v: RonValue) -> Value {
match v {
RonValue::Unit => Value::Null,
RonValue::Bool(b) => Value::Bool(b),
RonValue::Number(n) => {
match n {
ron::value::Number::I32(i) => Value::I32(i),
ron::value::Number::I64(i) => {
if let Ok(n) = i32::try_from(i) {
Value::I32(n)
} else {
Value::F64(i as f64)
}
}
ron::value::Number::F32(f) => Value::F64(f64::from(f.0)),
ron::value::Number::F64(f) => Value::F64(f.0),
ron::value::Number::U32(u) => Value::U32(u),
ron::value::Number::U64(u) => {
if let Ok(n) = u32::try_from(u) {
Value::U32(n)
} else {
Value::F64(u as f64)
}
}
ron::value::Number::I8(i) => Value::I32(i32::from(i)),
ron::value::Number::I16(i) => Value::I32(i32::from(i)),
ron::value::Number::U8(u) => Value::U32(u32::from(u)),
ron::value::Number::U16(u) => Value::U32(u32::from(u)),
#[allow(unreachable_patterns)]
_ => Value::Null,
}
}
RonValue::Char(c) => Value::String(c.to_string()),
RonValue::String(s) => Value::String(s),
RonValue::Bytes(b) => Value::Bytes(b),
RonValue::Seq(arr) => {
Value::Array(arr.into_iter().map(convert_ron_value).collect())
}
RonValue::Map(map) => {
Value::Map(
map.into_iter()
.map(|(k, v)| (convert_ron_key(k), convert_ron_value(v)))
.collect(),
)
}
RonValue::Option(Some(v)) => convert_ron_value(*v),
RonValue::Option(None) => Value::Null,
}
}
#[cfg(feature = "parse")]
fn convert_ron_key(k: RonValue) -> String {
match k {
RonValue::String(s) => s,
RonValue::Char(c) => c.to_string(),
other => format!("{other:?}"),
}
}
#[cfg(test)]
#[cfg(feature = "parse")]
mod tests {
use super::*;
#[test]
fn test_parse_registry_data_object() {
let ron = br#"ItemDef(id: 1, name: "Wood")"#;
let value = parse_registry_data(ron).unwrap();
assert_eq!(value.get("id").unwrap().as_u32().unwrap(), 1);
assert_eq!(value.get("name").unwrap().as_str().unwrap(), "Wood");
}
#[test]
fn test_parse_registry_data_array() {
let ron = br#"[1, 2, 3]"#;
let value = parse_registry_data(ron).unwrap();
assert_eq!(value.as_array().unwrap().len(), 3);
}
#[test]
fn test_parse_registry_list_ok() {
let ron = br#"[ItemDef(id: 1), ItemDef(id: 2)]"#;
let items = parse_registry_list(ron).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].get("id").unwrap().as_u32().unwrap(), 1);
}
#[test]
fn test_parse_registry_list_not_array() {
let ron = br#"ItemDef(id: 1)"#;
let err = parse_registry_list(ron).unwrap_err();
assert!(matches!(err, ServiceError::TypeMismatch { .. }));
}
#[test]
fn test_parse_invalid_ron() {
let ron = b"not valid ron {{{";
assert!(parse_registry_data(ron).is_err());
}
#[test]
fn test_registry_add_file() {
let mut reg = Registry::new();
reg.add_file("items", "wood.ron", b"hello".to_vec());
assert!(reg.has_domain("items"));
assert!(!reg.is_empty());
assert_eq!(reg.len(), 1);
}
#[test]
fn test_registry_file_ok() {
let mut reg = Registry::new();
reg.add_file("items", "wood.ron", b"hello".to_vec());
let data = reg.file("items", "wood.ron").unwrap();
assert_eq!(data, b"hello");
}
#[test]
fn test_registry_file_not_found() {
let reg = Registry::new();
assert!(reg.file("nope", "x.ron").is_err());
}
#[test]
fn test_registry_domain() {
let mut reg = Registry::new();
reg.add_file("items", "a.ron", vec![]);
let files = reg.domain("items").unwrap();
assert_eq!(files.len(), 1);
assert!(reg.domain("nope").is_err());
}
#[test]
fn test_registry_domain_names() {
let mut reg = Registry::new();
reg.add_file("items", "a.ron", vec![]);
reg.add_file("recipes", "b.ron", vec![]);
let names: Vec<&str> = reg.domain_names().collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&"items"));
assert!(names.contains(&"recipes"));
}
#[test]
fn test_registry_default_empty() {
let reg = Registry::default();
assert!(reg.is_empty());
}
#[test]
fn test_registry_clone() {
let mut reg = Registry::new();
reg.add_file("test", "f.ron", vec![1, 2, 3]);
let cloned = reg.clone();
assert_eq!(cloned.file("test", "f.ron").unwrap(), &[1, 2, 3]);
}
}