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 #![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}