Skip to main content

structfs_core_store/
reference.rs

1//! The Reference pattern - a placeholder for a value at another path.
2//!
3//! References enable lazy loading, pagination, and shallow responses while
4//! maintaining consistency with the underlying data model. They are the key
5//! to HATEOAS in StructFS: clients discover and navigate the API by following
6//! embedded references, not by constructing paths from out-of-band knowledge.
7
8use std::collections::BTreeMap;
9
10use crate::Value;
11
12/// Type information for a referenced value.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TypeInfo {
15    /// Type name (e.g., "integer", "string", "handle", "action", "collection").
16    pub name: String,
17
18    /// Optional schema describing the structure of the type.
19    pub schema: Option<BTreeMap<String, TypeDescriptor>>,
20}
21
22impl TypeInfo {
23    /// Create a TypeInfo with just a name.
24    pub fn new(name: impl Into<String>) -> Self {
25        Self {
26            name: name.into(),
27            schema: None,
28        }
29    }
30
31    /// Convert to a Value::Map representation.
32    pub fn to_value(&self) -> Value {
33        let mut map = BTreeMap::new();
34        map.insert("name".to_string(), Value::String(self.name.clone()));
35
36        if let Some(ref schema) = self.schema {
37            let schema_map: BTreeMap<String, Value> = schema
38                .iter()
39                .map(|(k, v)| (k.clone(), v.to_value()))
40                .collect();
41            map.insert("schema".to_string(), Value::Map(schema_map));
42        }
43
44        Value::Map(map)
45    }
46
47    /// Try to parse a TypeInfo from a Value.
48    pub fn from_value(value: &Value) -> Option<Self> {
49        let map = match value {
50            Value::Map(m) => m,
51            _ => return None,
52        };
53
54        let name = match map.get("name") {
55            Some(Value::String(s)) => s.clone(),
56            _ => return None,
57        };
58
59        let schema = map.get("schema").and_then(|v| {
60            if let Value::Map(schema_map) = v {
61                let mut result = BTreeMap::new();
62                for (k, v) in schema_map {
63                    if let Some(td) = TypeDescriptor::from_value(v) {
64                        result.insert(k.clone(), td);
65                    }
66                }
67                Some(result)
68            } else {
69                None
70            }
71        });
72
73        Some(Self { name, schema })
74    }
75}
76
77/// Describes a field's type within a schema.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct TypeDescriptor {
80    /// The type of this field.
81    pub type_info: TypeInfo,
82}
83
84impl TypeDescriptor {
85    /// Create a TypeDescriptor from a type name.
86    pub fn new(type_name: impl Into<String>) -> Self {
87        Self {
88            type_info: TypeInfo::new(type_name),
89        }
90    }
91
92    /// Convert to a Value::Map representation.
93    pub fn to_value(&self) -> Value {
94        let mut map = BTreeMap::new();
95        map.insert("type".to_string(), self.type_info.to_value());
96        Value::Map(map)
97    }
98
99    /// Try to parse a TypeDescriptor from a Value.
100    pub fn from_value(value: &Value) -> Option<Self> {
101        let map = match value {
102            Value::Map(m) => m,
103            _ => return None,
104        };
105
106        let type_info = map.get("type").and_then(TypeInfo::from_value)?;
107
108        Some(Self { type_info })
109    }
110}
111
112/// A reference to a value at another path.
113///
114/// References are the foundation of HATEOAS in StructFS. Instead of embedding
115/// full values or documenting path construction rules, stores return references
116/// that clients can follow.
117///
118/// # Examples
119///
120/// ```
121/// use structfs_core_store::Reference;
122///
123/// // Minimal reference
124/// let r = Reference::new("handles/0");
125///
126/// // Reference with type hint
127/// let r = Reference::with_type("handles/0", "handle");
128///
129/// // Reference to an action
130/// let r = Reference::with_type("meta/open", "action");
131/// ```
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct Reference {
134    /// The path to the referenced value.
135    pub path: String,
136
137    /// Optional type information.
138    pub type_info: Option<TypeInfo>,
139}
140
141impl Reference {
142    /// Create a minimal reference with just a path.
143    pub fn new(path: impl Into<String>) -> Self {
144        Self {
145            path: path.into(),
146            type_info: None,
147        }
148    }
149
150    /// Create a reference with a type name.
151    pub fn with_type(path: impl Into<String>, type_name: impl Into<String>) -> Self {
152        Self {
153            path: path.into(),
154            type_info: Some(TypeInfo::new(type_name)),
155        }
156    }
157
158    /// Convert to a Value::Map representation.
159    ///
160    /// This produces the canonical reference format:
161    /// ```json
162    /// {"path": "handles/0", "type": {"name": "handle"}}
163    /// ```
164    pub fn to_value(&self) -> Value {
165        let mut map = BTreeMap::new();
166        map.insert("path".to_string(), Value::String(self.path.clone()));
167
168        if let Some(ref ti) = self.type_info {
169            map.insert("type".to_string(), ti.to_value());
170        }
171
172        Value::Map(map)
173    }
174
175    /// Try to parse a Reference from a Value.
176    ///
177    /// A value is a reference if it's a map containing a `path` key whose
178    /// value is a string.
179    pub fn from_value(value: &Value) -> Option<Self> {
180        let map = match value {
181            Value::Map(m) => m,
182            _ => return None,
183        };
184
185        let path = match map.get("path") {
186            Some(Value::String(s)) => s.clone(),
187            _ => return None,
188        };
189
190        let type_info = map.get("type").and_then(TypeInfo::from_value);
191
192        Some(Self { path, type_info })
193    }
194
195    /// Check if a Value is a reference.
196    ///
197    /// A value is a reference if it's a map containing a `path` key whose
198    /// value is a string.
199    pub fn is_reference(value: &Value) -> bool {
200        match value {
201            Value::Map(m) => matches!(m.get("path"), Some(Value::String(_))),
202            _ => false,
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn reference_new() {
213        let r = Reference::new("handles/0");
214        assert_eq!(r.path, "handles/0");
215        assert!(r.type_info.is_none());
216    }
217
218    #[test]
219    fn reference_with_type() {
220        let r = Reference::with_type("handles/0", "handle");
221        assert_eq!(r.path, "handles/0");
222        assert_eq!(r.type_info.as_ref().unwrap().name, "handle");
223    }
224
225    #[test]
226    fn reference_to_value_minimal() {
227        let r = Reference::new("handles/0");
228        let v = r.to_value();
229
230        if let Value::Map(map) = v {
231            assert_eq!(map.get("path"), Some(&Value::String("handles/0".into())));
232            assert!(!map.contains_key("type"));
233        } else {
234            panic!("Expected map");
235        }
236    }
237
238    #[test]
239    fn reference_to_value_with_type() {
240        let r = Reference::with_type("handles/0", "handle");
241        let v = r.to_value();
242
243        if let Value::Map(map) = v {
244            assert_eq!(map.get("path"), Some(&Value::String("handles/0".into())));
245            if let Some(Value::Map(type_map)) = map.get("type") {
246                assert_eq!(type_map.get("name"), Some(&Value::String("handle".into())));
247            } else {
248                panic!("Expected type map");
249            }
250        } else {
251            panic!("Expected map");
252        }
253    }
254
255    #[test]
256    fn reference_from_value_minimal() {
257        let mut map = BTreeMap::new();
258        map.insert("path".to_string(), Value::String("handles/0".into()));
259        let v = Value::Map(map);
260
261        let r = Reference::from_value(&v).unwrap();
262        assert_eq!(r.path, "handles/0");
263        assert!(r.type_info.is_none());
264    }
265
266    #[test]
267    fn reference_from_value_with_type() {
268        let mut type_map = BTreeMap::new();
269        type_map.insert("name".to_string(), Value::String("handle".into()));
270
271        let mut map = BTreeMap::new();
272        map.insert("path".to_string(), Value::String("handles/0".into()));
273        map.insert("type".to_string(), Value::Map(type_map));
274        let v = Value::Map(map);
275
276        let r = Reference::from_value(&v).unwrap();
277        assert_eq!(r.path, "handles/0");
278        assert_eq!(r.type_info.as_ref().unwrap().name, "handle");
279    }
280
281    #[test]
282    fn reference_from_value_not_map() {
283        assert!(Reference::from_value(&Value::String("x".into())).is_none());
284    }
285
286    #[test]
287    fn reference_from_value_no_path() {
288        let mut map = BTreeMap::new();
289        map.insert("other".to_string(), Value::String("x".into()));
290        assert!(Reference::from_value(&Value::Map(map)).is_none());
291    }
292
293    #[test]
294    fn reference_from_value_path_not_string() {
295        let mut map = BTreeMap::new();
296        map.insert("path".to_string(), Value::Integer(42));
297        assert!(Reference::from_value(&Value::Map(map)).is_none());
298    }
299
300    #[test]
301    fn is_reference_true() {
302        let mut map = BTreeMap::new();
303        map.insert("path".to_string(), Value::String("x".into()));
304        assert!(Reference::is_reference(&Value::Map(map)));
305    }
306
307    #[test]
308    fn is_reference_false_not_map() {
309        assert!(!Reference::is_reference(&Value::String("x".into())));
310    }
311
312    #[test]
313    fn is_reference_false_no_path() {
314        let mut map = BTreeMap::new();
315        map.insert("other".to_string(), Value::String("x".into()));
316        assert!(!Reference::is_reference(&Value::Map(map)));
317    }
318
319    #[test]
320    fn is_reference_false_path_not_string() {
321        let mut map = BTreeMap::new();
322        map.insert("path".to_string(), Value::Integer(42));
323        assert!(!Reference::is_reference(&Value::Map(map)));
324    }
325
326    #[test]
327    fn reference_roundtrip() {
328        let original = Reference::with_type("meta/handles/0", "handle");
329        let value = original.to_value();
330        let parsed = Reference::from_value(&value).unwrap();
331        assert_eq!(original, parsed);
332    }
333
334    #[test]
335    fn type_info_new() {
336        let ti = TypeInfo::new("handle");
337        assert_eq!(ti.name, "handle");
338        assert!(ti.schema.is_none());
339    }
340
341    #[test]
342    fn type_info_to_value() {
343        let ti = TypeInfo::new("integer");
344        let v = ti.to_value();
345
346        if let Value::Map(map) = v {
347            assert_eq!(map.get("name"), Some(&Value::String("integer".into())));
348        } else {
349            panic!("Expected map");
350        }
351    }
352
353    #[test]
354    fn type_info_with_schema() {
355        let mut schema = BTreeMap::new();
356        schema.insert("position".to_string(), TypeDescriptor::new("integer"));
357
358        let ti = TypeInfo {
359            name: "handle".to_string(),
360            schema: Some(schema),
361        };
362
363        let v = ti.to_value();
364        if let Value::Map(map) = v {
365            assert!(map.contains_key("schema"));
366        } else {
367            panic!("Expected map");
368        }
369    }
370
371    #[test]
372    fn type_info_from_value() {
373        let mut map = BTreeMap::new();
374        map.insert("name".to_string(), Value::String("handle".into()));
375        let v = Value::Map(map);
376
377        let ti = TypeInfo::from_value(&v).unwrap();
378        assert_eq!(ti.name, "handle");
379    }
380
381    #[test]
382    fn type_info_from_value_not_map() {
383        assert!(TypeInfo::from_value(&Value::String("x".into())).is_none());
384    }
385
386    #[test]
387    fn type_info_from_value_no_name() {
388        let map = BTreeMap::new();
389        assert!(TypeInfo::from_value(&Value::Map(map)).is_none());
390    }
391
392    #[test]
393    fn type_descriptor_new() {
394        let td = TypeDescriptor::new("string");
395        assert_eq!(td.type_info.name, "string");
396    }
397
398    #[test]
399    fn type_descriptor_to_value() {
400        let td = TypeDescriptor::new("integer");
401        let v = td.to_value();
402
403        if let Value::Map(map) = v {
404            assert!(map.contains_key("type"));
405        } else {
406            panic!("Expected map");
407        }
408    }
409
410    #[test]
411    fn type_descriptor_from_value() {
412        let mut type_map = BTreeMap::new();
413        type_map.insert("name".to_string(), Value::String("integer".into()));
414
415        let mut map = BTreeMap::new();
416        map.insert("type".to_string(), Value::Map(type_map));
417
418        let td = TypeDescriptor::from_value(&Value::Map(map)).unwrap();
419        assert_eq!(td.type_info.name, "integer");
420    }
421
422    #[test]
423    fn type_descriptor_from_value_not_map() {
424        assert!(TypeDescriptor::from_value(&Value::String("x".into())).is_none());
425    }
426
427    #[test]
428    fn type_descriptor_from_value_no_type() {
429        let map = BTreeMap::new();
430        assert!(TypeDescriptor::from_value(&Value::Map(map)).is_none());
431    }
432}