sinusoidal 0.6.0

The official SDK to write rust apps for the Sinusoidal Systems Digital Measurement Platform
Documentation
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,
}

// This is what you get from the framework when registering.
#[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(),
    }
  }
}

// This is what you put in your settings.
#[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 {
      // Only one variant for now
      SysStream::Time
    }
  }

  /// Generate a string that doesn't contain ":" to not mess with the
  /// StreamId parser.
  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());
  }
}