1use std::sync::mpsc;
2
3use graphwalker_service::{
4 convert_graphml, validate_model, ExecutionId, ExecutionLimits, ExecutionRegistry,
5 StartExecution,
6};
7use serde_json::{json, Value};
8use tokio::sync::oneshot;
9use tracing::debug;
10
11pub enum Command {
15 Load {
16 json_body: String,
17 seed: Option<u64>,
18 global_data: Option<String>,
19 reply: oneshot::Sender<Result<Value, String>>,
20 },
21 Check {
22 json_body: String,
23 reply: oneshot::Sender<Result<Value, String>>,
24 },
25 HasNext {
26 reply: oneshot::Sender<Result<Value, String>>,
27 },
28 GetNext {
29 verbose: bool,
30 reply: oneshot::Sender<Result<Value, String>>,
31 },
32 GetData {
33 reply: oneshot::Sender<Result<Value, String>>,
34 },
35 SetData {
36 script: String,
37 reply: oneshot::Sender<Result<Value, String>>,
38 },
39 Restart {
40 reply: oneshot::Sender<Result<Value, String>>,
41 },
42 GetStatistics {
43 reply: oneshot::Sender<Result<Value, String>>,
44 },
45 GetModel {
46 reply: oneshot::Sender<Result<Value, String>>,
47 },
48 UpdateAllElements {
49 reply: oneshot::Sender<Result<Value, String>>,
50 },
51 ConvertGraphml {
52 graphml: String,
53 reply: oneshot::Sender<Result<Value, String>>,
54 },
55}
56
57struct MachineState {
58 registry: ExecutionRegistry,
59 execution_id: Option<ExecutionId>,
60}
61
62impl MachineState {
63 fn new() -> Self {
64 Self {
67 registry: ExecutionRegistry::new(ExecutionLimits { max_executions: 2 }),
68 execution_id: None,
69 }
70 }
71
72 fn execution_id(&self) -> Result<&ExecutionId, String> {
73 self.execution_id
74 .as_ref()
75 .ok_or_else(|| "No model(s) are loaded.".to_string())
76 }
77}
78
79pub fn spawn_machine_thread() -> mpsc::Sender<Command> {
80 spawn_machine_thread_with_state(MachineState::new())
81}
82
83pub fn spawn_machine_thread_with_model(
84 json_body: String,
85 seed: Option<u64>,
86) -> Result<mpsc::Sender<Command>, String> {
87 let mut state = MachineState::new();
88 handle_load(&mut state, &json_body, seed, None)?;
89 Ok(spawn_machine_thread_with_state(state))
90}
91
92fn spawn_machine_thread_with_state(mut state: MachineState) -> mpsc::Sender<Command> {
93 let (tx, rx) = mpsc::channel::<Command>();
94 std::thread::spawn(move || {
95 while let Ok(command) = rx.recv() {
96 match command {
97 Command::Load {
98 json_body,
99 seed,
100 global_data,
101 reply,
102 } => {
103 let _ = reply.send(handle_load(&mut state, &json_body, seed, global_data));
104 }
105 Command::Check { json_body, reply } => {
106 let _ = reply.send(handle_check(&json_body));
107 }
108 Command::HasNext { reply } => {
109 let _ = reply.send(handle_has_next(&state));
110 }
111 Command::GetNext { verbose, reply } => {
112 let _ = reply.send(handle_get_next(&state, verbose));
113 }
114 Command::GetData { reply } => {
115 let _ = reply.send(handle_get_data(&state));
116 }
117 Command::SetData { script, reply } => {
118 let _ = reply.send(handle_set_data(&state, &script));
119 }
120 Command::Restart { reply } => {
121 let _ = reply.send(handle_restart(&state));
122 }
123 Command::GetStatistics { reply } => {
124 let _ = reply.send(handle_get_statistics(&state));
125 }
126 Command::GetModel { reply } => {
127 let _ = reply.send(handle_get_model(&state));
128 }
129 Command::UpdateAllElements { reply } => {
130 let _ = reply.send(handle_update_all_elements(&state));
131 }
132 Command::ConvertGraphml { graphml, reply } => {
133 let _ = reply.send(handle_convert_graphml(&graphml));
134 }
135 }
136 }
137 });
138 tx
139}
140
141pub fn handle_check(json_body: &str) -> Result<Value, String> {
142 let model = serde_json::from_str(json_body).map_err(|error| error.to_string())?;
143 let result = validate_model(&model).map_err(|error| error.to_string())?;
144 let messages = result
145 .issues
146 .into_iter()
147 .map(|issue| issue.message)
148 .collect::<Vec<_>>();
149 Ok(json!({"result": "ok", "issues": messages}))
150}
151
152fn handle_load(
153 state: &mut MachineState,
154 json_body: &str,
155 seed: Option<u64>,
156 global_data: Option<String>,
157) -> Result<Value, String> {
158 let model = serde_json::from_str(json_body).map_err(|error| error.to_string())?;
159 let started = state
160 .registry
161 .start(StartExecution {
162 model,
163 seed,
164 global_data,
165 })
166 .map_err(|error| error.to_string())?;
167
168 if let Some(previous) = state.execution_id.replace(started.execution_id) {
169 let _ = state.registry.close(&previous);
170 }
171 Ok(json!({"result": "ok", "seed": started.seed}))
172}
173
174fn handle_has_next(state: &MachineState) -> Result<Value, String> {
175 let status = state
176 .registry
177 .status(state.execution_id()?)
178 .map_err(|error| error.to_string())?;
179 Ok(json!({"result": "ok", "hasNext": status.has_next.to_string()}))
180}
181
182fn handle_get_next(state: &MachineState, verbose: bool) -> Result<Value, String> {
183 let step = state
184 .registry
185 .next_step(state.execution_id()?)
186 .map_err(|error| error.to_string())?;
187 let element = step
188 .element
189 .ok_or_else(|| "No next step is available".to_string())?;
190
191 debug!(
192 model = element.model_id,
193 element = element.name,
194 data = element.data,
195 "getNext"
196 );
197 let mut response = json!({
198 "result": "ok",
199 "currentElementName": element.name,
200 "currentElementID": element.id,
201 "modelId": element.model_id,
202 });
203 if verbose {
204 response["data"] = json!(element.data);
205 response["visitedCount"] = json!(element.visited_count);
206 response["totalCount"] = json!(element.total_count);
207 response["stopConditionFulfillment"] = json!(element.stop_condition_fulfillment);
208 }
209 Ok(response)
210}
211
212fn handle_get_data(state: &MachineState) -> Result<Value, String> {
213 let data = state
214 .registry
215 .data(state.execution_id()?)
216 .map_err(|error| error.to_string())?;
217 Ok(json!({"result": "ok", "data": data}))
218}
219
220fn handle_set_data(state: &MachineState, script: &str) -> Result<Value, String> {
221 state
222 .registry
223 .set_data(state.execution_id()?, script)
224 .map_err(|error| error.to_string())?;
225 Ok(json!({"result": "ok"}))
226}
227
228fn handle_restart(state: &MachineState) -> Result<Value, String> {
229 state
230 .registry
231 .restart(state.execution_id()?)
232 .map_err(|error| error.to_string())?;
233 Ok(json!({"result": "ok"}))
234}
235
236fn handle_get_statistics(state: &MachineState) -> Result<Value, String> {
237 let statistics = state
238 .registry
239 .statistics(state.execution_id()?)
240 .map_err(|error| error.to_string())?;
241 Ok(json!({
242 "result": "ok",
243 "totalNumberOfVertices": statistics.total_vertices,
244 "totalNumberOfEdges": statistics.total_edges,
245 "totalNumberOfVisitedVertices": statistics.visited_vertices,
246 "totalNumberOfVisitedEdges": statistics.visited_edges,
247 "totalNumberOfUnvisitedVertices": statistics.unvisited_vertices,
248 "totalNumberOfUnvisitedEdges": statistics.unvisited_edges,
249 "vertexCoverage": statistics.vertex_coverage,
250 "edgeCoverage": statistics.edge_coverage,
251 }))
252}
253
254fn handle_get_model(state: &MachineState) -> Result<Value, String> {
255 let model = state
256 .registry
257 .model(state.execution_id()?)
258 .map_err(|error| error.to_string())?;
259 Ok(json!({"result": "ok", "models": model.model.to_string()}))
260}
261
262fn handle_update_all_elements(state: &MachineState) -> Result<Value, String> {
263 let elements = state
264 .registry
265 .elements(state.execution_id()?)
266 .map_err(|error| error.to_string())?
267 .into_iter()
268 .map(|element| {
269 json!({
270 "modelId": element.model_id,
271 "elementId": element.element_id,
272 "visitedCount": element.visited_count,
273 })
274 })
275 .collect::<Vec<_>>();
276 Ok(json!({"result": "ok", "elements": elements}))
277}
278
279pub fn handle_convert_graphml(graphml: &str) -> Result<Value, String> {
280 let result = convert_graphml(graphml).map_err(|error| error.to_string())?;
281 Ok(json!({"result": "ok", "models": result.model.to_string()}))
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 const MODEL: &str = r#"{
289 "models": [{
290 "name": "Small",
291 "generator": "random(edge_coverage(100))",
292 "startElementId": "e0",
293 "vertices": [
294 {"name": "v_A", "id": "n0"},
295 {"name": "v_B", "id": "n1"}
296 ],
297 "edges": [
298 {"name": "e_Start", "id": "e0", "targetVertexId": "n0"},
299 {"name": "e_AB", "id": "e1", "sourceVertexId": "n0", "targetVertexId": "n1"},
300 {"name": "e_Loop", "id": "e2", "sourceVertexId": "n1", "targetVertexId": "n1"},
301 {"name": "e_BA", "id": "e3", "sourceVertexId": "n1", "targetVertexId": "n0"}
302 ]
303 }]
304 }"#;
305
306 #[test]
307 fn legacy_execution_response_shapes_are_preserved() {
308 let mut state = MachineState::new();
309 let loaded = handle_load(&mut state, MODEL, Some(42), None).unwrap();
310 assert_eq!(loaded, json!({"result": "ok", "seed": 42}));
311
312 let has_next = handle_has_next(&state).unwrap();
313 assert_eq!(has_next, json!({"result": "ok", "hasNext": "true"}));
314
315 let step = handle_get_next(&state, false).unwrap();
316 assert_eq!(step["result"], "ok");
317 assert_eq!(step["currentElementName"], "e_Start");
318 assert_eq!(step["currentElementID"], "e0");
319 assert!(step.get("data").is_none());
320
321 let statistics = handle_get_statistics(&state).unwrap();
322 assert_eq!(statistics["totalNumberOfVertices"], 2);
323 assert_eq!(statistics["totalNumberOfEdges"], 4);
324 assert_eq!(statistics["totalNumberOfVisitedEdges"], 1);
325
326 let returned_model = handle_get_model(&state).unwrap();
327 assert_eq!(returned_model["result"], "ok");
328 assert!(returned_model["models"].as_str().is_some());
329 }
330
331 #[test]
332 fn verbose_websocket_step_shape_is_preserved() {
333 let mut state = MachineState::new();
334 handle_load(
335 &mut state,
336 MODEL,
337 Some(42),
338 Some("sessionValue=7".to_string()),
339 )
340 .unwrap();
341
342 let step = handle_get_next(&state, true).unwrap();
343 assert_eq!(step["visitedCount"], 1);
344 assert_eq!(step["totalCount"], 1);
345 assert!(step["stopConditionFulfillment"].is_number());
346 assert!(step["data"].as_str().unwrap().contains("sessionValue=7"));
347 }
348
349 #[test]
350 fn failed_load_does_not_replace_the_active_execution() {
351 let mut state = MachineState::new();
352 handle_load(&mut state, MODEL, Some(42), None).unwrap();
353 let execution_id = state.execution_id.clone();
354
355 assert!(handle_load(&mut state, r#"{"models": []}"#, Some(1), None).is_err());
356 assert_eq!(state.execution_id, execution_id);
357 assert_eq!(handle_has_next(&state).unwrap()["hasNext"], "true");
358 }
359}