use serde::{Deserialize, Serialize};
#[doc(hidden)]
pub mod sealing {
pub trait Sealed {}
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Connection {
Can(Can),
I2c(I2c),
Spi(Spi),
Serial(Serial),
Uart(Uart),
Usb(Usb),
Gpio(Gpio),
}
impl Connection {
#[must_use]
pub const fn kind(&self) -> ConnectionKind {
match self {
Self::Can(_) => ConnectionKind::Can,
Self::I2c(_) => ConnectionKind::I2c,
Self::Spi(_) => ConnectionKind::Spi,
Self::Serial(_) => ConnectionKind::Serial,
Self::Uart(_) => ConnectionKind::Uart,
Self::Usb(_) => ConnectionKind::Usb,
Self::Gpio(_) => ConnectionKind::Gpio,
}
}
}
#[derive(
phoxal_macros::DescribeWire, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum ConnectionKind {
Can,
I2c,
Spi,
Serial,
Uart,
Usb,
Gpio,
}
impl ConnectionKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Can => "can",
Self::I2c => "i2c",
Self::Spi => "spi",
Self::Serial => "serial",
Self::Uart => "uart",
Self::Usb => "usb",
Self::Gpio => "gpio",
}
}
pub const ALL: [Self; 7] = [
Self::Can,
Self::I2c,
Self::Spi,
Self::Serial,
Self::Uart,
Self::Usb,
Self::Gpio,
];
}
impl std::fmt::Display for ConnectionKind {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error(
"this driver accepts a {expected} connection, but the component instance authors {authored}"
)]
pub struct ConnectionKindMismatch {
pub expected: ConnectionKind,
pub authored: ConnectionKind,
}
pub trait ConnectionPayload: sealing::Sealed + Sized {
const KIND: Option<ConnectionKind>;
fn from_connection(connection: Connection) -> Result<Self, ConnectionKindMismatch>;
}
impl sealing::Sealed for Connection {}
impl ConnectionPayload for Connection {
const KIND: Option<ConnectionKind> = None;
fn from_connection(connection: Connection) -> Result<Self, ConnectionKindMismatch> {
Ok(connection)
}
}
macro_rules! payloads {
($($variant:ident),+ $(,)?) => {
$(
impl sealing::Sealed for $variant {}
impl ConnectionPayload for $variant {
const KIND: Option<ConnectionKind> = Some(ConnectionKind::$variant);
fn from_connection(
connection: Connection,
) -> Result<Self, ConnectionKindMismatch> {
match connection {
Connection::$variant(payload) => Ok(payload),
other => Err(ConnectionKindMismatch {
expected: ConnectionKind::$variant,
authored: other.kind(),
}),
}
}
}
)+
};
}
payloads!(Can, I2c, Spi, Serial, Uart, Usb, Gpio);
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Can {
pub bus: u8,
pub node_id: u8,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct I2c {
pub bus: u8,
pub address: u16,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Spi {
pub bus: u8,
pub chip_select: u8,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Serial {
pub port: String,
pub baud: u32,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Uart {
pub port: String,
pub baud_rate: u32,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Usb {
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Gpio {
pub chip: String,
pub pins: Vec<GpioPin>,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct GpioPin {
pub line: u16,
pub direction: GpioDirection,
#[serde(default)]
pub active_low: bool,
}
#[derive(
phoxal_macros::DescribeWire,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum GpioDirection {
Input,
Output,
}
#[cfg(test)]
mod tests {
use super::{
Can, Connection, ConnectionKind, ConnectionKindMismatch, ConnectionPayload, Gpio,
GpioDirection, GpioPin, Serial,
};
fn can() -> Connection {
Connection::Can(Can { bus: 0, node_id: 1 })
}
#[test]
fn a_connection_is_one_internally_tagged_map() {
let json = serde_json::to_value(can()).expect("a connection serializes");
assert_eq!(
json,
serde_json::json!({"type": "can", "bus": 0, "node_id": 1})
);
let decoded: Connection = serde_json::from_value(json).expect("its own output parses");
assert_eq!(decoded, can());
}
#[test]
fn an_unknown_key_inside_a_connection_is_rejected() {
let error = serde_json::from_value::<Connection>(
serde_json::json!({"type": "can", "bus": 0, "node_id": 1, "nod_id": 2}),
)
.expect_err("an undeclared key inside a connection must not parse");
assert!(
format!("{error}").contains("unknown field `nod_id`"),
"{error}"
);
}
#[test]
fn every_kind_round_trips_through_its_wire_token() {
for kind in ConnectionKind::ALL {
let json = serde_json::to_string(&kind).expect("a unit variant serializes");
assert_eq!(json, format!("\"{}\"", kind.as_str()));
let decoded: ConnectionKind =
serde_json::from_str(&json).expect("its own token parses back");
assert_eq!(decoded, kind);
}
}
#[test]
fn a_declared_payload_accepts_only_its_own_kind() {
assert_eq!(
<Serial as ConnectionPayload>::KIND,
Some(ConnectionKind::Serial)
);
let serial = Connection::Serial(Serial {
port: "/dev/ttyUSB0".to_owned(),
baud: 115_200,
});
assert_eq!(serial.kind(), ConnectionKind::Serial);
assert_eq!(
Serial::from_connection(serial).expect("the declared kind converts"),
Serial {
port: "/dev/ttyUSB0".to_owned(),
baud: 115_200,
}
);
assert_eq!(
Serial::from_connection(can()).expect_err("another kind must not convert"),
ConnectionKindMismatch {
expected: ConnectionKind::Serial,
authored: ConnectionKind::Can,
}
);
}
#[test]
fn the_undeclared_case_is_the_enum_itself() {
assert_eq!(<Connection as ConnectionPayload>::KIND, None);
assert_eq!(
Connection::from_connection(can()).expect("the enum accepts every kind"),
can()
);
}
#[test]
fn a_gpio_block_keeps_its_authored_spelling() {
let connection: Connection = serde_json::from_value(serde_json::json!({
"type": "gpio",
"chip": "gpiochip0",
"pins": [{"line": 1, "direction": "output"}],
}))
.expect("a gpio connection parses");
assert_eq!(
connection,
Connection::Gpio(Gpio {
chip: "gpiochip0".to_owned(),
pins: vec![GpioPin {
line: 1,
direction: GpioDirection::Output,
active_low: false,
}],
})
);
}
}