Skip to main content

sim_lib_stream_device/
sample.rs

1//! Fail-closed device sample values and stream packet helpers.
2
3use std::fmt;
4
5use sim_kernel::{Expr, Symbol};
6use sim_lib_stream_core::{DataPacket, StreamPacket};
7use sim_value::{access, build};
8
9/// Result type returned by device sample decoders.
10pub type DeviceSampleResult<T> = std::result::Result<T, DeviceSampleError>;
11
12/// A strict device sample decoding error.
13///
14/// The error intentionally carries a human-readable message only. Device sample
15/// parsers reject malformed data at the first invalid field rather than trying
16/// to coerce or infer a sample from partial input.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct DeviceSampleError {
19    message: String,
20}
21
22impl DeviceSampleError {
23    /// Builds a device sample error from a message.
24    pub fn new(message: impl Into<String>) -> Self {
25        Self {
26            message: message.into(),
27        }
28    }
29
30    /// Returns the error message.
31    pub fn message(&self) -> &str {
32        &self.message
33    }
34}
35
36impl fmt::Display for DeviceSampleError {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.write_str(&self.message)
39    }
40}
41
42impl std::error::Error for DeviceSampleError {}
43
44impl From<DeviceSampleError> for sim_kernel::Error {
45    fn from(error: DeviceSampleError) -> Self {
46        sim_kernel::Error::Eval(format!("device sample error: {error}"))
47    }
48}
49
50/// Trait implemented by concrete device sample records.
51///
52/// A sample has a stable kind tag, a monotone sequence number, and strict
53/// expression round-tripping. [`from_expr`](Self::from_expr) is fail-closed: it
54/// returns an error for missing fields, wrong field types, unknown kind tags, or
55/// malformed sequence numbers.
56pub trait DeviceSample: Sized + Clone {
57    /// Stable bare sample kind, such as `device-caps`.
58    fn sample_kind() -> &'static str;
59
60    /// Monotone sequence number for ordering and aging samples.
61    fn seq(&self) -> u64;
62
63    /// Encodes the sample as a self-describing expression map.
64    fn to_expr(&self) -> Expr;
65
66    /// Decodes the sample from its expression map.
67    fn from_expr(expr: &Expr) -> DeviceSampleResult<Self>;
68}
69
70/// Reports whether a sample survives a strict expression round trip.
71pub fn roundtrip_ok<S>(sample: &S) -> bool
72where
73    S: DeviceSample + PartialEq,
74{
75    S::from_expr(&sample.to_expr())
76        .map(|decoded| &decoded == sample)
77        .unwrap_or(false)
78}
79
80/// Wraps a device sample as an ordinary stream data packet.
81pub fn sample_packet<S: DeviceSample>(sample: &S) -> StreamPacket {
82    StreamPacket::Data(DataPacket::new(
83        sample_kind_symbol(S::sample_kind()),
84        sample.to_expr(),
85    ))
86}
87
88/// Returns the stable record tag for device sample maps.
89pub fn device_sample_record_symbol() -> Symbol {
90    Symbol::qualified("stream", "device-sample")
91}
92
93/// Returns the qualified sample-kind symbol for `kind`.
94pub fn sample_kind_symbol(kind: &str) -> Symbol {
95    Symbol::qualified("stream/device-sample", kind)
96}
97
98/// Returns the qualified sample-kind symbol for [`DeviceCaps`].
99pub fn device_caps_sample_kind_symbol() -> Symbol {
100    sample_kind_symbol(DeviceCaps::sample_kind())
101}
102
103/// A sample describing a device's stream-facing capabilities.
104///
105/// `DeviceCaps` is the base sample every concrete device instance can emit
106/// before it starts producing richer sensor-specific sample kinds.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct DeviceCaps {
109    seq: u64,
110    device: Symbol,
111    streams: Vec<Symbol>,
112    inputs: Vec<Symbol>,
113    outputs: Vec<Symbol>,
114}
115
116impl DeviceCaps {
117    /// Builds a device capabilities sample.
118    pub fn new(
119        seq: u64,
120        device: Symbol,
121        streams: Vec<Symbol>,
122        inputs: Vec<Symbol>,
123        outputs: Vec<Symbol>,
124    ) -> Self {
125        Self {
126            seq,
127            device,
128            streams,
129            inputs,
130            outputs,
131        }
132    }
133
134    /// Builds the deterministic demo capability sample used by tests and docs.
135    pub fn demo(seq: u64) -> Self {
136        Self::new(
137            seq,
138            Symbol::qualified("device", "modeled-edge"),
139            vec![
140                Symbol::qualified("device/stream", "battery"),
141                Symbol::qualified("device/stream", "motion"),
142            ],
143            vec![Symbol::qualified("device/input", "button")],
144            vec![
145                Symbol::qualified("device/output", "screen"),
146                Symbol::qualified("device/output", "haptic"),
147            ],
148        )
149    }
150
151    /// Returns the device identity symbol.
152    pub fn device(&self) -> &Symbol {
153        &self.device
154    }
155
156    /// Returns the advertised sample stream symbols.
157    pub fn streams(&self) -> &[Symbol] {
158        &self.streams
159    }
160
161    /// Returns the input capability symbols.
162    pub fn inputs(&self) -> &[Symbol] {
163        &self.inputs
164    }
165
166    /// Returns the output capability symbols.
167    pub fn outputs(&self) -> &[Symbol] {
168        &self.outputs
169    }
170
171    /// Wraps this capabilities sample as a stream data packet.
172    pub fn to_stream_packet(&self) -> StreamPacket {
173        sample_packet(self)
174    }
175}
176
177impl DeviceSample for DeviceCaps {
178    fn sample_kind() -> &'static str {
179        "device-caps"
180    }
181
182    fn seq(&self) -> u64 {
183        self.seq
184    }
185
186    fn to_expr(&self) -> Expr {
187        build::map(vec![
188            ("kind", Expr::Symbol(device_sample_record_symbol())),
189            ("sample", Expr::Symbol(device_caps_sample_kind_symbol())),
190            ("seq", build::uint(self.seq)),
191            ("device", Expr::Symbol(self.device.clone())),
192            ("streams", symbols_expr(&self.streams)),
193            ("inputs", symbols_expr(&self.inputs)),
194            ("outputs", symbols_expr(&self.outputs)),
195        ])
196    }
197
198    fn from_expr(expr: &Expr) -> DeviceSampleResult<Self> {
199        let entries = entries(expr)?;
200        expect_record_tag(entries)?;
201        expect_sample_kind(entries, &device_caps_sample_kind_symbol())?;
202        Ok(Self::new(
203            seq_field(entries)?,
204            symbol_field(entries, "device")?.clone(),
205            symbol_list_field(entries, "streams")?,
206            symbol_list_field(entries, "inputs")?,
207            symbol_list_field(entries, "outputs")?,
208        ))
209    }
210}
211
212pub(crate) fn decode_known_sample(expr: &Expr) -> DeviceSampleResult<()> {
213    let entries = entries(expr)?;
214    expect_record_tag(entries)?;
215    let kind = symbol_field(entries, "sample")?;
216    if kind == &device_caps_sample_kind_symbol() {
217        DeviceCaps::from_expr(expr)?;
218        return Ok(());
219    }
220    Err(DeviceSampleError::new(format!(
221        "unknown device sample kind {kind}"
222    )))
223}
224
225pub(crate) fn sample_constructor_args(expr: &Expr) -> DeviceSampleResult<Vec<Expr>> {
226    decode_known_sample(expr)?;
227    Ok(vec![expr.clone()])
228}
229
230fn symbols_expr(symbols: &[Symbol]) -> Expr {
231    build::list(symbols.iter().cloned().map(Expr::Symbol).collect())
232}
233
234fn entries(expr: &Expr) -> DeviceSampleResult<&[(Expr, Expr)]> {
235    access::map_entries(expr, "device sample map").map_err(kernel_error)
236}
237
238fn expect_record_tag(entries: &[(Expr, Expr)]) -> DeviceSampleResult<()> {
239    let actual = symbol_field(entries, "kind")?;
240    let expected = device_sample_record_symbol();
241    if actual == &expected {
242        Ok(())
243    } else {
244        Err(DeviceSampleError::new(format!(
245            "device sample kind tag must be {expected}, found {actual}"
246        )))
247    }
248}
249
250fn expect_sample_kind(entries: &[(Expr, Expr)], expected: &Symbol) -> DeviceSampleResult<()> {
251    let actual = symbol_field(entries, "sample")?;
252    if actual == expected {
253        Ok(())
254    } else {
255        Err(DeviceSampleError::new(format!(
256            "device sample record must be {expected}, found {actual}"
257        )))
258    }
259}
260
261fn seq_field(entries: &[(Expr, Expr)]) -> DeviceSampleResult<u64> {
262    let value = field(entries, "seq")?;
263    let Expr::Number(number) = value else {
264        return Err(DeviceSampleError::new(format!(
265            "device sample seq must be a u64 number, found {}",
266            sim_value::kind::expr_kind(value)
267        )));
268    };
269    if !matches!(number.domain.name.as_ref(), "i64" | "u64") {
270        return Err(DeviceSampleError::new(format!(
271            "device sample seq must use an integer domain, found {}",
272            number.domain
273        )));
274    }
275    number
276        .canonical
277        .parse::<u64>()
278        .map_err(|err| DeviceSampleError::new(format!("invalid device sample seq: {err}")))
279}
280
281fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> DeviceSampleResult<&'a Symbol> {
282    access::entry_required_sym(entries, name, "device sample").map_err(kernel_error)
283}
284
285fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> DeviceSampleResult<&'a Expr> {
286    access::entry_required(entries, name, "device sample").map_err(kernel_error)
287}
288
289fn symbol_list_field(entries: &[(Expr, Expr)], name: &str) -> DeviceSampleResult<Vec<Symbol>> {
290    let items =
291        access::entry_required_list(entries, name, "device sample").map_err(kernel_error)?;
292    items
293        .iter()
294        .map(|item| match item {
295            Expr::Symbol(symbol) => Ok(symbol.clone()),
296            other => Err(DeviceSampleError::new(format!(
297                "device sample {name} entries must be symbols, found {}",
298                sim_value::kind::expr_kind(other)
299            ))),
300        })
301        .collect()
302}
303
304fn kernel_error(error: sim_kernel::Error) -> DeviceSampleError {
305    DeviceSampleError::new(error.to_string())
306}