mqdb-core 0.5.1

Core types, storage, schema, and protocol for MQDB
Documentation
// Copyright 2025-2026 LabOverWire. All rights reserved.
// SPDX-License-Identifier: AGPL-3.0-only

use crate::error::{Error, Result};
use crate::keys;
use serde_json::Value;

#[derive(Clone)]
pub struct Entity {
    pub name: String,
    pub id: String,
    pub data: Value,
}

impl Entity {
    #[allow(clippy::must_use_candidate)]
    pub fn new(name: String, id: String, data: Value) -> Self {
        Self { name, id, data }
    }

    /// # Errors
    /// Returns an error if the data doesn't have an 'id' field.
    pub fn from_json(name: String, mut data: Value) -> Result<Self> {
        let id = data
            .get("id")
            .and_then(Value::as_str)
            .map(std::string::ToString::to_string)
            .or_else(|| {
                data.get("id")
                    .and_then(Value::as_i64)
                    .map(|n| n.to_string())
            })
            .ok_or_else(|| Error::Validation("missing 'id' field".into()))?;

        if let Value::Object(ref mut obj) = data {
            obj.remove("id");
        }

        Ok(Self { name, id, data })
    }

    #[must_use]
    pub fn to_json(&self) -> Value {
        let mut data = self.data.clone();
        if let Value::Object(ref mut obj) = data {
            obj.insert("id".to_string(), Value::String(self.id.clone()));
        }
        data
    }

    #[must_use]
    pub fn key(&self) -> Vec<u8> {
        keys::encode_data_key(&self.name, &self.id)
    }

    /// # Errors
    /// Returns an error if serialization fails.
    pub fn serialize(&self) -> Result<Vec<u8>> {
        let json_data = serde_json::to_vec(&self.data)?;
        Ok(crate::checksum::encode_with_checksum(&json_data))
    }

    /// # Errors
    /// Returns an error if checksum verification fails or data is invalid.
    pub fn deserialize(name: String, id: String, data: &[u8]) -> Result<Self> {
        let json_data = crate::checksum::decode_and_verify(data).map_err(|err| {
            tracing::warn!("checksum verification failed for {name}/{id}: {err}");
            Error::Corruption {
                entity: name.clone(),
                id: id.clone(),
            }
        })?;
        let value: Value = serde_json::from_slice(json_data)?;
        Ok(Self {
            name,
            id,
            data: value,
        })
    }

    #[must_use]
    pub fn get_field(&self, field: &str) -> Option<&Value> {
        self.data.get(field)
    }

    #[must_use]
    pub fn extract_index_values(&self, fields: &[String]) -> Vec<(String, Vec<u8>)> {
        let mut values = Vec::new();

        for field in fields {
            if let Some(value) = self.get_field(field)
                && let Ok(encoded) = keys::encode_value_for_index(value)
            {
                values.push((field.clone(), encoded));
            }
        }

        values
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_entity_from_json() {
        let data = json!({
            "id": "123",
            "name": "John",
            "email": "john@example.com"
        });

        let entity = Entity::from_json("users".into(), data).unwrap();
        assert_eq!(entity.id, "123");
        assert_eq!(entity.name, "users");
        assert_eq!(entity.get_field("name").unwrap(), "John");
    }

    #[test]
    fn test_entity_to_json() {
        let entity = Entity::new("users".into(), "123".into(), json!({"name": "John"}));

        let json = entity.to_json();
        assert_eq!(json["id"], "123");
        assert_eq!(json["name"], "John");
    }
}