Skip to main content

graphwalker_restful/
actor.rs

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
11/// Legacy transport commands. Domain behavior lives in `graphwalker-service`;
12/// this actor only maps typed service results to the established REST and
13/// WebSocket JSON shapes.
14pub 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        // Loading is transactional: allow the replacement worker to start
65        // before closing the currently active execution.
66        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}