1use std::{
4 collections::{BTreeMap, VecDeque},
5 fmt,
6 time::Duration,
7};
8
9use sim_kernel::{CapabilityName, Expr, Symbol};
10
11pub type DeviceResult<T> = std::result::Result<T, DeviceError>;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum DeviceError {
17 Unsupported,
19 Sample(String),
21 Host(String),
23 Contract(String),
25}
26
27impl fmt::Display for DeviceError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::Unsupported => f.write_str("device provider is unsupported"),
31 Self::Sample(message) => write!(f, "device sample error: {message}"),
32 Self::Host(message) => f.write_str(message),
33 Self::Contract(message) => write!(f, "device contract error: {message}"),
34 }
35 }
36}
37
38impl std::error::Error for DeviceError {}
39
40impl From<DeviceError> for sim_kernel::Error {
41 fn from(error: DeviceError) -> Self {
42 match error {
43 DeviceError::Unsupported => {
44 Self::HostError("device provider is unsupported".to_owned())
45 }
46 DeviceError::Sample(message) => Self::Eval(format!("device sample error: {message}")),
47 DeviceError::Host(message) => Self::HostError(message),
48 DeviceError::Contract(message) => {
49 Self::Eval(format!("device contract error: {message}"))
50 }
51 }
52 }
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct DeviceProfile {
58 pub device: Symbol,
60 pub streams: Vec<Symbol>,
62 pub inputs: Vec<Symbol>,
64 pub outputs: Vec<Symbol>,
66 pub sample_kinds: Vec<Symbol>,
68}
69
70impl DeviceProfile {
71 pub fn new(
73 device: Symbol,
74 streams: Vec<Symbol>,
75 inputs: Vec<Symbol>,
76 outputs: Vec<Symbol>,
77 sample_kinds: Vec<Symbol>,
78 ) -> Self {
79 Self {
80 device,
81 streams,
82 inputs,
83 outputs,
84 sample_kinds,
85 }
86 }
87
88 pub fn modeled_edge() -> Self {
90 Self::new(
91 Symbol::qualified("device", "modeled-edge"),
92 vec![
93 Symbol::qualified("device/stream", "battery"),
94 Symbol::qualified("device/stream", "motion"),
95 ],
96 vec![Symbol::qualified("device/input", "button")],
97 vec![
98 Symbol::qualified("device/output", "screen"),
99 Symbol::qualified("device/output", "haptic"),
100 ],
101 vec![device_sample_kind_symbol("device-caps")],
102 )
103 }
104
105 pub fn supports_sample_kind(&self, sample_kind: &Symbol) -> bool {
107 self.sample_kinds.contains(sample_kind)
108 }
109}
110
111pub trait DeviceSample: Sized {
113 fn sample_kind() -> &'static str;
115
116 fn to_expr(&self) -> Expr;
118
119 fn from_expr(expr: &Expr) -> DeviceResult<Self>;
121}
122
123pub fn device_sample_kind_symbol(kind: &str) -> Symbol {
125 Symbol::qualified("stream/device-sample", kind)
126}
127
128pub trait DeviceProvider: Send {
130 fn open(&self) -> DeviceResult<OpenedSession>;
132}
133
134pub trait ObservationSession: Send {
146 fn profile(&self) -> &DeviceProfile;
148
149 fn start(&mut self) -> DeviceResult<()>;
151
152 fn poll(&mut self, kind: &str) -> DeviceResult<Option<Expr>>;
154
155 fn stop(&mut self) -> DeviceResult<()>;
157}
158
159pub trait EffectSession: ObservationSession {
161 fn invoke(&mut self, request: EffectRequest) -> DeviceResult<EffectReceipt>;
163}
164
165pub enum OpenedSession {
167 Observe(Box<dyn ObservationSession>),
169 Effect(Box<dyn EffectSession>),
171}
172
173impl OpenedSession {
174 pub fn observation(&mut self) -> &mut dyn ObservationSession {
176 match self {
177 Self::Observe(session) => session.as_mut(),
178 Self::Effect(session) => session.as_mut(),
179 }
180 }
181
182 pub fn effect(&mut self) -> Option<&mut (dyn EffectSession + '_)> {
184 match self {
185 Self::Observe(_) => None,
186 Self::Effect(session) => Some(session.as_mut()),
187 }
188 }
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum ProviderTransport {
194 Cassette,
196 Ble,
198 Usb,
200 Native,
202 Import,
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum IdempotencePolicy {
209 Idempotent,
211 Keyed,
213 AtMostOnce,
215}
216
217#[derive(Clone, Debug, PartialEq, Eq)]
219pub enum ReversalPolicy {
220 Irreversible,
222 Effect(Symbol),
224}
225
226#[derive(Clone, Debug, PartialEq, Eq)]
228pub struct EffectBounds {
229 pub max_request_bytes: usize,
231 pub max_invocations: u64,
233}
234
235#[derive(Clone, Debug, PartialEq, Eq)]
237pub struct EffectDescriptor {
238 pub id: Symbol,
240 pub shape: Symbol,
242 pub capability: CapabilityName,
244 pub bounds: EffectBounds,
246 pub requires_arm: bool,
248 pub expires_after: Duration,
250 pub receipt: Symbol,
252 pub idempotence: IdempotencePolicy,
254 pub reversal: ReversalPolicy,
256 pub local_stop: Symbol,
258}
259
260impl EffectDescriptor {
261 pub fn validate(&self) -> DeviceResult<()> {
263 if self.shape.name.is_empty()
264 || self.capability.as_str().is_empty()
265 || self.bounds.max_request_bytes == 0
266 || self.bounds.max_invocations == 0
267 || self.expires_after.is_zero()
268 || self.receipt.name.is_empty()
269 || self.local_stop.name.is_empty()
270 {
271 return Err(DeviceError::Contract(format!(
272 "effect {} has an incomplete authority descriptor",
273 self.id
274 )));
275 }
276 if matches!(&self.reversal, ReversalPolicy::Effect(effect) if effect == &self.id) {
277 return Err(DeviceError::Contract(format!(
278 "effect {} reverses itself",
279 self.id
280 )));
281 }
282 Ok(())
283 }
284
285 pub fn authorize(&self, request: &EffectRequest) -> DeviceResult<()> {
287 self.validate()?;
288 if request.descriptor != self.id {
289 return Err(DeviceError::Contract("forged effect descriptor".to_owned()));
290 }
291 if !request.grants.contains(&self.capability) {
292 return Err(DeviceError::Contract(format!(
293 "missing capability {}",
294 self.capability
295 )));
296 }
297 if self.requires_arm && request.arm.as_deref().is_none_or(str::is_empty) {
298 return Err(DeviceError::Contract(
299 "effect request is not armed".to_owned(),
300 ));
301 }
302 if request.invoked_at_ms < request.armed_at_ms
303 || Duration::from_millis(request.invoked_at_ms - request.armed_at_ms)
304 > self.expires_after
305 {
306 return Err(DeviceError::Contract(
307 "effect request arm has expired".to_owned(),
308 ));
309 }
310 if matches!(self.idempotence, IdempotencePolicy::Keyed)
311 && request.idempotence_key.as_deref().is_none_or(str::is_empty)
312 {
313 return Err(DeviceError::Contract(
314 "effect request has no idempotence key".to_owned(),
315 ));
316 }
317 if format!("{:?}", request.payload).len() > self.bounds.max_request_bytes {
318 return Err(DeviceError::Contract(
319 "effect request exceeds its size bound".to_owned(),
320 ));
321 }
322 Ok(())
323 }
324}
325
326pub fn standard_effect_descriptor(id: &str) -> EffectDescriptor {
328 EffectDescriptor {
329 id: Symbol::qualified("device/effect", id),
330 shape: Symbol::qualified("shape/device-effect", id),
331 capability: CapabilityName::new(format!("device.effect.{id}")),
332 bounds: EffectBounds {
333 max_request_bytes: 64 * 1024,
334 max_invocations: u64::MAX,
335 },
336 requires_arm: true,
337 expires_after: Duration::from_secs(30),
338 receipt: Symbol::qualified("device/receipt", id),
339 idempotence: IdempotencePolicy::Keyed,
340 reversal: ReversalPolicy::Irreversible,
341 local_stop: Symbol::qualified("device/effect", "stop"),
342 }
343}
344
345#[derive(Clone, Debug, Default)]
347pub struct EffectRegistry(BTreeMap<Symbol, EffectDescriptor>);
348
349impl EffectRegistry {
350 pub fn new(descriptors: impl IntoIterator<Item = EffectDescriptor>) -> DeviceResult<Self> {
352 let mut entries = BTreeMap::new();
353 for descriptor in descriptors {
354 descriptor.validate()?;
355 let id = descriptor.id.clone();
356 if entries.insert(id.clone(), descriptor).is_some() {
357 return Err(DeviceError::Contract(format!(
358 "duplicate effect descriptor {id}"
359 )));
360 }
361 }
362 Ok(Self(entries))
363 }
364
365 pub fn get(&self, id: &Symbol) -> Option<&EffectDescriptor> {
367 self.0.get(id)
368 }
369}
370
371#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct ProviderManifest {
374 pub version: u16,
376 pub id: Symbol,
378 pub transport: ProviderTransport,
380 pub profile: String,
382 pub observations: Vec<Symbol>,
384 pub effects: Vec<Symbol>,
386 pub consent: Vec<CapabilityName>,
388 pub stale_after: Duration,
390 pub cassette: String,
392 pub fallback: Symbol,
394}
395
396impl ProviderManifest {
397 pub fn validate(&self, registry: &EffectRegistry) -> DeviceResult<()> {
399 if self.version != 1 {
400 return Err(DeviceError::Contract(format!(
401 "unsupported provider manifest version {}",
402 self.version
403 )));
404 }
405 if self.stale_after.is_zero() {
406 return Err(DeviceError::Contract(
407 "provider manifest has no stale bound".to_owned(),
408 ));
409 }
410 for value in [&self.profile, &self.cassette] {
411 let lower = value.to_ascii_lowercase();
412 if lower.contains("secret") || lower.contains("token") || lower.contains("password") {
413 return Err(DeviceError::Contract(
414 "provider manifest contains secret material".to_owned(),
415 ));
416 }
417 if value.is_empty() {
418 return Err(DeviceError::Contract(
419 "provider manifest has an empty content reference".to_owned(),
420 ));
421 }
422 }
423 for effect in &self.effects {
424 if registry.get(effect).is_none() {
425 return Err(DeviceError::Contract(format!(
426 "undeclared effect descriptor {effect}"
427 )));
428 }
429 }
430 Ok(())
431 }
432}
433
434#[derive(Clone, Debug, PartialEq)]
436pub struct EffectRequest {
437 pub descriptor: Symbol,
439 pub payload: Expr,
441 pub arm: Option<String>,
443 pub idempotence_key: Option<String>,
445 pub grants: Vec<CapabilityName>,
447 pub armed_at_ms: u64,
449 pub invoked_at_ms: u64,
451}
452
453#[derive(Clone, Debug, PartialEq, Eq)]
455pub struct EffectReceipt {
456 pub kind: Symbol,
458 pub descriptor: Symbol,
460 pub sequence: u64,
462}
463
464pub struct FakeEffectSession {
466 profile: DeviceProfile,
467 registry: EffectRegistry,
468 receipts: BTreeMap<String, EffectReceipt>,
469 invocations: BTreeMap<Symbol, u64>,
470 stopped: bool,
471 sequence: u64,
472}
473
474impl FakeEffectSession {
475 pub fn new(profile: DeviceProfile, registry: EffectRegistry) -> Self {
477 Self {
478 profile,
479 registry,
480 receipts: BTreeMap::new(),
481 invocations: BTreeMap::new(),
482 stopped: false,
483 sequence: 0,
484 }
485 }
486}
487
488impl ObservationSession for FakeEffectSession {
489 fn profile(&self) -> &DeviceProfile {
490 &self.profile
491 }
492 fn start(&mut self) -> DeviceResult<()> {
493 self.stopped = false;
494 Ok(())
495 }
496 fn poll(&mut self, _kind: &str) -> DeviceResult<Option<Expr>> {
497 Ok(None)
498 }
499 fn stop(&mut self) -> DeviceResult<()> {
500 self.stopped = true;
501 Ok(())
502 }
503}
504
505impl EffectSession for FakeEffectSession {
506 fn invoke(&mut self, request: EffectRequest) -> DeviceResult<EffectReceipt> {
507 if self.stopped {
508 return Err(DeviceError::Host(
509 "effect session is locally stopped".into(),
510 ));
511 }
512 let descriptor = self
513 .registry
514 .get(&request.descriptor)
515 .ok_or_else(|| DeviceError::Contract("effect is not registered".into()))?;
516 descriptor.authorize(&request)?;
517 let count = self
518 .invocations
519 .entry(request.descriptor.clone())
520 .or_default();
521 if *count >= descriptor.bounds.max_invocations {
522 return Err(DeviceError::Contract(
523 "effect invocation bound exhausted".into(),
524 ));
525 }
526 if let Some(key) = request.idempotence_key.as_ref()
527 && let Some(receipt) = self.receipts.get(key)
528 {
529 return Ok(receipt.clone());
530 }
531 *count += 1;
532 self.sequence += 1;
533 let receipt = EffectReceipt {
534 kind: descriptor.receipt.clone(),
535 descriptor: descriptor.id.clone(),
536 sequence: self.sequence,
537 };
538 if let Some(key) = request.idempotence_key {
539 self.receipts.insert(key, receipt.clone());
540 }
541 Ok(receipt)
542 }
543}
544
545pub fn poll_device_sample<S>(session: &mut dyn ObservationSession) -> DeviceResult<Option<S>>
547where
548 S: DeviceSample,
549{
550 session
551 .poll(S::sample_kind())?
552 .map(|expr| S::from_expr(&expr))
553 .transpose()
554}
555
556#[derive(Clone, Debug, PartialEq, Eq)]
558pub struct StubProvider {
559 profile: DeviceProfile,
560}
561
562impl StubProvider {
563 pub fn new(profile: DeviceProfile) -> Self {
565 Self { profile }
566 }
567
568 pub fn profile(&self) -> &DeviceProfile {
570 &self.profile
571 }
572
573 pub fn session(&self) -> StubSession {
575 StubSession::new(self.profile.clone())
576 }
577}
578
579impl DeviceProvider for StubProvider {
580 fn open(&self) -> DeviceResult<OpenedSession> {
581 Err(DeviceError::Unsupported)
582 }
583}
584
585#[derive(Clone, Debug, PartialEq, Eq)]
587pub struct StubSession {
588 profile: DeviceProfile,
589}
590
591#[derive(Clone, Debug)]
593pub struct ObservationCassette {
594 profile: DeviceProfile,
595 samples: Vec<Expr>,
596}
597
598impl ObservationCassette {
599 pub fn new(profile: DeviceProfile, samples: Vec<Expr>) -> Self {
601 Self { profile, samples }
602 }
603}
604
605impl DeviceProvider for ObservationCassette {
606 fn open(&self) -> DeviceResult<OpenedSession> {
607 Ok(OpenedSession::Observe(Box::new(
608 ObservationCassetteSession {
609 profile: self.profile.clone(),
610 samples: self.samples.clone().into(),
611 },
612 )))
613 }
614}
615
616struct ObservationCassetteSession {
617 profile: DeviceProfile,
618 samples: VecDeque<Expr>,
619}
620
621impl ObservationSession for ObservationCassetteSession {
622 fn profile(&self) -> &DeviceProfile {
623 &self.profile
624 }
625 fn start(&mut self) -> DeviceResult<()> {
626 Ok(())
627 }
628 fn poll(&mut self, _kind: &str) -> DeviceResult<Option<Expr>> {
629 Ok(self.samples.pop_front())
630 }
631 fn stop(&mut self) -> DeviceResult<()> {
632 Ok(())
633 }
634}
635
636impl StubSession {
637 pub fn new(profile: DeviceProfile) -> Self {
639 Self { profile }
640 }
641}
642
643impl ObservationSession for StubSession {
644 fn profile(&self) -> &DeviceProfile {
645 &self.profile
646 }
647
648 fn start(&mut self) -> DeviceResult<()> {
649 Err(DeviceError::Unsupported)
650 }
651
652 fn poll(&mut self, _kind: &str) -> DeviceResult<Option<Expr>> {
653 Err(DeviceError::Unsupported)
654 }
655
656 fn stop(&mut self) -> DeviceResult<()> {
657 Ok(())
658 }
659}