mir_analyzer/
php_version.rs1use std::fmt;
8use std::str::FromStr;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct PhpVersion {
12 major: u8,
13 minor: u8,
14}
15
16impl PhpVersion {
17 pub const LATEST: PhpVersion = PhpVersion::new(8, 5);
18
19 pub const fn new(major: u8, minor: u8) -> Self {
20 Self { major, minor }
21 }
22
23 pub const fn major(self) -> u8 {
24 self.major
25 }
26
27 pub const fn minor(self) -> u8 {
28 self.minor
29 }
30
31 pub fn includes_symbol(self, since: Option<&str>, removed: Option<&str>) -> bool {
41 let parse_php = |s: &str| -> Option<PhpVersion> {
42 let v = s.parse::<PhpVersion>().ok()?;
43 if v.major() >= 4 && v.major() <= PhpVersion::LATEST.major() {
47 Some(v)
48 } else {
49 None
50 }
51 };
52 if let Some(s) = since.and_then(parse_php) {
53 if self < s {
54 return false;
55 }
56 }
57 if let Some(r) = removed.and_then(parse_php) {
58 if self >= r {
59 return false;
60 }
61 }
62 true
63 }
64}
65
66impl Default for PhpVersion {
67 fn default() -> Self {
68 Self::LATEST
69 }
70}
71
72impl fmt::Display for PhpVersion {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}.{}", self.major, self.minor)
75 }
76}
77
78#[derive(Debug, thiserror::Error)]
79#[error("invalid PHP version `{0}`: expected `MAJOR.MINOR` (e.g. `8.2`)")]
80pub struct ParsePhpVersionError(pub String);
81
82impl FromStr for PhpVersion {
83 type Err = ParsePhpVersionError;
84
85 fn from_str(s: &str) -> Result<Self, Self::Err> {
86 let mut parts = s.trim().split('.');
87 let major = parts
88 .next()
89 .and_then(|p| p.parse::<u8>().ok())
90 .ok_or_else(|| ParsePhpVersionError(s.to_string()))?;
91 let minor = match parts.next() {
92 Some(p) => p
93 .parse::<u8>()
94 .map_err(|_| ParsePhpVersionError(s.to_string()))?,
95 None => 0,
96 };
97 Ok(Self::new(major, minor))
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn parses_major_minor() {
108 assert_eq!("8.2".parse::<PhpVersion>().unwrap(), PhpVersion::new(8, 2));
109 }
110
111 #[test]
112 fn parses_major_minor_patch() {
113 assert_eq!(
114 "8.3.7".parse::<PhpVersion>().unwrap(),
115 PhpVersion::new(8, 3)
116 );
117 }
118
119 #[test]
120 fn parses_major_only() {
121 assert_eq!("7".parse::<PhpVersion>().unwrap(), PhpVersion::new(7, 0));
122 }
123
124 #[test]
125 fn rejects_garbage() {
126 assert!("x.y".parse::<PhpVersion>().is_err());
127 assert!("8.x".parse::<PhpVersion>().is_err());
128 assert!("".parse::<PhpVersion>().is_err());
129 }
130
131 #[test]
132 fn ordered_by_major_then_minor() {
133 assert!(PhpVersion::new(8, 1) < PhpVersion::new(8, 2));
134 assert!(PhpVersion::new(7, 4) < PhpVersion::new(8, 0));
135 }
136
137 #[test]
138 fn displays_as_major_dot_minor() {
139 assert_eq!(PhpVersion::new(8, 3).to_string(), "8.3");
140 }
141
142 #[test]
143 fn includes_symbol_respects_since() {
144 assert!(!PhpVersion::new(7, 4).includes_symbol(Some("8.0"), None));
145 assert!(PhpVersion::new(8, 0).includes_symbol(Some("8.0"), None));
146 assert!(PhpVersion::new(8, 5).includes_symbol(Some("8.0"), None));
147 }
148
149 #[test]
150 fn includes_symbol_respects_removed() {
151 assert!(PhpVersion::new(7, 4).includes_symbol(None, Some("8.0")));
152 assert!(!PhpVersion::new(8, 0).includes_symbol(None, Some("8.0")));
153 assert!(!PhpVersion::new(8, 5).includes_symbol(None, Some("8.0")));
154 }
155
156 #[test]
157 fn includes_symbol_ignores_library_versions() {
158 assert!(PhpVersion::new(8, 5).includes_symbol(Some("9.12"), None));
161 assert!(PhpVersion::new(8, 5).includes_symbol(Some("1.17"), None));
164 assert!(PhpVersion::new(8, 5).includes_symbol(Some("12.0"), None));
165 }
166
167 #[test]
168 fn includes_symbol_ignores_garbage() {
169 assert!(PhpVersion::new(8, 5).includes_symbol(Some("PECL"), None));
170 assert!(PhpVersion::new(8, 5).includes_symbol(Some(""), None));
171 }
172}