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 has_default_value = false;
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                    has_default_value = true;
185                    if let Some(value) = value.into_expression() {
186                        self.value = value
187                    } else {
188                        return Err(RuleConfigurationError::UnexpectedValueType(key));
189                    }
190                }
191                "env" | "env_json" => {
192                    let variable_name = value.expect_string(&key)?;
193                    if let Some(os_value) = env::var_os(&variable_name) {
194                        if let Some(value) = os_value.to_str() {
195                            self.value = if key.as_str() == "env_json" {
196                                let json_value = json5::from_str::<serde_json::Value>(value).map_err(|err| {
197                                    RuleConfigurationError::UnexpectedValue {
198                                        property: key.clone(),
199                                        message: format!(
200                                            "invalid json data assigned to the `{}` environment variable: {}",
201                                            &variable_name,
202                                            err
203                                        ),
204                                    }
205                                })?;
206
207                                to_expression(&json_value).map_err(|err| {
208                                    RuleConfigurationError::UnexpectedValue {
209                                        property: key,
210                                        message: format!(
211                                            "unable to convert json data assigned to the `{}` environment variable to a lua expression: {}",
212                                            &variable_name,
213                                            err
214                                        ),
215                                    }
216                                })?
217                            } else {
218                                StringExpression::from_value(value).into()
219                            };
220                        } else {
221                            return Err(RuleConfigurationError::UnexpectedValue {
222                                property: key,
223                                message: format!(
224                                    "invalid string assigned to the `{}` environment variable",
225                                    &variable_name,
226                                ),
227                            });
228                        }
229                    } else {
230                        default_value_expected = Some(variable_name);
231                    };
232                }
233                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
234            }
235        }
236
237        if !has_default_value {
238            if let Some(variable_name) = default_value_expected {
239                log::warn!(
240                    "environment variable `{}` is not defined. The rule `{}` will use `nil`",
241                    &variable_name,
242                    INJECT_GLOBAL_VALUE_RULE_NAME,
243                );
244            }
245        }
246
247        Ok(())
248    }
249
250    fn get_name(&self) -> &'static str {
251        INJECT_GLOBAL_VALUE_RULE_NAME
252    }
253
254    fn serialize_to_properties(&self) -> RuleProperties {
255        let mut rules = self.original_properties.clone();
256
257        rules.insert(
258            "identifier".to_owned(),
259            RulePropertyValue::String(self.identifier.clone()),
260        );
261
262        rules
263    }
264}
265
266#[cfg(test)]
267mod test {
268    use super::*;
269    use crate::rules::Rule;
270
271    use insta::assert_json_snapshot;
272
273    #[test]
274    fn configure_without_identifier_property_should_error() {
275        let result = json5::from_str::<Box<dyn Rule>>(
276            r#"{
277            rule: 'inject_global_value',
278        }"#,
279        );
280
281        insta::assert_snapshot!(result.unwrap_err().to_string(), @"missing required field 'identifier'");
282    }
283
284    #[test]
285    fn configure_with_value_and_env_properties_should_error() {
286        let result = json5::from_str::<Box<dyn Rule>>(
287            r#"{
288            rule: 'inject_global_value',
289            identifier: 'DEV',
290            value: false,
291            env: "VAR",
292        }"#,
293        );
294
295        insta::assert_snapshot!(result.unwrap_err().to_string(), @"the fields `value` and `env` cannot be defined together");
296    }
297
298    #[test]
299    fn configure_with_value_and_default_value_properties_should_error() {
300        let result = json5::from_str::<Box<dyn Rule>>(
301            r#"{
302            rule: 'inject_global_value',
303            identifier: 'DEV',
304            value: false,
305            default_value: true,
306        }"#,
307        );
308
309        insta::assert_snapshot!(result.unwrap_err().to_string(), @"the fields `value` and `default_value` cannot be defined together");
310    }
311
312    #[test]
313    fn deserialize_from_string_notation_should_error() {
314        let result = json5::from_str::<Box<dyn Rule>>("'inject_global_value'");
315
316        insta::assert_snapshot!(result.unwrap_err().to_string(), @"missing required field 'identifier'");
317    }
318
319    #[test]
320    fn serialize_inject_nil_as_foo() {
321        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::nil("foo"));
322
323        assert_json_snapshot!("inject_nil_value_as_foo", rule);
324    }
325
326    #[test]
327    fn serialize_inject_true_as_foo() {
328        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::boolean("foo", true));
329
330        assert_json_snapshot!("inject_true_value_as_foo", rule);
331    }
332
333    #[test]
334    fn serialize_inject_false_as_foo() {
335        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::boolean("foo", false));
336
337        assert_json_snapshot!("inject_false_value_as_foo", rule);
338    }
339
340    #[test]
341    fn serialize_inject_string_as_var() {
342        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::string("VAR", "hello"));
343
344        assert_json_snapshot!("inject_hello_value_as_var", rule);
345    }
346
347    #[test]
348    fn serialize_inject_integer_as_var() {
349        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", 1.0));
350
351        assert_json_snapshot!("inject_integer_value_as_var", rule);
352    }
353
354    #[test]
355    fn serialize_inject_negative_integer_as_var() {
356        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", -100.0));
357
358        assert_json_snapshot!("inject_negative_integer_value_as_var", rule);
359    }
360
361    #[test]
362    fn serialize_inject_float_as_var() {
363        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", 123.45));
364
365        assert_json_snapshot!("inject_float_value_as_var", rule);
366    }
367
368    #[test]
369    fn serialization_round_trip_with_mixed_array() {
370        let rule: Box<dyn Rule> = json5::from_str(
371            r#"{
372            rule: 'inject_global_value',
373            identifier: 'foo',
374            value: ["hello", true, 1, 0.5, -1.35],
375        }"#,
376        )
377        .unwrap();
378
379        assert_json_snapshot!(rule, @r###"
380        {
381          "rule": "inject_global_value",
382          "identifier": "foo",
383          "value": [
384            "hello",
385            true,
386            1,
387            0.5,
388            -1.35
389          ]
390        }
391        "###);
392    }
393
394    #[test]
395    fn serialization_round_trip_with_object_value() {
396        let rule: Box<dyn Rule> = json5::from_str(
397            r#"{
398            rule: 'inject_global_value',
399            identifier: 'foo',
400            value: {
401                f0: 'world',
402                f1: true,
403                f2: 1,
404                f3: 0.5,
405                f4: -1.35,
406                f5: [1, 2, 3],
407            },
408        }"#,
409        )
410        .unwrap();
411
412        assert_json_snapshot!(rule, @r###"
413        {
414          "rule": "inject_global_value",
415          "identifier": "foo",
416          "value": {
417            "f0": "world",
418            "f1": true,
419            "f2": 1,
420            "f3": 0.5,
421            "f4": -1.35,
422            "f5": [
423              1,
424              2,
425              3
426            ]
427          }
428        }
429        "###);
430    }
431}