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 => self.0 >= 0x08_03_00,
307            Feature::AsymmetricVisibility
308            | Feature::LazyObjects
309            | Feature::HighlightStringDoesNotReturnFalse
310            | Feature::PropertyHooks
311            | Feature::NewWithoutParentheses
312            | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
313            Feature::ClosureInConstantExpressions
314            | Feature::ConstantAttributes
315            | Feature::NoDiscardAttribute
316            | Feature::VoidCast
317            | Feature::CloneWith
318            | Feature::AsymmetricVisibilityForStaticProperties
319            | Feature::ClosureCreationInConstantExpressions
320            | Feature::PipeOperator => self.0 >= 0x08_05_00,
321            Feature::CallableInstanceMethods
322            | Feature::LegacyConstructor
323            | Feature::UnsetCast
324            | Feature::CaseInsensitiveConstantNames
325            | Feature::ArrayFunctionsReturnNullWithNonArray
326            | Feature::SubstrReturnFalseInsteadOfEmptyString
327            | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
328            | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
329            | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
330            Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
331            Feature::PassNoneEncodings => self.0 < 0x07_03_00,
332            Feature::PartialFunctionApplication => self.0 >= 0x08_06_00,
333            _ => true,
334        }
335    }
336
337    /// Checks if a given [`Feature`] is deprecated in this PHP version.
338    ///
339    /// Returns `true` if the feature is *considered deprecated* at or above
340    /// certain version thresholds. The threshold logic is encoded within the `match`.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use mago_php_version::PHPVersion;
346    /// use mago_php_version::feature::Feature;
347    ///
348    /// let version = PHPVersion::new(8, 0, 0);
349    /// assert!(version.is_deprecated(Feature::RequiredParameterAfterOptional));
350    /// assert!(!version.is_deprecated(Feature::DynamicProperties)); // that is 8.2+
351    /// ```
352    #[must_use]
353    pub const fn is_deprecated(&self, feature: Feature) -> bool {
354        match feature {
355            Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
356            Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
357            Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
358            Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
359            Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
360            Feature::SwitchSemicolonSeparators => self.0 >= 0x08_05_00,
361            _ => false,
362        }
363    }
364
365    /// Converts this `PHPVersion` into a raw version ID (e.g. `80400` for `8.4.0`).
366    ///
367    /// This is the inverse of [`from_version_id`].
368    ///
369    /// # Examples
370    ///
371    /// ```
372    /// use mago_php_version::PHPVersion;
373    ///
374    /// let version = PHPVersion::new(8, 4, 0);
375    /// assert_eq!(version.to_version_id(), 0x080400);
376    /// ```
377    #[must_use]
378    pub const fn to_version_id(&self) -> u32 {
379        self.0
380    }
381}
382
383impl PHPVersionRange {
384    /// Represents the range of PHP versions from 7.0.0 to 7.99.99.
385    pub const PHP7: PHPVersionRange = Self::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 99, 99));
386
387    /// Represents the range of PHP versions from 8.0.0 to 8.99.99.
388    pub const PHP8: PHPVersionRange = Self::between(PHPVersion::new(8, 0, 0), PHPVersion::new(8, 99, 99));
389
390    /// Creates a new `PHPVersionRange` that includes all versions.
391    #[must_use]
392    pub const fn any() -> Self {
393        Self { min: None, max: None }
394    }
395
396    /// Creates a new `PHPVersionRange` that includes all versions up to (and including) the specified version.
397    #[must_use]
398    pub const fn until(version: PHPVersion) -> Self {
399        Self { min: None, max: Some(version) }
400    }
401
402    /// Creates a new `PHPVersionRange` that includes all versions from (and including) the specified version.
403    #[must_use]
404    pub const fn from(version: PHPVersion) -> Self {
405        Self { min: Some(version), max: None }
406    }
407
408    /// Creates a new `PHPVersionRange` that includes all versions between (and including) the specified minimum and maximum versions.
409    #[must_use]
410    pub const fn between(min: PHPVersion, max: PHPVersion) -> Self {
411        Self { min: Some(min), max: Some(max) }
412    }
413
414    /// Checks if this version range supports the given `PHPVersion`.
415    #[inline]
416    #[must_use]
417    pub const fn includes(&self, version: PHPVersion) -> bool {
418        if let Some(min) = self.min
419            && version.0 < min.0
420        {
421            return false;
422        }
423
424        if let Some(max) = self.max
425            && version.0 > max.0
426        {
427            return false;
428        }
429
430        true
431    }
432}
433
434impl std::default::Default for PHPVersion {
435    fn default() -> Self {
436        Self::LATEST
437    }
438}
439
440impl std::fmt::Display for PHPVersion {
441    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
442        write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
443    }
444}
445
446impl Serialize for PHPVersion {
447    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
448    where
449        S: Serializer,
450    {
451        serializer.serialize_str(&self.to_string())
452    }
453}
454
455impl<'de> Deserialize<'de> for PHPVersion {
456    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
457    where
458        D: Deserializer<'de>,
459    {
460        let s = String::deserialize(deserializer)?;
461
462        s.parse().map_err(serde::de::Error::custom)
463    }
464}
465
466impl FromStr for PHPVersion {
467    type Err = ParsingError;
468
469    fn from_str(s: &str) -> Result<Self, Self::Err> {
470        if s.is_empty() {
471            return Err(ParsingError::InvalidFormat);
472        }
473
474        let parts = s.split('.').collect::<Vec<_>>();
475        match parts.len() {
476            1 => {
477                let major = parts[0].parse()?;
478
479                Ok(Self::new(major, 0, 0))
480            }
481            2 => {
482                let major = parts[0].parse()?;
483                let minor = parts[1].parse()?;
484
485                Ok(Self::new(major, minor, 0))
486            }
487            3 => {
488                let major = parts[0].parse()?;
489                let minor = parts[1].parse()?;
490                let patch = parts[2].parse()?;
491
492                Ok(Self::new(major, minor, patch))
493            }
494            _ => Err(ParsingError::InvalidFormat),
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_version() {
505        let version = PHPVersion::new(7, 4, 0);
506        assert_eq!(version.major(), 7);
507        assert_eq!(version.minor(), 4);
508        assert_eq!(version.patch(), 0);
509    }
510
511    #[test]
512    fn test_display() {
513        let version = PHPVersion::new(7, 4, 0);
514        assert_eq!(version.to_string(), "7.4.0");
515    }
516
517    #[test]
518    fn test_from_str_single_segment() {
519        let v: PHPVersion = "7".parse().unwrap();
520        assert_eq!(v.major(), 7);
521        assert_eq!(v.minor(), 0);
522        assert_eq!(v.patch(), 0);
523        assert_eq!(v.to_string(), "7.0.0");
524    }
525
526    #[test]
527    fn test_from_str_two_segments() {
528        let v: PHPVersion = "7.4".parse().unwrap();
529        assert_eq!(v.major(), 7);
530        assert_eq!(v.minor(), 4);
531        assert_eq!(v.patch(), 0);
532        assert_eq!(v.to_string(), "7.4.0");
533    }
534
535    #[test]
536    fn test_from_str_three_segments() {
537        let v: PHPVersion = "8.1.2".parse().unwrap();
538        assert_eq!(v.major(), 8);
539        assert_eq!(v.minor(), 1);
540        assert_eq!(v.patch(), 2);
541        assert_eq!(v.to_string(), "8.1.2");
542    }
543
544    #[test]
545    fn test_from_str_invalid() {
546        let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
547        assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
548
549        let err = "".parse::<PHPVersion>().unwrap_err();
550        assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
551
552        let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
553        assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
554
555        let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
556        assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
557
558        let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
559        assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
560    }
561
562    #[test]
563    fn test_is_supported_features_before_8() {
564        let v_7_4_0 = PHPVersion::new(7, 4, 0);
565
566        assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
567        assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
568
569        assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
570        assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
571    }
572
573    #[test]
574    fn test_is_supported_features_8_0_0() {
575        let v_8_0_0 = PHPVersion::new(8, 0, 0);
576
577        assert!(v_8_0_0.is_supported(Feature::NamedArguments));
578        assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
579    }
580
581    #[test]
582    fn test_is_deprecated_features() {
583        let v_7_4_0 = PHPVersion::new(7, 4, 0);
584        assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
585        assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
586
587        let v_8_0_0 = PHPVersion::new(8, 0, 0);
588        assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
589        assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
590
591        let v_8_2_0 = PHPVersion::new(8, 2, 0);
592        assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
593    }
594
595    #[test]
596    fn test_serde_serialize() {
597        let v_7_4_0 = PHPVersion::new(7, 4, 0);
598        let json = serde_json::to_string(&v_7_4_0).unwrap();
599        assert_eq!(json, "\"7.4.0\"");
600    }
601
602    #[test]
603    fn test_serde_deserialize() {
604        let json = "\"7.4.0\"";
605        let v: PHPVersion = serde_json::from_str(json).unwrap();
606        assert_eq!(v.major(), 7);
607        assert_eq!(v.minor(), 4);
608        assert_eq!(v.patch(), 0);
609
610        let json = "\"7.4\"";
611        let v: PHPVersion = serde_json::from_str(json).unwrap();
612        assert_eq!(v.major(), 7);
613        assert_eq!(v.minor(), 4);
614        assert_eq!(v.patch(), 0);
615    }
616
617    #[test]
618    fn test_serde_round_trip() {
619        let original = PHPVersion::new(8, 1, 5);
620        let serialized = serde_json::to_string(&original).unwrap();
621        let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
622        assert_eq!(original, deserialized);
623        assert_eq!(serialized, "\"8.1.5\"");
624    }
625}