opltypes/
ruleset.rs

1//! Defines the `RuleSet` field for the `meets` table and CONFIG files.
2
3use serde::de::{self, Deserialize, Visitor};
4use serde::ser::Serialize;
5
6use std::fmt;
7use std::str::FromStr;
8
9/// A rule of competition.
10///
11/// By default, all equipment divisions are assumed to be separate.
12#[derive(Copy, Clone, Debug, Deserialize, Display, EnumString, PartialEq, Serialize)]
13pub enum Rule {
14    /// Lifters in "Raw" and "Wraps" compete in the same category.
15    CombineRawAndWraps,
16
17    /// Lifters in "Single-ply" and "Multi-ply" compete in the same category.
18    CombineSingleAndMulti,
19
20    /// There was no equipment category: everyone competed together.
21    CombineAllEquipment,
22
23    /// Fourth attempts can be lower than other attempts.
24    FourthAttemptsMayLower,
25}
26
27/// Packed storage for all active RuleSet.
28///
29/// There are two equivalent ways RuleSet may be stored:
30///
31/// 1. When stored textually, rules should be in one string, separated by spaces.
32///    This is how rules are specified in meet.csv files, for example.
33///
34/// 2. When serialized by the compiler for the server, rules may be stored as
35///    a simple number, representing a bitfield of Rules. This is to save space,
36///    since the RuleSet field will be attached to each meet, and the Rule strings
37///    themselves are long.
38///
39/// It's expected that the human-consumable openpowerlifting.csv will not include
40/// the RuleSet of each meet, and therefore it's safe to serialize to a number.
41#[derive(Copy, Clone, Debug, Default, PartialEq)]
42pub struct RuleSet(u32);
43
44#[derive(Copy, Clone, Debug, Display, PartialEq)]
45pub enum RuleSetParseError {
46    UnknownRule,
47}
48
49impl RuleSet {
50    /// Whether a given Rule is active.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// # use opltypes::{Rule, RuleSet};
56    /// let ruleset = "CombineRawAndWraps".parse::<RuleSet>().unwrap();
57    /// assert!(ruleset.contains(Rule::CombineRawAndWraps));
58    /// assert!(!ruleset.contains(Rule::CombineSingleAndMulti));
59    /// ```
60    pub fn contains(self, rule: Rule) -> bool {
61        self.0 & (1 << (rule as u32)) != 0
62    }
63
64    /// Adds a given Rule to the set.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # use opltypes::{Rule, RuleSet};
70    /// let mut ruleset = RuleSet::default();
71    /// ruleset.add(Rule::CombineSingleAndMulti);
72    /// assert!(!ruleset.contains(Rule::CombineRawAndWraps));
73    /// assert!(ruleset.contains(Rule::CombineSingleAndMulti));
74    /// ```
75    pub fn add(&mut self, rule: Rule) {
76        self.0 |= 1 << (rule as u32);
77    }
78}
79
80impl Serialize for RuleSet {
81    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
82        if self.0 == 0 {
83            // Output nothing instead of zero to save some space.
84            serializer.serialize_str("")
85        } else {
86            serializer.serialize_u32(self.0)
87        }
88    }
89}
90
91impl FromStr for RuleSet {
92    type Err = RuleSetParseError;
93
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        // The empty string corresponds to default rules.
96        if s.is_empty() {
97            return Ok(RuleSet::default());
98        }
99
100        // If specifed as a number, import the number directly.
101        if let Ok(n) = s.parse::<u32>() {
102            return Ok(RuleSet { 0: n });
103        }
104
105        // Otherwise assume it's a space-delimited string.
106        let mut ruleset = RuleSet::default();
107        for substr in s.split(' ') {
108            if let Ok(rule) = substr.parse::<Rule>() {
109                ruleset.add(rule);
110            } else {
111                return Err(RuleSetParseError::UnknownRule);
112            }
113        }
114        Ok(ruleset)
115    }
116}
117
118struct RuleSetVisitor;
119
120impl<'de> Visitor<'de> for RuleSetVisitor {
121    type Value = RuleSet;
122
123    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
124        formatter.write_str("a space-separated list of rules")
125    }
126
127    fn visit_str<E: de::Error>(self, value: &str) -> Result<RuleSet, E> {
128        RuleSet::from_str(value).map_err(E::custom)
129    }
130}
131
132impl<'de> Deserialize<'de> for RuleSet {
133    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<RuleSet, D::Error> {
134        deserializer.deserialize_str(RuleSetVisitor)
135    }
136}
137
138#[cfg(test)]
139mod test {
140    use super::*;
141
142    #[test]
143    fn test_rule_basic() {
144        let rule = "CombineRawAndWraps".parse::<Rule>().unwrap();
145        assert_eq!(rule, Rule::CombineRawAndWraps);
146        let rule = "CombineSingleAndMulti".parse::<Rule>().unwrap();
147        assert_eq!(rule, Rule::CombineSingleAndMulti);
148    }
149
150    #[test]
151    fn test_ruleset_basic() {
152        let ruleset = "CombineRawAndWraps".parse::<RuleSet>().unwrap();
153        assert_eq!(ruleset.contains(Rule::CombineRawAndWraps), true);
154        assert_eq!(ruleset.contains(Rule::CombineSingleAndMulti), false);
155
156        let s = "CombineRawAndWraps CombineSingleAndMulti";
157        let ruleset = s.parse::<RuleSet>().unwrap();
158        assert_eq!(ruleset.contains(Rule::CombineRawAndWraps), true);
159        assert_eq!(ruleset.contains(Rule::CombineSingleAndMulti), true);
160
161        let ruleset = "".parse::<RuleSet>().unwrap();
162        assert_eq!(ruleset.contains(Rule::CombineRawAndWraps), false);
163        assert_eq!(ruleset.contains(Rule::CombineSingleAndMulti), false);
164    }
165
166    /// This test hardcodes the ordering of the Rule enum, so it may break.
167    #[test]
168    fn test_ruleset_u32() {
169        let ruleset = "2".parse::<RuleSet>().unwrap();
170        assert_eq!(ruleset.contains(Rule::CombineRawAndWraps), false);
171        assert_eq!(ruleset.contains(Rule::CombineSingleAndMulti), true);
172    }
173
174    #[test]
175    fn test_ruleset_errors() {
176        let s = "CombineFloobAndBleeb";
177        assert!(s.parse::<RuleSet>().is_err());
178
179        let s = "CombineRawAndWraps CombineFloobAndBleeb";
180        assert!(s.parse::<RuleSet>().is_err());
181
182        let s = " CombineRawAndWraps";
183        assert!(s.parse::<RuleSet>().is_err());
184
185        let s = "-0";
186        assert!(s.parse::<RuleSet>().is_err());
187    }
188}