Skip to main content

arora_types/
call.rs

1use std::rc::Rc;
2
3use derive_more::Display;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::value::{StructureField, Value};
8
9/// A call is described like a structure in arora engine.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[cfg_attr(feature = "derive", derive(crate::AroraType))]
12#[cfg_attr(feature = "derive", arora(id = "9fe95b4c-08f9-48cf-8c10-99686e7b2afd"))]
13pub struct Call {
14  /// The ID of the module where to find the function ID.
15  /// If absent, look for it locally.
16  #[cfg_attr(feature = "derive", arora(id = "8c4633be-578a-405e-9c58-75c3bbf194be"))]
17  #[serde(default)]
18  pub module_id: Option<Uuid>,
19  /// The function ID to call.
20  #[cfg_attr(feature = "derive", arora(id = "1bd7f3de-6413-45b2-8813-8a2f415dd29d"))]
21  pub id: Uuid,
22  /// Arguments to call the function with. Their arora types are not known
23  /// statically — each argument names a field id and carries a value of any
24  /// type — so on the schema this is an opaque key/value bag.
25  #[cfg_attr(
26    feature = "derive",
27    arora(id = "7852eaaf-23e9-4762-a206-cc4b9c9984a7", keyvalue)
28  )]
29  #[serde(default)]
30  pub args: Vec<StructureField>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct CallResult {
35  pub ret: Value,
36  #[serde(default)]
37  pub mutated: Vec<StructureField>,
38}
39
40/// Anything that can be invoked through a [`CallBridge`], e.g. a
41/// behavior-tree tick registered as an indirect callable.
42pub trait Callable {
43  fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, CallError>;
44}
45
46/// The interface a module uses to call back into its host (the engine, or a
47/// mock in tests). It lives here, in the interface layer, so module-shaped
48/// libraries can make host calls without depending on the engine crate.
49pub trait CallBridge {
50  /// Dispatch `call` to the module it names: the call is the full description
51  /// of the invocation, and one naming no module is refused.
52  fn arora_call(&mut self, call: Call) -> Result<CallResult, CallError>;
53
54  /// Registers the given function in the executor and associates it to an
55  /// identifier generated on the fly. The function is made available to
56  /// every module by calling `arora_dispatch_indirect(id: u64) -> Value`.
57  fn arora_register_callable(&mut self, callable: Rc<dyn Callable>) -> CallableId;
58
59  /// Unregisters the function associated to the given identifier.
60  fn arora_unregister_callable(&mut self, callable_id: &CallableId);
61
62  /// Calls a callable that was registered.
63  fn arora_call_indirect(&mut self, callable_id: &CallableId) -> Result<Value, CallError>;
64}
65
66#[derive(Display, Debug)]
67pub enum CallError {
68  Generic {
69    message: String,
70  },
71  ModuleNotFound {
72    id: Uuid,
73  },
74  FunctionNotFound {
75    id: Uuid,
76  },
77  Trap {
78    message: String,
79  },
80  Internal {
81    message: String,
82  },
83  /// The guest returned a structured error via TYPE_ERROR instead of trapping.
84  Guest {
85    message: String,
86  },
87}
88
89impl std::error::Error for CallError {}
90
91#[derive(Clone, Hash, Eq, PartialEq)]
92pub struct CallableId {
93  pub id: u64,
94}
95
96impl From<u64> for CallableId {
97  fn from(id: u64) -> Self {
98    Self { id }
99  }
100}
101
102impl Callable for CallableId {
103  fn call(&self, caller: &mut dyn CallBridge) -> Result<Value, CallError> {
104    caller.arora_call_indirect(self)
105  }
106}
107
108#[cfg(test)]
109mod tests {
110  use crate::value::{Structure, Value};
111
112  use super::*;
113  use std::str::FromStr;
114  use uuid::Uuid;
115
116  #[test]
117  pub fn parse_call_test() {
118    // The call format keeps values in their singleton-map YAML form.
119    let call: Call = serde_yaml::with::singleton_map_recursive::deserialize(
120      serde_yaml::Deserializer::from_str(CALL_TEST),
121    )
122    .unwrap();
123    assert_eq!(
124      call.id,
125      Uuid::from_str("07f5740c-ba4a-45af-8ec5-bedde5737e99").unwrap()
126    );
127    if let Value::Structure(Structure { id, fields }) = &call.args[1].value.as_ref() {
128      assert_eq!(
129        *id,
130        Uuid::from_str("7f9aedf8-dbde-4020-b5f4-c28a6635ae7c").unwrap()
131      );
132      if let Value::I32(v) = fields[1].value.as_ref() {
133        assert_eq!(*v, 113);
134      } else {
135        panic!("expected i32 value under second field of struct arg");
136      }
137    } else {
138      panic!("expected a string under arg 55dbec70-1c3a-433e-a6e6-27446b7f065e");
139    }
140  }
141
142  #[test]
143  pub fn parse_call_test_2() {
144    let call: Call = serde_yaml::with::singleton_map_recursive::deserialize(
145      serde_yaml::Deserializer::from_str(CALL_TEST_2),
146    )
147    .unwrap();
148    assert_eq!(
149      call.id,
150      Uuid::from_str("b213a552-77ad-465a-a26d-352e8eccfd63").unwrap()
151    );
152    assert_eq!(call.args.len(), 2);
153  }
154
155  /// A `Call` describes itself as an arora structure: its module/function ids
156  /// are uuids, and its args are an opaque key/value bag (arg values are
157  /// dynamically typed, so they carry no static schema).
158  #[cfg(feature = "derive")]
159  #[test]
160  fn call_arora_type_shapes_its_fields() {
161    use crate::module::low::TypeRef;
162    use crate::ty::low::TypeKind;
163    use crate::AroraType;
164
165    let ty = Call::arora_type();
166    let TypeKind::Structure(structure) = &ty.kind else {
167      panic!("Call is a structure type");
168    };
169    let field = |id: &str| {
170      structure.fields[&Uuid::from_str(id).unwrap()]
171        .type_ref
172        .clone()
173    };
174    // module_id: an optional uuid.
175    assert!(matches!(
176      field("8c4633be-578a-405e-9c58-75c3bbf194be"),
177      TypeRef::Option { id } if id == *crate::ty::UUID_ID
178    ));
179    // id: a scalar uuid.
180    assert!(matches!(
181      field("1bd7f3de-6413-45b2-8813-8a2f415dd29d"),
182      TypeRef::Scalar { id } if id == *crate::ty::UUID_ID
183    ));
184    // args: an opaque key/value bag.
185    assert!(matches!(
186      field("7852eaaf-23e9-4762-a206-cc4b9c9984a7"),
187      TypeRef::Scalar { id } if id == *crate::ty::KEY_VALUE_ID
188    ));
189  }
190
191  /// Proves the call-bridge interface stands on its own: a module-shaped
192  /// library can register and invoke callables with no engine present.
193  #[test]
194  fn callable_round_trips_through_a_mock_bridge() {
195    use std::collections::HashMap;
196    use std::rc::Rc;
197
198    #[derive(Default)]
199    struct MockBridge {
200      registered: HashMap<CallableId, Rc<dyn Callable>>,
201      next_id: u64,
202    }
203
204    impl CallBridge for MockBridge {
205      fn arora_call(&mut self, _call: Call) -> Result<CallResult, CallError> {
206        Err(CallError::Generic {
207          message: "the mock has no modules".to_string(),
208        })
209      }
210      fn arora_register_callable(&mut self, callable: Rc<dyn Callable>) -> CallableId {
211        let id = CallableId::from(self.next_id);
212        self.next_id += 1;
213        self.registered.insert(id.clone(), callable);
214        id
215      }
216      fn arora_unregister_callable(&mut self, callable_id: &CallableId) {
217        self.registered.remove(callable_id);
218      }
219      fn arora_call_indirect(&mut self, callable_id: &CallableId) -> Result<Value, CallError> {
220        let callable = self
221          .registered
222          .get(callable_id)
223          .cloned()
224          .ok_or(CallError::Generic {
225            message: "unknown callable".to_string(),
226          })?;
227        callable.call(self)
228      }
229    }
230
231    struct Answer;
232    impl Callable for Answer {
233      fn call(&self, _caller: &mut dyn CallBridge) -> Result<Value, CallError> {
234        Ok(Value::I32(42))
235      }
236    }
237
238    let mut bridge = MockBridge::default();
239    let id = bridge.arora_register_callable(Rc::new(Answer));
240    let result = bridge.arora_call_indirect(&id).unwrap();
241    assert!(matches!(result, Value::I32(42)));
242
243    bridge.arora_unregister_callable(&id);
244    assert!(bridge.arora_call_indirect(&id).is_err());
245  }
246
247  pub const CALL_TEST: &str = "\
248id: 07f5740c-ba4a-45af-8ec5-bedde5737e99
249args:
250- id: b41899c3-66dc-40d4-ab61-d1ccf5231c88
251  value:
252    enum:
253      id: 325a5767-e344-4532-860e-0749bcf2e428
254      variant_id: 766e9e9a-446d-4e46-83e6-14b7ca101169
255      value: unit
256- id: 63086e48-804f-403a-8862-3358ddedc08d
257  value:
258    struct:
259      id: 7f9aedf8-dbde-4020-b5f4-c28a6635ae7c
260      fields:
261      - id: 7d94a956-e50d-4cc4-9714-f62e1f9b134e
262        value:
263          enums:
264            id: 325a5767-e344-4532-860e-0749bcf2e428
265            elements:
266              - variant_id: 2468f46c-bb60-425c-9a4d-9ad326ccc7e2
267                value: unit
268      - id: 5ffa9104-1e5c-4026-943f-8db38bd34563
269        value:
270          i32: 113
271";
272
273  pub const CALL_TEST_2: &str = "\
274id: b213a552-77ad-465a-a26d-352e8eccfd63
275args:
276- id: 55dbec70-1c3a-433e-a6e6-27446b7f065e
277  value:
278    u32: 42
279- id: abf9ca4e-e03f-431a-a32b-4911f809c399
280  value:
281    u32: 64
282";
283}