version-number 1.0.0

Two and three component 'major.minor' and 'major.minor.patch' version number parsing
Documentation
use crate::{BaseVersion, FullVersion, Version};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;

struct FromStrVisitor<T> {
    expecting: &'static str,
    marker: PhantomData<T>,
}

impl<T> FromStrVisitor<T> {
    fn new(expecting: &'static str) -> Self {
        Self {
            expecting,
            marker: PhantomData,
        }
    }
}

impl<'de, T> Visitor<'de> for FromStrVisitor<T>
where
    T: FromStr,
    T::Err: fmt::Display,
{
    type Value = T;

    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.expecting)
    }

    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        value.parse().map_err(E::custom)
    }
}

impl Serialize for BaseVersion {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl<'de> Deserialize<'de> for BaseVersion {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(FromStrVisitor::new(
            "a two-component `major.minor` version number",
        ))
    }
}

impl Serialize for FullVersion {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl<'de> Deserialize<'de> for FullVersion {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(FromStrVisitor::new(
            "a three-component `major.minor.patch` version number",
        ))
    }
}

impl Serialize for Version {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl<'de> Deserialize<'de> for Version {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(FromStrVisitor::new(
            "a two- or three-component `major.minor` or `major.minor.patch` version number",
        ))
    }
}

#[cfg(test)]
mod tests {
    use crate::{BaseVersion, FullVersion, Version};

    #[yare::parameterized(
        base = { Version::new_base_version(1, 2), "\"1.2\"" },
        full = { Version::new_full_version(1, 2, 3), "\"1.2.3\"" },
    )]
    fn version_round_trip(version: Version, json: &str) {
        assert_eq!(serde_json::to_string(&version).unwrap(), json);
        assert_eq!(serde_json::from_str::<Version>(json).unwrap(), version);
    }

    #[test]
    fn base_version_round_trip() {
        let version = BaseVersion::new(1, 2);

        assert_eq!(serde_json::to_string(&version).unwrap(), "\"1.2\"");
        assert_eq!(
            serde_json::from_str::<BaseVersion>("\"1.2\"").unwrap(),
            version
        );
    }

    #[test]
    fn full_version_round_trip() {
        let version = FullVersion::new(1, 2, 3);

        assert_eq!(serde_json::to_string(&version).unwrap(), "\"1.2.3\"");
        assert_eq!(
            serde_json::from_str::<FullVersion>("\"1.2.3\"").unwrap(),
            version
        );
    }

    #[yare::parameterized(
        empty = { "\"\"" },
        one_component = { "\"1\"" },
        four_components = { "\"1.2.3.4\"" },
        pre_release = { "\"1.2.3-beta\"" },
        leading_zero = { "\"01.2\"" },
        number = { "1" },
    )]
    fn version_invalid(json: &str) {
        assert!(serde_json::from_str::<Version>(json).is_err());
    }

    #[test]
    fn base_version_rejects_full_version() {
        assert!(serde_json::from_str::<BaseVersion>("\"1.2.3\"").is_err());
    }

    #[test]
    fn full_version_rejects_base_version() {
        assert!(serde_json::from_str::<FullVersion>("\"1.2\"").is_err());
    }
}