pgdo/runtime/
constraint.rs

1use std::{error, fmt, str::FromStr};
2
3use globset::{Error as GlobError, Glob, GlobBuilder, GlobMatcher};
4
5use crate::version::{self, PartialVersion, VersionError};
6
7use super::Runtime;
8
9#[derive(Debug)]
10pub enum ConstraintError {
11    GlobError(GlobError),
12    VersionError(version::VersionError),
13}
14
15impl fmt::Display for ConstraintError {
16    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
17        use ConstraintError::*;
18        match self {
19            GlobError(ref error) => write!(fmt, "could not parse constraint: {error}",),
20            VersionError(ref error) => write!(
21                fmt,
22                "could not parse version constraint {text:?}: {error}",
23                text = error.text().unwrap_or("<unknown>")
24            ),
25        }
26    }
27}
28
29impl error::Error for ConstraintError {
30    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
31        match *self {
32            ConstraintError::GlobError(ref error) => Some(error),
33            ConstraintError::VersionError(ref error) => Some(error),
34        }
35    }
36}
37
38impl From<GlobError> for ConstraintError {
39    fn from(error: GlobError) -> ConstraintError {
40        ConstraintError::GlobError(error)
41    }
42}
43
44impl From<version::VersionError> for ConstraintError {
45    fn from(error: version::VersionError) -> ConstraintError {
46        ConstraintError::VersionError(error)
47    }
48}
49
50/// A constraint used when selecting a PostgreSQL runtime.
51#[derive(Clone, Debug)]
52pub enum Constraint {
53    /// Match the runtime's `bindir`.
54    BinDir(GlobMatcher),
55    /// Match the given version.
56    Version(PartialVersion),
57    /// Either constraint can be satisfied.
58    Either(Box<Constraint>, Box<Constraint>),
59    /// Both constraints must be satisfied.
60    Both(Box<Constraint>, Box<Constraint>),
61    /// Invert the given constraint; use `!constraint` for the same effect.
62    Not(Box<Constraint>),
63    /// Match any runtime.
64    Anything,
65    /// Match no runtimes at all.
66    Nothing,
67}
68
69impl Constraint {
70    /// Match the given runtime's `bindir` against this glob pattern.
71    ///
72    /// The [syntax](https://docs.rs/globset/latest/globset/index.html#syntax)
73    /// comes from the [globset](https://crates.io/crates/globset) crate.
74    /// However, here we deviate from its default rules:
75    ///
76    /// - `*` and `?` do **not** match path separators (`/`); use `**` for that.
77    /// - empty alternators, e.g. `{,.rs}` are allowed.
78    ///
79    /// Use [`glob`][`Self::glob`] if you want to select your own rules.
80    pub fn path(pattern: &str) -> Result<Self, GlobError> {
81        Ok(Self::BinDir(
82            GlobBuilder::new(pattern)
83                .literal_separator(true)
84                .empty_alternates(true)
85                .build()?
86                .compile_matcher(),
87        ))
88    }
89
90    /// Match the given runtime's `bindir` against this glob.
91    pub fn glob(glob: &Glob) -> Self {
92        Self::BinDir(glob.compile_matcher())
93    }
94
95    /// Match the given runtime against this version.
96    pub fn version(version: &str) -> Result<Self, VersionError> {
97        Ok(Self::Version(version.parse()?))
98    }
99
100    /// Match **any** of the given constraints.
101    ///
102    /// If there are no constraints, this returns [`Self::Nothing`].
103    pub fn any<C: IntoIterator<Item = Constraint>>(constraints: C) -> Self {
104        constraints
105            .into_iter()
106            .reduce(|a, b| a | b)
107            .unwrap_or(Self::Nothing)
108    }
109
110    /// Match **all** of the given constraints.
111    ///
112    /// If there are no constraints, this returns [`Self::Anything`].
113    pub fn all<C: IntoIterator<Item = Constraint>>(constraints: C) -> Self {
114        constraints
115            .into_iter()
116            .reduce(|a, b| a & b)
117            .unwrap_or(Self::Anything)
118    }
119
120    /// Does the given runtime match this constraint?
121    pub fn matches(&self, runtime: &Runtime) -> bool {
122        match self {
123            Self::BinDir(matcher) => matcher.is_match(&runtime.bindir),
124            Self::Version(version) => version.compatible(runtime.version),
125            Self::Either(ca, cb) => ca.matches(runtime) || cb.matches(runtime),
126            Self::Both(ca, cb) => ca.matches(runtime) && cb.matches(runtime),
127            Self::Not(constraint) => !constraint.matches(runtime),
128            Self::Anything => true,
129            Self::Nothing => false,
130        }
131    }
132}
133
134impl std::ops::Not for Constraint {
135    type Output = Self;
136
137    /// Invert this constraint.
138    fn not(self) -> Self::Output {
139        match self {
140            Self::Anything => Self::Nothing,
141            Self::Nothing => Self::Anything,
142            Self::Not(constraint) => *constraint,
143            _ => Self::Not(Box::new(self)),
144        }
145    }
146}
147
148impl std::ops::BitOr for Constraint {
149    type Output = Self;
150
151    /// Match either of the constraints.
152    fn bitor(self, rhs: Self) -> Self::Output {
153        match (self, rhs) {
154            (Self::Anything, _) | (_, Self::Anything) => Self::Anything,
155            (Self::Nothing, c) | (c, Self::Nothing) => c,
156            (ca, cb) => Self::Either(Box::new(ca), Box::new(cb)),
157        }
158    }
159}
160
161impl std::ops::BitAnd for Constraint {
162    type Output = Self;
163
164    /// Match both the constraints.
165    fn bitand(self, rhs: Self) -> Self::Output {
166        match (self, rhs) {
167            (Self::Anything, c) | (c, Self::Anything) => c,
168            (Self::Nothing, _) | (_, Self::Nothing) => Self::Nothing,
169            (ca, cb) => Self::Both(Box::new(ca), Box::new(cb)),
170        }
171    }
172}
173
174impl From<PartialVersion> for Constraint {
175    /// Convert a [`PartialVersion`] into a [`Constraint::Version`].
176    fn from(version: PartialVersion) -> Self {
177        Self::Version(version)
178    }
179}
180
181impl FromStr for Constraint {
182    type Err = ConstraintError;
183
184    /// Parse a constraint from a string.
185    ///
186    /// If it contains a path separator, it will be parsed as a glob pattern,
187    /// otherwise it will be parsed as a version constraint.
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        if s.contains(std::path::MAIN_SEPARATOR) {
190            Ok(Self::path(s)?)
191        } else {
192            Ok(Self::version(s)?)
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::Constraint;
200    use super::PartialVersion;
201
202    /// An example constraint.
203    const CONSTRAINT: Constraint = Constraint::Version(PartialVersion::Post10m(13));
204
205    #[test]
206    fn test_not() {
207        let c1 = Constraint::Version(PartialVersion::Post10m(13));
208        assert!(matches!(c1, Constraint::Version(_)));
209        let c2 = !c1;
210        assert!(matches!(c2, Constraint::Not(_)));
211        let c3 = !c2;
212        assert!(matches!(c3, Constraint::Version(_)));
213    }
214
215    #[test]
216    fn test_not_anything_and_nothing() {
217        let c1 = Constraint::Anything;
218        let c2 = !c1;
219        assert!(matches!(c2, Constraint::Nothing));
220        let c3 = !c2;
221        assert!(matches!(c3, Constraint::Anything));
222    }
223
224    #[test]
225    fn test_or() {
226        assert!(matches!(
227            Constraint::Anything | CONSTRAINT.clone(),
228            Constraint::Anything
229        ));
230        assert!(matches!(
231            CONSTRAINT.clone() | Constraint::Anything,
232            Constraint::Anything
233        ));
234        assert!(matches!(
235            Constraint::Nothing | CONSTRAINT.clone(),
236            Constraint::Version(_)
237        ));
238        assert!(matches!(
239            CONSTRAINT.clone() | Constraint::Nothing,
240            Constraint::Version(_)
241        ));
242    }
243
244    #[test]
245    fn test_or_anything_and_nothing() {
246        assert!(matches!(
247            Constraint::Anything | Constraint::Anything,
248            Constraint::Anything
249        ));
250        assert!(matches!(
251            Constraint::Nothing | Constraint::Anything,
252            Constraint::Anything
253        ));
254        assert!(matches!(
255            Constraint::Anything | Constraint::Nothing,
256            Constraint::Anything
257        ));
258    }
259
260    #[test]
261    fn test_and() {
262        assert!(matches!(
263            Constraint::Anything & CONSTRAINT.clone(),
264            Constraint::Version(_)
265        ));
266        assert!(matches!(
267            CONSTRAINT.clone() & Constraint::Anything,
268            Constraint::Version(_)
269        ));
270        assert!(matches!(
271            Constraint::Nothing & CONSTRAINT.clone(),
272            Constraint::Nothing
273        ));
274        assert!(matches!(
275            CONSTRAINT.clone() & Constraint::Nothing,
276            Constraint::Nothing
277        ));
278    }
279
280    #[test]
281    fn test_and_anything_and_nothing() {
282        assert!(matches!(
283            Constraint::Anything & Constraint::Anything,
284            Constraint::Anything
285        ));
286        assert!(matches!(
287            Constraint::Nothing & Constraint::Anything,
288            Constraint::Nothing
289        ));
290        assert!(matches!(
291            Constraint::Anything & Constraint::Nothing,
292            Constraint::Nothing
293        ));
294    }
295
296    #[test]
297    fn test_any() {
298        assert!(matches!(Constraint::any([]), Constraint::Nothing));
299        assert!(matches!(
300            Constraint::any([
301                Constraint::Anything,
302                Constraint::Nothing,
303                Constraint::Nothing
304            ]),
305            Constraint::Anything
306        ));
307        assert!(matches!(
308            Constraint::any([Constraint::Nothing, CONSTRAINT.clone(), Constraint::Nothing]),
309            Constraint::Version(_)
310        ));
311        assert!(matches!(
312            Constraint::any([
313                Constraint::Anything,
314                CONSTRAINT.clone(),
315                Constraint::Nothing
316            ]),
317            Constraint::Anything
318        ));
319        assert!(matches!(
320            Constraint::any([CONSTRAINT.clone(), CONSTRAINT.clone()]),
321            Constraint::Either(ca, cb)
322                if matches!(*ca, Constraint::Version(_))
323                && matches!(*cb, Constraint::Version(_))
324        ));
325    }
326
327    #[test]
328    fn test_all() {
329        assert!(matches!(Constraint::all([]), Constraint::Anything));
330        assert!(matches!(
331            Constraint::all([
332                Constraint::Anything,
333                Constraint::Anything,
334                Constraint::Anything
335            ]),
336            Constraint::Anything
337        ));
338        assert!(matches!(
339            Constraint::all([
340                Constraint::Anything,
341                CONSTRAINT.clone(),
342                Constraint::Anything,
343            ]),
344            Constraint::Version(_),
345        ));
346        assert!(matches!(
347            Constraint::all([
348                Constraint::Anything,
349                CONSTRAINT.clone(),
350                Constraint::Nothing,
351            ]),
352            Constraint::Nothing,
353        ));
354        assert!(matches!(
355            Constraint::all([CONSTRAINT.clone(), CONSTRAINT.clone()]),
356            Constraint::Both(ca, cb)
357                if matches!(*ca, Constraint::Version(_))
358                && matches!(*cb, Constraint::Version(_))
359        ));
360    }
361}