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