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}