voronoi-go 1.0.1

Core rules and engine for Voronoi Go.
Documentation
//! Serde implementations for the types whose wire shape is not the derived one.
//!
//! `docs/protocol.md` is the contract: a point is `[x, y]`, and a per-colour
//! pair is `{"black": …, "white": …}`. Everything else derives.

use serde::ser::{SerializeStruct, SerializeTuple};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::{PerColor, Point};

impl Serialize for Point {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut pair = serializer.serialize_tuple(2)?;
        pair.serialize_element(&self.x)?;
        pair.serialize_element(&self.y)?;
        pair.end()
    }
}

impl<'de> Deserialize<'de> for Point {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let [x, y] = <[f64; 2]>::deserialize(deserializer)?;
        Ok(Self::new(x, y))
    }
}

impl<T: Serialize> Serialize for PerColor<T> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut pair = serializer.serialize_struct("PerColor", 2)?;
        pair.serialize_field("black", self.black())?;
        pair.serialize_field("white", self.white())?;
        pair.end()
    }
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for PerColor<T> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        struct Pair<T> {
            black: T,
            white: T,
        }

        let Pair { black, white } = Pair::deserialize(deserializer)?;
        Ok(Self::new(black, white))
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::expect_used)]

    use crate::{Color, GameDelta, PerColor, Point, Stone, StoneId};

    #[test]
    fn a_point_is_a_two_element_array() {
        let json = serde_json::to_string(&Point::new(4.0, -1.5)).unwrap();
        assert_eq!(json, "[4.0,-1.5]");
        let back: Point = serde_json::from_str(&json).unwrap();
        assert_eq!(back, Point::new(4.0, -1.5));
    }

    #[test]
    fn a_color_is_a_lowercase_string() {
        assert_eq!(serde_json::to_string(&Color::Black).unwrap(), "\"black\"");
        assert_eq!(serde_json::to_string(&Color::White).unwrap(), "\"white\"");
        let back: Color = serde_json::from_str("\"white\"").unwrap();
        assert_eq!(back, Color::White);
    }

    #[test]
    fn a_stone_matches_the_protocol_shape() {
        let stone = Stone::new(StoneId::new(3), Color::Black, Point::new(4.0, 4.0));
        let json = serde_json::to_string(&stone).unwrap();
        assert_eq!(json, r#"{"id":3,"color":"black","pos":[4.0,4.0]}"#);
        assert_eq!(serde_json::from_str::<Stone>(&json).unwrap(), stone);
    }

    #[test]
    fn a_delta_matches_the_protocol_shape() {
        let stone = Stone::new(StoneId::new(3), Color::Black, Point::new(4.0, 4.0));
        let delta = GameDelta::capture(stone, vec![StoneId::new(1), StoneId::new(7)]);
        let json = serde_json::to_string(&delta).unwrap();
        assert_eq!(
            json,
            r#"{"newStone":{"id":3,"color":"black","pos":[4.0,4.0]},"capturedStoneIds":[1,7]}"#
        );
        assert_eq!(serde_json::from_str::<GameDelta>(&json).unwrap(), delta);
    }

    #[test]
    fn a_pass_delta_has_a_null_stone() {
        let json = serde_json::to_string(&GameDelta::pass()).unwrap();
        assert_eq!(json, r#"{"newStone":null,"capturedStoneIds":[]}"#);
    }

    #[test]
    fn a_per_color_pair_is_keyed_by_colour_name() {
        let counts = PerColor::new(3_u32, 1);
        let json = serde_json::to_string(&counts).unwrap();
        assert_eq!(json, r#"{"black":3,"white":1}"#);
        assert_eq!(
            serde_json::from_str::<PerColor<u32>>(&json).unwrap(),
            counts
        );
    }
}