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