1use std::collections::BTreeMap;
8use std::fmt;
9
10use serde::Serialize;
11
12use crate::stream::{QueueHandles, make_queue};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
17pub struct SchemaId {
18 pub name: &'static str,
20 pub version: u32,
22}
23
24impl SchemaId {
25 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
37pub trait EventSchema: 'static {
39 const SCHEMA: SchemaId;
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum PropValue {
46 Bool(bool),
48 Int(i64),
50 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
84pub struct Props(BTreeMap<&'static str, PropValue>);
85
86impl Props {
87 pub fn new() -> Self {
89 Self::default()
90 }
91
92 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 pub fn get(&self, key: &str) -> Option<&PropValue> {
100 self.0.get(key)
101 }
102
103 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &PropValue)> {
105 self.0.iter().map(|(key, value)| (*key, value))
106 }
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
111pub enum Requirement {
112 Equals(PropValue),
114 OneOf(Vec<PropValue>),
116 AtLeast(i64),
118 AtMost(i64),
120 Within {
122 min: i64,
124 max: i64,
126 },
127}
128
129impl Requirement {
130 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
168pub struct Requires(BTreeMap<&'static str, Requirement>);
169
170impl Requires {
171 pub fn new() -> Self {
173 Self::default()
174 }
175
176 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 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 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 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 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 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Requirement)> {
215 self.0.iter().map(|(key, requirement)| (*key, requirement))
216 }
217
218 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#[derive(Clone, Debug)]
227pub struct OutputSpec {
228 pub name: &'static str,
230 pub schema: SchemaId,
232 pub props: Props,
234 factory: fn(usize) -> QueueHandles,
236}
237
238impl OutputSpec {
239 pub fn new<T: EventSchema>(name: &'static str, props: Props) -> Self {
241 Self::with_schema::<T>(name, T::SCHEMA, props)
242 }
243
244 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#[derive(Clone, Debug, PartialEq, Eq)]
263pub struct InputSpec {
264 pub name: &'static str,
266 pub schema: SchemaId,
268 pub requires: Requires,
270}
271
272impl InputSpec {
273 pub fn new<T: EventSchema>(name: &'static str, requires: Requires) -> Self {
275 Self::with_schema(name, T::SCHEMA, requires)
276 }
277
278 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}