nodedb_types/sync/
shape.rs1use serde::{Deserialize, Serialize};
11
12#[derive(
14 Debug, Clone, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
15)]
16pub struct ShapeDefinition {
17 pub shape_id: String,
19 pub tenant_id: u32,
21 pub shape_type: ShapeType,
23 pub description: String,
25 #[serde(default)]
28 pub field_filter: Vec<String>,
29}
30
31#[derive(
33 Debug, Clone, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
34)]
35pub enum ShapeType {
36 Document {
40 collection: String,
41 predicate: Vec<u8>,
43 },
44
45 Graph {
50 root_nodes: Vec<String>,
51 max_depth: usize,
52 edge_label: Option<String>,
53 },
54
55 Vector {
59 collection: String,
60 field_name: Option<String>,
61 },
62}
63
64impl ShapeDefinition {
65 pub fn could_match(&self, collection: &str, _doc_id: &str) -> bool {
70 match &self.shape_type {
71 ShapeType::Document {
72 collection: shape_coll,
73 ..
74 } => shape_coll == collection,
75 ShapeType::Graph { root_nodes, .. } => {
76 !root_nodes.is_empty()
78 }
79 ShapeType::Vector {
80 collection: shape_coll,
81 ..
82 } => shape_coll == collection,
83 }
84 }
85
86 pub fn collection(&self) -> Option<&str> {
88 match &self.shape_type {
89 ShapeType::Document { collection, .. } => Some(collection),
90 ShapeType::Vector { collection, .. } => Some(collection),
91 ShapeType::Graph { .. } => None,
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn document_shape_matches_collection() {
102 let shape = ShapeDefinition {
103 shape_id: "s1".into(),
104 tenant_id: 1,
105 shape_type: ShapeType::Document {
106 collection: "orders".into(),
107 predicate: Vec::new(),
108 },
109 description: "all orders".into(),
110 field_filter: vec![],
111 };
112
113 assert!(shape.could_match("orders", "o1"));
114 assert!(!shape.could_match("users", "u1"));
115 assert_eq!(shape.collection(), Some("orders"));
116 }
117
118 #[test]
119 fn graph_shape() {
120 let shape = ShapeDefinition {
121 shape_id: "g1".into(),
122 tenant_id: 1,
123 shape_type: ShapeType::Graph {
124 root_nodes: vec!["alice".into()],
125 max_depth: 2,
126 edge_label: Some("KNOWS".into()),
127 },
128 description: "alice's network".into(),
129 field_filter: vec![],
130 };
131
132 assert!(shape.could_match("any_collection", "any_doc"));
133 assert_eq!(shape.collection(), None);
134 }
135
136 #[test]
137 fn vector_shape() {
138 let shape = ShapeDefinition {
139 shape_id: "v1".into(),
140 tenant_id: 1,
141 shape_type: ShapeType::Vector {
142 collection: "embeddings".into(),
143 field_name: Some("title".into()),
144 },
145 description: "title embeddings".into(),
146 field_filter: vec![],
147 };
148
149 assert!(shape.could_match("embeddings", "e1"));
150 assert!(!shape.could_match("other", "e1"));
151 }
152
153 #[test]
154 fn msgpack_roundtrip() {
155 let shape = ShapeDefinition {
156 shape_id: "test".into(),
157 tenant_id: 5,
158 shape_type: ShapeType::Document {
159 collection: "users".into(),
160 predicate: vec![1, 2, 3],
161 },
162 description: "test shape".into(),
163 field_filter: vec![],
164 };
165 let bytes = rmp_serde::to_vec_named(&shape).unwrap();
166 let decoded: ShapeDefinition = rmp_serde::from_slice(&bytes).unwrap();
167 assert_eq!(decoded.shape_id, "test");
168 assert_eq!(decoded.tenant_id, 5);
169 assert!(matches!(decoded.shape_type, ShapeType::Document { .. }));
170 }
171}