use std::{
collections::{BTreeMap, VecDeque},
fmt,
time::Duration,
};
use sim_kernel::{CapabilityName, Expr, Symbol};
pub type DeviceResult<T> = std::result::Result<T, DeviceError>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviceError {
Unsupported,
Sample(String),
Host(String),
Contract(String),
}
impl fmt::Display for DeviceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported => f.write_str("device provider is unsupported"),
Self::Sample(message) => write!(f, "device sample error: {message}"),
Self::Host(message) => f.write_str(message),
Self::Contract(message) => write!(f, "device contract error: {message}"),
}
}
}
impl std::error::Error for DeviceError {}
impl From<DeviceError> for sim_kernel::Error {
fn from(error: DeviceError) -> Self {
match error {
DeviceError::Unsupported => {
Self::HostError("device provider is unsupported".to_owned())
}
DeviceError::Sample(message) => Self::Eval(format!("device sample error: {message}")),
DeviceError::Host(message) => Self::HostError(message),
DeviceError::Contract(message) => {
Self::Eval(format!("device contract error: {message}"))
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeviceProfile {
pub device: Symbol,
pub streams: Vec<Symbol>,
pub inputs: Vec<Symbol>,
pub outputs: Vec<Symbol>,
pub sample_kinds: Vec<Symbol>,
}
impl DeviceProfile {
pub fn new(
device: Symbol,
streams: Vec<Symbol>,
inputs: Vec<Symbol>,
outputs: Vec<Symbol>,
sample_kinds: Vec<Symbol>,
) -> Self {
Self {
device,
streams,
inputs,
outputs,
sample_kinds,
}
}
pub fn modeled_edge() -> Self {
Self::new(
Symbol::qualified("device", "modeled-edge"),
vec![
Symbol::qualified("device/stream", "battery"),
Symbol::qualified("device/stream", "motion"),
],
vec![Symbol::qualified("device/input", "button")],
vec![
Symbol::qualified("device/output", "screen"),
Symbol::qualified("device/output", "haptic"),
],
vec![device_sample_kind_symbol("device-caps")],
)
}
pub fn supports_sample_kind(&self, sample_kind: &Symbol) -> bool {
self.sample_kinds.contains(sample_kind)
}
}
pub trait DeviceSample: Sized {
fn sample_kind() -> &'static str;
fn to_expr(&self) -> Expr;
fn from_expr(expr: &Expr) -> DeviceResult<Self>;
}
pub fn device_sample_kind_symbol(kind: &str) -> Symbol {
Symbol::qualified("stream/device-sample", kind)
}
pub trait DeviceProvider: Send {
fn open(&self) -> DeviceResult<OpenedSession>;
}
pub trait ObservationSession: Send {
fn profile(&self) -> &DeviceProfile;
fn start(&mut self) -> DeviceResult<()>;
fn poll(&mut self, kind: &str) -> DeviceResult<Option<Expr>>;
fn stop(&mut self) -> DeviceResult<()>;
}
pub trait EffectSession: ObservationSession {
fn invoke(&mut self, request: EffectRequest) -> DeviceResult<EffectReceipt>;
}
pub enum OpenedSession {
Observe(Box<dyn ObservationSession>),
Effect(Box<dyn EffectSession>),
}
impl OpenedSession {
pub fn observation(&mut self) -> &mut dyn ObservationSession {
match self {
Self::Observe(session) => session.as_mut(),
Self::Effect(session) => session.as_mut(),
}
}
pub fn effect(&mut self) -> Option<&mut (dyn EffectSession + '_)> {
match self {
Self::Observe(_) => None,
Self::Effect(session) => Some(session.as_mut()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProviderTransport {
Cassette,
Ble,
Usb,
Native,
Import,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IdempotencePolicy {
Idempotent,
Keyed,
AtMostOnce,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReversalPolicy {
Irreversible,
Effect(Symbol),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EffectBounds {
pub max_request_bytes: usize,
pub max_invocations: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EffectDescriptor {
pub id: Symbol,
pub shape: Symbol,
pub capability: CapabilityName,
pub bounds: EffectBounds,
pub requires_arm: bool,
pub expires_after: Duration,
pub receipt: Symbol,
pub idempotence: IdempotencePolicy,
pub reversal: ReversalPolicy,
pub local_stop: Symbol,
}
impl EffectDescriptor {
pub fn validate(&self) -> DeviceResult<()> {
if self.shape.name.is_empty()
|| self.capability.as_str().is_empty()
|| self.bounds.max_request_bytes == 0
|| self.bounds.max_invocations == 0
|| self.expires_after.is_zero()
|| self.receipt.name.is_empty()
|| self.local_stop.name.is_empty()
{
return Err(DeviceError::Contract(format!(
"effect {} has an incomplete authority descriptor",
self.id
)));
}
if matches!(&self.reversal, ReversalPolicy::Effect(effect) if effect == &self.id) {
return Err(DeviceError::Contract(format!(
"effect {} reverses itself",
self.id
)));
}
Ok(())
}
pub fn authorize(&self, request: &EffectRequest) -> DeviceResult<()> {
self.validate()?;
if request.descriptor != self.id {
return Err(DeviceError::Contract("forged effect descriptor".to_owned()));
}
if !request.grants.contains(&self.capability) {
return Err(DeviceError::Contract(format!(
"missing capability {}",
self.capability
)));
}
if self.requires_arm && request.arm.as_deref().is_none_or(str::is_empty) {
return Err(DeviceError::Contract(
"effect request is not armed".to_owned(),
));
}
if request.invoked_at_ms < request.armed_at_ms
|| Duration::from_millis(request.invoked_at_ms - request.armed_at_ms)
> self.expires_after
{
return Err(DeviceError::Contract(
"effect request arm has expired".to_owned(),
));
}
if matches!(self.idempotence, IdempotencePolicy::Keyed)
&& request.idempotence_key.as_deref().is_none_or(str::is_empty)
{
return Err(DeviceError::Contract(
"effect request has no idempotence key".to_owned(),
));
}
if format!("{:?}", request.payload).len() > self.bounds.max_request_bytes {
return Err(DeviceError::Contract(
"effect request exceeds its size bound".to_owned(),
));
}
Ok(())
}
}
pub fn standard_effect_descriptor(id: &str) -> EffectDescriptor {
EffectDescriptor {
id: Symbol::qualified("device/effect", id),
shape: Symbol::qualified("shape/device-effect", id),
capability: CapabilityName::new(format!("device.effect.{id}")),
bounds: EffectBounds {
max_request_bytes: 64 * 1024,
max_invocations: u64::MAX,
},
requires_arm: true,
expires_after: Duration::from_secs(30),
receipt: Symbol::qualified("device/receipt", id),
idempotence: IdempotencePolicy::Keyed,
reversal: ReversalPolicy::Irreversible,
local_stop: Symbol::qualified("device/effect", "stop"),
}
}
#[derive(Clone, Debug, Default)]
pub struct EffectRegistry(BTreeMap<Symbol, EffectDescriptor>);
impl EffectRegistry {
pub fn new(descriptors: impl IntoIterator<Item = EffectDescriptor>) -> DeviceResult<Self> {
let mut entries = BTreeMap::new();
for descriptor in descriptors {
descriptor.validate()?;
let id = descriptor.id.clone();
if entries.insert(id.clone(), descriptor).is_some() {
return Err(DeviceError::Contract(format!(
"duplicate effect descriptor {id}"
)));
}
}
Ok(Self(entries))
}
pub fn get(&self, id: &Symbol) -> Option<&EffectDescriptor> {
self.0.get(id)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProviderManifest {
pub version: u16,
pub id: Symbol,
pub transport: ProviderTransport,
pub profile: String,
pub observations: Vec<Symbol>,
pub effects: Vec<Symbol>,
pub consent: Vec<CapabilityName>,
pub stale_after: Duration,
pub cassette: String,
pub fallback: Symbol,
}
impl ProviderManifest {
pub fn validate(&self, registry: &EffectRegistry) -> DeviceResult<()> {
if self.version != 1 {
return Err(DeviceError::Contract(format!(
"unsupported provider manifest version {}",
self.version
)));
}
if self.stale_after.is_zero() {
return Err(DeviceError::Contract(
"provider manifest has no stale bound".to_owned(),
));
}
for value in [&self.profile, &self.cassette] {
let lower = value.to_ascii_lowercase();
if lower.contains("secret") || lower.contains("token") || lower.contains("password") {
return Err(DeviceError::Contract(
"provider manifest contains secret material".to_owned(),
));
}
if value.is_empty() {
return Err(DeviceError::Contract(
"provider manifest has an empty content reference".to_owned(),
));
}
}
for effect in &self.effects {
if registry.get(effect).is_none() {
return Err(DeviceError::Contract(format!(
"undeclared effect descriptor {effect}"
)));
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct EffectRequest {
pub descriptor: Symbol,
pub payload: Expr,
pub arm: Option<String>,
pub idempotence_key: Option<String>,
pub grants: Vec<CapabilityName>,
pub armed_at_ms: u64,
pub invoked_at_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EffectReceipt {
pub kind: Symbol,
pub descriptor: Symbol,
pub sequence: u64,
}
pub struct FakeEffectSession {
profile: DeviceProfile,
registry: EffectRegistry,
receipts: BTreeMap<String, EffectReceipt>,
invocations: BTreeMap<Symbol, u64>,
stopped: bool,
sequence: u64,
}
impl FakeEffectSession {
pub fn new(profile: DeviceProfile, registry: EffectRegistry) -> Self {
Self {
profile,
registry,
receipts: BTreeMap::new(),
invocations: BTreeMap::new(),
stopped: false,
sequence: 0,
}
}
}
impl ObservationSession for FakeEffectSession {
fn profile(&self) -> &DeviceProfile {
&self.profile
}
fn start(&mut self) -> DeviceResult<()> {
self.stopped = false;
Ok(())
}
fn poll(&mut self, _kind: &str) -> DeviceResult<Option<Expr>> {
Ok(None)
}
fn stop(&mut self) -> DeviceResult<()> {
self.stopped = true;
Ok(())
}
}
impl EffectSession for FakeEffectSession {
fn invoke(&mut self, request: EffectRequest) -> DeviceResult<EffectReceipt> {
if self.stopped {
return Err(DeviceError::Host(
"effect session is locally stopped".into(),
));
}
let descriptor = self
.registry
.get(&request.descriptor)
.ok_or_else(|| DeviceError::Contract("effect is not registered".into()))?;
descriptor.authorize(&request)?;
let count = self
.invocations
.entry(request.descriptor.clone())
.or_default();
if *count >= descriptor.bounds.max_invocations {
return Err(DeviceError::Contract(
"effect invocation bound exhausted".into(),
));
}
if let Some(key) = request.idempotence_key.as_ref()
&& let Some(receipt) = self.receipts.get(key)
{
return Ok(receipt.clone());
}
*count += 1;
self.sequence += 1;
let receipt = EffectReceipt {
kind: descriptor.receipt.clone(),
descriptor: descriptor.id.clone(),
sequence: self.sequence,
};
if let Some(key) = request.idempotence_key {
self.receipts.insert(key, receipt.clone());
}
Ok(receipt)
}
}
pub fn poll_device_sample<S>(session: &mut dyn ObservationSession) -> DeviceResult<Option<S>>
where
S: DeviceSample,
{
session
.poll(S::sample_kind())?
.map(|expr| S::from_expr(&expr))
.transpose()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StubProvider {
profile: DeviceProfile,
}
impl StubProvider {
pub fn new(profile: DeviceProfile) -> Self {
Self { profile }
}
pub fn profile(&self) -> &DeviceProfile {
&self.profile
}
pub fn session(&self) -> StubSession {
StubSession::new(self.profile.clone())
}
}
impl DeviceProvider for StubProvider {
fn open(&self) -> DeviceResult<OpenedSession> {
Err(DeviceError::Unsupported)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StubSession {
profile: DeviceProfile,
}
#[derive(Clone, Debug)]
pub struct ObservationCassette {
profile: DeviceProfile,
samples: Vec<Expr>,
}
impl ObservationCassette {
pub fn new(profile: DeviceProfile, samples: Vec<Expr>) -> Self {
Self { profile, samples }
}
}
impl DeviceProvider for ObservationCassette {
fn open(&self) -> DeviceResult<OpenedSession> {
Ok(OpenedSession::Observe(Box::new(
ObservationCassetteSession {
profile: self.profile.clone(),
samples: self.samples.clone().into(),
},
)))
}
}
struct ObservationCassetteSession {
profile: DeviceProfile,
samples: VecDeque<Expr>,
}
impl ObservationSession for ObservationCassetteSession {
fn profile(&self) -> &DeviceProfile {
&self.profile
}
fn start(&mut self) -> DeviceResult<()> {
Ok(())
}
fn poll(&mut self, _kind: &str) -> DeviceResult<Option<Expr>> {
Ok(self.samples.pop_front())
}
fn stop(&mut self) -> DeviceResult<()> {
Ok(())
}
}
impl StubSession {
pub fn new(profile: DeviceProfile) -> Self {
Self { profile }
}
}
impl ObservationSession for StubSession {
fn profile(&self) -> &DeviceProfile {
&self.profile
}
fn start(&mut self) -> DeviceResult<()> {
Err(DeviceError::Unsupported)
}
fn poll(&mut self, _kind: &str) -> DeviceResult<Option<Expr>> {
Err(DeviceError::Unsupported)
}
fn stop(&mut self) -> DeviceResult<()> {
Ok(())
}
}