use crate::error::ParseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EventId(u32);
impl EventId {
#[must_use]
pub const fn new(id: u32) -> Self {
Self(id)
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
impl std::fmt::Display for EventId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl serde::Serialize for EventId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u32(self.0)
}
}
impl<'de> serde::Deserialize<'de> for EventId {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(u32::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GlwEventKey(String);
impl GlwEventKey {
#[must_use]
pub fn new<S: Into<String>>(key: S) -> Self {
Self(key.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for GlwEventKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl serde::Serialize for GlwEventKey {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for GlwEventKey {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(String::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WindDirection(u16);
impl WindDirection {
pub fn try_new(degrees: u16) -> Result<Self, ParseError> {
if degrees > 359 {
return Err(ParseError::OutOfRange {
field: "wind_direction",
value: degrees.to_string(),
allowed: "0..=359",
});
}
Ok(Self(degrees))
}
#[must_use]
pub const fn degrees(self) -> u16 {
self.0
}
}
impl serde::Serialize for WindDirection {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u16(self.0)
}
}
impl<'de> serde::Deserialize<'de> for WindDirection {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = u16::deserialize(deserializer)?;
Self::try_new(v).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KnotSpeed(f32);
impl KnotSpeed {
#[must_use]
pub const fn new(knots: f32) -> Self {
Self(knots)
}
#[must_use]
pub const fn knots(self) -> f32 {
self.0
}
}
impl serde::Serialize for KnotSpeed {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f32(self.0)
}
}
impl<'de> serde::Deserialize<'de> for KnotSpeed {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(f32::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GustsPercent(u8);
impl GustsPercent {
pub fn try_new(percent: u8) -> Result<Self, ParseError> {
if percent > 100 {
return Err(ParseError::OutOfRange {
field: "gusts_percent",
value: percent.to_string(),
allowed: "0..=100",
});
}
Ok(Self(percent))
}
#[must_use]
pub const fn percent(self) -> u8 {
self.0
}
}
impl serde::Serialize for GustsPercent {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(self.0)
}
}
impl<'de> serde::Deserialize<'de> for GustsPercent {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = u8::deserialize(deserializer)?;
Self::try_new(v).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShiftsDegrees(u16);
impl ShiftsDegrees {
pub fn try_new(degrees: u16) -> Result<Self, ParseError> {
if degrees > 180 {
return Err(ParseError::OutOfRange {
field: "shifts_degrees",
value: degrees.to_string(),
allowed: "0..=180",
});
}
Ok(Self(degrees))
}
#[must_use]
pub const fn degrees(self) -> u16 {
self.0
}
}
impl serde::Serialize for ShiftsDegrees {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u16(self.0)
}
}
impl<'de> serde::Deserialize<'de> for ShiftsDegrees {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = u16::deserialize(deserializer)?;
Self::try_new(v).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Period(u32);
impl Period {
#[must_use]
pub const fn new(seconds: u32) -> Self {
Self(seconds)
}
#[must_use]
pub const fn seconds(self) -> u32 {
self.0
}
}
impl serde::Serialize for Period {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u32(self.0)
}
}
impl<'de> serde::Deserialize<'de> for Period {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(u32::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WaveHeight(f32);
impl WaveHeight {
#[must_use]
pub const fn new(meters: f32) -> Self {
Self(meters)
}
#[must_use]
pub const fn meters(self) -> f32 {
self.0
}
}
impl serde::Serialize for WaveHeight {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f32(self.0)
}
}
impl<'de> serde::Deserialize<'de> for WaveHeight {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(f32::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WaveLength(f32);
impl WaveLength {
#[must_use]
pub const fn new(meters: f32) -> Self {
Self(meters)
}
#[must_use]
pub const fn meters(self) -> f32 {
self.0
}
}
impl serde::Serialize for WaveLength {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f32(self.0)
}
}
impl<'de> serde::Deserialize<'de> for WaveLength {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(f32::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PercentVariance(u8);
impl PercentVariance {
pub fn try_new(percent: u8) -> Result<Self, ParseError> {
if percent > 100 {
return Err(ParseError::OutOfRange {
field: "percent_variance",
value: percent.to_string(),
allowed: "0..=100",
});
}
Ok(Self(percent))
}
#[must_use]
pub const fn percent(self) -> u8 {
self.0
}
}
impl serde::Serialize for PercentVariance {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(self.0)
}
}
impl<'de> serde::Deserialize<'de> for PercentVariance {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = u8::deserialize(deserializer)?;
Self::try_new(v).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WaveEffects {
pub speed: bool,
pub steer: bool,
}
impl serde::Serialize for WaveEffects {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct as _;
let mut s = serializer.serialize_struct("WaveEffects", 2)?;
s.serialize_field("speed", &u8::from(self.speed))?;
s.serialize_field("steer", &u8::from(self.steer))?;
s.end()
}
}
impl<'de> serde::Deserialize<'de> for WaveEffects {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Raw {
Bitmask(u8),
Object {
#[serde(default)]
speed: u8,
#[serde(default)]
steer: u8,
},
}
let raw = Raw::deserialize(deserializer)?;
Ok(match raw {
Raw::Bitmask(n) => Self {
speed: (n & 0b10) != 0,
steer: (n & 0b01) != 0,
},
Raw::Object { speed, steer } => Self {
speed: speed != 0,
steer: steer != 0,
},
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WaterDepth(f32);
impl WaterDepth {
pub fn try_new(meters: f32) -> Result<Self, ParseError> {
if meters > 0.0 {
Ok(Self(meters))
} else {
Err(ParseError::OutOfRange {
field: "water_depth",
value: meters.to_string(),
allowed: "> 0",
})
}
}
#[must_use]
pub const fn meters(self) -> f32 {
self.0
}
}
impl serde::Serialize for WaterDepth {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f32(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WaterDepthSetting {
Disabled,
Set(WaterDepth),
}
impl serde::Serialize for WaterDepthSetting {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match *self {
Self::Disabled => serializer.serialize_f32(0.0),
Self::Set(d) => serializer.serialize_f32(d.meters()),
}
}
}
impl<'de> serde::Deserialize<'de> for WaterDepthSetting {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = f32::deserialize(deserializer)?;
let is_zero = v == 0.0;
if is_zero {
Ok(Self::Disabled)
} else {
WaterDepth::try_new(v)
.map(Self::Set)
.map_err(serde::de::Error::custom)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MarginMeters(u8);
impl MarginMeters {
#[must_use]
pub fn new_clamped(meters: u16) -> Self {
if meters > 100 {
tracing::warn!(
requested = meters,
clamped = 100u8,
"GLW margin exceeds documented maximum (100m); clamping",
);
Self(100)
} else {
Self(u8::try_from(meters).unwrap_or(100))
}
}
#[must_use]
pub const fn meters(self) -> u8 {
self.0
}
}
impl serde::Serialize for MarginMeters {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(self.0)
}
}
impl<'de> serde::Deserialize<'de> for MarginMeters {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new_clamped(u16::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RadiusMeters(f32);
impl RadiusMeters {
pub fn try_new(meters: f32) -> Result<Self, ParseError> {
if meters < 0.0 {
return Err(ParseError::OutOfRange {
field: "radius_meters",
value: meters.to_string(),
allowed: ">= 0",
});
}
Ok(Self(meters))
}
#[must_use]
pub const fn meters(self) -> f32 {
self.0
}
}
impl serde::Serialize for RadiusMeters {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_f32(self.0)
}
}
impl<'de> serde::Deserialize<'de> for RadiusMeters {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = f32::deserialize(deserializer)?;
Self::try_new(v).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Overlap(bool);
impl Overlap {
#[must_use]
pub const fn new(v: bool) -> Self {
Self(v)
}
#[must_use]
pub const fn get(self) -> bool {
self.0
}
}
impl serde::Serialize for Overlap {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u8(u8::from(self.0))
}
}
impl<'de> serde::Deserialize<'de> for Overlap {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(u8::deserialize(deserializer)? != 0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BaseWind {
#[serde(rename = "dir")]
pub direction: WindDirection,
pub speed: KnotSpeed,
pub gusts: GustsPercent,
pub shifts: ShiftsDegrees,
pub period: Period,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BaseWaves {
pub height: WaveHeight,
pub speed: KnotSpeed,
pub length: WaveLength,
pub effects: WaveEffects,
pub height_var: PercentVariance,
pub length_var: PercentVariance,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BaseCurrents {
pub speed: KnotSpeed,
#[serde(rename = "dir")]
pub direction: WindDirection,
pub water_depth: WaterDepthSetting,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Base {
pub wind: BaseWind,
pub waves: BaseWaves,
pub currents: BaseCurrents,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct WindOverride {
#[serde(rename = "dir", skip_serializing_if = "Option::is_none")]
pub direction: Option<WindDirection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<KnotSpeed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gusts: Option<GustsPercent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shifts: Option<ShiftsDegrees>,
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<Period>,
}
impl WindOverride {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.direction.is_none()
&& self.speed.is_none()
&& self.gusts.is_none()
&& self.shifts.is_none()
&& self.period.is_none()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct WavesOverride {
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<WaveHeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<KnotSpeed>,
#[serde(skip_serializing_if = "Option::is_none")]
pub length: Option<WaveLength>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effects: Option<WaveEffects>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height_var: Option<PercentVariance>,
#[serde(skip_serializing_if = "Option::is_none")]
pub length_var: Option<PercentVariance>,
}
impl WavesOverride {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.height.is_none()
&& self.speed.is_none()
&& self.length.is_none()
&& self.effects.is_none()
&& self.height_var.is_none()
&& self.length_var.is_none()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct CurrentsOverride {
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<KnotSpeed>,
#[serde(rename = "dir", skip_serializing_if = "Option::is_none")]
pub direction: Option<WindDirection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub water_depth: Option<WaterDepthSetting>,
}
impl CurrentsOverride {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.speed.is_none() && self.direction.is_none() && self.water_depth.is_none()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Area {
pub name: String,
pub grid_rectangle: sl_types::map::GridRectangle,
pub margin: MarginMeters,
pub overlap: Overlap,
pub wind: Option<WindOverride>,
pub waves: Option<WavesOverride>,
pub currents: Option<CurrentsOverride>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Circle {
pub name: String,
pub center_sim: sl_types::map::GridCoordinates,
pub center_point: sl_types::map::RegionCoordinates,
pub radius: RadiusMeters,
pub margin: MarginMeters,
pub overlap: Overlap,
pub wind: Option<WindOverride>,
pub waves: Option<WavesOverride>,
pub currents: Option<CurrentsOverride>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct AreaList(pub Vec<Area>);
impl AreaList {
#[must_use]
pub fn as_slice(&self) -> &[Area] {
&self.0
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl serde::Serialize for AreaList {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap as _;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for area in &self.0 {
map.serialize_entry(&area.name, &AreaSerializable::from(area))?;
}
map.end()
}
}
#[derive(serde::Serialize)]
struct AreaSerializable<'a> {
#[serde(rename = "coordSW")]
coord_sw: SimCoord,
#[serde(rename = "coordNE")]
coord_ne: SimCoord,
margin: &'a MarginMeters,
overlap: &'a Overlap,
#[serde(skip_serializing_if = "Option::is_none")]
wind: Option<&'a WindOverride>,
#[serde(skip_serializing_if = "Option::is_none")]
waves: Option<&'a WavesOverride>,
#[serde(skip_serializing_if = "Option::is_none")]
currents: Option<&'a CurrentsOverride>,
}
impl<'a> From<&'a Area> for AreaSerializable<'a> {
fn from(a: &'a Area) -> Self {
Self {
coord_sw: SimCoord::from_grid(&a.grid_rectangle, GridCorner::Sw),
coord_ne: SimCoord::from_grid(&a.grid_rectangle, GridCorner::Ne),
margin: &a.margin,
overlap: &a.overlap,
wind: a.wind.as_ref(),
waves: a.waves.as_ref(),
currents: a.currents.as_ref(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CircleList(pub Vec<Circle>);
impl CircleList {
#[must_use]
pub fn as_slice(&self) -> &[Circle] {
&self.0
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl serde::Serialize for CircleList {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap as _;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for circle in &self.0 {
map.serialize_entry(&circle.name, &CircleSerializable::from(circle))?;
}
map.end()
}
}
#[derive(serde::Serialize)]
struct CircleSerializable<'a> {
#[serde(rename = "centerSim")]
center_sim: SimCoord,
#[serde(rename = "centerPoint")]
center_point: RegionXY,
radius: &'a RadiusMeters,
margin: &'a MarginMeters,
overlap: &'a Overlap,
#[serde(skip_serializing_if = "Option::is_none")]
wind: Option<&'a WindOverride>,
#[serde(skip_serializing_if = "Option::is_none")]
waves: Option<&'a WavesOverride>,
#[serde(skip_serializing_if = "Option::is_none")]
currents: Option<&'a CurrentsOverride>,
}
impl<'a> From<&'a Circle> for CircleSerializable<'a> {
fn from(c: &'a Circle) -> Self {
Self {
center_sim: SimCoord {
x: c.center_sim.x(),
y: c.center_sim.y(),
},
center_point: RegionXY {
x: c.center_point.x(),
y: c.center_point.y(),
},
radius: &c.radius,
margin: &c.margin,
overlap: &c.overlap,
wind: c.wind.as_ref(),
waves: c.waves.as_ref(),
currents: c.currents.as_ref(),
}
}
}
#[derive(Debug, Clone, Copy)]
enum GridCorner {
Sw,
Ne,
}
#[derive(Debug, Clone, Copy, serde::Serialize)]
struct SimCoord {
x: u32,
y: u32,
}
impl SimCoord {
fn from_grid(rect: &sl_types::map::GridRectangle, which: GridCorner) -> Self {
use sl_types::map::GridRectangleLike as _;
let gc = match which {
GridCorner::Sw => rect.lower_left_corner(),
GridCorner::Ne => rect.upper_right_corner(),
};
Self {
x: gc.x(),
y: gc.y(),
}
}
}
#[derive(Debug, Clone, Copy, serde::Serialize)]
struct RegionXY {
x: f32,
y: f32,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GlwEvent {
pub event_id: EventId,
pub event_name: String,
pub event_key: GlwEventKey,
#[serde(default)]
pub event_num: Option<u32>,
pub director_name: String,
pub director_key: String,
#[serde(default)]
pub sail_mode: Option<u8>,
#[serde(default)]
pub extra1: String,
#[serde(default)]
pub extra2: String,
pub base: Base,
#[serde(default, deserialize_with = "crate::parse::deserialize_area_list")]
pub areas: AreaList,
#[serde(default, deserialize_with = "crate::parse::deserialize_circle_list")]
pub circles: CircleList,
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn wind_direction_rejects_out_of_range() {
assert!(WindDirection::try_new(0).is_ok(), "0 is in range");
assert!(WindDirection::try_new(359).is_ok(), "359 is in range");
assert!(WindDirection::try_new(360).is_err(), "360 is out of range");
assert!(WindDirection::try_new(999).is_err(), "999 is out of range");
}
#[test]
fn gusts_rejects_out_of_range() {
assert!(GustsPercent::try_new(0).is_ok(), "0 is in range");
assert!(GustsPercent::try_new(100).is_ok(), "100 is in range");
assert!(GustsPercent::try_new(101).is_err(), "101 is out of range");
}
#[test]
fn shifts_rejects_out_of_range() {
assert!(ShiftsDegrees::try_new(180).is_ok(), "180 is in range");
assert!(ShiftsDegrees::try_new(181).is_err(), "181 is out of range");
}
#[test]
fn margin_clamps_at_100() {
assert_eq!(MarginMeters::new_clamped(0).meters(), 0);
assert_eq!(MarginMeters::new_clamped(100).meters(), 100);
assert_eq!(MarginMeters::new_clamped(150).meters(), 100);
}
#[test]
fn water_depth_rejects_zero_and_negative() {
assert!(WaterDepth::try_new(1.0).is_ok(), "1.0 is positive");
assert!(WaterDepth::try_new(0.0).is_err(), "0.0 is rejected");
assert!(WaterDepth::try_new(-0.5).is_err(), "negative rejected");
}
#[test]
fn water_depth_setting_maps_zero_to_disabled() -> Result<(), Box<dyn std::error::Error>> {
let disabled: WaterDepthSetting = serde_json::from_str("0")?;
assert_eq!(disabled, WaterDepthSetting::Disabled);
let positive: WaterDepthSetting = serde_json::from_str("6.5")?;
match positive {
WaterDepthSetting::Set(d) => {
assert!((d.meters() - 6.5).abs() < 1e-6, "6.5 round-trips");
}
WaterDepthSetting::Disabled => {
return Err("6.5 must not become Disabled".into());
}
}
let negative: Result<WaterDepthSetting, _> = serde_json::from_str("-1");
assert!(negative.is_err(), "negative rejected");
Ok(())
}
#[test]
fn overlap_from_0_and_1() -> Result<(), Box<dyn std::error::Error>> {
let off: Overlap = serde_json::from_str("0")?;
assert_eq!(off, Overlap::new(false));
let on: Overlap = serde_json::from_str("1")?;
assert_eq!(on, Overlap::new(true));
Ok(())
}
#[test]
fn wave_effects_from_object() -> Result<(), Box<dyn std::error::Error>> {
let we: WaveEffects = serde_json::from_str(r#"{"speed":1,"steer":0}"#)?;
assert!(we.speed, "speed=1 turns on");
assert!(!we.steer, "steer=0 stays off");
Ok(())
}
#[test]
fn wave_effects_from_bitmask() -> Result<(), Box<dyn std::error::Error>> {
let we: WaveEffects = serde_json::from_str("3")?;
assert!(we.speed && we.steer, "bitmask 3 = both on");
let we: WaveEffects = serde_json::from_str("2")?;
assert!(we.speed && !we.steer, "bitmask 2 = speed only");
let we: WaveEffects = serde_json::from_str("1")?;
assert!(!we.speed && we.steer, "bitmask 1 = steer only");
let we: WaveEffects = serde_json::from_str("0")?;
assert!(!we.speed && !we.steer, "bitmask 0 = neither");
Ok(())
}
}