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