Skip to main content

arora_behavior/
interpreter_module.rs

1//! The behavior interpreter as a module: the well-known ids and call
2//! conventions under which the runtime exposes its one
3//! [`BehaviorInterpreter`](crate::BehaviorInterpreter) on the engine.
4//!
5//! The runtime assembles a function module under [`ID`] (the engine's generic
6//! module builder) with two functions attached to the interpreter's entry
7//! points:
8//!
9//! - [`LOAD`] → [`BehaviorInterpreter::load`](crate::BehaviorInterpreter::load):
10//!   replace the running behavior with a whole [`Graph`];
11//! - [`EDIT`] → [`BehaviorInterpreter::apply`](crate::BehaviorInterpreter::apply):
12//!   apply a [`GraphDiff`] to it.
13//!
14//! Both payloads travel as one structured [`Value`] argument, converted
15//! generically through [`arora_types::value_serde`] — any serde type flows,
16//! no bespoke encoding. A `Call{module_id: ID, id: LOAD|EDIT}` — a remote's
17//! `BridgeOp::Call`, or a behavior's own call bridge — reaches the
18//! interpreter through the engine's normal dispatch, like any module
19//! function.
20
21use arora_types::call::Call;
22use arora_types::value::StructureField;
23use arora_types::value_serde;
24use serde::de::DeserializeOwned;
25use serde::Serialize;
26use uuid::{uuid, Uuid};
27
28use crate::graph::{Graph, GraphDiff};
29
30/// Module id under which the runtime registers the behavior interpreter on
31/// the engine. Self-identifying like the vizij type ids: the ASCII bytes of
32/// "arora" lead the UUID, a small offset tails it.
33pub const ID: Uuid = uuid!("61726f72-6100-0000-0000-000000000001");
34
35/// Function id of **edit**: apply a [`GraphDiff`] to the running behavior.
36pub const EDIT: Uuid = uuid!("61726f72-6100-0000-0000-000000000002");
37
38/// Argument id of [`EDIT`]'s one argument: the [`GraphDiff`], as a structured
39/// [`Value`](arora_types::value::Value).
40pub const EDIT_ARG: Uuid = uuid!("61726f72-6100-0000-0000-000000000003");
41
42/// Function id of **load**: replace the running behavior with a [`Graph`].
43pub const LOAD: Uuid = uuid!("61726f72-6100-0000-0000-000000000004");
44
45/// Argument id of [`LOAD`]'s one argument: the [`Graph`], as a structured
46/// [`Value`](arora_types::value::Value).
47pub const LOAD_ARG: Uuid = uuid!("61726f72-6100-0000-0000-000000000005");
48
49/// Build the [`Call`] that applies `diff` to the running behavior.
50pub fn encode_edit(diff: &GraphDiff) -> Call {
51    encode(EDIT, EDIT_ARG, diff)
52}
53
54/// Read the [`GraphDiff`] out of an [`EDIT`] call.
55pub fn decode_edit(call: &Call) -> Result<GraphDiff, String> {
56    decode(call, EDIT, EDIT_ARG, "GraphDiff")
57}
58
59/// Build the [`Call`] that loads `graph` as the running behavior.
60pub fn encode_load(graph: &Graph) -> Call {
61    encode(LOAD, LOAD_ARG, graph)
62}
63
64/// Read the [`Graph`] out of a [`LOAD`] call.
65pub fn decode_load(call: &Call) -> Result<Graph, String> {
66    decode(call, LOAD, LOAD_ARG, "Graph")
67}
68
69fn encode<T: Serialize>(function: Uuid, arg: Uuid, payload: &T) -> Call {
70    let value = value_serde::to_value(payload).expect("a graph payload converts to a Value");
71    Call {
72        module_id: Some(ID),
73        id: function,
74        args: vec![StructureField {
75            id: arg,
76            value: Box::new(value),
77        }],
78    }
79}
80
81fn decode<T: DeserializeOwned>(
82    call: &Call,
83    function: Uuid,
84    arg: Uuid,
85    what: &str,
86) -> Result<T, String> {
87    if call.id != function {
88        return Err(format!(
89            "not the expected interpreter function: {}",
90            call.id
91        ));
92    }
93    let field = call
94        .args
95        .iter()
96        .find(|field| field.id == arg)
97        .ok_or_else(|| format!("the call is missing its {what} argument"))?;
98    value_serde::from_value(field.value.as_ref().clone())
99        .map_err(|e| format!("malformed {what}: {e}"))
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn edit_call_round_trips_the_diff() {
108        let diff = GraphDiff {
109            remove_nodes: vec![Uuid::from_u128(7)],
110            set_root: Some(Uuid::from_u128(9)),
111            ..Default::default()
112        };
113        let call = encode_edit(&diff);
114        assert_eq!(call.module_id, Some(ID));
115        assert_eq!(call.id, EDIT);
116        assert_eq!(decode_edit(&call).unwrap(), diff);
117    }
118
119    #[test]
120    fn load_call_round_trips_the_graph() {
121        let mut graph = Graph::empty();
122        graph.root = Some(Uuid::from_u128(3));
123        let call = encode_load(&graph);
124        assert_eq!(call.module_id, Some(ID));
125        assert_eq!(call.id, LOAD);
126        assert_eq!(decode_load(&call).unwrap(), graph);
127    }
128
129    #[test]
130    fn decode_rejects_malformed_calls() {
131        // Wrong function id.
132        let mut call = encode_edit(&GraphDiff::default());
133        call.id = Uuid::from_u128(1);
134        assert!(decode_edit(&call).is_err());
135
136        // Missing the payload argument.
137        let mut call = encode_edit(&GraphDiff::default());
138        call.args.clear();
139        assert!(decode_edit(&call).is_err());
140
141        // The argument does not decode into the expected payload.
142        let mut call = encode_edit(&GraphDiff::default());
143        call.args[0].value = Box::new(arora_types::value::Value::Boolean(true));
144        assert!(decode_edit(&call).is_err());
145        let mut call = encode_load(&Graph::empty());
146        call.args[0].value = Box::new(arora_types::value::Value::String("not a graph".to_string()));
147        assert!(decode_load(&call).is_err());
148    }
149}