darklua_core/rules/
inject_value.rs

1use num_traits::ToPrimitive;
2
3use crate::nodes::{Block, Expression, ParentheseExpression, Prefix, StringExpression};
4use crate::process::{to_expression, IdentifierTracker, NodeProcessor, NodeVisitor, ScopeVisitor};
5use crate::rules::{
6    Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties,
7    RulePropertyValue,
8};
9
10use std::{env, ops};
11
12use super::{verify_property_collisions, verify_required_properties};
13
14#[derive(Debug, Clone)]
15struct ValueInjection {
16    identifier: String,
17    expression: Expression,
18    identifier_tracker: IdentifierTracker,
19}
20
21impl ValueInjection {
22    pub fn new<S: Into<String>, E: Into<Expression>>(identifier: S, expression: E) -> Self {
23        Self {
24            identifier: identifier.into(),
25            expression: expression.into(),
26            identifier_tracker: IdentifierTracker::default(),
27        }
28    }
29}
30
31impl ops::Deref for ValueInjection {
32    type Target = IdentifierTracker;
33
34    fn deref(&self) -> &Self::Target {
35        &self.identifier_tracker
36    }
37}
38
39impl ops::DerefMut for ValueInjection {
40    fn deref_mut(&mut self) -> &mut Self::Target {
41        &mut self.identifier_tracker
42    }
43}
44
45impl NodeProcessor for ValueInjection {
46    fn process_expression(&mut self, expression: &mut Expression) {
47        let replace = match expression {
48            Expression::Identifier(identifier) => {
49                &self.identifier == identifier.get_name()
50                    && !self.is_identifier_used(&self.identifier)
51            }
52            Expression::Field(field) => {
53                &self.identifier == field.get_field().get_name()
54                    && !self.is_identifier_used("_G")
55                    && matches!(field.get_prefix(), Prefix::Identifier(prefix) if prefix.get_name() == "_G")
56            }
57            Expression::Index(index) => {
58                !self.is_identifier_used("_G")
59                    && matches!(index.get_index(), Expression::String(string) if string.get_string_value() == Some(&self.identifier))
60                    && matches!(index.get_prefix(), Prefix::Identifier(prefix) if prefix.get_name() == "_G")
61            }
62            _ => false,
63        };
64
65        if replace {
66            let new_expression = self.expression.clone();
67            *expression = new_expression;
68        }
69    }
70
71    fn process_prefix_expression(&mut self, prefix: &mut Prefix) {
72        let replace = match prefix {
73            Prefix::Identifier(identifier) => &self.identifier == identifier.get_name(),
74            _ => false,
75        };
76
77        if replace {
78            let new_prefix = ParentheseExpression::new(self.expression.clone()).into();
79            *prefix = new_prefix;
80        }
81    }
82}
83
84pub const INJECT_GLOBAL_VALUE_RULE_NAME: &str = "inject_global_value";
85
86/// A rule to replace global variables with values.
87#[derive(Debug, PartialEq)]
88pub struct InjectGlobalValue {
89    identifier: String,
90    value: Expression,
91    original_properties: RuleProperties,
92}
93
94fn properties_with_value(value: impl Into<RulePropertyValue>) -> RuleProperties {
95    let mut properties = RuleProperties::new();
96    properties.insert("value".to_owned(), value.into());
97    properties
98}
99
100impl InjectGlobalValue {
101    pub fn nil(identifier: impl Into<String>) -> Self {
102        Self {
103            identifier: identifier.into(),
104            value: Expression::nil(),
105            original_properties: properties_with_value(RulePropertyValue::None),
106        }
107    }
108
109    pub fn boolean(identifier: impl Into<String>, value: bool) -> Self {
110        Self {
111            identifier: identifier.into(),
112            value: Expression::from(value),
113            original_properties: properties_with_value(value),
114        }
115    }
116
117    pub fn string(identifier: impl Into<String>, value: impl Into<String>) -> Self {
118        let value = value.into();
119        let original_properties = properties_with_value(&value);
120        Self {
121            identifier: identifier.into(),
122            value: StringExpression::from_value(value).into(),
123            original_properties,
124        }
125    }
126
127    pub fn number(identifier: impl Into<String>, value: f64) -> Self {
128        Self {
129            identifier: identifier.into(),
130            value: Expression::from(value),
131            original_properties: if let Some(integer) = value
132                .to_usize()
133                .filter(|integer| integer.to_f64() == Some(value))
134            {
135                properties_with_value(integer)
136            } else {
137                properties_with_value(value)
138            },
139        }
140    }
141}
142
143impl Default for InjectGlobalValue {
144    fn default() -> Self {
145        Self {
146            identifier: "".to_owned(),
147            value: Expression::nil(),
148            original_properties: RuleProperties::new(),
149        }
150    }
151}
152
153impl FlawlessRule for InjectGlobalValue {
154    fn flawless_process(&self, block: &mut Block, _: &Context) {
155        let mut processor = ValueInjection::new(&self.identifier, self.value.clone());
156        ScopeVisitor::visit_block(block, &mut processor);
157    }
158}
159
160impl RuleConfiguration for InjectGlobalValue {
161    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
162        verify_required_properties(&properties, &["identifier"])?;
163        verify_property_collisions(&properties, &["value", "env", "env_json"])?;
164        verify_property_collisions(&properties, &["value", "default_value"])?;
165
166        let mut default_value_expected = None;
167        let mut default_value_expression: Option<Expression> = None;
168
169        self.original_properties = properties.clone();
170
171        for (key, value) in properties {
172            match key.as_str() {
173                "identifier" => {
174                    self.identifier = value.expect_string(&key)?;
175                }
176                "value" => {
177                    if let Some(value) = value.into_expression() {
178                        self.value = value
179                    } else {
180                        return Err(RuleConfigurationError::UnexpectedValueType(key));
181                    }
182                }
183                "default_value" => {
184                    if let Some(expr) = value.into_expression() {
185                        default_value_expression = Some(expr);
186                    } else {
187                        return Err(RuleConfigurationError::UnexpectedValueType(key));
188                    }
189                }
190                "env" | "env_json" => {
191                    let variable_name = value.expect_string(&key)?;
192                    if let Some(os_value) = env::var_os(&variable_name) {
193                        if let Some(value) = os_value.to_str() {
194                            self.value = if key.as_str() == "env_json" {
195                                let json_value = json5::from_str::<serde_json::Value>(value).map_err(|err| {
196                                    RuleConfigurationError::UnexpectedValue {
197                                        property: key.clone(),
198                                        message: format!(
199                                            "invalid json data assigned to the `{}` environment variable: {}",
200                                            &variable_name,
201                                            err
202                                        ),
203                                    }
204                                })?;
205
206                                to_expression(&json_value).map_err(|err| {
207                                    RuleConfigurationError::UnexpectedValue {
208                                        property: key,
209                                        message: format!(
210                                            "unable to convert json data assigned to the `{}` environment variable to a lua expression: {}",
211                                            &variable_name,
212                                            err
213                                        ),
214                                    }
215                                })?
216                            } else {
217                                StringExpression::from_value(value).into()
218                            };
219                        } else {
220                            return Err(RuleConfigurationError::UnexpectedValue {
221                                property: key,
222                                message: format!(
223                                    "invalid string assigned to the `{}` environment variable",
224                                    &variable_name,
225                                ),
226                            });
227                        }
228                    } else {
229                        default_value_expected = Some(variable_name);
230                    };
231                }
232                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
233            }
234        }
235
236        if let Some(variable_name) = default_value_expected {
237            if let Some(expr) = default_value_expression {
238                self.value = expr;
239            } else {
240                log::warn!(
241                    "environment variable `{}` is not defined. The rule `{}` will use `nil`",
242                    &variable_name,
243                    INJECT_GLOBAL_VALUE_RULE_NAME,
244                );
245            }
246        }
247
248        Ok(())
249    }
250
251    fn get_name(&self) -> &'static str {
252        INJECT_GLOBAL_VALUE_RULE_NAME
253    }
254
255    fn serialize_to_properties(&self) -> RuleProperties {
256        let mut rules = self.original_properties.clone();
257
258        rules.insert(
259            "identifier".to_owned(),
260            RulePropertyValue::String(self.identifier.clone()),
261        );
262
263        rules
264    }
265}
266
267#[cfg(test)]
268mod test {
269    use super::*;
270    use crate::rules::Rule;
271
272    use insta::assert_json_snapshot;
273
274    #[test]
275    fn configure_without_identifier_property_should_error() {
276        let result = json5::from_str::<Box<dyn Rule>>(
277            r#"{
278            rule: 'inject_global_value',
279        }"#,
280        );
281
282        insta::assert_snapshot!(result.unwrap_err().to_string(), @"missing required field 'identifier'");
283    }
284
285    #[test]
286    fn configure_with_value_and_env_properties_should_error() {
287        let result = json5::from_str::<Box<dyn Rule>>(
288            r#"{
289            rule: 'inject_global_value',
290            identifier: 'DEV',
291            value: false,
292            env: "VAR",
293        }"#,
294        );
295
296        insta::assert_snapshot!(result.unwrap_err().to_string(), @"the fields `value` and `env` cannot be defined together");
297    }
298
299    #[test]
300    fn configure_with_value_and_default_value_properties_should_error() {
301        let result = json5::from_str::<Box<dyn Rule>>(
302            r#"{
303            rule: 'inject_global_value',
304            identifier: 'DEV',
305            value: false,
306            default_value: true,
307        }"#,
308        );
309
310        insta::assert_snapshot!(result.unwrap_err().to_string(), @"the fields `value` and `default_value` cannot be defined together");
311    }
312
313    #[test]
314    fn deserialize_from_string_notation_should_error() {
315        let result = json5::from_str::<Box<dyn Rule>>("'inject_global_value'");
316
317        insta::assert_snapshot!(result.unwrap_err().to_string(), @"missing required field 'identifier'");
318    }
319
320    #[test]
321    fn serialize_inject_nil_as_foo() {
322        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::nil("foo"));
323
324        assert_json_snapshot!("inject_nil_value_as_foo", rule);
325    }
326
327    #[test]
328    fn serialize_inject_true_as_foo() {
329        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::boolean("foo", true));
330
331        assert_json_snapshot!("inject_true_value_as_foo", rule);
332    }
333
334    #[test]
335    fn serialize_inject_false_as_foo() {
336        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::boolean("foo", false));
337
338        assert_json_snapshot!("inject_false_value_as_foo", rule);
339    }
340
341    #[test]
342    fn serialize_inject_string_as_var() {
343        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::string("VAR", "hello"));
344
345        assert_json_snapshot!("inject_hello_value_as_var", rule);
346    }
347
348    #[test]
349    fn serialize_inject_integer_as_var() {
350        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", 1.0));
351
352        assert_json_snapshot!("inject_integer_value_as_var", rule);
353    }
354
355    #[test]
356    fn serialize_inject_negative_integer_as_var() {
357        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", -100.0));
358
359        assert_json_snapshot!("inject_negative_integer_value_as_var", rule);
360    }
361
362    #[test]
363    fn serialize_inject_float_as_var() {
364        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", 123.45));
365
366        assert_json_snapshot!("inject_float_value_as_var", rule);
367    }
368
369    #[test]
370    fn serialization_round_trip_with_mixed_array() {
371        let rule: Box<dyn Rule> = json5::from_str(
372            r#"{
373            rule: 'inject_global_value',
374            identifier: 'foo',
375            value: ["hello", true, 1, 0.5, -1.35],
376        }"#,
377        )
378        .unwrap();
379
380        assert_json_snapshot!(rule, @r###"
381        {
382          "rule": "inject_global_value",
383          "identifier": "foo",
384          "value": [
385            "hello",
386            true,
387            1,
388            0.5,
389            -1.35
390          ]
391        }
392        "###);
393    }
394
395    #[test]
396    fn serialization_round_trip_with_object_value() {
397        let rule: Box<dyn Rule> = json5::from_str(
398            r#"{
399            rule: 'inject_global_value',
400            identifier: 'foo',
401            value: {
402                f0: 'world',
403                f1: true,
404                f2: 1,
405                f3: 0.5,
406                f4: -1.35,
407                f5: [1, 2, 3],
408            },
409        }"#,
410        )
411        .unwrap();
412
413        assert_json_snapshot!(rule, @r###"
414        {
415          "rule": "inject_global_value",
416          "identifier": "foo",
417          "value": {
418            "f0": "world",
419            "f1": true,
420            "f2": 1,
421            "f3": 0.5,
422            "f4": -1.35,
423            "f5": [
424              1,
425              2,
426              3
427            ]
428          }
429        }
430        "###);
431    }
432}