js-semver 0.4.0

Parser and evaluator for npm's flavor of Semantic Versioning, compliant with node-semver
Documentation
use core::fmt;

use crate::range::Range;
use crate::version::Version;

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

impl<'de> serde::Deserialize<'de> for Version {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct VersionVisitor;

        impl serde::de::Visitor<'_> for VersionVisitor {
            type Value = Version;

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

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

        deserializer.deserialize_str(VersionVisitor)
    }
}

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

impl<'de> serde::Deserialize<'de> for Range {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct RangeVisitor;

        impl serde::de::Visitor<'_> for RangeVisitor {
            type Value = Range;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("a semantic version range string")
            }

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

        deserializer.deserialize_str(RangeVisitor)
    }
}

#[cfg(test)]
mod tests {
    #[cfg(not(feature = "std"))]
    use alloc::string::ToString;

    use super::*;

    #[test]
    fn version_roundtrip() {
        let version: Version = "1.2.3-alpha.1+build.42".parse().unwrap();
        let json = serde_json::to_string(&version).unwrap();
        assert_eq!(json, "\"1.2.3-alpha.1+build.42\"");
        let parsed: Version = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, version);
    }

    #[test]
    fn range_roundtrip() {
        let range: Range = "^1.2.3 || *".parse().unwrap();
        let json = serde_json::to_string(&range).unwrap();
        assert_eq!(json, "\"*\"");
        let parsed: Range = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.to_string(), range.to_string());
    }

    #[test]
    fn deserialize_errors() {
        assert!(serde_json::from_str::<Version>("\"bad\"").is_err());
        assert!(serde_json::from_str::<Range>("\"^\"").is_err());
        assert!(serde_json::from_str::<Version>("123").is_err());
        assert!(serde_json::from_str::<Range>("123").is_err());
    }

    #[test]
    fn version_deserializes_owned_string() {
        let version: Version = serde_json::from_value(serde_json::json!("1.2.3")).unwrap();
        assert_eq!(version, Version::new(1, 2, 3));
    }

    #[test]
    fn range_deserializes_owned_string_from_package_json() {
        let package: serde_json::Value =
            serde_json::from_str(r#"{"dependencies":{"react":"^19.0.0"}}"#).unwrap();
        let range: Range =
            serde_json::from_value(package["dependencies"]["react"].clone()).unwrap();
        assert!(range.satisfies(&Version::new(19, 1, 0)));
    }

    #[test]
    fn version_and_range_deserialize_transient_strings() {
        let version: Version = serde_json::from_str(r#""1.2.\u0033""#).unwrap();
        assert_eq!(version, Version::new(1, 2, 3));

        let range: Range = serde_json::from_str(r#""^19.\u0030.0""#).unwrap();
        assert!(range.satisfies(&Version::new(19, 1, 0)));
    }
}