use std::{error::Error, time::Duration};
use duration_string::DurationString;
use serde::{Deserialize, Serialize};
use super::lighting::Lighting;
#[cfg(not(test))]
pub const DEFAULT_OLA_PORT: u16 = 9010;
pub const DEFAULT_DMX_DIMMING_SPEED_MODIFIER: f64 = 1.0;
pub const DEFAULT_DMX_PLAYBACK_DELAY: Duration = Duration::ZERO;
#[derive(Deserialize, Serialize, Clone)]
pub struct Dmx {
dim_speed_modifier: Option<f64>,
playback_delay: Option<String>,
ola_port: Option<u16>,
universes: Vec<Universe>,
lighting: Option<Lighting>,
#[serde(default)]
null_client: bool,
}
impl Dmx {
#[cfg(test)]
pub fn new(
dim_speed_modifier: Option<f64>,
playback_delay: Option<String>,
ola_port: Option<u16>,
universes: Vec<Universe>,
lighting: Option<Lighting>,
) -> Dmx {
Dmx {
dim_speed_modifier,
playback_delay,
ola_port,
universes,
lighting,
null_client: false,
}
}
#[cfg(test)]
pub fn get_ola_port(&self) -> Option<u16> {
self.ola_port
}
pub fn dimming_speed_modifier(&self) -> f64 {
self.dim_speed_modifier
.unwrap_or(DEFAULT_DMX_DIMMING_SPEED_MODIFIER)
}
pub fn playback_delay(&self) -> Result<Duration, Box<dyn Error>> {
super::parse_playback_delay(&self.playback_delay, DEFAULT_DMX_PLAYBACK_DELAY)
}
#[cfg(not(test))]
pub fn ola_port(&self) -> u16 {
self.ola_port.unwrap_or(DEFAULT_OLA_PORT)
}
pub fn universes(&self) -> &[Universe] {
&self.universes
}
pub fn lighting(&self) -> Option<&Lighting> {
self.lighting.as_ref()
}
pub fn null_client(&self) -> bool {
self.null_client
}
pub fn lighting_mut(&mut self) -> Option<&mut Lighting> {
self.lighting.as_mut()
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if let Some(modifier) = self.dim_speed_modifier {
if modifier <= 0.0 {
errors.push("dmx dim_speed_modifier must be greater than 0".to_string());
}
}
if let Some(ref delay) = self.playback_delay {
if DurationString::from_string(delay.clone()).is_err() {
errors.push(format!(
"dmx playback_delay '{}' is not a valid duration",
delay
));
}
}
for (i, universe) in self.universes.iter().enumerate() {
if universe.name.trim().is_empty() {
errors.push(format!("dmx universe[{}]: name must not be empty", i));
}
}
let mut seen_names = std::collections::HashSet::new();
for universe in &self.universes {
if !seen_names.insert(&universe.name) {
errors.push(format!(
"dmx universe name '{}' is duplicated",
universe.name
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Universe {
universe: u16,
name: String,
}
impl Universe {
#[cfg(test)]
pub fn new(universe: u16, name: String) -> Universe {
Universe { universe, name }
}
pub fn universe(&self) -> u16 {
self.universe
}
pub fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dmx_ola_port_field() {
let dmx = Dmx::new(None, None, Some(9090), vec![], None);
assert_eq!(dmx.get_ola_port(), Some(9090));
let dmx = Dmx::new(None, None, None, vec![], None);
assert_eq!(dmx.get_ola_port(), None);
}
#[test]
fn dimming_speed_modifier_default() {
let dmx = Dmx::new(None, None, None, vec![], None);
assert!(
(dmx.dimming_speed_modifier() - DEFAULT_DMX_DIMMING_SPEED_MODIFIER).abs()
< f64::EPSILON
);
}
#[test]
fn dimming_speed_modifier_custom() {
let dmx = Dmx::new(Some(2.5), None, None, vec![], None);
assert!((dmx.dimming_speed_modifier() - 2.5).abs() < f64::EPSILON);
}
#[test]
fn playback_delay_default() {
let dmx = Dmx::new(None, None, None, vec![], None);
assert_eq!(dmx.playback_delay().unwrap(), DEFAULT_DMX_PLAYBACK_DELAY);
}
#[test]
fn playback_delay_valid() {
let dmx = Dmx::new(None, Some("500ms".to_string()), None, vec![], None);
assert_eq!(dmx.playback_delay().unwrap(), Duration::from_millis(500));
}
#[test]
fn playback_delay_invalid() {
let dmx = Dmx::new(None, Some("not_a_duration".to_string()), None, vec![], None);
assert!(dmx.playback_delay().is_err());
}
#[test]
fn null_client_default() {
let dmx = Dmx::new(None, None, None, vec![], None);
assert!(!dmx.null_client());
}
#[test]
fn universes_empty() {
let dmx = Dmx::new(None, None, None, vec![], None);
assert!(dmx.universes().is_empty());
}
#[test]
fn universes_populated() {
let dmx = Dmx::new(
None,
None,
None,
vec![
Universe::new(1, "front".to_string()),
Universe::new(2, "back".to_string()),
],
None,
);
let unis = dmx.universes();
assert_eq!(unis.len(), 2);
assert_eq!(unis[0].universe(), 1);
assert_eq!(unis[0].name(), "front");
assert_eq!(unis[1].universe(), 2);
assert_eq!(unis[1].name(), "back");
}
#[test]
fn lighting_none() {
let dmx = Dmx::new(None, None, None, vec![], None);
assert!(dmx.lighting().is_none());
}
#[test]
fn lighting_some() {
let lighting = Lighting::new(Some("venue1".to_string()), None, None, None);
let dmx = Dmx::new(None, None, None, vec![], Some(lighting));
assert!(dmx.lighting().is_some());
assert_eq!(dmx.lighting().unwrap().current_venue(), Some("venue1"));
}
#[test]
fn serde_round_trip() {
let yaml = r#"
dim_speed_modifier: 1.5
playback_delay: "200ms"
ola_port: 9020
universes:
- universe: 1
name: main
- universe: 2
name: aux
null_client: true
"#;
let dmx: Dmx = config::Config::builder()
.add_source(config::File::from_str(yaml, config::FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert!((dmx.dimming_speed_modifier() - 1.5).abs() < f64::EPSILON);
assert_eq!(dmx.playback_delay().unwrap(), Duration::from_millis(200));
assert_eq!(dmx.get_ola_port(), Some(9020));
assert_eq!(dmx.universes().len(), 2);
assert!(dmx.null_client());
}
#[test]
fn serde_minimal() {
let yaml = r#"
universes: []
"#;
let dmx: Dmx = config::Config::builder()
.add_source(config::File::from_str(yaml, config::FileFormat::Yaml))
.build()
.unwrap()
.try_deserialize()
.unwrap();
assert!(
(dmx.dimming_speed_modifier() - DEFAULT_DMX_DIMMING_SPEED_MODIFIER).abs()
< f64::EPSILON
);
assert_eq!(dmx.playback_delay().unwrap(), DEFAULT_DMX_PLAYBACK_DELAY);
assert!(!dmx.null_client());
assert!(dmx.universes().is_empty());
assert!(dmx.lighting().is_none());
}
}