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#[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(®ex_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}