1use actix_web::{HttpRequest, HttpResponse, web};
4
5use haystack_core::data::{HCol, HDict, HGrid};
6use haystack_core::kinds::{Kind, Number};
7
8use crate::content;
9use crate::error::HaystackError;
10use crate::state::AppState;
11
12pub async fn handle_export(
17 req: HttpRequest,
18 state: web::Data<AppState>,
19) -> Result<HttpResponse, HaystackError> {
20 let accept = req
21 .headers()
22 .get("Accept")
23 .and_then(|v| v.to_str().ok())
24 .unwrap_or("");
25
26 let grid = state
27 .graph
28 .read(|g| g.to_grid(""))
29 .map_err(|e| HaystackError::internal(format!("export failed: {e}")))?;
30
31 let (encoded, ct) = content::encode_response_grid(&grid, accept)
32 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
33
34 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
35}
36
37pub async fn handle_import(
44 req: HttpRequest,
45 body: String,
46 state: web::Data<AppState>,
47) -> Result<HttpResponse, HaystackError> {
48 let content_type = req
49 .headers()
50 .get("Content-Type")
51 .and_then(|v| v.to_str().ok())
52 .unwrap_or("");
53 let accept = req
54 .headers()
55 .get("Accept")
56 .and_then(|v| v.to_str().ok())
57 .unwrap_or("");
58
59 let request_grid = content::decode_request_grid(&body, content_type)
60 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
61
62 let mut count: usize = 0;
63
64 for row in &request_grid.rows {
65 let ref_val = match row.id() {
66 Some(r) => r.val.clone(),
67 None => {
68 continue;
70 }
71 };
72
73 if state.graph.contains(&ref_val) {
74 state.graph.update(&ref_val, row.clone()).map_err(|e| {
76 HaystackError::internal(format!("update failed for {ref_val}: {e}"))
77 })?;
78 } else {
79 state
81 .graph
82 .add(row.clone())
83 .map_err(|e| HaystackError::internal(format!("add failed for {ref_val}: {e}")))?;
84 }
85
86 count += 1;
87 }
88
89 let mut row = HDict::new();
91 row.set("count", Kind::Number(Number::new(count as f64, None)));
92
93 let cols = vec![HCol::new("count")];
94 let result_grid = HGrid::from_parts(HDict::new(), cols, vec![row]);
95
96 let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
97 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
98
99 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
100}
101
102#[cfg(test)]
103mod tests {
104 use actix_web::App;
105 use actix_web::test as actix_test;
106 use actix_web::web;
107
108 use haystack_core::data::{HCol, HDict, HGrid};
109 use haystack_core::graph::{EntityGraph, SharedGraph};
110 use haystack_core::kinds::{HRef, Kind, Number};
111
112 use crate::actions::ActionRegistry;
113 use crate::auth::AuthManager;
114 use crate::his_store::HisStore;
115 use crate::state::AppState;
116 use crate::ws::WatchManager;
117
118 fn test_app_state() -> web::Data<AppState> {
119 web::Data::new(AppState {
120 graph: SharedGraph::new(EntityGraph::new()),
121 namespace: parking_lot::RwLock::new(haystack_core::ontology::DefNamespace::new()),
122 auth: AuthManager::empty(),
123 watches: WatchManager::new(),
124 actions: ActionRegistry::new(),
125 his: HisStore::new(),
126 started_at: std::time::Instant::now(),
127 federation: crate::federation::Federation::new(),
128 })
129 }
130
131 fn make_site(id: &str) -> HDict {
132 let mut d = HDict::new();
133 d.set("id", Kind::Ref(HRef::from_val(id)));
134 d.set("site", Kind::Marker);
135 d.set("dis", Kind::Str(format!("Site {id}")));
136 d.set(
137 "area",
138 Kind::Number(Number::new(4500.0, Some("ft\u{00b2}".into()))),
139 );
140 d
141 }
142
143 fn make_equip(id: &str, site_ref: &str) -> HDict {
144 let mut d = HDict::new();
145 d.set("id", Kind::Ref(HRef::from_val(id)));
146 d.set("equip", Kind::Marker);
147 d.set("dis", Kind::Str(format!("Equip {id}")));
148 d.set("siteRef", Kind::Ref(HRef::from_val(site_ref)));
149 d
150 }
151
152 fn encode_grid_zinc(grid: &HGrid) -> String {
153 let codec = haystack_core::codecs::codec_for("text/zinc").unwrap();
154 codec.encode_grid(grid).unwrap()
155 }
156
157 fn decode_grid_zinc(body: &str) -> HGrid {
158 let codec = haystack_core::codecs::codec_for("text/zinc").unwrap();
159 codec.decode_grid(body).unwrap()
160 }
161
162 #[actix_web::test]
163 async fn export_empty_graph() {
164 let state = test_app_state();
165 let app = actix_test::init_service(
166 App::new()
167 .app_data(state.clone())
168 .route("/api/export", web::post().to(super::handle_export)),
169 )
170 .await;
171
172 let req = actix_test::TestRequest::post()
173 .uri("/api/export")
174 .insert_header(("Accept", "text/zinc"))
175 .to_request();
176
177 let resp = actix_test::call_service(&app, req).await;
178 assert_eq!(resp.status(), 200);
179
180 let body = actix_test::read_body(resp).await;
181 let body_str = std::str::from_utf8(&body).unwrap();
182 let grid = decode_grid_zinc(body_str);
183 assert!(grid.is_empty());
184 }
185
186 #[actix_web::test]
187 async fn import_entities() {
188 let state = test_app_state();
189 let app = actix_test::init_service(
190 App::new()
191 .app_data(state.clone())
192 .route("/api/import", web::post().to(super::handle_import)),
193 )
194 .await;
195
196 let site = make_site("site-1");
198 let equip = make_equip("equip-1", "site-1");
199 let cols = vec![
200 HCol::new("area"),
201 HCol::new("dis"),
202 HCol::new("equip"),
203 HCol::new("id"),
204 HCol::new("site"),
205 HCol::new("siteRef"),
206 ];
207 let import_grid = HGrid::from_parts(HDict::new(), cols, vec![site, equip]);
208 let body = encode_grid_zinc(&import_grid);
209
210 let req = actix_test::TestRequest::post()
211 .uri("/api/import")
212 .insert_header(("Content-Type", "text/zinc"))
213 .insert_header(("Accept", "text/zinc"))
214 .set_payload(body)
215 .to_request();
216
217 let resp = actix_test::call_service(&app, req).await;
218 assert_eq!(resp.status(), 200);
219
220 let resp_body = actix_test::read_body(resp).await;
221 let resp_str = std::str::from_utf8(&resp_body).unwrap();
222 let result_grid = decode_grid_zinc(resp_str);
223 assert_eq!(result_grid.len(), 1);
224
225 let count_row = result_grid.row(0).unwrap();
227 match count_row.get("count") {
228 Some(Kind::Number(n)) => assert_eq!(n.val as usize, 2),
229 other => panic!("expected Number count, got {other:?}"),
230 }
231
232 assert_eq!(state.graph.len(), 2);
234 assert!(state.graph.contains("site-1"));
235 assert!(state.graph.contains("equip-1"));
236 }
237
238 #[actix_web::test]
239 async fn import_updates_existing_entities() {
240 let state = test_app_state();
241
242 state.graph.add(make_site("site-1")).unwrap();
244
245 let app = actix_test::init_service(
246 App::new()
247 .app_data(state.clone())
248 .route("/api/import", web::post().to(super::handle_import)),
249 )
250 .await;
251
252 let mut updated_site = HDict::new();
254 updated_site.set("id", Kind::Ref(HRef::from_val("site-1")));
255 updated_site.set("site", Kind::Marker);
256 updated_site.set("dis", Kind::Str("Updated Site".into()));
257 updated_site.set(
258 "area",
259 Kind::Number(Number::new(9000.0, Some("ft\u{00b2}".into()))),
260 );
261
262 let cols = vec![
263 HCol::new("area"),
264 HCol::new("dis"),
265 HCol::new("id"),
266 HCol::new("site"),
267 ];
268 let import_grid = HGrid::from_parts(HDict::new(), cols, vec![updated_site]);
269 let body = encode_grid_zinc(&import_grid);
270
271 let req = actix_test::TestRequest::post()
272 .uri("/api/import")
273 .insert_header(("Content-Type", "text/zinc"))
274 .insert_header(("Accept", "text/zinc"))
275 .set_payload(body)
276 .to_request();
277
278 let resp = actix_test::call_service(&app, req).await;
279 assert_eq!(resp.status(), 200);
280
281 assert_eq!(state.graph.len(), 1);
283
284 let entity = state.graph.get("site-1").unwrap();
286 assert_eq!(entity.get("dis"), Some(&Kind::Str("Updated Site".into())));
287 }
288
289 #[actix_web::test]
290 async fn import_then_export_roundtrip() {
291 let state = test_app_state();
292 let app = actix_test::init_service(
293 App::new()
294 .app_data(state.clone())
295 .route("/api/import", web::post().to(super::handle_import))
296 .route("/api/export", web::post().to(super::handle_export)),
297 )
298 .await;
299
300 let site = make_site("site-1");
302 let equip = make_equip("equip-1", "site-1");
303 let cols = vec![
304 HCol::new("area"),
305 HCol::new("dis"),
306 HCol::new("equip"),
307 HCol::new("id"),
308 HCol::new("site"),
309 HCol::new("siteRef"),
310 ];
311 let import_grid = HGrid::from_parts(HDict::new(), cols, vec![site, equip]);
312 let body = encode_grid_zinc(&import_grid);
313
314 let import_req = actix_test::TestRequest::post()
315 .uri("/api/import")
316 .insert_header(("Content-Type", "text/zinc"))
317 .insert_header(("Accept", "text/zinc"))
318 .set_payload(body)
319 .to_request();
320
321 let import_resp = actix_test::call_service(&app, import_req).await;
322 assert_eq!(import_resp.status(), 200);
323
324 let export_req = actix_test::TestRequest::post()
326 .uri("/api/export")
327 .insert_header(("Accept", "text/zinc"))
328 .to_request();
329
330 let export_resp = actix_test::call_service(&app, export_req).await;
331 assert_eq!(export_resp.status(), 200);
332
333 let export_body = actix_test::read_body(export_resp).await;
334 let export_str = std::str::from_utf8(&export_body).unwrap();
335 let exported_grid = decode_grid_zinc(export_str);
336
337 assert_eq!(exported_grid.len(), 2);
339
340 assert!(exported_grid.col("id").is_some());
342 assert!(exported_grid.col("dis").is_some());
343
344 let mut ids: Vec<String> = exported_grid
346 .rows
347 .iter()
348 .filter_map(|r| r.id().map(|r| r.val.clone()))
349 .collect();
350 ids.sort();
351 assert_eq!(ids, vec!["equip-1", "site-1"]);
352 }
353}