1use crate::expression::functions::BuiltinFunctions;
5use crate::expression::parser::{BinaryOp, Expr, Reference, ReferencePart, UnaryOp};
6use crate::parser::models::Value;
7
8use std::collections::HashMap;
9use std::fmt;
10
11#[derive(Debug, Clone)]
13pub struct EvalError {
14 pub message: String,
15}
16
17impl fmt::Display for EvalError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(f, "evaluation error: {}", self.message)
20 }
21}
22
23impl std::error::Error for EvalError {}
24
25impl EvalError {
26 pub fn new(message: impl Into<String>) -> Self {
27 Self {
28 message: message.into(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct ExpressionContext {
36 pub variables: HashMap<String, Value>,
38
39 pub parameters: HashMap<String, Value>,
41
42 pub pipeline: PipelineContext,
44
45 pub stage: Option<StageContext>,
47
48 pub job: Option<JobContext>,
50
51 pub steps: HashMap<String, StepContext>,
53
54 pub dependencies: DependenciesContext,
56
57 pub env: HashMap<String, Value>,
59
60 pub resources: ResourcesContext,
62}
63
64#[derive(Debug, Clone, Default)]
65pub struct PipelineContext {
66 pub name: Option<String>,
68 pub workspace: Option<String>,
70}
71
72#[derive(Debug, Clone, Default)]
73pub struct StageContext {
74 pub name: String,
76 pub display_name: Option<String>,
78}
79
80#[derive(Debug, Clone, Default)]
81pub struct JobContext {
82 pub name: String,
84 pub display_name: Option<String>,
86 pub agent: AgentContext,
88 pub status: JobStatusContext,
90}
91
92#[derive(Debug, Clone, Default)]
93pub struct AgentContext {
94 pub name: Option<String>,
95 pub os: Option<String>,
96 pub os_architecture: Option<String>,
97 pub temp_directory: Option<String>,
98 pub tools_directory: Option<String>,
99 pub work_folder: Option<String>,
100 pub build_directory: Option<String>,
101}
102
103#[derive(Debug, Clone, Default)]
104pub struct JobStatusContext {
105 pub succeeded: bool,
106 pub failed: bool,
107 pub canceled: bool,
108}
109
110#[derive(Debug, Clone, Default)]
111pub struct StepContext {
112 pub outputs: HashMap<String, Value>,
114 pub status: StepStatusContext,
116}
117
118#[derive(Debug, Clone, Default)]
119pub struct StepStatusContext {
120 pub succeeded: bool,
121 pub failed: bool,
122 pub skipped: bool,
123}
124
125#[derive(Debug, Clone, Default)]
126pub struct DependenciesContext {
127 pub stages: HashMap<String, StageDependency>,
129 pub jobs: HashMap<String, JobDependency>,
131}
132
133#[derive(Debug, Clone, Default)]
134pub struct StageDependency {
135 pub outputs: HashMap<String, HashMap<String, Value>>,
136 pub result: String,
137}
138
139#[derive(Debug, Clone, Default)]
140pub struct JobDependency {
141 pub outputs: HashMap<String, Value>,
142 pub result: String,
143}
144
145#[derive(Debug, Clone, Default)]
146pub struct ResourcesContext {
147 pub pipelines: HashMap<String, PipelineResourceContext>,
148 pub repositories: HashMap<String, RepositoryResourceContext>,
149}
150
151#[derive(Debug, Clone, Default)]
152pub struct PipelineResourceContext {
153 pub pipeline_id: Option<String>,
154 pub run_name: Option<String>,
155 pub run_id: Option<String>,
156 pub run_uri: Option<String>,
157 pub source_branch: Option<String>,
158 pub source_commit: Option<String>,
159 pub source_provider: Option<String>,
160 pub requested_for: Option<String>,
161 pub requested_for_id: Option<String>,
162}
163
164#[derive(Debug, Clone, Default)]
165pub struct RepositoryResourceContext {
166 pub name: Option<String>,
167 pub repo_type: Option<String>,
168 pub ref_name: Option<String>,
169 pub version: Option<String>,
170}
171
172pub struct Evaluator<'a> {
174 context: &'a ExpressionContext,
175 functions: BuiltinFunctions,
176}
177
178impl<'a> Evaluator<'a> {
179 pub fn new(context: &'a ExpressionContext) -> Self {
180 Self {
181 context,
182 functions: BuiltinFunctions::new(),
183 }
184 }
185
186 pub fn eval(&self, expr: &Expr) -> Result<Value, EvalError> {
188 match expr {
189 Expr::Null => Ok(Value::Null),
190 Expr::Bool(b) => Ok(Value::Bool(*b)),
191 Expr::Number(n) => Ok(Value::Number(*n)),
192 Expr::String(s) => Ok(Value::String(s.clone())),
193
194 Expr::Reference(reference) => self.eval_reference(reference),
195
196 Expr::FunctionCall { name, args } => self.eval_function(name, args),
197
198 Expr::Index { object, index } => {
199 let obj = self.eval(object)?;
200 let idx = self.eval(index)?;
201 self.eval_index(&obj, &idx)
202 }
203
204 Expr::Member { object, property } => {
205 let obj = self.eval(object)?;
206 self.eval_member(&obj, property)
207 }
208
209 Expr::Unary { op, expr } => {
210 let val = self.eval(expr)?;
211 self.eval_unary(*op, &val)
212 }
213
214 Expr::Binary { op, left, right } => {
215 match op {
217 BinaryOp::And => {
218 let left_val = self.eval(left)?;
219 if !left_val.is_truthy() {
220 return Ok(Value::Bool(false));
221 }
222 let right_val = self.eval(right)?;
223 Ok(Value::Bool(right_val.is_truthy()))
224 }
225 BinaryOp::Or => {
226 let left_val = self.eval(left)?;
227 if left_val.is_truthy() {
228 return Ok(Value::Bool(true));
229 }
230 let right_val = self.eval(right)?;
231 Ok(Value::Bool(right_val.is_truthy()))
232 }
233 _ => {
234 let left_val = self.eval(left)?;
235 let right_val = self.eval(right)?;
236 self.eval_binary(*op, &left_val, &right_val)
237 }
238 }
239 }
240
241 Expr::Ternary {
242 condition,
243 then_expr,
244 else_expr,
245 } => {
246 let cond = self.eval(condition)?;
247 if cond.is_truthy() {
248 self.eval(then_expr)
249 } else {
250 self.eval(else_expr)
251 }
252 }
253
254 Expr::Array(items) => {
255 let values: Result<Vec<Value>, EvalError> =
256 items.iter().map(|e| self.eval(e)).collect();
257 Ok(Value::Array(values?))
258 }
259
260 Expr::Object(pairs) => {
261 let mut map = HashMap::new();
262 for (key, value_expr) in pairs {
263 map.insert(key.clone(), self.eval(value_expr)?);
264 }
265 Ok(Value::Object(map))
266 }
267 }
268 }
269
270 fn eval_reference(&self, reference: &Reference) -> Result<Value, EvalError> {
271 let mut current: Option<Value> = None;
272
273 for (i, part) in reference.parts.iter().enumerate() {
274 match part {
275 ReferencePart::Property(name) => {
276 if i == 0 {
277 current = Some(self.lookup_context(name)?);
279 } else {
280 let obj = current.ok_or_else(|| EvalError::new("invalid reference"))?;
281 current = Some(self.eval_member(&obj, name)?);
282 }
283 }
284 ReferencePart::Index(index_expr) => {
285 let obj = current.ok_or_else(|| EvalError::new("invalid index access"))?;
286 let index = self.eval(index_expr)?;
287 current = Some(self.eval_index(&obj, &index)?);
288 }
289 }
290 }
291
292 current.ok_or_else(|| EvalError::new("empty reference"))
293 }
294
295 fn lookup_context(&self, name: &str) -> Result<Value, EvalError> {
296 if let Some(value) = self.context.parameters.get(name) {
299 let is_primary_context =
306 matches!(name.to_lowercase().as_str(), "variables" | "parameters");
307 if !is_primary_context {
308 return Ok(value.clone());
309 }
310 }
311
312 match name.to_lowercase().as_str() {
313 "variables" => Ok(Value::Object(
314 self.context
315 .variables
316 .iter()
317 .map(|(k, v)| (k.clone(), v.clone()))
318 .collect(),
319 )),
320 "parameters" => Ok(Value::Object(
321 self.context
322 .parameters
323 .iter()
324 .map(|(k, v)| (k.clone(), v.clone()))
325 .collect(),
326 )),
327 "pipeline" => self.pipeline_to_value(),
328 "stage" => self.stage_to_value(),
329 "job" => self.job_to_value(),
330 "steps" => Ok(Value::Object(
331 self.context
332 .steps
333 .iter()
334 .map(|(k, v)| (k.clone(), self.step_context_to_value(v)))
335 .collect(),
336 )),
337 "dependencies" => self.dependencies_to_value(),
338 "stagedependencies" => self.stage_dependencies_to_value(),
339 "env" => Ok(Value::Object(
340 self.context
341 .env
342 .iter()
343 .map(|(k, v)| (k.clone(), v.clone()))
344 .collect(),
345 )),
346 "resources" => self.resources_to_value(),
347
348 _ => {
350 if let Some(value) = self.context.variables.get(name) {
352 return Ok(value.clone());
353 }
354 if let Some(value) = self.context.parameters.get(name) {
356 return Ok(value.clone());
357 }
358 Ok(Value::String(String::new()))
360 }
361 }
362 }
363
364 fn pipeline_to_value(&self) -> Result<Value, EvalError> {
365 let mut map = HashMap::new();
366 if let Some(name) = &self.context.pipeline.name {
367 map.insert("name".to_string(), Value::String(name.clone()));
368 }
369 if let Some(workspace) = &self.context.pipeline.workspace {
370 map.insert("workspace".to_string(), Value::String(workspace.clone()));
371 }
372 Ok(Value::Object(map))
373 }
374
375 fn stage_to_value(&self) -> Result<Value, EvalError> {
376 let Some(stage) = &self.context.stage else {
377 return Ok(Value::Null);
378 };
379
380 let mut map = HashMap::new();
381 map.insert("name".to_string(), Value::String(stage.name.clone()));
382 if let Some(display_name) = &stage.display_name {
383 map.insert(
384 "displayName".to_string(),
385 Value::String(display_name.clone()),
386 );
387 }
388 Ok(Value::Object(map))
389 }
390
391 fn job_to_value(&self) -> Result<Value, EvalError> {
392 let Some(job) = &self.context.job else {
393 return Ok(Value::Null);
394 };
395
396 let mut map = HashMap::new();
397 map.insert("name".to_string(), Value::String(job.name.clone()));
398 if let Some(display_name) = &job.display_name {
399 map.insert(
400 "displayName".to_string(),
401 Value::String(display_name.clone()),
402 );
403 }
404
405 let mut agent = HashMap::new();
407 if let Some(name) = &job.agent.name {
408 agent.insert("name".to_string(), Value::String(name.clone()));
409 }
410 if let Some(os) = &job.agent.os {
411 agent.insert("os".to_string(), Value::String(os.clone()));
412 }
413 map.insert("agent".to_string(), Value::Object(agent));
414
415 Ok(Value::Object(map))
416 }
417
418 fn step_context_to_value(&self, step: &StepContext) -> Value {
419 let mut map = HashMap::new();
420
421 let outputs: HashMap<String, Value> = step
423 .outputs
424 .iter()
425 .map(|(k, v)| (k.clone(), v.clone()))
426 .collect();
427 map.insert("outputs".to_string(), Value::Object(outputs));
428
429 Value::Object(map)
430 }
431
432 fn dependencies_to_value(&self) -> Result<Value, EvalError> {
433 let mut map = HashMap::new();
434
435 for (name, dep) in &self.context.dependencies.stages {
437 let mut stage_map = HashMap::new();
438 stage_map.insert("result".to_string(), Value::String(dep.result.clone()));
439
440 let mut outputs = HashMap::new();
441 for (job_name, job_outputs) in &dep.outputs {
442 outputs.insert(
443 job_name.clone(),
444 Value::Object(
445 job_outputs
446 .iter()
447 .map(|(k, v)| (k.clone(), v.clone()))
448 .collect(),
449 ),
450 );
451 }
452 stage_map.insert("outputs".to_string(), Value::Object(outputs));
453
454 map.insert(name.clone(), Value::Object(stage_map));
455 }
456
457 for (name, dep) in &self.context.dependencies.jobs {
459 let mut job_map = HashMap::new();
460 job_map.insert("result".to_string(), Value::String(dep.result.clone()));
461 job_map.insert(
462 "outputs".to_string(),
463 Value::Object(
464 dep.outputs
465 .iter()
466 .map(|(k, v)| (k.clone(), v.clone()))
467 .collect(),
468 ),
469 );
470 map.insert(name.clone(), Value::Object(job_map));
471 }
472
473 Ok(Value::Object(map))
474 }
475
476 fn stage_dependencies_to_value(&self) -> Result<Value, EvalError> {
479 let mut map = HashMap::new();
480
481 for (stage_name, dep) in &self.context.dependencies.stages {
482 let mut stage_map = HashMap::new();
483 stage_map.insert("result".to_string(), Value::String(dep.result.clone()));
484
485 for (job_name, job_outputs) in &dep.outputs {
487 let mut job_map = HashMap::new();
488 job_map.insert(
489 "outputs".to_string(),
490 Value::Object(
491 job_outputs
492 .iter()
493 .map(|(k, v)| (k.clone(), v.clone()))
494 .collect(),
495 ),
496 );
497 stage_map.insert(job_name.clone(), Value::Object(job_map));
498 }
499
500 map.insert(stage_name.clone(), Value::Object(stage_map));
501 }
502
503 Ok(Value::Object(map))
504 }
505
506 fn resources_to_value(&self) -> Result<Value, EvalError> {
507 let mut map = HashMap::new();
508
509 let mut pipelines = HashMap::new();
511 for (name, resource) in &self.context.resources.pipelines {
512 let mut resource_map = HashMap::new();
513 if let Some(id) = &resource.pipeline_id {
514 resource_map.insert("pipelineID".to_string(), Value::String(id.clone()));
515 }
516 if let Some(name) = &resource.run_name {
517 resource_map.insert("runName".to_string(), Value::String(name.clone()));
518 }
519 pipelines.insert(name.clone(), Value::Object(resource_map));
520 }
521 map.insert("pipelines".to_string(), Value::Object(pipelines));
522
523 let mut repos = HashMap::new();
525 for (name, resource) in &self.context.resources.repositories {
526 let mut resource_map = HashMap::new();
527 if let Some(n) = &resource.name {
528 resource_map.insert("name".to_string(), Value::String(n.clone()));
529 }
530 if let Some(t) = &resource.repo_type {
531 resource_map.insert("type".to_string(), Value::String(t.clone()));
532 }
533 repos.insert(name.clone(), Value::Object(resource_map));
534 }
535 map.insert("repositories".to_string(), Value::Object(repos));
536
537 Ok(Value::Object(map))
538 }
539
540 fn eval_function(&self, name: &str, args: &[Expr]) -> Result<Value, EvalError> {
541 let evaluated_args: Result<Vec<Value>, EvalError> =
542 args.iter().map(|a| self.eval(a)).collect();
543 self.functions.call(name, evaluated_args?, self.context)
544 }
545
546 fn eval_index(&self, object: &Value, index: &Value) -> Result<Value, EvalError> {
547 match (object, index) {
548 (Value::Array(arr), Value::Number(n)) => {
549 let i = *n as usize;
550 arr.get(i)
551 .cloned()
552 .ok_or_else(|| EvalError::new(format!("array index {} out of bounds", i)))
553 }
554 (Value::Object(map), Value::String(key)) => {
555 Ok(map.get(key).cloned().unwrap_or(Value::Null))
556 }
557 (Value::Object(map), Value::Number(n)) => {
558 let key = n.to_string();
559 Ok(map.get(&key).cloned().unwrap_or(Value::Null))
560 }
561 (Value::String(s), Value::Number(n)) => {
562 let i = *n as usize;
563 s.chars()
564 .nth(i)
565 .map(|c| Value::String(c.to_string()))
566 .ok_or_else(|| EvalError::new(format!("string index {} out of bounds", i)))
567 }
568 _ => Err(EvalError::new(format!(
569 "cannot index {:?} with {:?}",
570 object, index
571 ))),
572 }
573 }
574
575 fn eval_member(&self, object: &Value, property: &str) -> Result<Value, EvalError> {
576 match object {
577 Value::Object(map) => Ok(map.get(property).cloned().unwrap_or(Value::Null)),
578 Value::Array(arr) if property == "length" => Ok(Value::Number(arr.len() as f64)),
579 Value::String(s) if property == "length" => Ok(Value::Number(s.len() as f64)),
580 _ => Err(EvalError::new(format!(
581 "cannot access property '{}' on {:?}",
582 property, object
583 ))),
584 }
585 }
586
587 fn eval_unary(&self, op: UnaryOp, value: &Value) -> Result<Value, EvalError> {
588 match op {
589 UnaryOp::Not => Ok(Value::Bool(!value.is_truthy())),
590 UnaryOp::Neg => match value {
591 Value::Number(n) => Ok(Value::Number(-n)),
592 _ => Err(EvalError::new("cannot negate non-number")),
593 },
594 }
595 }
596
597 fn eval_binary(&self, op: BinaryOp, left: &Value, right: &Value) -> Result<Value, EvalError> {
598 match op {
599 BinaryOp::Add => self.eval_add(left, right),
601 BinaryOp::Sub => self.eval_numeric_op(left, right, |a, b| a - b),
602 BinaryOp::Mul => self.eval_numeric_op(left, right, |a, b| a * b),
603 BinaryOp::Div => self.eval_numeric_op(left, right, |a, b| a / b),
604 BinaryOp::Mod => self.eval_numeric_op(left, right, |a, b| a % b),
605
606 BinaryOp::Eq => Ok(Value::Bool(self.values_equal(left, right))),
608 BinaryOp::Ne => Ok(Value::Bool(!self.values_equal(left, right))),
609 BinaryOp::Lt => self.eval_comparison(left, right, |a, b| a < b),
610 BinaryOp::Le => self.eval_comparison(left, right, |a, b| a <= b),
611 BinaryOp::Gt => self.eval_comparison(left, right, |a, b| a > b),
612 BinaryOp::Ge => self.eval_comparison(left, right, |a, b| a >= b),
613
614 BinaryOp::And | BinaryOp::Or => unreachable!("handled in eval()"),
616 }
617 }
618
619 fn eval_add(&self, left: &Value, right: &Value) -> Result<Value, EvalError> {
620 match (left, right) {
621 (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
622 (Value::String(a), Value::String(b)) => Ok(Value::String(format!("{}{}", a, b))),
623 (Value::String(a), b) => Ok(Value::String(format!("{}{}", a, b.as_string()))),
624 (a, Value::String(b)) => Ok(Value::String(format!("{}{}", a.as_string(), b))),
625 _ => Err(EvalError::new("cannot add these types")),
626 }
627 }
628
629 fn eval_numeric_op<F>(&self, left: &Value, right: &Value, op: F) -> Result<Value, EvalError>
630 where
631 F: FnOnce(f64, f64) -> f64,
632 {
633 let a = left
634 .as_number()
635 .ok_or_else(|| EvalError::new("left operand is not a number"))?;
636 let b = right
637 .as_number()
638 .ok_or_else(|| EvalError::new("right operand is not a number"))?;
639 Ok(Value::Number(op(a, b)))
640 }
641
642 fn eval_comparison<F>(&self, left: &Value, right: &Value, op: F) -> Result<Value, EvalError>
643 where
644 F: FnOnce(f64, f64) -> bool,
645 {
646 let a = left
647 .as_number()
648 .ok_or_else(|| EvalError::new("left operand is not comparable"))?;
649 let b = right
650 .as_number()
651 .ok_or_else(|| EvalError::new("right operand is not comparable"))?;
652 Ok(Value::Bool(op(a, b)))
653 }
654
655 fn values_equal(&self, left: &Value, right: &Value) -> bool {
656 match (left, right) {
657 (Value::Null, Value::Null) => true,
658 (Value::Bool(a), Value::Bool(b)) => a == b,
659 (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
660 (Value::String(a), Value::String(b)) => a.to_lowercase() == b.to_lowercase(),
661 (Value::Number(a), Value::String(b)) | (Value::String(b), Value::Number(a)) => b
663 .parse::<f64>()
664 .map(|n| (a - n).abs() < f64::EPSILON)
665 .unwrap_or(false),
666 (Value::Bool(a), Value::String(b)) | (Value::String(b), Value::Bool(a)) => {
667 let b_lower = b.to_lowercase();
668 (*a && b_lower == "true") || (!*a && b_lower == "false")
669 }
670 _ => false,
671 }
672 }
673}
674
675pub struct ExpressionEngine {
677 context: ExpressionContext,
678}
679
680impl ExpressionEngine {
681 pub fn new(context: ExpressionContext) -> Self {
682 Self { context }
683 }
684
685 pub fn evaluate_compile_time(&self, expr: &str) -> Result<Value, EvalError> {
687 use crate::expression::parser::ExprParser;
688
689 let ast = ExprParser::parse_str(expr)
690 .map_err(|e| EvalError::new(format!("parse error: {}", e)))?;
691
692 let evaluator = Evaluator::new(&self.context);
693 evaluator.eval(&ast)
694 }
695
696 pub fn evaluate_runtime(&self, expr: &str) -> Result<Value, EvalError> {
698 self.evaluate_compile_time(expr)
700 }
701
702 pub fn substitute_macros(&self, text: &str) -> Result<String, EvalError> {
704 use crate::expression::lexer::{extract_expressions, ExpressionType};
705
706 let expressions = extract_expressions(text);
707 let mut result = String::new();
708
709 for expr in expressions {
710 match expr {
711 ExpressionType::Text(s) => result.push_str(&s),
712 ExpressionType::Macro(var_path) => {
713 let value = self.resolve_variable_path(&var_path)?;
714 result.push_str(&value.as_string());
715 }
716 ExpressionType::CompileTime(expr) => {
717 let value = self.evaluate_compile_time(&expr)?;
718 result.push_str(&value.as_string());
719 }
720 ExpressionType::Runtime(expr) => {
721 let value = self.evaluate_runtime(&expr)?;
722 result.push_str(&value.as_string());
723 }
724 }
725 }
726
727 Ok(result)
728 }
729
730 fn resolve_variable_path(&self, path: &str) -> Result<Value, EvalError> {
731 let parts: Vec<&str> = path.split('.').collect();
733
734 if parts.len() == 1 {
735 if let Some(value) = self.context.variables.get(parts[0]) {
737 return Ok(value.clone());
738 }
739 if let Some(value) = self.context.parameters.get(parts[0]) {
740 return Ok(value.clone());
741 }
742 return Ok(Value::String(String::new()));
744 }
745
746 let prefix = parts[0].to_lowercase();
748 let rest = &parts[1..];
749
750 match prefix.as_str() {
751 "variables" => {
752 let var_name = rest.join(".");
753 Ok(self
754 .context
755 .variables
756 .get(&var_name)
757 .cloned()
758 .unwrap_or(Value::String(String::new())))
759 }
760 "parameters" => {
761 let param_name = rest.join(".");
762 Ok(self
763 .context
764 .parameters
765 .get(¶m_name)
766 .cloned()
767 .unwrap_or(Value::Null))
768 }
769 "env" => {
770 let env_name = rest.join(".");
771 Ok(self
772 .context
773 .env
774 .get(&env_name)
775 .cloned()
776 .unwrap_or(Value::String(String::new())))
777 }
778 _ => {
779 if let Some(value) = self.context.variables.get(path) {
781 return Ok(value.clone());
782 }
783 Ok(Value::String(String::new()))
784 }
785 }
786 }
787
788 pub fn context_mut(&mut self) -> &mut ExpressionContext {
790 &mut self.context
791 }
792
793 pub fn context(&self) -> &ExpressionContext {
795 &self.context
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 fn make_context() -> ExpressionContext {
804 let mut ctx = ExpressionContext::default();
805 ctx.variables
806 .insert("foo".to_string(), Value::String("bar".to_string()));
807 ctx.variables.insert("num".to_string(), Value::Number(42.0));
808 ctx.variables.insert(
809 "Build.SourceBranch".to_string(),
810 Value::String("refs/heads/main".to_string()),
811 );
812 ctx.parameters
813 .insert("config".to_string(), Value::String("Release".to_string()));
814 ctx
815 }
816
817 #[test]
818 fn test_eval_literals() {
819 let engine = ExpressionEngine::new(ExpressionContext::default());
820
821 assert_eq!(engine.evaluate_compile_time("null").unwrap(), Value::Null);
822 assert_eq!(
823 engine.evaluate_compile_time("true").unwrap(),
824 Value::Bool(true)
825 );
826 assert_eq!(
827 engine.evaluate_compile_time("42").unwrap(),
828 Value::Number(42.0)
829 );
830 assert_eq!(
831 engine.evaluate_compile_time("'hello'").unwrap(),
832 Value::String("hello".to_string())
833 );
834 }
835
836 #[test]
837 fn test_eval_variable_reference() {
838 let engine = ExpressionEngine::new(make_context());
839
840 assert_eq!(
841 engine.evaluate_compile_time("variables.foo").unwrap(),
842 Value::String("bar".to_string())
843 );
844 assert_eq!(
845 engine.evaluate_compile_time("variables['foo']").unwrap(),
846 Value::String("bar".to_string())
847 );
848 }
849
850 #[test]
851 fn test_eval_parameter_reference() {
852 let engine = ExpressionEngine::new(make_context());
853
854 assert_eq!(
855 engine.evaluate_compile_time("parameters.config").unwrap(),
856 Value::String("Release".to_string())
857 );
858 }
859
860 #[test]
861 fn test_eval_comparison() {
862 let engine = ExpressionEngine::new(make_context());
863
864 assert_eq!(
865 engine
866 .evaluate_compile_time("variables.foo == 'bar'")
867 .unwrap(),
868 Value::Bool(true)
869 );
870 assert_eq!(
871 engine.evaluate_compile_time("variables.num > 40").unwrap(),
872 Value::Bool(true)
873 );
874 }
875
876 #[test]
877 fn test_eval_logical() {
878 let engine = ExpressionEngine::new(make_context());
879
880 assert_eq!(
881 engine.evaluate_compile_time("true && true").unwrap(),
882 Value::Bool(true)
883 );
884 assert_eq!(
885 engine.evaluate_compile_time("true && false").unwrap(),
886 Value::Bool(false)
887 );
888 assert_eq!(
889 engine.evaluate_compile_time("false || true").unwrap(),
890 Value::Bool(true)
891 );
892 assert_eq!(
893 engine.evaluate_compile_time("!false").unwrap(),
894 Value::Bool(true)
895 );
896 }
897
898 #[test]
899 fn test_eval_ternary() {
900 let engine = ExpressionEngine::new(make_context());
901
902 assert_eq!(
903 engine.evaluate_compile_time("true ? 'yes' : 'no'").unwrap(),
904 Value::String("yes".to_string())
905 );
906 assert_eq!(
907 engine
908 .evaluate_compile_time("false ? 'yes' : 'no'")
909 .unwrap(),
910 Value::String("no".to_string())
911 );
912 }
913
914 #[test]
915 fn test_substitute_macros() {
916 let engine = ExpressionEngine::new(make_context());
917
918 assert_eq!(
919 engine.substitute_macros("Value: $(foo)").unwrap(),
920 "Value: bar"
921 );
922 assert_eq!(
923 engine
924 .substitute_macros("Branch: $(Build.SourceBranch)")
925 .unwrap(),
926 "Branch: refs/heads/main"
927 );
928 }
929
930 #[test]
931 fn test_substitute_mixed() {
932 let engine = ExpressionEngine::new(make_context());
933
934 assert_eq!(
935 engine
936 .substitute_macros("Config: ${{ parameters.config }} on $(Build.SourceBranch)")
937 .unwrap(),
938 "Config: Release on refs/heads/main"
939 );
940 }
941
942 #[test]
943 fn test_undefined_variable() {
944 let engine = ExpressionEngine::new(make_context());
945
946 assert_eq!(engine.substitute_macros("$(undefined)").unwrap(), "");
948 }
949}