darklua_core/rules/
rule_property.rs

1use std::collections::HashMap;
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    nodes::{DecimalNumber, Expression, StringExpression, TableEntry, TableExpression},
8    process::to_expression,
9};
10
11use super::{
12    require::{LuauRequireMode, PathRequireMode},
13    RequireMode, RobloxRequireMode, RuleConfigurationError,
14};
15
16pub type RuleProperties = HashMap<String, RulePropertyValue>;
17
18/// In order to be able to weakly-type the properties of any rule, this enum makes it possible to
19/// easily use serde to gather the value associated with a property.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(untagged, rename_all = "snake_case")]
22pub enum RulePropertyValue {
23    Boolean(bool),
24    String(String),
25    Usize(usize),
26    Float(f64),
27    StringList(Vec<String>),
28    RequireMode(RequireMode),
29    None,
30    #[doc(hidden)]
31    Map(serde_json::Map<String, serde_json::Value>),
32    #[doc(hidden)]
33    Array(Vec<serde_json::Value>),
34}
35
36impl RulePropertyValue {
37    pub(crate) fn expect_bool(self, key: &str) -> Result<bool, RuleConfigurationError> {
38        if let Self::Boolean(value) = self {
39            Ok(value)
40        } else {
41            Err(RuleConfigurationError::BooleanExpected(key.to_owned()))
42        }
43    }
44
45    pub(crate) fn expect_string(self, key: &str) -> Result<String, RuleConfigurationError> {
46        if let Self::String(value) = self {
47            Ok(value)
48        } else {
49            Err(RuleConfigurationError::StringExpected(key.to_owned()))
50        }
51    }
52
53    pub(crate) fn expect_string_list(
54        self,
55        key: &str,
56    ) -> Result<Vec<String>, RuleConfigurationError> {
57        if let Self::StringList(value) = self {
58            Ok(value)
59        } else {
60            Err(RuleConfigurationError::StringListExpected(key.to_owned()))
61        }
62    }
63
64    pub(crate) fn expect_regex_list(self, key: &str) -> Result<Vec<Regex>, RuleConfigurationError> {
65        if let Self::StringList(value) = self {
66            value
67                .into_iter()
68                .map(|regex_str| {
69                    Regex::new(&regex_str).map_err(|err| RuleConfigurationError::UnexpectedValue {
70                        property: key.to_owned(),
71                        message: format!("invalid regex provided `{}`\n  {}", regex_str, err),
72                    })
73                })
74                .collect()
75        } else {
76            Err(RuleConfigurationError::StringListExpected(key.to_owned()))
77        }
78    }
79
80    pub(crate) fn expect_require_mode(
81        self,
82        key: &str,
83    ) -> Result<RequireMode, RuleConfigurationError> {
84        match self {
85            Self::RequireMode(require_mode) => Ok(require_mode),
86            Self::String(value) => {
87                value
88                    .parse()
89                    .map_err(|err: String| RuleConfigurationError::UnexpectedValue {
90                        property: key.to_owned(),
91                        message: err,
92                    })
93            }
94            _ => Err(RuleConfigurationError::RequireModeExpected(key.to_owned())),
95        }
96    }
97
98    pub(crate) fn into_expression(self) -> Option<Expression> {
99        match self {
100            Self::None => Some(Expression::nil()),
101            Self::String(value) => Some(StringExpression::from_value(value).into()),
102            Self::Boolean(value) => Some(Expression::from(value)),
103            Self::Usize(value) => Some(DecimalNumber::new(value as f64).into()),
104            Self::Float(value) => Some(Expression::from(value)),
105            Self::StringList(value) => Some(
106                TableExpression::new(
107                    value
108                        .into_iter()
109                        .map(|element| {
110                            TableEntry::from_value(StringExpression::from_value(element))
111                        })
112                        .collect(),
113                )
114                .into(),
115            ),
116            Self::RequireMode(require_mode) => to_expression(&require_mode).ok(),
117            Self::Map(value) => to_expression(&value).ok(),
118            Self::Array(value) => to_expression(&value).ok(),
119        }
120    }
121}
122
123impl From<bool> for RulePropertyValue {
124    fn from(value: bool) -> Self {
125        Self::Boolean(value)
126    }
127}
128
129impl From<&str> for RulePropertyValue {
130    fn from(value: &str) -> Self {
131        Self::String(value.to_owned())
132    }
133}
134
135impl From<&String> for RulePropertyValue {
136    fn from(value: &String) -> Self {
137        Self::String(value.to_owned())
138    }
139}
140
141impl From<String> for RulePropertyValue {
142    fn from(value: String) -> Self {
143        Self::String(value)
144    }
145}
146
147impl From<usize> for RulePropertyValue {
148    fn from(value: usize) -> Self {
149        Self::Usize(value)
150    }
151}
152
153impl From<f64> for RulePropertyValue {
154    fn from(value: f64) -> Self {
155        Self::Float(value)
156    }
157}
158
159impl From<&RequireMode> for RulePropertyValue {
160    fn from(value: &RequireMode) -> Self {
161        match value {
162            RequireMode::Path(mode) => {
163                if mode == &PathRequireMode::default() {
164                    return Self::from("path");
165                }
166            }
167            RequireMode::Luau(mode) => {
168                if mode == &LuauRequireMode::default() {
169                    return Self::from("luau");
170                }
171            }
172            RequireMode::Roblox(mode) => {
173                if mode == &RobloxRequireMode::default() {
174                    return Self::from("roblox");
175                }
176            }
177        }
178
179        Self::RequireMode(value.clone())
180    }
181}
182
183impl<T: Into<RulePropertyValue>> From<Option<T>> for RulePropertyValue {
184    fn from(value: Option<T>) -> Self {
185        match value {
186            Some(value) => value.into(),
187            None => Self::None,
188        }
189    }
190}
191
192#[cfg(test)]
193mod test {
194    #![allow(clippy::approx_constant)]
195    use super::*;
196
197    #[test]
198    fn from_true() {
199        assert_eq!(
200            RulePropertyValue::from(true),
201            RulePropertyValue::Boolean(true)
202        );
203    }
204
205    #[test]
206    fn from_false() {
207        assert_eq!(
208            RulePropertyValue::from(false),
209            RulePropertyValue::Boolean(false)
210        );
211    }
212
213    #[test]
214    fn from_string() {
215        assert_eq!(
216            RulePropertyValue::from(String::from("hello")),
217            RulePropertyValue::String(String::from("hello"))
218        );
219    }
220
221    #[test]
222    fn from_string_ref() {
223        assert_eq!(
224            RulePropertyValue::from(&String::from("hello")),
225            RulePropertyValue::String(String::from("hello"))
226        );
227    }
228
229    #[test]
230    fn from_str() {
231        assert_eq!(
232            RulePropertyValue::from("hello"),
233            RulePropertyValue::String(String::from("hello"))
234        );
235    }
236
237    #[test]
238    fn from_usize() {
239        assert_eq!(RulePropertyValue::from(6), RulePropertyValue::Usize(6));
240    }
241
242    #[test]
243    fn from_float() {
244        assert_eq!(RulePropertyValue::from(1.0), RulePropertyValue::Float(1.0));
245    }
246
247    #[test]
248    fn from_boolean_option_some() {
249        let bool = Some(true);
250        assert_eq!(
251            RulePropertyValue::from(bool),
252            RulePropertyValue::Boolean(true)
253        );
254    }
255
256    #[test]
257    fn from_boolean_option_none() {
258        let bool: Option<bool> = None;
259        assert_eq!(RulePropertyValue::from(bool), RulePropertyValue::None);
260    }
261
262    mod parse {
263        use super::*;
264
265        fn parse_rule_property(json: &str, expect_property: RulePropertyValue) {
266            let parsed: RulePropertyValue = serde_json::from_str(json).unwrap();
267            assert_eq!(parsed, expect_property);
268        }
269
270        #[test]
271        fn parse_boolean_true() {
272            parse_rule_property("true", RulePropertyValue::Boolean(true));
273        }
274
275        #[test]
276        fn parse_boolean_false() {
277            parse_rule_property("false", RulePropertyValue::Boolean(false));
278        }
279
280        #[test]
281        fn parse_string() {
282            parse_rule_property(
283                r#""hello world""#,
284                RulePropertyValue::String("hello world".to_owned()),
285            );
286        }
287
288        #[test]
289        fn parse_empty_string() {
290            parse_rule_property(r#""""#, RulePropertyValue::String("".to_owned()));
291        }
292
293        #[test]
294        fn parse_string_with_escapes() {
295            parse_rule_property(
296                r#""hello\nworld\twith\"quotes\"""#,
297                RulePropertyValue::String("hello\nworld\twith\"quotes\"".to_owned()),
298            );
299        }
300
301        #[test]
302        fn parse_usize_zero() {
303            parse_rule_property("0", RulePropertyValue::Usize(0));
304        }
305
306        #[test]
307        fn parse_usize_positive() {
308            parse_rule_property("42", RulePropertyValue::Usize(42));
309        }
310
311        #[test]
312        fn parse_float_zero() {
313            parse_rule_property("0.0", RulePropertyValue::Float(0.0));
314        }
315
316        #[test]
317        fn parse_float_positive() {
318            parse_rule_property("3.14159", RulePropertyValue::Float(3.14159));
319        }
320
321        #[test]
322        fn parse_float_negative() {
323            parse_rule_property("-2.718", RulePropertyValue::Float(-2.718));
324        }
325
326        #[test]
327        fn parse_float_scientific_notation() {
328            parse_rule_property("1.23e-4", RulePropertyValue::Float(1.23e-4));
329        }
330
331        #[test]
332        fn parse_string_list_empty() {
333            parse_rule_property("[]", RulePropertyValue::StringList(vec![]));
334        }
335
336        #[test]
337        fn parse_string_list_single() {
338            parse_rule_property(
339                r#"["hello"]"#,
340                RulePropertyValue::StringList(vec!["hello".to_owned()]),
341            );
342        }
343
344        #[test]
345        fn parse_string_list_multiple() {
346            parse_rule_property(
347                r#"["hello", "world", "test"]"#,
348                RulePropertyValue::StringList(vec![
349                    "hello".to_owned(),
350                    "world".to_owned(),
351                    "test".to_owned(),
352                ]),
353            );
354        }
355
356        #[test]
357        fn parse_string_list_with_empty_strings() {
358            parse_rule_property(
359                r#"["", "hello", ""]"#,
360                RulePropertyValue::StringList(vec![
361                    "".to_owned(),
362                    "hello".to_owned(),
363                    "".to_owned(),
364                ]),
365            );
366        }
367
368        #[test]
369        fn parse_require_mode_path_string() {
370            parse_rule_property(r#""path""#, RulePropertyValue::String("path".to_owned()));
371        }
372
373        #[test]
374        fn parse_require_mode_luau_string() {
375            parse_rule_property(r#""luau""#, RulePropertyValue::String("luau".to_owned()));
376        }
377
378        #[test]
379        fn parse_require_mode_roblox_string() {
380            parse_rule_property(
381                r#""roblox""#,
382                RulePropertyValue::String("roblox".to_owned()),
383            );
384        }
385
386        #[test]
387        fn parse_require_mode_path_object() {
388            parse_rule_property(
389                r#"{"name": "path"}"#,
390                RulePropertyValue::RequireMode(RequireMode::Path(PathRequireMode::default())),
391            );
392        }
393
394        #[test]
395        fn parse_require_mode_path_object_with_options() {
396            parse_rule_property(
397                r#"{"name": "path", "module_folder_name": "index"}"#,
398                RulePropertyValue::RequireMode(RequireMode::Path(PathRequireMode::new("index"))),
399            );
400        }
401
402        #[test]
403        fn parse_require_mode_roblox_object() {
404            parse_rule_property(
405                r#"{"name": "roblox"}"#,
406                RulePropertyValue::RequireMode(RequireMode::Roblox(RobloxRequireMode::default())),
407            );
408        }
409
410        #[test]
411        fn parse_require_mode_roblox_object_with_options() {
412            parse_rule_property(
413                r#"{"name": "roblox", "rojo_sourcemap": "./sourcemap.json"}"#,
414                RulePropertyValue::RequireMode(RequireMode::Roblox(
415                    serde_json::from_str::<RobloxRequireMode>(
416                        r#"{"rojo_sourcemap": "./sourcemap.json"}"#,
417                    )
418                    .unwrap(),
419                )),
420            );
421        }
422
423        #[test]
424        fn parse_require_mode_luau_object() {
425            parse_rule_property(
426                r#"{ "name": "luau" }"#,
427                RulePropertyValue::RequireMode(RequireMode::Luau(LuauRequireMode::default())),
428            );
429        }
430
431        #[test]
432        fn parse_null_as_none() {
433            parse_rule_property("null", RulePropertyValue::None);
434        }
435    }
436}