Skip to main content

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, RuleMetadata, 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    metadata: RuleMetadata,
90    identifier: String,
91    value: Expression,
92    original_properties: RuleProperties,
93}
94
95fn properties_with_value(value: impl Into<RulePropertyValue>) -> RuleProperties {
96    let mut properties = RuleProperties::new();
97    properties.insert("value".to_owned(), value.into());
98    properties
99}
100
101impl InjectGlobalValue {
102    pub fn nil(identifier: impl Into<String>) -> Self {
103        Self {
104            metadata: RuleMetadata::default(),
105            identifier: identifier.into(),
106            value: Expression::nil(),
107            original_properties: properties_with_value(RulePropertyValue::None),
108        }
109    }
110
111    pub fn boolean(identifier: impl Into<String>, value: bool) -> Self {
112        Self {
113            metadata: RuleMetadata::default(),
114            identifier: identifier.into(),
115            value: Expression::from(value),
116            original_properties: properties_with_value(value),
117        }
118    }
119
120    pub fn string(identifier: impl Into<String>, value: impl Into<String>) -> Self {
121        let value = value.into();
122        let original_properties = properties_with_value(&value);
123        Self {
124            metadata: RuleMetadata::default(),
125            identifier: identifier.into(),
126            value: StringExpression::from_value(value).into(),
127            original_properties,
128        }
129    }
130
131    pub fn number(identifier: impl Into<String>, value: f64) -> Self {
132        Self {
133            metadata: RuleMetadata::default(),
134            identifier: identifier.into(),
135            value: Expression::from(value),
136            original_properties: if let Some(integer) = value
137                .to_usize()
138                .filter(|integer| integer.to_f64() == Some(value))
139            {
140                properties_with_value(integer)
141            } else {
142                properties_with_value(value)
143            },
144        }
145    }
146}
147
148impl Default for InjectGlobalValue {
149    fn default() -> Self {
150        Self {
151            metadata: RuleMetadata::default(),
152            identifier: "".to_owned(),
153            value: Expression::nil(),
154            original_properties: RuleProperties::new(),
155        }
156    }
157}
158
159impl FlawlessRule for InjectGlobalValue {
160    fn flawless_process(&self, block: &mut Block, _: &Context) {
161        let mut processor = ValueInjection::new(&self.identifier, self.value.clone());
162        ScopeVisitor::visit_block(block, &mut processor);
163    }
164}
165
166impl RuleConfiguration for InjectGlobalValue {
167    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
168        verify_required_properties(&properties, &["identifier"])?;
169        verify_property_collisions(&properties, &["value", "env", "env_json"])?;
170        verify_property_collisions(&properties, &["value", "default_value"])?;
171
172        let mut default_value_expected = None;
173        let mut default_value_expression: Option<Expression> = None;
174
175        self.original_properties = properties.clone();
176
177        for (key, value) in properties {
178            match key.as_str() {
179                "identifier" => {
180                    self.identifier = value.expect_string(&key)?;
181                }
182                "value" => {
183                    if let Some(value) = value.into_expression() {
184                        self.value = value
185                    } else {
186                        return Err(RuleConfigurationError::UnexpectedValueType(key));
187                    }
188                }
189                "default_value" => {
190                    if let Some(expr) = value.into_expression() {
191                        default_value_expression = Some(expr);
192                    } else {
193                        return Err(RuleConfigurationError::UnexpectedValueType(key));
194                    }
195                }
196                "env" | "env_json" => {
197                    let variable_name = value.expect_string(&key)?;
198                    if let Some(os_value) = env::var_os(&variable_name) {
199                        if let Some(value) = os_value.to_str() {
200                            self.value = if key.as_str() == "env_json" {
201                                let json_value = json5::from_str::<serde_json::Value>(value).map_err(|err| {
202                                    RuleConfigurationError::UnexpectedValue {
203                                        property: key.clone(),
204                                        message: format!(
205                                            "invalid json data assigned to the `{}` environment variable: {}",
206                                            &variable_name,
207                                            err
208                                        ),
209                                    }
210                                })?;
211
212                                to_expression(&json_value).map_err(|err| {
213                                    RuleConfigurationError::UnexpectedValue {
214                                        property: key,
215                                        message: format!(
216                                            "unable to convert json data assigned to the `{}` environment variable to a lua expression: {}",
217                                            &variable_name,
218                                            err
219                                        ),
220                                    }
221                                })?
222                            } else {
223                                StringExpression::from_value(value).into()
224                            };
225                        } else {
226                            return Err(RuleConfigurationError::UnexpectedValue {
227                                property: key,
228                                message: format!(
229                                    "invalid string assigned to the `{}` environment variable",
230                                    &variable_name,
231                                ),
232                            });
233                        }
234                    } else {
235                        default_value_expected = Some(variable_name);
236                    };
237                }
238                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
239            }
240        }
241
242        if let Some(variable_name) = default_value_expected {
243            if let Some(expr) = default_value_expression {
244                self.value = expr;
245            } else {
246                log::warn!(
247                    "environment variable `{}` is not defined. The rule `{}` will use `nil`",
248                    &variable_name,
249                    INJECT_GLOBAL_VALUE_RULE_NAME,
250                );
251            }
252        }
253
254        Ok(())
255    }
256
257    fn get_name(&self) -> &'static str {
258        INJECT_GLOBAL_VALUE_RULE_NAME
259    }
260
261    fn serialize_to_properties(&self) -> RuleProperties {
262        let mut rules = self.original_properties.clone();
263
264        rules.insert(
265            "identifier".to_owned(),
266            RulePropertyValue::String(self.identifier.clone()),
267        );
268
269        rules
270    }
271
272    fn set_metadata(&mut self, metadata: RuleMetadata) {
273        self.metadata = metadata;
274    }
275
276    fn metadata(&self) -> &RuleMetadata {
277        &self.metadata
278    }
279}
280
281#[cfg(test)]
282mod test {
283    use super::*;
284    use crate::rules::Rule;
285
286    use insta::assert_json_snapshot;
287
288    #[test]
289    fn configure_without_identifier_property_should_error() {
290        let result = json5::from_str::<Box<dyn Rule>>(
291            r#"{
292            rule: 'inject_global_value',
293        }"#,
294        );
295
296        insta::assert_snapshot!(result.unwrap_err().to_string(), @"missing required field 'identifier' at line 1 column 1");
297    }
298
299    #[test]
300    fn configure_with_value_and_env_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            env: "VAR",
307        }"#,
308        );
309
310        insta::assert_snapshot!(result.unwrap_err().to_string(), @"the fields `value` and `env` cannot be defined together at line 1 column 1");
311    }
312
313    #[test]
314    fn configure_with_value_and_default_value_properties_should_error() {
315        let result = json5::from_str::<Box<dyn Rule>>(
316            r#"{
317            rule: 'inject_global_value',
318            identifier: 'DEV',
319            value: false,
320            default_value: true,
321        }"#,
322        );
323
324        insta::assert_snapshot!(result.unwrap_err().to_string(), @"the fields `value` and `default_value` cannot be defined together at line 1 column 1");
325    }
326
327    #[test]
328    fn deserialize_from_string_notation_should_error() {
329        let result = json5::from_str::<Box<dyn Rule>>("'inject_global_value'");
330
331        insta::assert_snapshot!(result.unwrap_err().to_string(), @"missing required field 'identifier' at line 1 column 1");
332    }
333
334    #[test]
335    fn serialize_inject_nil_as_foo() {
336        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::nil("foo"));
337
338        assert_json_snapshot!(rule, @r###"
339        {
340          "rule": "inject_global_value",
341          "identifier": "foo",
342          "value": null
343        }
344        "###);
345    }
346
347    #[test]
348    fn serialize_inject_true_as_foo() {
349        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::boolean("foo", true));
350
351        assert_json_snapshot!(rule, @r###"
352        {
353          "rule": "inject_global_value",
354          "identifier": "foo",
355          "value": true
356        }
357        "###);
358    }
359
360    #[test]
361    fn serialize_inject_false_as_foo() {
362        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::boolean("foo", false));
363
364        assert_json_snapshot!(rule, @r###"
365        {
366          "rule": "inject_global_value",
367          "identifier": "foo",
368          "value": false
369        }
370        "###);
371    }
372
373    #[test]
374    fn serialize_inject_string_as_var() {
375        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::string("VAR", "hello"));
376
377        assert_json_snapshot!(rule, @r###"
378        {
379          "rule": "inject_global_value",
380          "identifier": "VAR",
381          "value": "hello"
382        }
383        "###);
384    }
385
386    #[test]
387    fn serialize_inject_integer_as_var() {
388        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", 1.0));
389
390        assert_json_snapshot!(rule, @r###"
391        {
392          "rule": "inject_global_value",
393          "identifier": "VAR",
394          "value": 1
395        }
396        "###);
397    }
398
399    #[test]
400    fn serialize_inject_negative_integer_as_var() {
401        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", -100.0));
402
403        assert_json_snapshot!(rule, @r###"
404        {
405          "rule": "inject_global_value",
406          "identifier": "VAR",
407          "value": -100.0
408        }
409        "###);
410    }
411
412    #[test]
413    fn serialize_inject_float_as_var() {
414        let rule: Box<dyn Rule> = Box::new(InjectGlobalValue::number("VAR", 123.45));
415
416        assert_json_snapshot!(rule, @r###"
417        {
418          "rule": "inject_global_value",
419          "identifier": "VAR",
420          "value": 123.45
421        }
422        "###);
423    }
424
425    #[test]
426    fn serialization_round_trip_with_mixed_array() {
427        let rule: Box<dyn Rule> = json5::from_str(
428            r#"{
429            rule: 'inject_global_value',
430            identifier: 'foo',
431            value: ["hello", true, 1, 0.5, -1.35],
432        }"#,
433        )
434        .unwrap();
435
436        assert_json_snapshot!(rule, @r###"
437        {
438          "rule": "inject_global_value",
439          "identifier": "foo",
440          "value": [
441            "hello",
442            true,
443            1,
444            0.5,
445            -1.35
446          ]
447        }
448        "###);
449    }
450
451    #[test]
452    fn serialization_round_trip_with_object_value() {
453        let rule: Box<dyn Rule> = json5::from_str(
454            r#"{
455            rule: 'inject_global_value',
456            identifier: 'foo',
457            value: {
458                f0: 'world',
459                f1: true,
460                f2: 1,
461                f3: 0.5,
462                f4: -1.35,
463                f5: [1, 2, 3],
464            },
465        }"#,
466        )
467        .unwrap();
468
469        assert_json_snapshot!(rule, @r###"
470        {
471          "rule": "inject_global_value",
472          "identifier": "foo",
473          "value": {
474            "f0": "world",
475            "f1": true,
476            "f2": 1,
477            "f3": 0.5,
478            "f4": -1.35,
479            "f5": [
480              1,
481              2,
482              3
483            ]
484          }
485        }
486        "###);
487    }
488}