use serde::{Deserialize, Deserializer};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub enum SysStream {
Time,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StreamId {
Sv {
appid: u32,
svid: String,
simulated: bool,
},
App {
app_name: String,
stream_name: String,
},
Sys {
stream: SysStream,
},
Goose {
go_id: String,
},
}
impl StreamId {
pub const SYS_TIME: Self = Self::Sys {
stream: SysStream::Time,
};
}
impl FromStr for StreamId {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(':').collect();
match parts.as_slice() {
["App", app_name, stream_name] => {
let app_name = app_name.to_string();
let stream_name = stream_name.to_string();
Ok(StreamId::App {
app_name,
stream_name,
})
}
["SV", appid, svid] | ["SV", appid, svid, "test"] => {
let appid = u32::from_str_radix(appid, 16)
.map_err(|_| "Invalid hex integer for appid".to_string())?;
let svid = svid.to_string();
let simulated = parts.len() > 3;
Ok(StreamId::Sv {
appid,
svid,
simulated,
})
}
["Sys", "Time"] => Ok(StreamId::Sys {
stream: SysStream::Time,
}),
["GOOSE", go_id @ ..] => Ok(StreamId::Goose {
go_id: go_id.join(":"),
}),
_ => Err(format!("Invalid format StreamId: {s}").to_string()),
}
}
}
impl fmt::Display for SysStream {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SysStream::Time => write!(f, "Time"),
}
}
}
impl fmt::Display for StreamId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
StreamId::App {
app_name,
stream_name,
} => {
write!(f, "App:{app_name}:{stream_name}")
}
StreamId::Sv {
appid,
svid,
simulated: false,
} => {
write!(f, "SV:{appid:x}:{svid}")
}
StreamId::Sv {
appid,
svid,
simulated: true,
} => {
write!(f, "SV:{appid:x}:{svid}:test")
}
StreamId::Sys { stream } => {
write!(f, "Sys:{stream}")
}
StreamId::Goose { go_id } => {
write!(f, "GOOSE:{go_id}")
}
}
}
}
impl<'de> Deserialize<'de> for StreamId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
StreamId::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SampleRate {
SamplesPerSecond(i64),
SecondsPerSample(i64),
SamplesPerPeriod(i64),
SamplesPerCycle(i64),
Variable(String),
}
#[derive(Debug, Clone, Deserialize)]
pub enum StreamValueType {
Voltage,
Current,
Energy,
Power,
Frequency,
Other,
}
#[derive(Debug, Clone, Deserialize)]
pub enum SIUnit {
Ampere,
Volt,
Radian,
Watt,
Joule,
Hz,
VArs,
SIOther,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StreamLayoutValue {
pub name: String,
pub r#type: StreamValueType,
pub unit: SIUnit,
pub mag: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DataStreamInfo {
pub name: String,
pub sample_rate: SampleRate,
pub fields: Vec<StreamLayoutValue>,
}
impl DataStreamInfo {
pub fn find_field(&self, name: &str) -> Result<(usize, &StreamLayoutValue), std::io::Error> {
self
.fields
.iter()
.enumerate()
.find(|(_, f)| f.name == name)
.ok_or_else(|| {
std::io::Error::other(format!(
"Field '{}' not found in stream '{}'",
name, self.name
))
})
}
pub fn find_field_idx(&self, name: &str) -> Result<usize, std::io::Error> {
self.find_field(name).map(|(idx, _)| idx)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct GooseStreamInfo {
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SysStreamInfo {
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StreamInfo {
DataStreamInfo(DataStreamInfo),
GooseStreamInfo(GooseStreamInfo),
SysStreamInfo(SysStreamInfo),
}
impl StreamInfo {
pub fn name(&self) -> String {
match self {
Self::DataStreamInfo(info) => info.name.clone(),
Self::GooseStreamInfo(info) => info.name.clone(),
Self::SysStreamInfo(info) => info.name.clone(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct InputStream {
#[serde(rename = "$input_stream")]
pub name: StreamId,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OutputStream {
#[serde(rename = "$output_stream")]
pub name: String,
pub sample_rate: SampleRate,
pub fields: Vec<StreamLayoutValue>,
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck::{Arbitrary, Gen};
impl Arbitrary for SysStream {
fn arbitrary(_g: &mut Gen) -> SysStream {
SysStream::Time
}
}
fn arbitrary_string_no_colon(g: &mut Gen) -> String {
let s: String = Arbitrary::arbitrary(g);
s.replace(":", "")
}
impl Arbitrary for StreamId {
fn arbitrary(g: &mut Gen) -> StreamId {
match g.choose(&[0, 1, 2, 3]).unwrap() {
0 => StreamId::Sv {
appid: Arbitrary::arbitrary(g),
svid: arbitrary_string_no_colon(g),
simulated: Arbitrary::arbitrary(g),
},
1 => StreamId::App {
app_name: arbitrary_string_no_colon(g),
stream_name: arbitrary_string_no_colon(g),
},
2 => StreamId::Sys {
stream: Arbitrary::arbitrary(g),
},
3 => StreamId::Goose {
go_id: Arbitrary::arbitrary(g),
},
_ => unreachable!(),
}
}
}
#[quickcheck_macros::quickcheck]
fn streamid_roundtrip(id: StreamId) -> bool {
StreamId::from_str(&id.to_string()) == Ok(id)
}
#[test]
fn test_stream_id_conversion() {
let test_cases = vec![
(
StreamId::App {
app_name: "my-app".to_string(),
stream_name: "my-stream".to_string(),
},
"App:my-app:my-stream",
),
(
StreamId::Sv {
appid: 0xab12,
svid: "my-svid".to_string(),
simulated: false,
},
"SV:ab12:my-svid",
),
(
StreamId::Sv {
appid: 0xab12,
svid: "my-svid".to_string(),
simulated: true,
},
"SV:ab12:my-svid:test",
),
(
StreamId::Sys {
stream: SysStream::Time,
},
"Sys:Time",
),
(
StreamId::Goose {
go_id: "my:goose:id".to_string(),
},
"GOOSE:my:goose:id",
),
];
for (stream_id, expected_str) in test_cases {
assert_eq!(stream_id.to_string(), expected_str);
assert_eq!(StreamId::from_str(expected_str).unwrap(), stream_id);
}
}
#[test]
fn test_stream_id_from_str_invalid() {
assert!(StreamId::from_str("App:my-app").is_err());
assert!(StreamId::from_str("SV:not-hex:my-svid").is_err());
assert!(StreamId::from_str("Unknown:Id").is_err());
assert!(StreamId::from_str("").is_err());
}
}