issue_states/
condition.rs

1// Issue states
2//
3// Copyright (c) 2018 Julian Ganz
4//
5// MIT License
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be included in all
15// copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23// SOFTWARE.
24//
25
26//! Issue states and conditions
27//!
28//! This module provides the `Condition` trait which will usually be implemented
29//! by the library's user.
30//!
31
32use std::error::Error as EError;
33use std::result::Result as RResult;
34
35use error::*;
36
37
38
39
40/// Trait for issue metadata conditions
41///
42/// A `Condition` represents a predicate for an issue state: a function mapping
43/// an issue to a boolean value indicating whether the condition is fulfilled or
44/// not. It is generally assumed that a condition consists of "condition atoms",
45/// which each specify a "singular" condition on a specific piece of metadata.
46///
47/// Whatever is used as type for conditions on metadata has to implement this
48/// trait. It enables `IssueStates` to evaluate the condition. Additionally, the
49/// `ConditionFactory` trait should be implemented in order to enable parsing
50/// conditions from configuration files.
51///
52pub trait Condition {
53    /// Type of the issue being evaluated
54    ///
55    /// Alternatively, some representation of the metadata may be used in place
56    /// of the issue type.
57    ///
58    type Issue;
59
60    /// Check whether the condition is satisfied by the issue provided
61    ///
62    fn satisfied_by(&self, issue: &Self::Issue) -> bool;
63}
64
65
66
67
68/// Match operators
69///
70/// These operators define how the piece of metadata queried from the issue is
71/// compared to the literal provided with the conditon atom. The former is
72/// considered the "left-hand value" while the latter is considered the
73/// "right-hand value" in this context.
74///
75#[derive(Debug, PartialEq, Eq)]
76pub enum MatchOp {
77    /// Match if the values are evivalent
78    Equivalence,
79    /// Match if the left-hand value is lower than the right-hand value.
80    LowerThan,
81    /// Match if the left-hand value is greater than the right-hand value.
82    GreaterThan,
83    /// Match if the left-hand value is lower than the right-hand value or
84    /// equal.
85    LowerThanOrEqual,
86    /// Match if the left-hand value is greater than the right-hand value or
87    /// equal.
88    GreaterThanOrEqual,
89    /// Match if the left-hand value contains or is equal to the right-hand
90    /// value.
91    Contains,
92}
93
94
95
96
97/// Factory trait for conditions
98///
99/// This trait allows issue states parsers to create conditions from a string
100/// representation. Implementers need not implement the actual parsing. Instead,
101/// the function `make_condition()` will be supplied with the components of a
102/// condition.
103///
104pub trait ConditionFactory<C>
105    where C: Condition + Sized
106{
107    type Error : From<Error> + EError;
108
109    /// Create a condition from bits and pieces
110    ///
111    /// The condition will be assembled from the "metadata identifier" (e.g. the
112    /// name of the piece of metadata), a flag indicating whether the condition
113    /// is negated or not and, optionally, the matching operator and a string
114    /// representation of the right-hand side value.
115    ///
116    /// If the operator and value are not present, the resulting condition is
117    /// expected to yield true if the piece of metadata denoted by the metadata
118    /// identifier is present, e.g. non-null.
119    ///
120    fn make_condition(
121        &self,
122        name: &str,
123        neg: bool,
124        val_op: Option<(MatchOp, &str)>
125    ) -> RResult<C, Self::Error>;
126
127    /// Parse a condition directly from a string
128    ///
129    /// This function parses a `Condition` directly from a string using the
130    /// `make_condition()` function.
131    ///
132    fn parse_condition(
133        &self,
134        string: &str,
135    ) -> RResult<C, Self::Error> {
136        parse_condition(string)
137            .map_err(From::from)
138            .and_then(|(name, neg, op_val)| self.make_condition(name, neg, op_val))
139    }
140}
141
142
143
144
145/// Parse the bits of a condition atom
146///
147/// This method parses a condition atom. It returns the "metadata identifier"
148/// (e.g. the name of the piece of metadata), a flag indicating whether the
149/// condition is negated or not and, optionally, the matching operator and a
150/// string representation of the right-hand side value.
151///
152/// The matching operator and value may be `None`. In this case, the condition
153/// parsed is expected to check for the existence of a piece of metadata.
154///
155pub fn parse_condition(string: &str) -> Result<(&str, bool, Option<(MatchOp, &str)>)> {
156    if let Some(pos) = string.find(|ref c| reserved_char(c)) {
157        if pos == 0 {
158            // The condition is either a negated existance (e.g. starts with
159            // `!`) or invalid.
160            let (neg, name) = string.split_at(1);
161            return if neg == "!" && !name.contains(|ref c| reserved_char(c)) {
162                Ok((name, true, None))
163            } else {
164                Err(Error::from(ErrorKind::ConditionParseError))
165            }
166        }
167
168        let (name, mut op_val) = string.split_at(pos);
169        let negated = op_val.starts_with('!');
170        if negated {
171            op_val = op_val.split_at(1).1;
172        }
173        Ok((name, negated, parse_op_val(op_val)?.into()))
174    } else {
175        // If the string representation does not contain any reserved
176        // characters, this condition is the existance of the piece of metadata.
177        Ok((string, false, None))
178    }
179}
180
181
182/// Check whether a character is a reserved character
183///
184fn reserved_char(c: &char) -> bool {
185    ['!', '=', '<', '>', '~'].contains(c)
186}
187
188
189/// Parse and extract the match operator and value from the compound
190///
191fn parse_op_val(string: &str) -> Result<(MatchOp, &str)> {
192    let mut chars = string.chars();
193
194    let (op, pos) = match chars.next() {
195        Some('=') => (MatchOp::Equivalence, 1),
196        Some('<') => match chars.next() {
197            Some('=') => (MatchOp::LowerThanOrEqual, 2),
198            _ => (MatchOp::LowerThan, 1),
199        },
200        Some('>') => match chars.next() {
201            Some('=') => (MatchOp::GreaterThanOrEqual, 2),
202            _ => (MatchOp::GreaterThan, 1),
203        },
204        Some('~') => (MatchOp::Contains, 1),
205        _ => return Err(Error::from(ErrorKind::ConditionParseError)),
206    };
207
208    Ok((op, string.split_at(pos).1))
209}
210
211
212
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    fn parse(string: &str) -> (&str, bool, Option<(MatchOp, &str)>) {
219        parse_condition(string).expect("Failed to parse condition atom!")
220    }
221
222    #[test]
223    fn smoke() {
224        assert_eq!(parse("foo"), ("foo", false, None));
225        assert_eq!(parse("!foo"), ("foo", true, None));
226        assert_eq!(parse("foo=bar"), ("foo", false, Some((MatchOp::Equivalence, "bar"))));
227        assert_eq!(parse("foo<bar"), ("foo", false, Some((MatchOp::LowerThan, "bar"))));
228        assert_eq!(parse("foo>bar"), ("foo", false, Some((MatchOp::GreaterThan, "bar"))));
229        assert_eq!(parse("foo<=bar"), ("foo", false, Some((MatchOp::LowerThanOrEqual, "bar"))));
230        assert_eq!(parse("foo>=bar"), ("foo", false, Some((MatchOp::GreaterThanOrEqual, "bar"))));
231        assert_eq!(parse("foo!~bar"), ("foo", true, Some((MatchOp::Contains, "bar"))));
232        assert_eq!(parse("foo!=bar"), ("foo", true, Some((MatchOp::Equivalence, "bar"))));
233        assert_eq!(parse("foo!<bar"), ("foo", true, Some((MatchOp::LowerThan, "bar"))));
234        assert_eq!(parse("foo!>bar"), ("foo", true, Some((MatchOp::GreaterThan, "bar"))));
235        assert_eq!(parse("foo!<=bar"), ("foo", true, Some((MatchOp::LowerThanOrEqual, "bar"))));
236        assert_eq!(parse("foo!>=bar"), ("foo", true, Some((MatchOp::GreaterThanOrEqual, "bar"))));
237        assert_eq!(parse("foo!~bar"), ("foo", true, Some((MatchOp::Contains, "bar"))));
238    }
239}