Skip to main content

uiharu_core/
schema.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! What ports carry and what they require of each other: the identity of an event schema, the
4//! properties an output declares (word width, bit order, which lines are present …) and the
5//! requirements an input places on them. The graph checks them when ports are connected.
6
7use std::collections::BTreeMap;
8use std::fmt;
9
10use serde::Serialize;
11
12use crate::stream::{QueueHandles, make_queue};
13
14/// The name and version of an event schema, such as `spi.events` version 1. Two ports are
15/// compatible only when both agree.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
17pub struct SchemaId {
18    /// The schema's name, dotted and lower case.
19    pub name: &'static str,
20    /// Its version; a new version is a different contract.
21    pub version: u32,
22}
23
24impl SchemaId {
25    /// The schema `name` at `version`.
26    pub const fn new(name: &'static str, version: u32) -> Self {
27        Self { name, version }
28    }
29}
30
31impl fmt::Display for SchemaId {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "{}@{}", self.name, self.version)
34    }
35}
36
37/// The payload type of an event stream, tied to its schema.
38pub trait EventSchema: 'static {
39    /// The schema this type implements.
40    const SCHEMA: SchemaId;
41}
42
43/// A property of an output port.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum PropValue {
46    /// A yes or no, such as whether a chip select line is present.
47    Bool(bool),
48    /// A count or width, such as the bits per word.
49    Int(i64),
50    /// A name, such as a bit order.
51    Text(String),
52}
53
54impl fmt::Display for PropValue {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            PropValue::Bool(value) => write!(f, "{value}"),
58            PropValue::Int(value) => write!(f, "{value}"),
59            PropValue::Text(value) => write!(f, "{value:?}"),
60        }
61    }
62}
63
64impl From<bool> for PropValue {
65    fn from(value: bool) -> Self {
66        PropValue::Bool(value)
67    }
68}
69
70impl From<i64> for PropValue {
71    fn from(value: i64) -> Self {
72        PropValue::Int(value)
73    }
74}
75
76impl From<&str> for PropValue {
77    fn from(value: &str) -> Self {
78        PropValue::Text(value.to_owned())
79    }
80}
81
82/// The properties of an output port, by name.
83#[derive(Clone, Debug, Default, PartialEq, Eq)]
84pub struct Props(BTreeMap<&'static str, PropValue>);
85
86impl Props {
87    /// No properties.
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Sets `key` to `value`, replacing an earlier value.
93    pub fn with(mut self, key: &'static str, value: impl Into<PropValue>) -> Self {
94        self.0.insert(key, value.into());
95        self
96    }
97
98    /// The value of `key`, if declared.
99    pub fn get(&self, key: &str) -> Option<&PropValue> {
100        self.0.get(key)
101    }
102
103    /// The properties in name order.
104    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &PropValue)> {
105        self.0.iter().map(|(key, value)| (*key, value))
106    }
107}
108
109/// What an input requires of one property of the output it is connected to.
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub enum Requirement {
112    /// The property must be declared with exactly this value.
113    Equals(PropValue),
114    /// The property must be declared with one of these values.
115    OneOf(Vec<PropValue>),
116    /// The property must be an integer of at least this value.
117    AtLeast(i64),
118    /// The property must be an integer of at most this value.
119    AtMost(i64),
120    /// The property must be an integer between these values, both included.
121    Within {
122        /// The smallest value accepted.
123        min: i64,
124        /// The largest value accepted.
125        max: i64,
126    },
127}
128
129impl Requirement {
130    /// Whether `value` (the output's property, if declared) satisfies the requirement.
131    pub fn accepts(&self, value: Option<&PropValue>) -> bool {
132        match (self, value) {
133            (Requirement::Equals(wanted), Some(found)) => wanted == found,
134            (Requirement::OneOf(wanted), Some(found)) => wanted.contains(found),
135            (Requirement::AtLeast(least), Some(PropValue::Int(found))) => found >= least,
136            (Requirement::AtMost(most), Some(PropValue::Int(found))) => found <= most,
137            (Requirement::Within { min, max }, Some(PropValue::Int(found))) => {
138                (min..=max).contains(&found)
139            }
140            _ => false,
141        }
142    }
143}
144
145impl fmt::Display for Requirement {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Requirement::Equals(value) => write!(f, "= {value}"),
149            Requirement::OneOf(values) => {
150                f.write_str("one of ")?;
151                for (index, value) in values.iter().enumerate() {
152                    if index > 0 {
153                        f.write_str(", ")?;
154                    }
155                    write!(f, "{value}")?;
156                }
157                Ok(())
158            }
159            Requirement::AtLeast(least) => write!(f, ">= {least}"),
160            Requirement::AtMost(most) => write!(f, "<= {most}"),
161            Requirement::Within { min, max } => write!(f, "in {min}..={max}"),
162        }
163    }
164}
165
166/// The requirements of an input port, by property name.
167#[derive(Clone, Debug, Default, PartialEq, Eq)]
168pub struct Requires(BTreeMap<&'static str, Requirement>);
169
170impl Requires {
171    /// No requirements.
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Requires `key` to equal `value`.
177    pub fn equals(mut self, key: &'static str, value: impl Into<PropValue>) -> Self {
178        self.0.insert(key, Requirement::Equals(value.into()));
179        self
180    }
181
182    /// Requires `key` to be an integer of at least `least`.
183    pub fn at_least(mut self, key: &'static str, least: i64) -> Self {
184        self.0.insert(key, Requirement::AtLeast(least));
185        self
186    }
187
188    /// Requires `key` to be an integer of at most `most`.
189    pub fn at_most(mut self, key: &'static str, most: i64) -> Self {
190        self.0.insert(key, Requirement::AtMost(most));
191        self
192    }
193
194    /// Requires `key` to be an integer from `min` to `max`, both included.
195    pub fn within(mut self, key: &'static str, min: i64, max: i64) -> Self {
196        self.0.insert(key, Requirement::Within { min, max });
197        self
198    }
199
200    /// Requires `key` to equal one of `values`.
201    pub fn one_of<V: Into<PropValue>>(
202        mut self,
203        key: &'static str,
204        values: impl IntoIterator<Item = V>,
205    ) -> Self {
206        self.0.insert(
207            key,
208            Requirement::OneOf(values.into_iter().map(Into::into).collect()),
209        );
210        self
211    }
212
213    /// The requirements in name order.
214    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Requirement)> {
215        self.0.iter().map(|(key, requirement)| (*key, requirement))
216    }
217
218    /// The first requirement `props` does not satisfy, if any.
219    pub fn unmet<'a>(&'a self, props: &Props) -> Option<(&'static str, &'a Requirement)> {
220        self.iter()
221            .find(|(key, requirement)| !requirement.accepts(props.get(key)))
222    }
223}
224
225/// An output port as a node declares it.
226#[derive(Clone, Debug)]
227pub struct OutputSpec {
228    /// The port's name within its node.
229    pub name: &'static str,
230    /// The schema of the events it carries.
231    pub schema: SchemaId,
232    /// What the node promises about the events.
233    pub props: Props,
234    /// Makes the queue behind the port, typed for its payload.
235    factory: fn(usize) -> QueueHandles,
236}
237
238impl OutputSpec {
239    /// A port named `name` carrying `T` events with `props`.
240    pub fn new<T: EventSchema>(name: &'static str, props: Props) -> Self {
241        Self::with_schema::<T>(name, T::SCHEMA, props)
242    }
243
244    /// A port named `name` carrying `schema` events as `T` values with `props`: for a node
245    /// whose contract is known only at run time, such as a decoder interpreted from its IR,
246    /// whose queue holds a payload type shared by every such contract.
247    pub fn with_schema<T: 'static>(name: &'static str, schema: SchemaId, props: Props) -> Self {
248        Self {
249            name,
250            schema,
251            props,
252            factory: make_queue::<T>,
253        }
254    }
255
256    pub(crate) fn make_queue(&self, capacity: usize) -> QueueHandles {
257        (self.factory)(capacity)
258    }
259}
260
261/// An input port as a node declares it.
262#[derive(Clone, Debug, PartialEq, Eq)]
263pub struct InputSpec {
264    /// The port's name within its node.
265    pub name: &'static str,
266    /// The schema of the events it reads.
267    pub schema: SchemaId,
268    /// What it requires of the output connected to it.
269    pub requires: Requires,
270}
271
272impl InputSpec {
273    /// A port named `name` reading `T` events from an output that satisfies `requires`.
274    pub fn new<T: EventSchema>(name: &'static str, requires: Requires) -> Self {
275        Self::with_schema(name, T::SCHEMA, requires)
276    }
277
278    /// A port named `name` reading `schema` events from an output that satisfies `requires`:
279    /// for a node whose contract is known only at run time. The node binds the port to the
280    /// payload type the schema's queues hold.
281    pub fn with_schema(name: &'static str, schema: SchemaId, requires: Requires) -> Self {
282        Self {
283            name,
284            schema,
285            requires,
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn requirements_check_values() {
296        let props = Props::new()
297            .with("wordBits", 8)
298            .with("cs", true)
299            .with("bitOrder", "msb-first");
300        let requires = Requires::new()
301            .equals("wordBits", 8)
302            .equals("cs", true)
303            .equals("bitOrder", "msb-first")
304            .at_least("channels", 1);
305        assert_eq!(requires.unmet(&props).map(|(key, _)| key), Some("channels"));
306        let props = props.with("channels", 4);
307        assert!(requires.unmet(&props).is_none());
308        let narrower = Requires::new().equals("wordBits", 16);
309        assert_eq!(narrower.unmet(&props).map(|(key, _)| key), Some("wordBits"));
310    }
311
312    #[test]
313    fn at_least_needs_an_integer() {
314        assert!(!Requirement::AtLeast(1).accepts(Some(&PropValue::Bool(true))));
315        assert!(!Requirement::AtLeast(1).accepts(None));
316        assert!(Requirement::AtLeast(1).accepts(Some(&PropValue::Int(1))));
317    }
318
319    #[test]
320    fn bounds_include_their_ends() {
321        let int = |value: i64| PropValue::Int(value);
322        assert!(Requirement::AtMost(20_000).accepts(Some(&int(20_000))));
323        assert!(!Requirement::AtMost(20_000).accepts(Some(&int(20_001))));
324        assert!(!Requirement::AtMost(20_000).accepts(Some(&PropValue::Text("fast".into()))));
325        let within = Requirement::Within {
326            min: 30_938,
327            max: 31_562,
328        };
329        assert!(within.accepts(Some(&int(30_938))));
330        assert!(within.accepts(Some(&int(31_562))));
331        assert!(!within.accepts(Some(&int(30_937))));
332        assert!(!within.accepts(Some(&int(31_563))));
333        assert!(!within.accepts(None));
334    }
335
336    #[test]
337    fn one_of_takes_any_listed_value() {
338        let requires = Requires::new()
339            .one_of("mode", [0, 3])
340            .one_of("parity", ["none", "even"]);
341        let props =
342            |mode: i64, parity: &str| Props::new().with("mode", mode).with("parity", parity);
343        assert!(requires.unmet(&props(0, "none")).is_none());
344        assert!(requires.unmet(&props(3, "even")).is_none());
345        assert_eq!(
346            requires.unmet(&props(1, "none")).map(|(key, _)| key),
347            Some("mode")
348        );
349        assert_eq!(
350            requires.unmet(&props(3, "odd")).map(|(key, _)| key),
351            Some("parity")
352        );
353    }
354
355    #[test]
356    fn requirements_name_themselves() {
357        assert_eq!(Requirement::Equals(8.into()).to_string(), "= 8");
358        assert_eq!(
359            Requirement::Equals("msb-first".into()).to_string(),
360            r#"= "msb-first""#
361        );
362        assert_eq!(
363            Requirement::OneOf(vec![0.into(), 3.into()]).to_string(),
364            "one of 0, 3"
365        );
366        assert_eq!(Requirement::AtLeast(1).to_string(), ">= 1");
367        assert_eq!(Requirement::AtMost(20_000).to_string(), "<= 20000");
368        assert_eq!(
369            Requirement::Within {
370                min: 1000,
371                max: 20_000
372            }
373            .to_string(),
374            "in 1000..=20000"
375        );
376    }
377}