Skip to main content

aegis_document/
types.rs

1//! Aegis Document Types
2//!
3//! Core data types for document storage.
4//!
5//! @version 0.1.0
6//! @author AutomataNexus Development Team
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value as JsonValue;
10use std::collections::HashMap;
11use std::fmt;
12
13// =============================================================================
14// Document ID
15// =============================================================================
16
17/// Unique identifier for a document.
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct DocumentId(pub String);
20
21impl DocumentId {
22    pub fn new(id: impl Into<String>) -> Self {
23        Self(id.into())
24    }
25
26    pub fn generate() -> Self {
27        Self(uuid())
28    }
29
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35impl fmt::Display for DocumentId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}", self.0)
38    }
39}
40
41impl From<String> for DocumentId {
42    fn from(s: String) -> Self {
43        Self(s)
44    }
45}
46
47impl From<&str> for DocumentId {
48    fn from(s: &str) -> Self {
49        Self(s.to_string())
50    }
51}
52
53fn uuid() -> String {
54    use std::time::{SystemTime, UNIX_EPOCH};
55    let duration = SystemTime::now()
56        .duration_since(UNIX_EPOCH)
57        .unwrap_or_default();
58    let nanos = duration.as_nanos();
59    let random: u64 = (nanos as u64).wrapping_mul(0x5851_f42d_4c95_7f2d);
60    format!("{:016x}{:016x}", nanos as u64, random)
61}
62
63// =============================================================================
64// Value
65// =============================================================================
66
67/// A document value that can be any JSON-compatible type.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(untagged)]
70#[derive(Default)]
71pub enum Value {
72    #[default]
73    Null,
74    Bool(bool),
75    Int(i64),
76    Float(f64),
77    String(String),
78    Array(Vec<Value>),
79    Object(HashMap<String, Value>),
80}
81
82impl Value {
83    pub fn is_null(&self) -> bool {
84        matches!(self, Self::Null)
85    }
86
87    pub fn is_bool(&self) -> bool {
88        matches!(self, Self::Bool(_))
89    }
90
91    pub fn is_number(&self) -> bool {
92        matches!(self, Self::Int(_) | Self::Float(_))
93    }
94
95    pub fn is_string(&self) -> bool {
96        matches!(self, Self::String(_))
97    }
98
99    pub fn is_array(&self) -> bool {
100        matches!(self, Self::Array(_))
101    }
102
103    pub fn is_object(&self) -> bool {
104        matches!(self, Self::Object(_))
105    }
106
107    pub fn as_bool(&self) -> Option<bool> {
108        match self {
109            Self::Bool(b) => Some(*b),
110            _ => None,
111        }
112    }
113
114    pub fn as_i64(&self) -> Option<i64> {
115        match self {
116            Self::Int(n) => Some(*n),
117            Self::Float(f) => Some(*f as i64),
118            _ => None,
119        }
120    }
121
122    pub fn as_f64(&self) -> Option<f64> {
123        match self {
124            Self::Float(f) => Some(*f),
125            Self::Int(n) => Some(*n as f64),
126            _ => None,
127        }
128    }
129
130    pub fn as_str(&self) -> Option<&str> {
131        match self {
132            Self::String(s) => Some(s),
133            _ => None,
134        }
135    }
136
137    pub fn as_array(&self) -> Option<&Vec<Value>> {
138        match self {
139            Self::Array(arr) => Some(arr),
140            _ => None,
141        }
142    }
143
144    pub fn as_object(&self) -> Option<&HashMap<String, Value>> {
145        match self {
146            Self::Object(obj) => Some(obj),
147            _ => None,
148        }
149    }
150
151    /// Get a value at a path (e.g., "user.address.city").
152    pub fn get_path(&self, path: &str) -> Option<&Value> {
153        let parts: Vec<&str> = path.split('.').collect();
154        self.get_path_parts(&parts)
155    }
156
157    fn get_path_parts(&self, parts: &[&str]) -> Option<&Value> {
158        if parts.is_empty() {
159            return Some(self);
160        }
161
162        let key = parts[0];
163        let rest = &parts[1..];
164
165        match self {
166            Self::Object(obj) => obj.get(key).and_then(|v| v.get_path_parts(rest)),
167            Self::Array(arr) => key
168                .parse::<usize>()
169                .ok()
170                .and_then(|idx| arr.get(idx))
171                .and_then(|v| v.get_path_parts(rest)),
172            _ => None,
173        }
174    }
175
176    /// Convert from serde_json::Value.
177    pub fn from_json(json: JsonValue) -> Self {
178        match json {
179            JsonValue::Null => Self::Null,
180            JsonValue::Bool(b) => Self::Bool(b),
181            JsonValue::Number(n) => {
182                if let Some(i) = n.as_i64() {
183                    Self::Int(i)
184                } else if let Some(f) = n.as_f64() {
185                    Self::Float(f)
186                } else {
187                    Self::Float(0.0)
188                }
189            }
190            JsonValue::String(s) => Self::String(s),
191            JsonValue::Array(arr) => Self::Array(arr.into_iter().map(Self::from_json).collect()),
192            JsonValue::Object(obj) => Self::Object(
193                obj.into_iter()
194                    .map(|(k, v)| (k, Self::from_json(v)))
195                    .collect(),
196            ),
197        }
198    }
199
200    /// Convert to serde_json::Value.
201    pub fn to_json(&self) -> JsonValue {
202        match self {
203            Self::Null => JsonValue::Null,
204            Self::Bool(b) => JsonValue::Bool(*b),
205            Self::Int(n) => JsonValue::Number((*n).into()),
206            Self::Float(f) => serde_json::Number::from_f64(*f)
207                .map(JsonValue::Number)
208                .unwrap_or(JsonValue::Null),
209            Self::String(s) => JsonValue::String(s.clone()),
210            Self::Array(arr) => JsonValue::Array(arr.iter().map(|v| v.to_json()).collect()),
211            Self::Object(obj) => {
212                JsonValue::Object(obj.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
213            }
214        }
215    }
216}
217
218impl From<bool> for Value {
219    fn from(b: bool) -> Self {
220        Self::Bool(b)
221    }
222}
223
224impl From<i64> for Value {
225    fn from(n: i64) -> Self {
226        Self::Int(n)
227    }
228}
229
230impl From<i32> for Value {
231    fn from(n: i32) -> Self {
232        Self::Int(n as i64)
233    }
234}
235
236impl From<f64> for Value {
237    fn from(f: f64) -> Self {
238        Self::Float(f)
239    }
240}
241
242impl From<String> for Value {
243    fn from(s: String) -> Self {
244        Self::String(s)
245    }
246}
247
248impl From<&str> for Value {
249    fn from(s: &str) -> Self {
250        Self::String(s.to_string())
251    }
252}
253
254impl From<Vec<Value>> for Value {
255    fn from(arr: Vec<Value>) -> Self {
256        Self::Array(arr)
257    }
258}
259
260impl From<HashMap<String, Value>> for Value {
261    fn from(obj: HashMap<String, Value>) -> Self {
262        Self::Object(obj)
263    }
264}
265
266/// Compare two optional Value references for sorting.
267/// None values sort last. Numeric types are compared cross-type.
268pub fn compare_values(a: Option<&Value>, b: Option<&Value>) -> std::cmp::Ordering {
269    use std::cmp::Ordering;
270    match (a, b) {
271        (None, None) => Ordering::Equal,
272        (None, Some(_)) => Ordering::Greater,
273        (Some(_), None) => Ordering::Less,
274        (Some(va), Some(vb)) => compare_value_inner(va, vb),
275    }
276}
277
278fn compare_value_inner(a: &Value, b: &Value) -> std::cmp::Ordering {
279    use std::cmp::Ordering;
280    match (a, b) {
281        (Value::Null, Value::Null) => Ordering::Equal,
282        (Value::Null, _) => Ordering::Less,
283        (_, Value::Null) => Ordering::Greater,
284        (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
285        (Value::Int(a), Value::Int(b)) => a.cmp(b),
286        (Value::Float(a), Value::Float(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
287        (Value::Int(a), Value::Float(b)) => (*a as f64).partial_cmp(b).unwrap_or(Ordering::Equal),
288        (Value::Float(a), Value::Int(b)) => a.partial_cmp(&(*b as f64)).unwrap_or(Ordering::Equal),
289        (Value::String(a), Value::String(b)) => a.cmp(b),
290        _ => Ordering::Equal,
291    }
292}
293
294// =============================================================================
295// Document
296// =============================================================================
297
298/// A document in the document store.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct Document {
301    #[serde(rename = "_id")]
302    pub id: DocumentId,
303    #[serde(flatten)]
304    pub data: HashMap<String, Value>,
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub _created_at: Option<i64>,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub _updated_at: Option<i64>,
309}
310
311impl Document {
312    /// Create a new document with an auto-generated ID.
313    pub fn new() -> Self {
314        Self {
315            id: DocumentId::generate(),
316            data: HashMap::new(),
317            _created_at: Some(current_timestamp()),
318            _updated_at: None,
319        }
320    }
321
322    /// Create a document with a specific ID.
323    pub fn with_id(id: impl Into<DocumentId>) -> Self {
324        Self {
325            id: id.into(),
326            data: HashMap::new(),
327            _created_at: Some(current_timestamp()),
328            _updated_at: None,
329        }
330    }
331
332    /// Create a document from JSON.
333    pub fn from_json(json: JsonValue) -> Option<Self> {
334        match json {
335            JsonValue::Object(obj) => {
336                let id = obj
337                    .get("_id")
338                    .and_then(|v| v.as_str())
339                    .map(DocumentId::new)
340                    .unwrap_or_else(DocumentId::generate);
341
342                let data: HashMap<String, Value> = obj
343                    .into_iter()
344                    .filter(|(k, _)| !k.starts_with('_'))
345                    .map(|(k, v)| (k, Value::from_json(v)))
346                    .collect();
347
348                Some(Self {
349                    id,
350                    data,
351                    _created_at: Some(current_timestamp()),
352                    _updated_at: None,
353                })
354            }
355            _ => None,
356        }
357    }
358
359    /// Convert to JSON.
360    pub fn to_json(&self) -> JsonValue {
361        let mut obj = serde_json::Map::new();
362        obj.insert("_id".to_string(), JsonValue::String(self.id.0.clone()));
363
364        if let Some(ts) = self._created_at {
365            obj.insert("_created_at".to_string(), JsonValue::Number(ts.into()));
366        }
367        if let Some(ts) = self._updated_at {
368            obj.insert("_updated_at".to_string(), JsonValue::Number(ts.into()));
369        }
370
371        for (k, v) in &self.data {
372            obj.insert(k.clone(), v.to_json());
373        }
374
375        JsonValue::Object(obj)
376    }
377
378    /// Get a field value.
379    pub fn get(&self, key: &str) -> Option<&Value> {
380        if key.contains('.') {
381            let parts: Vec<&str> = key.splitn(2, '.').collect();
382            self.data.get(parts[0]).and_then(|v| v.get_path(parts[1]))
383        } else {
384            self.data.get(key)
385        }
386    }
387
388    /// Set a field value.
389    pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
390        self.data.insert(key.into(), value.into());
391        self._updated_at = Some(current_timestamp());
392    }
393
394    /// Remove a field.
395    pub fn remove(&mut self, key: &str) -> Option<Value> {
396        let result = self.data.remove(key);
397        if result.is_some() {
398            self._updated_at = Some(current_timestamp());
399        }
400        result
401    }
402
403    /// Check if a field exists.
404    pub fn contains(&self, key: &str) -> bool {
405        self.get(key).is_some()
406    }
407
408    /// Get all field names.
409    pub fn keys(&self) -> impl Iterator<Item = &String> {
410        self.data.keys()
411    }
412
413    /// Get the number of fields.
414    pub fn len(&self) -> usize {
415        self.data.len()
416    }
417
418    /// Check if the document is empty.
419    pub fn is_empty(&self) -> bool {
420        self.data.is_empty()
421    }
422}
423
424impl Default for Document {
425    fn default() -> Self {
426        Self::new()
427    }
428}
429
430fn current_timestamp() -> i64 {
431    use std::time::{SystemTime, UNIX_EPOCH};
432    SystemTime::now()
433        .duration_since(UNIX_EPOCH)
434        .map(|d| d.as_millis() as i64)
435        .unwrap_or(0)
436}
437
438// =============================================================================
439// Tests
440// =============================================================================
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_document_id() {
448        let id1 = DocumentId::generate();
449        let id2 = DocumentId::generate();
450        assert_ne!(id1, id2);
451
452        let id3 = DocumentId::new("custom-id");
453        assert_eq!(id3.as_str(), "custom-id");
454    }
455
456    #[test]
457    fn test_value_types() {
458        let null = Value::Null;
459        assert!(null.is_null());
460
461        let boolean = Value::Bool(true);
462        assert!(boolean.is_bool());
463        assert_eq!(boolean.as_bool(), Some(true));
464
465        let number = Value::Int(42);
466        assert!(number.is_number());
467        assert_eq!(number.as_i64(), Some(42));
468
469        let string = Value::String("hello".to_string());
470        assert!(string.is_string());
471        assert_eq!(string.as_str(), Some("hello"));
472    }
473
474    #[test]
475    fn test_value_path() {
476        let mut inner = HashMap::new();
477        inner.insert("city".to_string(), Value::String("NYC".to_string()));
478
479        let mut outer = HashMap::new();
480        outer.insert("address".to_string(), Value::Object(inner));
481
482        let value = Value::Object(outer);
483
484        assert_eq!(
485            value.get_path("address.city").and_then(|v| v.as_str()),
486            Some("NYC")
487        );
488    }
489
490    #[test]
491    fn test_document() {
492        let mut doc = Document::new();
493        doc.set("name", "Alice");
494        doc.set("age", 30i64);
495
496        assert_eq!(doc.get("name").and_then(|v| v.as_str()), Some("Alice"));
497        assert_eq!(doc.get("age").and_then(|v| v.as_i64()), Some(30));
498
499        assert!(doc.contains("name"));
500        assert!(!doc.contains("email"));
501    }
502
503    #[test]
504    fn test_document_from_json() {
505        let json = serde_json::json!({
506            "_id": "doc123",
507            "name": "Bob",
508            "active": true
509        });
510
511        let doc = Document::from_json(json).unwrap();
512        assert_eq!(doc.id.as_str(), "doc123");
513        assert_eq!(doc.get("name").and_then(|v| v.as_str()), Some("Bob"));
514        assert_eq!(doc.get("active").and_then(|v| v.as_bool()), Some(true));
515    }
516
517    #[test]
518    fn test_json_conversion() {
519        let mut doc = Document::with_id("test-doc");
520        doc.set("count", 100i64);
521        doc.set("ratio", 0.5f64);
522
523        let json = doc.to_json();
524        assert_eq!(json["_id"], "test-doc");
525        assert_eq!(json["count"], 100);
526    }
527}