mago_php_version/
lib.rs

1use std::str::FromStr;
2
3use schemars::JsonSchema;
4use serde::Deserialize;
5use serde::Deserializer;
6use serde::Serialize;
7use serde::Serializer;
8
9use crate::error::ParsingError;
10use crate::feature::Feature;
11
12pub mod error;
13pub mod feature;
14
15/// Represents a PHP version in `(major, minor, patch)` format,
16/// packed internally into a single `u32` for easy comparison.
17///
18/// # Examples
19///
20/// ```
21/// use mago_php_version::PHPVersion;
22///
23/// let version = PHPVersion::new(8, 4, 0);
24/// assert_eq!(version.major(), 8);
25/// assert_eq!(version.minor(), 4);
26/// assert_eq!(version.patch(), 0);
27/// assert_eq!(version.to_version_id(), 0x08_04_00);
28/// assert_eq!(version.to_string(), "8.4.0");
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
31#[schemars(with = "String")]
32#[repr(transparent)]
33pub struct PHPVersion(u32);
34
35/// Represents a range of PHP versions, defined by a minimum and maximum version.
36///
37/// This is useful for specifying compatibility ranges, such as "supports PHP 7.0 to 7.4".
38///
39/// # Examples
40///
41/// ```
42/// use mago_php_version::PHPVersion;
43/// use mago_php_version::PHPVersionRange;
44///
45/// let range = PHPVersionRange::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 4, 99));
46///
47/// assert!(range.includes(PHPVersion::new(7, 2, 0))); // true
48/// assert!(!range.includes(PHPVersion::new(8, 0, 0))); // false
49/// ```
50#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, PartialOrd, Deserialize, Serialize, Default, Hash, JsonSchema)]
51pub struct PHPVersionRange {
52    pub min: Option<PHPVersion>,
53    pub max: Option<PHPVersion>,
54}
55
56impl PHPVersion {
57    /// The PHP 7.0 version.
58    pub const PHP70: PHPVersion = PHPVersion::new(7, 0, 0);
59
60    /// The PHP 7.1 version.
61    pub const PHP71: PHPVersion = PHPVersion::new(7, 1, 0);
62
63    /// The PHP 7.2 version.
64    pub const PHP72: PHPVersion = PHPVersion::new(7, 2, 0);
65
66    /// The PHP 7.3 version.
67    pub const PHP73: PHPVersion = PHPVersion::new(7, 3, 0);
68
69    /// The PHP 7.4 version.
70    pub const PHP74: PHPVersion = PHPVersion::new(7, 4, 0);
71
72    /// The PHP 8.0 version.
73    pub const PHP80: PHPVersion = PHPVersion::new(8, 0, 0);
74
75    /// The PHP 8.1 version.
76    pub const PHP81: PHPVersion = PHPVersion::new(8, 1, 0);
77
78    /// The PHP 8.2 version.
79    pub const PHP82: PHPVersion = PHPVersion::new(8, 2, 0);
80
81    /// The PHP 8.3 version.
82    pub const PHP83: PHPVersion = PHPVersion::new(8, 3, 0);
83
84    /// The PHP 8.4 version.
85    pub const PHP84: PHPVersion = PHPVersion::new(8, 4, 0);
86
87    /// The PHP 8.5 version.
88    pub const PHP85: PHPVersion = PHPVersion::new(8, 5, 0);
89
90    /// The PHP 8.6 version.
91    pub const PHP86: PHPVersion = PHPVersion::new(8, 6, 0);
92
93    /// Represents the latest stable PHP version actively supported or targeted by this crate.
94    ///
95    /// **Warning:** The specific PHP version this constant points to (e.g., `PHPVersion::PHP84`)
96    /// is subject to change frequently, potentially even in **minor or patch releases**
97    /// of this crate, as new PHP versions are released and our support baseline updates.
98    ///
99    /// **Do NOT rely on this constant having a fixed value across different crate versions.**
100    /// It is intended for features that should target "the most current PHP we know of now."
101    pub const LATEST: PHPVersion = PHPVersion::PHP85;
102
103    /// Represents an upcoming, future, or "next" PHP version that this crate is
104    /// anticipating or for which experimental support might be in development.
105    ///
106    /// **Warning:** The specific PHP version this constant points to (e.g., `PHPVersion::PHP85`)
107    /// is highly volatile and **WILL CHANGE frequently**, potentially even in **minor or patch
108    /// releases** of this crate, reflecting shifts in PHP's release cycle or our development focus.
109    ///
110    /// **Do NOT rely on this constant having a fixed value across different crate versions.**
111    /// Use with caution, primarily for internal or forward-looking features.
112    pub const NEXT: PHPVersion = PHPVersion::PHP86;
113
114    /// Creates a new `PHPVersion` from the provided `major`, `minor`, and `patch` values.
115    ///
116    /// The internal representation packs these three components into a single `u32`
117    /// for efficient comparisons.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use mago_php_version::PHPVersion;
123    ///
124    /// let version = PHPVersion::new(8, 1, 3);
125    /// assert_eq!(version.major(), 8);
126    /// assert_eq!(version.minor(), 1);
127    /// assert_eq!(version.patch(), 3);
128    /// ```
129    #[inline]
130    #[must_use]
131    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
132        Self((major << 16) | (minor << 8) | patch)
133    }
134
135    /// Creates a `PHPVersion` directly from a raw version ID (e.g. `80400` for `8.4.0`).
136    ///
137    /// This can be useful if you already have the numeric form. The higher bits represent
138    /// the major version, the next bits represent minor, and the lowest bits represent patch.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use mago_php_version::PHPVersion;
144    ///
145    /// // "8.4.0" => 0x080400 in hex, which is 525312 in decimal
146    /// let version = PHPVersion::from_version_id(0x080400);
147    /// assert_eq!(version.to_string(), "8.4.0");
148    /// ```
149    #[inline]
150    #[must_use]
151    pub const fn from_version_id(version_id: u32) -> Self {
152        Self(version_id)
153    }
154
155    /// Returns the **major** component of the PHP version.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use mago_php_version::PHPVersion;
161    ///
162    /// let version = PHPVersion::new(8, 2, 0);
163    /// assert_eq!(version.major(), 8);
164    /// ```
165    #[inline]
166    #[must_use]
167    pub const fn major(&self) -> u32 {
168        self.0 >> 16
169    }
170
171    /// Returns the **minor** component of the PHP version.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use mago_php_version::PHPVersion;
177    ///
178    /// let version = PHPVersion::new(8, 2, 0);
179    /// assert_eq!(version.minor(), 2);
180    /// ```
181    #[inline]
182    #[must_use]
183    pub const fn minor(&self) -> u32 {
184        (self.0 >> 8) & 0xff
185    }
186
187    /// Returns the **patch** component of the PHP version.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use mago_php_version::PHPVersion;
193    ///
194    /// let version = PHPVersion::new(8, 1, 13);
195    /// assert_eq!(version.patch(), 13);
196    /// ```
197    #[inline]
198    #[must_use]
199    pub const fn patch(&self) -> u32 {
200        self.0 & 0xff
201    }
202
203    /// Determines if this version is **at least** `major.minor.patch`.
204    ///
205    /// Returns `true` if `self >= (major.minor.patch)`.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use mago_php_version::PHPVersion;
211    ///
212    /// let version = PHPVersion::new(8, 0, 0);
213    /// assert!(version.is_at_least(8, 0, 0));
214    /// assert!(version.is_at_least(7, 4, 30)); // 8.0.0 is newer than 7.4.30
215    /// assert!(!version.is_at_least(8, 1, 0));
216    /// ```
217    #[must_use]
218    pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
219        self.0 >= ((major << 16) | (minor << 8) | patch)
220    }
221
222    /// Checks if a given [`Feature`] is supported by this PHP version.
223    ///
224    /// The logic is based on version thresholds (e.g. `>= 8.0.0` or `< 8.0.0`).
225    /// Each `Feature` variant corresponds to a behavior introduced, removed, or changed
226    /// at a particular version boundary.
227    ///
228    /// # Examples
229    ///
230    /// ```
231    /// use mago_php_version::PHPVersion;
232    /// use mago_php_version::feature::Feature;
233    ///
234    /// let version = PHPVersion::new(7, 4, 0);
235    /// assert!(version.is_supported(Feature::NullCoalesceAssign));
236    /// assert!(!version.is_supported(Feature::NamedArguments));
237    /// ```
238    #[must_use]
239    pub const fn is_supported(&self, feature: Feature) -> bool {
240        match feature {
241            Feature::NullableTypeHint
242            | Feature::IterableTypeHint
243            | Feature::VoidTypeHint
244            | Feature::ClassLikeConstantVisibilityModifiers
245            | Feature::CatchUnionType => self.0 >= 0x07_01_00,
246            Feature::TrailingCommaInListSyntax
247            | Feature::ParameterTypeWidening
248            | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
249            Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
250            Feature::NullCoalesceAssign
251            | Feature::ParameterContravariance
252            | Feature::ReturnCovariance
253            | Feature::PregUnmatchedAsNull
254            | Feature::ArrowFunctions
255            | Feature::NumericLiteralSeparator
256            | Feature::TypedProperties => self.0 >= 0x07_04_00,
257            Feature::NonCapturingCatches
258            | Feature::NativeUnionTypes
259            | Feature::LessOverriddenParametersWithVariadic
260            | Feature::ThrowExpression
261            | Feature::ClassConstantOnExpression
262            | Feature::PromotedProperties
263            | Feature::NamedArguments
264            | Feature::ThrowsTypeErrorForInternalFunctions
265            | Feature::ThrowsValueErrorForInternalFunctions
266            | Feature::HHPrintfSpecifier
267            | Feature::StricterRoundFunctions
268            | Feature::ThrowsOnInvalidMbStringEncoding
269            | Feature::WarnsAboutFinalPrivateMethods
270            | Feature::CastsNumbersToStringsOnLooseComparison
271            | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
272            | Feature::AbstractTraitMethods
273            | Feature::StaticReturnTypeHint
274            | Feature::AccessClassOnObject
275            | Feature::Attributes
276            | Feature::MixedTypeHint
277            | Feature::MatchExpression
278            | Feature::NullSafeOperator
279            | Feature::TrailingCommaInClosureUseList
280            | Feature::FalseCompoundTypeHint
281            | Feature::NullCompoundTypeHint
282            | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
283            Feature::FinalConstants
284            | Feature::ReadonlyProperties
285            | Feature::Enums
286            | Feature::PureIntersectionTypes
287            | Feature::TentativeReturnTypes
288            | Feature::NeverTypeHint
289            | Feature::ClosureCreation
290            | Feature::ArrayUnpackingWithStringKeys
291            | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
292            Feature::ConstantsInTraits
293            | Feature::StrSplitReturnsEmptyArray
294            | Feature::DisjunctiveNormalForm
295            | Feature::ReadonlyClasses
296            | Feature::NeverReturnTypeInArrowFunction
297            | Feature::PregCaptureOnlyNamedGroups
298            | Feature::TrueTypeHint
299            | Feature::FalseTypeHint
300            | Feature::NullTypeHint => self.0 >= 0x08_02_00,
301            Feature::JsonValidate
302            | Feature::TypedClassLikeConstants
303            | Feature::DateTimeExceptions
304            | Feature::OverrideAttribute
305            | Feature::DynamicClassConstantAccess
306            | Feature::ReadonlyAnonymousClasses
307            | Feature::ReadonlyPropertyReinitializationInClone => self.0 >= 0x08_03_00,
308            Feature::AsymmetricVisibility
309            | Feature::LazyObjects
310            | Feature::HighlightStringDoesNotReturnFalse
311            | Feature::PropertyHooks
312            | Feature::NewWithoutParentheses
313            | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
314            Feature::ClosureInConstantExpressions
315            | Feature::ConstantAttributes
316            | Feature::NoDiscardAttribute
317            | Feature::VoidCast
318            | Feature::CloneWith
319            | Feature::AsymmetricVisibilityForStaticProperties
320            | Feature::ClosureCreationInConstantExpressions
321            | Feature::PipeOperator => self.0 >= 0x08_05_00,
322            Feature::CallableInstanceMethods
323            | Feature::LegacyConstructor
324            | Feature::UnsetCast
325            | Feature::CaseInsensitiveConstantNames
326            | Feature::ArrayFunctionsReturnNullWithNonArray
327            | Feature::SubstrReturnFalseInsteadOfEmptyString
328            | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
329            | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
330            | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
331            Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
332            Feature::PassNoneEncodings => self.0 < 0x07_03_00,
333            Feature::PartialFunctionApplication => self.0 >= 0x08_06_00,
334            _ => true,
335        }
336    }
337
338    /// Checks if a given [`Feature`] is deprecated in this PHP version.
339    ///
340    /// Returns `true` if the feature is *considered deprecated* at or above
341    /// certain version thresholds. The threshold logic is encoded within the `match`.
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use mago_php_version::PHPVersion;
347    /// use mago_php_version::feature::Feature;
348    ///
349    /// let version = PHPVersion::new(8, 0, 0);
350    /// assert!(version.is_deprecated(Feature::RequiredParameterAfterOptional));
351    /// assert!(!version.is_deprecated(Feature::DynamicProperties)); // that is 8.2+
352    /// ```
353    #[must_use]
354    pub const fn is_deprecated(&self, feature: Feature) -> bool {
355        match feature {
356            Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
357            Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
358            Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
359            Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
360            Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
361            Feature::SwitchSemicolonSeparators => self.0 >= 0x08_05_00,
362            _ => false,
363        }
364    }
365
366    /// Converts this `PHPVersion` into a raw version ID (e.g. `80400` for `8.4.0`).
367    ///
368    /// This is the inverse of [`from_version_id`].
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use mago_php_version::PHPVersion;
374    ///
375    /// let version = PHPVersion::new(8, 4, 0);
376    /// assert_eq!(version.to_version_id(), 0x080400);
377    /// ```
378    #[must_use]
379    pub const fn to_version_id(&self) -> u32 {
380        self.0
381    }
382}
383
384impl PHPVersionRange {
385    /// Represents the range of PHP versions from 7.0.0 to 7.99.99.
386    pub const PHP7: PHPVersionRange = Self::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 99, 99));
387
388    /// Represents the range of PHP versions from 8.0.0 to 8.99.99.
389    pub const PHP8: PHPVersionRange = Self::between(PHPVersion::new(8, 0, 0), PHPVersion::new(8, 99, 99));
390
391    /// Creates a new `PHPVersionRange` that includes all versions.
392    #[must_use]
393    pub const fn any() -> Self {
394        Self { min: None, max: None }
395    }
396
397    /// Creates a new `PHPVersionRange` that includes all versions up to (and including) the specified version.
398    #[must_use]
399    pub const fn until(version: PHPVersion) -> Self {
400        Self { min: None, max: Some(version) }
401    }
402
403    /// Creates a new `PHPVersionRange` that includes all versions from (and including) the specified version.
404    #[must_use]
405    pub const fn from(version: PHPVersion) -> Self {
406        Self { min: Some(version), max: None }
407    }
408
409    /// Creates a new `PHPVersionRange` that includes all versions between (and including) the specified minimum and maximum versions.
410    #[must_use]
411    pub const fn between(min: PHPVersion, max: PHPVersion) -> Self {
412        Self { min: Some(min), max: Some(max) }
413    }
414
415    /// Checks if this version range supports the given `PHPVersion`.
416    #[inline]
417    #[must_use]
418    pub const fn includes(&self, version: PHPVersion) -> bool {
419        if let Some(min) = self.min
420            && version.0 < min.0
421        {
422            return false;
423        }
424
425        if let Some(max) = self.max
426            && version.0 > max.0
427        {
428            return false;
429        }
430
431        true
432    }
433}
434
435impl std::default::Default for PHPVersion {
436    fn default() -> Self {
437        Self::LATEST
438    }
439}
440
441impl std::fmt::Display for PHPVersion {
442    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
443        write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
444    }
445}
446
447impl Serialize for PHPVersion {
448    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
449    where
450        S: Serializer,
451    {
452        serializer.serialize_str(&self.to_string())
453    }
454}
455
456impl<'de> Deserialize<'de> for PHPVersion {
457    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
458    where
459        D: Deserializer<'de>,
460    {
461        let s = String::deserialize(deserializer)?;
462
463        s.parse().map_err(serde::de::Error::custom)
464    }
465}
466
467impl FromStr for PHPVersion {
468    type Err = ParsingError;
469
470    fn from_str(s: &str) -> Result<Self, Self::Err> {
471        if s.is_empty() {
472            return Err(ParsingError::InvalidFormat);
473        }
474
475        let parts = s.split('.').collect::<Vec<_>>();
476        match parts.len() {
477            1 => {
478                let major = parts[0].parse()?;
479
480                Ok(Self::new(major, 0, 0))
481            }
482            2 => {
483                let major = parts[0].parse()?;
484                let minor = parts[1].parse()?;
485
486                Ok(Self::new(major, minor, 0))
487            }
488            3 => {
489                let major = parts[0].parse()?;
490                let minor = parts[1].parse()?;
491                let patch = parts[2].parse()?;
492
493                Ok(Self::new(major, minor, patch))
494            }
495            _ => Err(ParsingError::InvalidFormat),
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_version() {
506        let version = PHPVersion::new(7, 4, 0);
507        assert_eq!(version.major(), 7);
508        assert_eq!(version.minor(), 4);
509        assert_eq!(version.patch(), 0);
510    }
511
512    #[test]
513    fn test_display() {
514        let version = PHPVersion::new(7, 4, 0);
515        assert_eq!(version.to_string(), "7.4.0");
516    }
517
518    #[test]
519    fn test_from_str_single_segment() {
520        let v: PHPVersion = "7".parse().unwrap();
521        assert_eq!(v.major(), 7);
522        assert_eq!(v.minor(), 0);
523        assert_eq!(v.patch(), 0);
524        assert_eq!(v.to_string(), "7.0.0");
525    }
526
527    #[test]
528    fn test_from_str_two_segments() {
529        let v: PHPVersion = "7.4".parse().unwrap();
530        assert_eq!(v.major(), 7);
531        assert_eq!(v.minor(), 4);
532        assert_eq!(v.patch(), 0);
533        assert_eq!(v.to_string(), "7.4.0");
534    }
535
536    #[test]
537    fn test_from_str_three_segments() {
538        let v: PHPVersion = "8.1.2".parse().unwrap();
539        assert_eq!(v.major(), 8);
540        assert_eq!(v.minor(), 1);
541        assert_eq!(v.patch(), 2);
542        assert_eq!(v.to_string(), "8.1.2");
543    }
544
545    #[test]
546    fn test_from_str_invalid() {
547        let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
548        assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
549
550        let err = "".parse::<PHPVersion>().unwrap_err();
551        assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
552
553        let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
554        assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
555
556        let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
557        assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
558
559        let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
560        assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
561    }
562
563    #[test]
564    fn test_is_supported_features_before_8() {
565        let v_7_4_0 = PHPVersion::new(7, 4, 0);
566
567        assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
568        assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
569
570        assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
571        assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
572    }
573
574    #[test]
575    fn test_is_supported_features_8_0_0() {
576        let v_8_0_0 = PHPVersion::new(8, 0, 0);
577
578        assert!(v_8_0_0.is_supported(Feature::NamedArguments));
579        assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
580    }
581
582    #[test]
583    fn test_is_deprecated_features() {
584        let v_7_4_0 = PHPVersion::new(7, 4, 0);
585        assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
586        assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
587
588        let v_8_0_0 = PHPVersion::new(8, 0, 0);
589        assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
590        assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
591
592        let v_8_2_0 = PHPVersion::new(8, 2, 0);
593        assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
594    }
595
596    #[test]
597    fn test_serde_serialize() {
598        let v_7_4_0 = PHPVersion::new(7, 4, 0);
599        let json = serde_json::to_string(&v_7_4_0).unwrap();
600        assert_eq!(json, "\"7.4.0\"");
601    }
602
603    #[test]
604    fn test_serde_deserialize() {
605        let json = "\"7.4.0\"";
606        let v: PHPVersion = serde_json::from_str(json).unwrap();
607        assert_eq!(v.major(), 7);
608        assert_eq!(v.minor(), 4);
609        assert_eq!(v.patch(), 0);
610
611        let json = "\"7.4\"";
612        let v: PHPVersion = serde_json::from_str(json).unwrap();
613        assert_eq!(v.major(), 7);
614        assert_eq!(v.minor(), 4);
615        assert_eq!(v.patch(), 0);
616    }
617
618    #[test]
619    fn test_serde_round_trip() {
620        let original = PHPVersion::new(8, 1, 5);
621        let serialized = serde_json::to_string(&original).unwrap();
622        let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
623        assert_eq!(original, deserialized);
624        assert_eq!(serialized, "\"8.1.5\"");
625    }
626}