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