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    use super::*;
195
196    #[test]
197    fn from_true() {
198        assert_eq!(
199            RulePropertyValue::from(true),
200            RulePropertyValue::Boolean(true)
201        );
202    }
203
204    #[test]
205    fn from_false() {
206        assert_eq!(
207            RulePropertyValue::from(false),
208            RulePropertyValue::Boolean(false)
209        );
210    }
211
212    #[test]
213    fn from_string() {
214        assert_eq!(
215            RulePropertyValue::from(String::from("hello")),
216            RulePropertyValue::String(String::from("hello"))
217        );
218    }
219
220    #[test]
221    fn from_string_ref() {
222        assert_eq!(
223            RulePropertyValue::from(&String::from("hello")),
224            RulePropertyValue::String(String::from("hello"))
225        );
226    }
227
228    #[test]
229    fn from_str() {
230        assert_eq!(
231            RulePropertyValue::from("hello"),
232            RulePropertyValue::String(String::from("hello"))
233        );
234    }
235
236    #[test]
237    fn from_usize() {
238        assert_eq!(RulePropertyValue::from(6), RulePropertyValue::Usize(6));
239    }
240
241    #[test]
242    fn from_float() {
243        assert_eq!(RulePropertyValue::from(1.0), RulePropertyValue::Float(1.0));
244    }
245
246    #[test]
247    fn from_boolean_option_some() {
248        let bool = Some(true);
249        assert_eq!(
250            RulePropertyValue::from(bool),
251            RulePropertyValue::Boolean(true)
252        );
253    }
254
255    #[test]
256    fn from_boolean_option_none() {
257        let bool: Option<bool> = None;
258        assert_eq!(RulePropertyValue::from(bool), RulePropertyValue::None);
259    }
260
261    mod parse {
262        use super::*;
263
264        fn parse_rule_property(json: &str, expect_property: RulePropertyValue) {
265            let parsed: RulePropertyValue = serde_json::from_str(json).unwrap();
266            assert_eq!(parsed, expect_property);
267        }
268
269        #[test]
270        fn parse_boolean_true() {
271            parse_rule_property("true", RulePropertyValue::Boolean(true));
272        }
273
274        #[test]
275        fn parse_boolean_false() {
276            parse_rule_property("false", RulePropertyValue::Boolean(false));
277        }
278
279        #[test]
280        fn parse_string() {
281            parse_rule_property(
282                r#""hello world""#,
283                RulePropertyValue::String("hello world".to_owned()),
284            );
285        }
286
287        #[test]
288        fn parse_empty_string() {
289            parse_rule_property(r#""""#, RulePropertyValue::String("".to_owned()));
290        }
291
292        #[test]
293        fn parse_string_with_escapes() {
294            parse_rule_property(
295                r#""hello\nworld\twith\"quotes\"""#,
296                RulePropertyValue::String("hello\nworld\twith\"quotes\"".to_owned()),
297            );
298        }
299
300        #[test]
301        fn parse_usize_zero() {
302            parse_rule_property("0", RulePropertyValue::Usize(0));
303        }
304
305        #[test]
306        fn parse_usize_positive() {
307            parse_rule_property("42", RulePropertyValue::Usize(42));
308        }
309
310        #[test]
311        fn parse_float_zero() {
312            parse_rule_property("0.0", RulePropertyValue::Float(0.0));
313        }
314
315        #[test]
316        fn parse_float_positive() {
317            parse_rule_property("3.14159", RulePropertyValue::Float(3.14159));
318        }
319
320        #[test]
321        fn parse_float_negative() {
322            parse_rule_property("-2.718", RulePropertyValue::Float(-2.718));
323        }
324
325        #[test]
326        fn parse_float_scientific_notation() {
327            parse_rule_property("1.23e-4", RulePropertyValue::Float(1.23e-4));
328        }
329
330        #[test]
331        fn parse_string_list_empty() {
332            parse_rule_property("[]", RulePropertyValue::StringList(vec![]));
333        }
334
335        #[test]
336        fn parse_string_list_single() {
337            parse_rule_property(
338                r#"["hello"]"#,
339                RulePropertyValue::StringList(vec!["hello".to_owned()]),
340            );
341        }
342
343        #[test]
344        fn parse_string_list_multiple() {
345            parse_rule_property(
346                r#"["hello", "world", "test"]"#,
347                RulePropertyValue::StringList(vec![
348                    "hello".to_owned(),
349                    "world".to_owned(),
350                    "test".to_owned(),
351                ]),
352            );
353        }
354
355        #[test]
356        fn parse_string_list_with_empty_strings() {
357            parse_rule_property(
358                r#"["", "hello", ""]"#,
359                RulePropertyValue::StringList(vec![
360                    "".to_owned(),
361                    "hello".to_owned(),
362                    "".to_owned(),
363                ]),
364            );
365        }
366
367        #[test]
368        fn parse_require_mode_path_string() {
369            parse_rule_property(r#""path""#, RulePropertyValue::String("path".to_owned()));
370        }
371
372        #[test]
373        fn parse_require_mode_luau_string() {
374            parse_rule_property(r#""luau""#, RulePropertyValue::String("luau".to_owned()));
375        }
376
377        #[test]
378        fn parse_require_mode_roblox_string() {
379            parse_rule_property(
380                r#""roblox""#,
381                RulePropertyValue::String("roblox".to_owned()),
382            );
383        }
384
385        #[test]
386        fn parse_require_mode_path_object() {
387            parse_rule_property(
388                r#"{"name": "path"}"#,
389                RulePropertyValue::RequireMode(RequireMode::Path(PathRequireMode::default())),
390            );
391        }
392
393        #[test]
394        fn parse_require_mode_path_object_with_options() {
395            parse_rule_property(
396                r#"{"name": "path", "module_folder_name": "index"}"#,
397                RulePropertyValue::RequireMode(RequireMode::Path(PathRequireMode::new("index"))),
398            );
399        }
400
401        #[test]
402        fn parse_require_mode_roblox_object() {
403            parse_rule_property(
404                r#"{"name": "roblox"}"#,
405                RulePropertyValue::RequireMode(RequireMode::Roblox(RobloxRequireMode::default())),
406            );
407        }
408
409        #[test]
410        fn parse_require_mode_roblox_object_with_options() {
411            parse_rule_property(
412                r#"{"name": "roblox", "rojo_sourcemap": "./sourcemap.json"}"#,
413                RulePropertyValue::RequireMode(RequireMode::Roblox(
414                    serde_json::from_str::<RobloxRequireMode>(
415                        r#"{"rojo_sourcemap": "./sourcemap.json"}"#,
416                    )
417                    .unwrap(),
418                )),
419            );
420        }
421
422        #[test]
423        fn parse_require_mode_luau_object() {
424            parse_rule_property(
425                r#"{ "name": "luau" }"#,
426                RulePropertyValue::RequireMode(RequireMode::Luau(LuauRequireMode::default())),
427            );
428        }
429
430        #[test]
431        fn parse_null_as_none() {
432            parse_rule_property("null", RulePropertyValue::None);
433        }
434    }
435}