use std::collections::HashMap;
use std::sync::Arc;
use arora_engine::module::{HostModule, ModuleBuilder};
use arora_hal::{hal_module, ComponentGlb, ComponentModel, Hal};
use arora_types::call::{Call, CallError, CallResult};
use arora_types::record::module::frozen::{Function, Parameter};
use arora_types::record::ty::{FrozenArray, FrozenOption, FrozenTy, PrimitiveKind};
use arora_types::record::FrozenReference;
use arora_types::value::Value;
use arora_types::AroraType;
pub(crate) fn module(hal: Arc<dyn Hal>) -> HostModule {
ModuleBuilder::new(hal_module::ID)
.described_function(hal_module::MODELS, "models", models_signature(), {
let hal = hal.clone();
move |_call| {
answer(Value::array_of_type::<ComponentModel>(
models(&*hal).into_iter().map(Value::from).collect(),
))
}
})
.described_function(hal_module::MODEL_GLB, "model_glb", model_glb_signature(), {
let hal = hal.clone();
move |call| {
let component = component_of(&call)?;
answer(Value::Option(
model_glb(&*hal, &component)?.map(|glb| Box::new(Value::ArrayU8(glb))),
))
}
})
.described_function(
hal_module::MODEL_GLBS,
"model_glbs",
model_glbs_signature(),
move |_call| {
answer(Value::array_of_type::<ComponentGlb>(
model_glbs(&*hal)?.into_iter().map(Value::from).collect(),
))
},
)
.build()
}
fn answer(ret: Value) -> Result<CallResult, CallError> {
Ok(CallResult {
ret,
mutated: Vec::new(),
})
}
fn guest(message: impl Into<String>) -> CallError {
CallError::Guest {
message: message.into(),
}
}
fn models(hal: &dyn Hal) -> Vec<ComponentModel> {
hal.assets()
.map(|assets| assets.models())
.unwrap_or_default()
}
fn model_glb(hal: &dyn Hal, component: &str) -> Result<Option<Vec<u8>>, CallError> {
let Some(assets) = hal.assets() else {
return Ok(None);
};
let servable = assets
.models()
.iter()
.any(|model| model.component == component && model.servable);
if !servable {
return Ok(None);
}
assets
.servable_glb(component)
.map_err(|e| guest(format!("the HAL could not read {component}'s model: {e}")))
}
fn model_glbs(hal: &dyn Hal) -> Result<Vec<ComponentGlb>, CallError> {
let Some(assets) = hal.assets() else {
return Ok(Vec::new());
};
let mut glbs = Vec::new();
for model in assets.models().into_iter().filter(|model| model.servable) {
let glb = assets.servable_glb(&model.component).map_err(|e| {
guest(format!(
"the HAL could not read {}'s model: {e}",
model.component
))
})?;
if let Some(glb) = glb {
glbs.push(ComponentGlb {
component: model.component,
glb,
});
}
}
Ok(glbs)
}
fn component_of(call: &Call) -> Result<String, CallError> {
match call
.args
.iter()
.find(|field| field.id == hal_module::MODEL_GLB_COMPONENT)
.map(|field| field.value.as_ref())
{
Some(Value::String(component)) => Ok(component.clone()),
Some(other) => Err(guest(format!(
"model_glb takes the component's name as a string, not {}",
other.kind()
))),
None => Err(guest("model_glb needs the component's name")),
}
}
fn array_of<T: AroraType>() -> FrozenTy {
FrozenTy::FrozenArray(FrozenArray {
reference: FrozenReference {
id: T::arora_type_id(),
version: T::arora_type_version(),
},
})
}
fn models_signature() -> Function {
Function {
parameters: HashMap::new(),
parameter_ordering: Vec::new(),
return_ty: array_of::<ComponentModel>(),
}
}
fn model_glb_signature() -> Function {
Function {
parameters: HashMap::from([(
hal_module::MODEL_GLB_COMPONENT,
Parameter {
name: "component".to_string(),
ty: FrozenTy::from(PrimitiveKind::String),
mutable: false,
},
)]),
parameter_ordering: vec![hal_module::MODEL_GLB_COMPONENT],
return_ty: FrozenTy::FrozenOption(FrozenOption {
element: Box::new(FrozenTy::from(PrimitiveKind::ArrayU8)),
}),
}
}
fn model_glbs_signature() -> Function {
Function {
parameters: HashMap::new(),
parameter_ordering: Vec::new(),
return_ty: array_of::<ComponentGlb>(),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use arora_bridge::Caller;
use arora_hal::{
content_hash, hal_module, ComponentGlb, ComponentModel, FakeHal, Hal, HalAssets,
HalDescription, HalResult, UpdatesStream, DEVICE,
};
use arora_types::call::Call;
use arora_types::data::{Key, State, StateChange};
use arora_types::value::{StructureField, Value};
use crate::caller_tests::settle;
use crate::Arora;
struct TwoModels(FakeHal);
fn model(component: &str, glb: &[u8], servable: bool) -> ComponentModel {
ComponentModel {
component: component.to_string(),
description: None,
reference: None,
content_hash: Some(content_hash(glb)),
servable,
mount: None,
}
}
#[async_trait::async_trait]
impl Hal for TwoModels {
async fn describe(&self) -> HalDescription {
self.0.describe().await
}
async fn read(&self, keys: &[Key]) -> HalResult<Vec<Option<Value>>> {
self.0.read(keys).await
}
async fn read_all(&self) -> HalResult<State> {
self.0.read_all().await
}
async fn write(&self, changes: StateChange) -> HalResult<()> {
self.0.write(changes).await
}
fn try_send(&self, changes: &StateChange) {
self.0.try_send(changes)
}
fn updates(&self) -> UpdatesStream {
self.0.updates()
}
fn assets(&self) -> Option<&dyn HalAssets> {
Some(self)
}
}
impl HalAssets for TwoModels {
fn models(&self) -> Vec<ComponentModel> {
vec![
model("face", b"face", true),
model("robot", b"robot", false),
]
}
fn servable_glb(&self, component: &str) -> HalResult<Option<Vec<u8>>> {
Ok(Some(component.as_bytes().to_vec()))
}
}
fn call(id: arora_types::Uuid, component: Option<&str>) -> Call {
Call {
module_id: Some(hal_module::ID),
id,
args: component
.map(|component| StructureField {
id: hal_module::MODEL_GLB_COMPONENT,
value: Box::new(Value::String(component.to_string())),
})
.into_iter()
.collect(),
}
}
fn glb(bytes: &[u8]) -> Value {
Value::Option(Some(Box::new(Value::ArrayU8(bytes.to_vec()))))
}
#[test]
fn the_device_component_s_model_is_listed_and_served() {
let hal = FakeHal::new();
let mut arora = Arora::builder()
.with_hal(Box::new(hal.clone()))
.build()
.expect("build the device");
let caller = arora.caller();
let none = settle(&mut arora, caller.call(call(hal_module::MODELS, None)))
.expect("the device answers");
assert!(none.ret.into_elements().unwrap().is_empty());
let none = settle(
&mut arora,
caller.call(call(hal_module::MODEL_GLB, Some(DEVICE))),
)
.expect("the device answers");
assert_eq!(none.ret, Value::Option(None));
hal.set_model_glb(b"glTF".to_vec());
let models: Vec<ComponentModel> =
settle(&mut arora, caller.call(call(hal_module::MODELS, None)))
.expect("the device answers")
.ret
.into_elements()
.unwrap()
.into_iter()
.map(|model| ComponentModel::try_from(model).unwrap())
.collect();
assert_eq!(models.len(), 1);
assert_eq!(models[0].component, DEVICE);
assert_eq!(models[0].content_hash, Some(content_hash(b"glTF")));
assert!(models[0].servable);
let model = settle(
&mut arora,
caller.call(call(hal_module::MODEL_GLB, Some(DEVICE))),
)
.expect("the device answers");
assert_eq!(model.ret, glb(b"glTF"));
}
#[test]
fn only_a_servable_model_is_served() {
let mut arora = Arora::builder()
.with_hal(Box::new(TwoModels(FakeHal::new())))
.build()
.expect("build the device");
let face = arora
.call(call(hal_module::MODEL_GLB, Some("face")))
.expect("the device answers");
assert_eq!(face.ret, glb(b"face"));
let robot = arora
.call(call(hal_module::MODEL_GLB, Some("robot")))
.expect("the device answers");
assert_eq!(robot.ret, Value::Option(None));
let unknown = arora
.call(call(hal_module::MODEL_GLB, Some("arm")))
.expect("the device answers");
assert_eq!(unknown.ret, Value::Option(None));
let glbs: Vec<ComponentGlb> = arora
.call(call(hal_module::MODEL_GLBS, None))
.expect("the device answers")
.ret
.into_elements()
.unwrap()
.into_iter()
.map(|glb| ComponentGlb::try_from(glb).unwrap())
.collect();
assert_eq!(
glbs,
vec![ComponentGlb {
component: "face".to_string(),
glb: b"face".to_vec(),
}]
);
let models = arora
.call(call(hal_module::MODELS, None))
.expect("the device answers")
.ret
.into_elements()
.unwrap();
assert_eq!(models.len(), 2, "both models are listed");
}
#[test]
fn model_glb_needs_a_component() {
let mut arora = Arora::builder().build().expect("build the device");
let error = arora
.call(call(hal_module::MODEL_GLB, None))
.expect_err("no component");
assert!(error.to_string().contains("component"), "{error}");
}
#[test]
fn the_device_describes_the_hal_module() {
let mut arora = Arora::builder().build().expect("build the device");
let caller = arora.caller();
let methods = settle(
&mut arora,
caller.describe_methods(Some("model".to_string())),
)
.expect("the device describes its methods");
let described: HashMap<_, _> = methods
.iter()
.filter(|method| method.module_id == hal_module::ID)
.map(|method| (method.name.as_str(), method.id))
.collect();
assert_eq!(
described,
HashMap::from([
("models", hal_module::MODELS),
("model_glb", hal_module::MODEL_GLB),
("model_glbs", hal_module::MODEL_GLBS),
])
);
let model_glb = methods
.iter()
.find(|method| method.id == hal_module::MODEL_GLB)
.unwrap();
assert_eq!(
model_glb.function.parameter_ordering,
vec![hal_module::MODEL_GLB_COMPONENT]
);
}
}