haystack_server/
connector.rs1use parking_lot::RwLock;
4
5use haystack_client::HaystackClient;
6use haystack_core::data::HDict;
7use haystack_core::kinds::{HRef, Kind};
8
9#[derive(Debug, Clone, serde::Deserialize)]
11pub struct ConnectorConfig {
12 pub name: String,
14 pub url: String,
16 pub username: String,
18 pub password: String,
20 pub id_prefix: Option<String>,
22}
23
24pub struct Connector {
26 pub config: ConnectorConfig,
27 cache: RwLock<Vec<HDict>>,
29}
30
31impl Connector {
32 pub fn new(config: ConnectorConfig) -> Self {
34 Self {
35 config,
36 cache: RwLock::new(Vec::new()),
37 }
38 }
39
40 pub async fn sync(&self) -> Result<usize, String> {
43 let client = HaystackClient::connect(
44 &self.config.url,
45 &self.config.username,
46 &self.config.password,
47 )
48 .await
49 .map_err(|e| format!("connection failed: {e}"))?;
50
51 let grid = client
52 .read("*", None)
53 .await
54 .map_err(|e| format!("read failed: {e}"))?;
55
56 let mut entities: Vec<HDict> = grid.rows.into_iter().collect();
57
58 if let Some(ref prefix) = self.config.id_prefix {
60 for entity in &mut entities {
61 prefix_refs(entity, prefix);
62 }
63 }
64
65 let count = entities.len();
66 *self.cache.write() = entities;
67 Ok(count)
68 }
69
70 pub fn cached_entities(&self) -> Vec<HDict> {
72 self.cache.read().clone()
73 }
74
75 pub fn entity_count(&self) -> usize {
77 self.cache.read().len()
78 }
79}
80
81pub fn prefix_refs(entity: &mut HDict, prefix: &str) {
86 let tag_names: Vec<String> = entity.tag_names().map(|s| s.to_string()).collect();
87
88 for name in &tag_names {
89 let should_prefix = name == "id" || name.ends_with("Ref");
90 if !should_prefix {
91 continue;
92 }
93
94 if let Some(Kind::Ref(r)) = entity.get(name) {
95 let new_val = format!("{}{}", prefix, r.val);
96 let new_ref = HRef::new(new_val, r.dis.clone());
97 entity.set(name.as_str(), Kind::Ref(new_ref));
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use haystack_core::kinds::HRef;
106
107 #[test]
108 fn connector_new_empty_cache() {
109 let config = ConnectorConfig {
110 name: "test".to_string(),
111 url: "http://localhost:8080/api".to_string(),
112 username: "user".to_string(),
113 password: "pass".to_string(),
114 id_prefix: None,
115 };
116 let connector = Connector::new(config);
117 assert_eq!(connector.entity_count(), 0);
118 assert!(connector.cached_entities().is_empty());
119 }
120
121 #[test]
122 fn connector_config_deserialization() {
123 let json = r#"{
124 "name": "Remote Server",
125 "url": "http://remote:8080/api",
126 "username": "admin",
127 "password": "secret",
128 "id_prefix": "r1-"
129 }"#;
130 let config: ConnectorConfig = serde_json::from_str(json).unwrap();
131 assert_eq!(config.name, "Remote Server");
132 assert_eq!(config.url, "http://remote:8080/api");
133 assert_eq!(config.username, "admin");
134 assert_eq!(config.password, "secret");
135 assert_eq!(config.id_prefix, Some("r1-".to_string()));
136 }
137
138 #[test]
139 fn connector_config_deserialization_without_prefix() {
140 let json = r#"{
141 "name": "Remote",
142 "url": "http://remote:8080/api",
143 "username": "admin",
144 "password": "secret"
145 }"#;
146 let config: ConnectorConfig = serde_json::from_str(json).unwrap();
147 assert_eq!(config.id_prefix, None);
148 }
149
150 #[test]
151 fn id_prefix_application() {
152 let mut entity = HDict::new();
153 entity.set("id", Kind::Ref(HRef::from_val("site-1")));
154 entity.set("dis", Kind::Str("Main Site".to_string()));
155 entity.set("site", Kind::Marker);
156 entity.set("siteRef", Kind::Ref(HRef::from_val("site-1")));
157 entity.set("equipRef", Kind::Ref(HRef::from_val("equip-1")));
158 entity.set(
159 "floorRef",
160 Kind::Ref(HRef::new("floor-1", Some("Floor 1".to_string()))),
161 );
162
163 prefix_refs(&mut entity, "r1-");
164
165 match entity.get("id") {
167 Some(Kind::Ref(r)) => assert_eq!(r.val, "r1-site-1"),
168 other => panic!("expected Ref, got {other:?}"),
169 }
170
171 match entity.get("siteRef") {
173 Some(Kind::Ref(r)) => assert_eq!(r.val, "r1-site-1"),
174 other => panic!("expected Ref, got {other:?}"),
175 }
176
177 match entity.get("equipRef") {
179 Some(Kind::Ref(r)) => assert_eq!(r.val, "r1-equip-1"),
180 other => panic!("expected Ref, got {other:?}"),
181 }
182
183 match entity.get("floorRef") {
185 Some(Kind::Ref(r)) => {
186 assert_eq!(r.val, "r1-floor-1");
187 assert_eq!(r.dis, Some("Floor 1".to_string()));
188 }
189 other => panic!("expected Ref, got {other:?}"),
190 }
191
192 assert_eq!(entity.get("dis"), Some(&Kind::Str("Main Site".to_string())));
194 assert_eq!(entity.get("site"), Some(&Kind::Marker));
195 }
196
197 #[test]
198 fn id_prefix_skips_non_ref_values() {
199 let mut entity = HDict::new();
200 entity.set("id", Kind::Ref(HRef::from_val("point-1")));
201 entity.set("customRef", Kind::Str("not-a-ref".to_string()));
203
204 prefix_refs(&mut entity, "p-");
205
206 match entity.get("id") {
208 Some(Kind::Ref(r)) => assert_eq!(r.val, "p-point-1"),
209 other => panic!("expected Ref, got {other:?}"),
210 }
211
212 assert_eq!(
214 entity.get("customRef"),
215 Some(&Kind::Str("not-a-ref".to_string()))
216 );
217 }
218}