use std::collections::BTreeMap;
use std::fmt;
use serde::Serialize;
use crate::stream::{QueueHandles, make_queue};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct SchemaId {
pub name: &'static str,
pub version: u32,
}
impl SchemaId {
pub const fn new(name: &'static str, version: u32) -> Self {
Self { name, version }
}
}
impl fmt::Display for SchemaId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.name, self.version)
}
}
pub trait EventSchema: 'static {
const SCHEMA: SchemaId;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PropValue {
Bool(bool),
Int(i64),
Text(String),
}
impl fmt::Display for PropValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropValue::Bool(value) => write!(f, "{value}"),
PropValue::Int(value) => write!(f, "{value}"),
PropValue::Text(value) => write!(f, "{value:?}"),
}
}
}
impl From<bool> for PropValue {
fn from(value: bool) -> Self {
PropValue::Bool(value)
}
}
impl From<i64> for PropValue {
fn from(value: i64) -> Self {
PropValue::Int(value)
}
}
impl From<&str> for PropValue {
fn from(value: &str) -> Self {
PropValue::Text(value.to_owned())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Props(BTreeMap<&'static str, PropValue>);
impl Props {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, key: &'static str, value: impl Into<PropValue>) -> Self {
self.0.insert(key, value.into());
self
}
pub fn get(&self, key: &str) -> Option<&PropValue> {
self.0.get(key)
}
pub fn iter(&self) -> impl Iterator<Item = (&'static str, &PropValue)> {
self.0.iter().map(|(key, value)| (*key, value))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Requirement {
Equals(PropValue),
OneOf(Vec<PropValue>),
AtLeast(i64),
AtMost(i64),
Within {
min: i64,
max: i64,
},
}
impl Requirement {
pub fn accepts(&self, value: Option<&PropValue>) -> bool {
match (self, value) {
(Requirement::Equals(wanted), Some(found)) => wanted == found,
(Requirement::OneOf(wanted), Some(found)) => wanted.contains(found),
(Requirement::AtLeast(least), Some(PropValue::Int(found))) => found >= least,
(Requirement::AtMost(most), Some(PropValue::Int(found))) => found <= most,
(Requirement::Within { min, max }, Some(PropValue::Int(found))) => {
(min..=max).contains(&found)
}
_ => false,
}
}
}
impl fmt::Display for Requirement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Requirement::Equals(value) => write!(f, "= {value}"),
Requirement::OneOf(values) => {
f.write_str("one of ")?;
for (index, value) in values.iter().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{value}")?;
}
Ok(())
}
Requirement::AtLeast(least) => write!(f, ">= {least}"),
Requirement::AtMost(most) => write!(f, "<= {most}"),
Requirement::Within { min, max } => write!(f, "in {min}..={max}"),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Requires(BTreeMap<&'static str, Requirement>);
impl Requires {
pub fn new() -> Self {
Self::default()
}
pub fn equals(mut self, key: &'static str, value: impl Into<PropValue>) -> Self {
self.0.insert(key, Requirement::Equals(value.into()));
self
}
pub fn at_least(mut self, key: &'static str, least: i64) -> Self {
self.0.insert(key, Requirement::AtLeast(least));
self
}
pub fn at_most(mut self, key: &'static str, most: i64) -> Self {
self.0.insert(key, Requirement::AtMost(most));
self
}
pub fn within(mut self, key: &'static str, min: i64, max: i64) -> Self {
self.0.insert(key, Requirement::Within { min, max });
self
}
pub fn one_of<V: Into<PropValue>>(
mut self,
key: &'static str,
values: impl IntoIterator<Item = V>,
) -> Self {
self.0.insert(
key,
Requirement::OneOf(values.into_iter().map(Into::into).collect()),
);
self
}
pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Requirement)> {
self.0.iter().map(|(key, requirement)| (*key, requirement))
}
pub fn unmet<'a>(&'a self, props: &Props) -> Option<(&'static str, &'a Requirement)> {
self.iter()
.find(|(key, requirement)| !requirement.accepts(props.get(key)))
}
}
#[derive(Clone, Debug)]
pub struct OutputSpec {
pub name: &'static str,
pub schema: SchemaId,
pub props: Props,
factory: fn(usize) -> QueueHandles,
}
impl OutputSpec {
pub fn new<T: EventSchema>(name: &'static str, props: Props) -> Self {
Self::with_schema::<T>(name, T::SCHEMA, props)
}
pub fn with_schema<T: 'static>(name: &'static str, schema: SchemaId, props: Props) -> Self {
Self {
name,
schema,
props,
factory: make_queue::<T>,
}
}
pub(crate) fn make_queue(&self, capacity: usize) -> QueueHandles {
(self.factory)(capacity)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InputSpec {
pub name: &'static str,
pub schema: SchemaId,
pub requires: Requires,
}
impl InputSpec {
pub fn new<T: EventSchema>(name: &'static str, requires: Requires) -> Self {
Self::with_schema(name, T::SCHEMA, requires)
}
pub fn with_schema(name: &'static str, schema: SchemaId, requires: Requires) -> Self {
Self {
name,
schema,
requires,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requirements_check_values() {
let props = Props::new()
.with("wordBits", 8)
.with("cs", true)
.with("bitOrder", "msb-first");
let requires = Requires::new()
.equals("wordBits", 8)
.equals("cs", true)
.equals("bitOrder", "msb-first")
.at_least("channels", 1);
assert_eq!(requires.unmet(&props).map(|(key, _)| key), Some("channels"));
let props = props.with("channels", 4);
assert!(requires.unmet(&props).is_none());
let narrower = Requires::new().equals("wordBits", 16);
assert_eq!(narrower.unmet(&props).map(|(key, _)| key), Some("wordBits"));
}
#[test]
fn at_least_needs_an_integer() {
assert!(!Requirement::AtLeast(1).accepts(Some(&PropValue::Bool(true))));
assert!(!Requirement::AtLeast(1).accepts(None));
assert!(Requirement::AtLeast(1).accepts(Some(&PropValue::Int(1))));
}
#[test]
fn bounds_include_their_ends() {
let int = |value: i64| PropValue::Int(value);
assert!(Requirement::AtMost(20_000).accepts(Some(&int(20_000))));
assert!(!Requirement::AtMost(20_000).accepts(Some(&int(20_001))));
assert!(!Requirement::AtMost(20_000).accepts(Some(&PropValue::Text("fast".into()))));
let within = Requirement::Within {
min: 30_938,
max: 31_562,
};
assert!(within.accepts(Some(&int(30_938))));
assert!(within.accepts(Some(&int(31_562))));
assert!(!within.accepts(Some(&int(30_937))));
assert!(!within.accepts(Some(&int(31_563))));
assert!(!within.accepts(None));
}
#[test]
fn one_of_takes_any_listed_value() {
let requires = Requires::new()
.one_of("mode", [0, 3])
.one_of("parity", ["none", "even"]);
let props =
|mode: i64, parity: &str| Props::new().with("mode", mode).with("parity", parity);
assert!(requires.unmet(&props(0, "none")).is_none());
assert!(requires.unmet(&props(3, "even")).is_none());
assert_eq!(
requires.unmet(&props(1, "none")).map(|(key, _)| key),
Some("mode")
);
assert_eq!(
requires.unmet(&props(3, "odd")).map(|(key, _)| key),
Some("parity")
);
}
#[test]
fn requirements_name_themselves() {
assert_eq!(Requirement::Equals(8.into()).to_string(), "= 8");
assert_eq!(
Requirement::Equals("msb-first".into()).to_string(),
r#"= "msb-first""#
);
assert_eq!(
Requirement::OneOf(vec![0.into(), 3.into()]).to_string(),
"one of 0, 3"
);
assert_eq!(Requirement::AtLeast(1).to_string(), ">= 1");
assert_eq!(Requirement::AtMost(20_000).to_string(), "<= 20000");
assert_eq!(
Requirement::Within {
min: 1000,
max: 20_000
}
.to_string(),
"in 1000..=20000"
);
}
}