Skip to main content

rite_runtime/
expressions.rs

1//! Expression evaluation for computed values.
2//!
3//! Evaluates pipeline expressions like `${artifact.ksr | sha256 | hex}` at runtime,
4//! resolving artifact references and applying transformations.
5
6use crate::actions::ArtifactValue;
7use crate::artifact_resolver::resolve_artifact_bytes;
8use crate::executor::ExecutionError;
9use crate::state::HandlerContext;
10use base16ct::lower::encode_string as hex_encode_string;
11use base32ct::{Base32Unpadded, Encoding as Base32Encoding};
12use base64ct::{Base64, Encoding as Base64Encoding};
13use rite_model::expression::{
14    ExprValue, Expression, Literal, PipeStage, RefType, Reference, StringPart, Value,
15};
16use rite_model::{ArtifactId, ParamId};
17use sha2::{Digest, Sha256, Sha384, Sha512};
18
19/// Evaluate an expression in the given context.
20///
21/// # Examples
22///
23/// ```text
24/// let expr = parse_expression("${artifact.ksr | sha256 | hex}")?;
25/// let result = evaluate(&expr, &context)?;
26/// // result is Value::String("<hex digest>")
27/// ```
28pub fn evaluate(expr: &Expression, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
29    match expr {
30        Expression::Reference(ref_) => evaluate_reference(ref_, ctx),
31        Expression::Literal(lit) => Ok(evaluate_literal(lit)),
32        Expression::Pipeline { source, stages } => {
33            let mut value = evaluate(source, ctx)?;
34            for stage in stages {
35                value = apply_pipe_stage(&value, stage, ctx)?;
36            }
37            Ok(value)
38        }
39    }
40}
41
42/// Evaluate a reference to get its value.
43fn evaluate_reference(ref_: &Reference, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
44    match ref_.ref_type {
45        RefType::Param => evaluate_param_ref(ref_, ctx),
46        RefType::Artifact => evaluate_artifact_ref(ref_, ctx),
47        // Role references evaluate to their ID as a string
48        // This allows ${role.name} to be used in role-related fields
49        RefType::Role => Ok(Value::String(ref_.name.clone())),
50    }
51}
52
53/// Evaluate a parameter reference.
54fn evaluate_param_ref(ref_: &Reference, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
55    let param_id = ParamId::new(&ref_.name);
56    let value = ctx.params.get(&param_id).ok_or_else(|| {
57        ExecutionError::InvalidParams(format!("Unknown parameter: {}", ref_.name))
58    })?;
59
60    json_to_value(value)
61}
62
63/// Convert a JSON value to our Value type.
64fn json_to_value(json: &serde_json::Value) -> Result<Value, ExecutionError> {
65    match json {
66        serde_json::Value::String(s) => Ok(Value::String(s.clone())),
67        serde_json::Value::Number(n) => {
68            if let Some(i) = n.as_i64() {
69                Ok(Value::Integer(i))
70            } else if let Some(f) = n.as_f64() {
71                #[allow(clippy::cast_possible_truncation)]
72                Ok(Value::Integer(f as i64))
73            } else {
74                Err(ExecutionError::InvalidParams(
75                    "Number too large".to_string(),
76                ))
77            }
78        }
79        serde_json::Value::Bool(b) => Ok(Value::Boolean(*b)),
80        serde_json::Value::Null => Ok(Value::Null),
81        serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
82            Err(ExecutionError::InvalidParams(
83                "Cannot use array or object as expression value".to_string(),
84            ))
85        }
86    }
87}
88
89/// Evaluate an artifact reference.
90fn evaluate_artifact_ref(ref_: &Reference, ctx: &HandlerContext) -> Result<Value, ExecutionError> {
91    let artifact_id = ArtifactId::new(&ref_.name);
92    let artifact = ctx.get_artifact(&artifact_id).ok_or_else(|| {
93        ExecutionError::InvalidParams(format!("Artifact '{}' not found", ref_.name))
94    })?;
95
96    match artifact {
97        // Text references (physical items) - return as string for display
98        ArtifactValue::Text(text) => {
99            if let Some(property) = &ref_.property {
100                return Err(ExecutionError::InvalidParams(format!(
101                    "Property access '.{property}' not supported for text artifacts"
102                )));
103            }
104            Ok(Value::String(text.clone()))
105        }
106
107        // Binary content (documents, crypto materials) - return as bytes
108        ArtifactValue::Bytes(bytes) => {
109            // Property access not supported for raw byte artifacts
110            if let Some(property) = &ref_.property {
111                return Err(ExecutionError::InvalidParams(format!(
112                    "Property access '.{property}' not supported for binary artifacts"
113                )));
114            }
115            Ok(Value::Bytes(bytes.clone()))
116        }
117
118        // Other artifact types - delegate to resolve_artifact_bytes
119        _ => {
120            let bytes =
121                resolve_artifact_bytes(ctx.artifacts, &artifact_id, ref_.property.as_deref())?;
122            Ok(Value::Bytes(bytes))
123        }
124    }
125}
126
127/// Evaluate a literal to a Value.
128fn evaluate_literal(lit: &Literal) -> Value {
129    match lit {
130        Literal::String(s) => Value::String(s.clone()),
131        Literal::Integer(i) => Value::Integer(*i),
132        #[allow(clippy::cast_possible_truncation)]
133        Literal::Float(f) => Value::Integer(*f as i64), // Truncate float to integer
134        Literal::Boolean(b) => Value::Boolean(*b),
135        Literal::Null => Value::Null,
136    }
137}
138
139/// Apply a pipe stage to a value.
140fn apply_pipe_stage(
141    input: &Value,
142    stage: &PipeStage,
143    ctx: &HandlerContext,
144) -> Result<Value, ExecutionError> {
145    match stage.function.as_str() {
146        // Hash functions: Bytes -> Bytes
147        "sha256" => apply_sha256(input),
148        "sha384" => apply_sha384(input),
149        "sha512" => apply_sha512(input),
150
151        // Encoding functions: Bytes -> String
152        "hex" => apply_hex(input),
153        "base32" => apply_base32(input),
154        "base64" => apply_base64(input),
155
156        // String functions: String -> String
157        "upper" => apply_upper(input),
158        "lower" => apply_lower(input),
159        "substr" => apply_substr(input, &stage.args, ctx),
160
161        // Bytes concatenation
162        "concat" => apply_concat(&stage.args, ctx),
163
164        unknown => Err(ExecutionError::InvalidParams(format!(
165            "Unknown function: {unknown}"
166        ))),
167    }
168}
169
170fn apply_sha256(input: &Value) -> Result<Value, ExecutionError> {
171    let bytes = input.as_bytes().ok_or_else(|| {
172        ExecutionError::InvalidParams(format!(
173            "sha256 expects bytes or string, got {}",
174            input.type_name()
175        ))
176    })?;
177
178    let mut hasher = Sha256::new();
179    hasher.update(&bytes);
180    let result = hasher.finalize();
181
182    Ok(Value::Bytes(result.to_vec()))
183}
184
185fn apply_sha384(input: &Value) -> Result<Value, ExecutionError> {
186    let bytes = input.as_bytes().ok_or_else(|| {
187        ExecutionError::InvalidParams(format!(
188            "sha384 expects bytes or string, got {}",
189            input.type_name()
190        ))
191    })?;
192
193    let mut hasher = Sha384::new();
194    hasher.update(&bytes);
195    let result = hasher.finalize();
196
197    Ok(Value::Bytes(result.to_vec()))
198}
199
200fn apply_sha512(input: &Value) -> Result<Value, ExecutionError> {
201    let bytes = input.as_bytes().ok_or_else(|| {
202        ExecutionError::InvalidParams(format!(
203            "sha512 expects bytes or string, got {}",
204            input.type_name()
205        ))
206    })?;
207
208    let mut hasher = Sha512::new();
209    hasher.update(&bytes);
210    let result = hasher.finalize();
211
212    Ok(Value::Bytes(result.to_vec()))
213}
214
215fn apply_hex(input: &Value) -> Result<Value, ExecutionError> {
216    let bytes = input.as_bytes().ok_or_else(|| {
217        ExecutionError::InvalidParams(format!(
218            "hex expects bytes or string, got {}",
219            input.type_name()
220        ))
221    })?;
222
223    let hex_string = hex_encode(&bytes);
224    Ok(Value::String(hex_string))
225}
226
227fn apply_base32(input: &Value) -> Result<Value, ExecutionError> {
228    let bytes = input.as_bytes().ok_or_else(|| {
229        ExecutionError::InvalidParams(format!(
230            "base32 expects bytes or string, got {}",
231            input.type_name()
232        ))
233    })?;
234
235    // base32ct returns lowercase, but RFC 4648 specifies uppercase
236    let encoded = Base32Unpadded::encode_string(&bytes).to_uppercase();
237    Ok(Value::String(encoded))
238}
239
240fn apply_base64(input: &Value) -> Result<Value, ExecutionError> {
241    let bytes = input.as_bytes().ok_or_else(|| {
242        ExecutionError::InvalidParams(format!(
243            "base64 expects bytes or string, got {}",
244            input.type_name()
245        ))
246    })?;
247
248    let encoded = Base64::encode_string(&bytes);
249    Ok(Value::String(encoded))
250}
251
252/// Encode bytes as lowercase hex string using constant-time encoding.
253fn hex_encode(bytes: &[u8]) -> String {
254    hex_encode_string(bytes)
255}
256
257fn apply_upper(input: &Value) -> Result<Value, ExecutionError> {
258    let s = input.as_string().ok_or_else(|| {
259        ExecutionError::InvalidParams(format!("upper expects string, got {}", input.type_name()))
260    })?;
261
262    Ok(Value::String(s.to_uppercase()))
263}
264
265fn apply_lower(input: &Value) -> Result<Value, ExecutionError> {
266    let s = input.as_string().ok_or_else(|| {
267        ExecutionError::InvalidParams(format!("lower expects string, got {}", input.type_name()))
268    })?;
269
270    Ok(Value::String(s.to_lowercase()))
271}
272
273fn apply_substr(
274    input: &Value,
275    args: &[Expression],
276    ctx: &HandlerContext,
277) -> Result<Value, ExecutionError> {
278    let s = input.as_string().ok_or_else(|| {
279        ExecutionError::InvalidParams(format!("substr expects string, got {}", input.type_name()))
280    })?;
281
282    let [start_arg, len_arg] = args else {
283        return Err(ExecutionError::InvalidParams(
284            "substr requires 2 arguments: start and length".to_string(),
285        ));
286    };
287
288    let start_i64 = evaluate(start_arg, ctx)?
289        .as_integer()
290        .ok_or_else(|| ExecutionError::InvalidParams("substr start must be integer".to_string()))?;
291
292    let len_i64 = evaluate(len_arg, ctx)?.as_integer().ok_or_else(|| {
293        ExecutionError::InvalidParams("substr length must be integer".to_string())
294    })?;
295
296    // Clamp negative values to 0, then convert to usize
297    let start = usize::try_from(start_i64.max(0)).unwrap_or(0).min(s.len());
298    let len = usize::try_from(len_i64.max(0)).unwrap_or(0);
299    let end = start.saturating_add(len).min(s.len());
300
301    Ok(Value::String(
302        s.get(start..end).unwrap_or_default().to_string(),
303    ))
304}
305
306fn apply_concat(args: &[Expression], ctx: &HandlerContext) -> Result<Value, ExecutionError> {
307    let mut result = Vec::new();
308
309    for arg in args {
310        let value = evaluate(arg, ctx)?;
311        let bytes = value.as_bytes().ok_or_else(|| {
312            ExecutionError::InvalidParams(format!(
313                "concat expects bytes arguments, got {}",
314                value.type_name()
315            ))
316        })?;
317        result.extend(bytes);
318    }
319
320    Ok(Value::Bytes(result))
321}
322
323/// Evaluate an `ExprValue` to a JSON value.
324///
325/// The function receives pre-parsed expressions and performs NO string
326/// parsing. All `${...}` patterns have already been parsed by the resolver.
327///
328/// # Examples
329///
330/// ```text
331/// let expr_value = parse_expr_value(&json!({"message": "${param.name}"}));
332/// let result = evaluate_expr_value(&expr_value, &ctx)?;
333/// // result is a serde_json::Value with expressions evaluated
334/// ```
335pub fn evaluate_expr_value(
336    value: &ExprValue,
337    ctx: &HandlerContext,
338) -> Result<serde_json::Value, ExecutionError> {
339    match value {
340        ExprValue::Literal(lit) => Ok(literal_to_json(lit)),
341        ExprValue::Expr(expr) => {
342            let result = evaluate(expr, ctx)?;
343            Ok(value_to_json(&result))
344        }
345        ExprValue::Interpolated(parts) => {
346            let mut result = String::new();
347            for part in parts {
348                match part {
349                    StringPart::Literal(s) => result.push_str(s),
350                    StringPart::Expr(expr) => {
351                        let val = evaluate(expr, ctx)?;
352                        result.push_str(&val.to_string());
353                    }
354                }
355            }
356            Ok(serde_json::Value::String(result))
357        }
358        ExprValue::Object(map) => {
359            let mut result = serde_json::Map::new();
360            for (k, v) in map {
361                result.insert(k.clone(), evaluate_expr_value(v, ctx)?);
362            }
363            Ok(serde_json::Value::Object(result))
364        }
365        ExprValue::Array(arr) => {
366            let result: Result<Vec<_>, _> =
367                arr.iter().map(|v| evaluate_expr_value(v, ctx)).collect();
368            Ok(serde_json::Value::Array(result?))
369        }
370    }
371}
372
373/// Convert a Literal to a JSON value.
374fn literal_to_json(lit: &Literal) -> serde_json::Value {
375    match lit {
376        Literal::String(s) => serde_json::Value::String(s.clone()),
377        Literal::Integer(i) => serde_json::Value::Number((*i).into()),
378        Literal::Float(f) => serde_json::Number::from_f64(*f)
379            .map_or(serde_json::Value::Null, serde_json::Value::Number),
380        Literal::Boolean(b) => serde_json::Value::Bool(*b),
381        Literal::Null => serde_json::Value::Null,
382    }
383}
384
385/// Evaluate an `ExprValue` to a string.
386///
387/// Useful for description fields that should always produce a string.
388pub fn evaluate_expr_value_to_string(
389    value: &ExprValue,
390    ctx: &HandlerContext,
391) -> Result<String, ExecutionError> {
392    match value {
393        ExprValue::Literal(Literal::String(s)) => Ok(s.clone()),
394        ExprValue::Literal(lit) => {
395            // Convert other literals to string representation
396            let json = literal_to_json(lit);
397            Ok(match json {
398                serde_json::Value::String(s) => s,
399                serde_json::Value::Number(n) => n.to_string(),
400                serde_json::Value::Bool(b) => b.to_string(),
401                serde_json::Value::Null => "null".to_string(),
402                _ => json.to_string(),
403            })
404        }
405        ExprValue::Expr(expr) => {
406            let result = evaluate(expr, ctx)?;
407            Ok(result.to_string())
408        }
409        ExprValue::Interpolated(parts) => {
410            let mut result = String::new();
411            for part in parts {
412                match part {
413                    StringPart::Literal(s) => result.push_str(s),
414                    StringPart::Expr(expr) => {
415                        let val = evaluate(expr, ctx)?;
416                        result.push_str(&val.to_string());
417                    }
418                }
419            }
420            Ok(result)
421        }
422        ExprValue::Object(_) | ExprValue::Array(_) => {
423            // For complex types, return JSON string representation
424            let json = evaluate_expr_value(value, ctx)?;
425            Ok(json.to_string())
426        }
427    }
428}
429
430/// Convert a Value to a JSON value for action parameters.
431pub fn value_to_json(value: &Value) -> serde_json::Value {
432    match value {
433        Value::Bytes(b) => {
434            // Bytes are encoded as base64 for JSON
435            serde_json::Value::String(Base64::encode_string(b))
436        }
437        Value::String(s) => serde_json::Value::String(s.clone()),
438        Value::Integer(i) => serde_json::Value::Number((*i).into()),
439        Value::Boolean(b) => serde_json::Value::Bool(*b),
440        Value::Null => serde_json::Value::Null,
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use crate::actions::ArtifactValue;
448    use rite_model::{MaterialId, RoleId};
449    use std::collections::HashMap;
450
451    fn empty_context() -> HandlerContext<'static> {
452        static EMPTY_PARAMS: std::sync::LazyLock<HashMap<ParamId, serde_json::Value>> =
453            std::sync::LazyLock::new(HashMap::new);
454        static EMPTY_ARTIFACTS: std::sync::LazyLock<HashMap<ArtifactId, ArtifactValue>> =
455            std::sync::LazyLock::new(HashMap::new);
456        static EMPTY_ROLES: std::sync::LazyLock<HashMap<RoleId, String>> =
457            std::sync::LazyLock::new(HashMap::new);
458        static EMPTY_MATERIALS: std::sync::LazyLock<HashMap<MaterialId, String>> =
459            std::sync::LazyLock::new(HashMap::new);
460        HandlerContext {
461            params: &EMPTY_PARAMS,
462            artifacts: &EMPTY_ARTIFACTS,
463            roles: &EMPTY_ROLES,
464            materials: &EMPTY_MATERIALS,
465        }
466    }
467
468    /// Create a `HandlerContext` from params and artifacts for tests.
469    /// Note: This leaks memory, only use in tests.
470    fn make_context(
471        params: HashMap<ParamId, serde_json::Value>,
472        artifacts: HashMap<ArtifactId, ArtifactValue>,
473    ) -> HandlerContext<'static> {
474        static EMPTY_ROLES: std::sync::LazyLock<HashMap<RoleId, String>> =
475            std::sync::LazyLock::new(HashMap::new);
476        static EMPTY_MATERIALS: std::sync::LazyLock<HashMap<MaterialId, String>> =
477            std::sync::LazyLock::new(HashMap::new);
478        let params_box = Box::leak(Box::new(params));
479        let artifacts_box = Box::leak(Box::new(artifacts));
480        HandlerContext {
481            params: params_box,
482            artifacts: artifacts_box,
483            roles: &EMPTY_ROLES,
484            materials: &EMPTY_MATERIALS,
485        }
486    }
487
488    #[test]
489    fn test_sha256_of_string() {
490        let input = Value::String("hello".to_string());
491        let result = apply_sha256(&input).unwrap();
492
493        if let Value::Bytes(bytes) = result {
494            assert_eq!(bytes.len(), 32); // SHA-256 produces 32 bytes
495            let hex = hex_encode(&bytes);
496            assert_eq!(
497                hex,
498                "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
499            );
500        } else {
501            panic!("Expected Bytes");
502        }
503    }
504
505    #[test]
506    fn test_sha256_of_bytes() {
507        let input = Value::Bytes(b"hello".to_vec());
508        let result = apply_sha256(&input).unwrap();
509
510        if let Value::Bytes(bytes) = result {
511            assert_eq!(bytes.len(), 32);
512        } else {
513            panic!("Expected Bytes");
514        }
515    }
516
517    #[test]
518    fn test_hex_encoding() {
519        let input = Value::Bytes(vec![0xde, 0xad, 0xbe, 0xef]);
520        let result = apply_hex(&input).unwrap();
521
522        assert_eq!(result, Value::String("deadbeef".to_string()));
523    }
524
525    #[test]
526    fn test_base32_encoding() {
527        let input = Value::Bytes(b"hello".to_vec());
528        let result = apply_base32(&input).unwrap();
529
530        // Base32 unpadded encoding of "hello" (RFC 4648 uppercase)
531        assert_eq!(result, Value::String("NBSWY3DP".to_string()));
532    }
533
534    #[test]
535    fn test_base64_encoding() {
536        let input = Value::Bytes(b"hello".to_vec());
537        let result = apply_base64(&input).unwrap();
538
539        assert_eq!(result, Value::String("aGVsbG8=".to_string()));
540    }
541
542    #[test]
543    fn test_upper() {
544        let input = Value::String("hello".to_string());
545        let result = apply_upper(&input).unwrap();
546
547        assert_eq!(result, Value::String("HELLO".to_string()));
548    }
549
550    #[test]
551    fn test_lower() {
552        let input = Value::String("HELLO".to_string());
553        let result = apply_lower(&input).unwrap();
554
555        assert_eq!(result, Value::String("hello".to_string()));
556    }
557
558    #[test]
559    fn test_pipeline_sha256_hex() {
560        let ctx = empty_context();
561
562        // Simulate: "hello" | sha256 | hex
563        let expr = Expression::Pipeline {
564            source: Box::new(Expression::Literal(Literal::String("hello".to_string()))),
565            stages: vec![PipeStage::new("sha256"), PipeStage::new("hex")],
566        };
567
568        let result = evaluate(&expr, &ctx).unwrap();
569
570        if let Value::String(s) = result {
571            assert_eq!(
572                s,
573                "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
574            );
575        } else {
576            panic!("Expected String");
577        }
578    }
579
580    #[test]
581    fn test_pipeline_sha256_hex_upper() {
582        let ctx = empty_context();
583
584        // Simulate: "hello" | sha256 | hex | upper
585        let expr = Expression::Pipeline {
586            source: Box::new(Expression::Literal(Literal::String("hello".to_string()))),
587            stages: vec![
588                PipeStage::new("sha256"),
589                PipeStage::new("hex"),
590                PipeStage::new("upper"),
591            ],
592        };
593
594        let result = evaluate(&expr, &ctx).unwrap();
595
596        if let Value::String(s) = result {
597            assert_eq!(
598                s,
599                "2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824"
600            );
601        } else {
602            panic!("Expected String");
603        }
604    }
605
606    #[test]
607    fn test_type_error_sha256_on_integer() {
608        let input = Value::Integer(42);
609        let result = apply_sha256(&input);
610
611        assert!(result.is_err());
612    }
613
614    #[test]
615    fn test_type_error_upper_on_bytes() {
616        let input = Value::Bytes(vec![1, 2, 3]);
617        let result = apply_upper(&input);
618
619        assert!(result.is_err());
620    }
621
622    #[test]
623    fn test_substr_via_pipeline() {
624        let ctx = empty_context();
625
626        // Test: "abcdefgh" | substr(0, 4)
627        let expr = Expression::Pipeline {
628            source: Box::new(Expression::Literal(Literal::String("abcdefgh".to_string()))),
629            stages: vec![PipeStage {
630                function: "substr".to_string(),
631                args: vec![
632                    Expression::Literal(Literal::Integer(0)),
633                    Expression::Literal(Literal::Integer(4)),
634                ],
635            }],
636        };
637
638        let result = evaluate(&expr, &ctx).unwrap();
639        assert_eq!(result, Value::String("abcd".to_string()));
640    }
641
642    #[test]
643    fn test_substr_from_middle_via_pipeline() {
644        let ctx = empty_context();
645
646        // Test: "abcdefgh" | substr(2, 4)
647        let expr = Expression::Pipeline {
648            source: Box::new(Expression::Literal(Literal::String("abcdefgh".to_string()))),
649            stages: vec![PipeStage {
650                function: "substr".to_string(),
651                args: vec![
652                    Expression::Literal(Literal::Integer(2)),
653                    Expression::Literal(Literal::Integer(4)),
654                ],
655            }],
656        };
657
658        let result = evaluate(&expr, &ctx).unwrap();
659        assert_eq!(result, Value::String("cdef".to_string()));
660    }
661
662    #[test]
663    fn test_artifact_evaluation() {
664        let mut params = HashMap::new();
665        params.insert(ParamId::new("name"), serde_json::json!("test"));
666
667        let mut artifacts = HashMap::new();
668        // Use Bytes which stores binary content
669        artifacts.insert(
670            ArtifactId::new("ksr"),
671            ArtifactValue::Bytes(b"KSR content".to_vec()),
672        );
673
674        let ctx = make_context(params, artifacts);
675
676        // Parse and evaluate: ${artifact.ksr | sha256 | hex}
677        let expr =
678            rite_model::expression::parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
679        let result = evaluate(&expr, &ctx).unwrap();
680
681        if let Value::String(s) = result {
682            assert_eq!(s.len(), 64); // SHA-256 hex is 64 chars
683        } else {
684            panic!("Expected String");
685        }
686    }
687
688    #[test]
689    fn test_param_evaluation() {
690        let mut params = HashMap::new();
691        params.insert(ParamId::new("expected_hash"), serde_json::json!("abc123"));
692
693        let artifacts = HashMap::new();
694        let ctx = make_context(params, artifacts);
695
696        let expr = rite_model::expression::parse_expression("${param.expected_hash}").unwrap();
697        let result = evaluate(&expr, &ctx).unwrap();
698
699        assert_eq!(result, Value::String("abc123".to_string()));
700    }
701
702    #[test]
703    fn test_sha384() {
704        let input = Value::String("hello".to_string());
705        let result = apply_sha384(&input).unwrap();
706
707        if let Value::Bytes(bytes) = result {
708            assert_eq!(bytes.len(), 48); // SHA-384 produces 48 bytes
709        } else {
710            panic!("Expected Bytes");
711        }
712    }
713
714    #[test]
715    fn test_sha512() {
716        let input = Value::String("hello".to_string());
717        let result = apply_sha512(&input).unwrap();
718
719        if let Value::Bytes(bytes) = result {
720            assert_eq!(bytes.len(), 64); // SHA-512 produces 64 bytes
721        } else {
722            panic!("Expected Bytes");
723        }
724    }
725
726    #[test]
727    fn test_concat_through_pipeline() {
728        let ctx = empty_context();
729
730        // Test concat via expression: concat("hello", " ", "world")
731        // concat takes all strings as args, ignores pipeline input
732        let expr = Expression::Pipeline {
733            source: Box::new(Expression::Literal(Literal::String("ignored".to_string()))),
734            stages: vec![PipeStage {
735                function: "concat".to_string(),
736                args: vec![
737                    Expression::Literal(Literal::String("hello".to_string())),
738                    Expression::Literal(Literal::String(" ".to_string())),
739                    Expression::Literal(Literal::String("world".to_string())),
740                ],
741            }],
742        };
743
744        let result = evaluate(&expr, &ctx).unwrap();
745
746        // concat returns bytes
747        if let Value::Bytes(b) = result {
748            assert_eq!(b, b"hello world".to_vec());
749        } else {
750            panic!("Expected Bytes, got {result:?}");
751        }
752    }
753
754    #[test]
755    fn test_parse_and_evaluate_pipeline() {
756        let ctx = empty_context();
757
758        // Test parsing a real expression string
759        let expr = rite_model::expression::parse_expression("${\"test\" | sha256 | hex}").unwrap();
760        let result = evaluate(&expr, &ctx).unwrap();
761
762        if let Value::String(s) = result {
763            // SHA-256 of "test" in hex
764            assert_eq!(
765                s,
766                "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
767            );
768        } else {
769            panic!("Expected String");
770        }
771    }
772
773    mod ceremony_examples {
774        use super::*;
775
776        fn ceremony_context() -> HandlerContext<'static> {
777            let params: HashMap<ParamId, serde_json::Value> = [
778                ("ceremony_id", serde_json::json!("KC-2024-Q4")),
779                ("hsm_serial", serde_json::json!("HSM-001")),
780                ("key_label", serde_json::json!("MASTER-KEY")),
781                ("expected_ksr_hash", serde_json::json!("a3b8f4e91c2d6078")),
782                ("zone_name", serde_json::json!(".")),
783                ("signing_validity_days", serde_json::json!(21)),
784            ]
785            .into_iter()
786            .map(|(k, v)| (ParamId::new(k), v))
787            .collect();
788
789            let mut artifacts: HashMap<ArtifactId, ArtifactValue> = HashMap::new();
790            artifacts.insert(
791                ArtifactId::new("ksr"),
792                ArtifactValue::Bytes(b"KSR test content".to_vec()),
793            );
794            artifacts.insert(
795                ArtifactId::new("master_public_key"),
796                ArtifactValue::Bytes(b"PUBLIC KEY DATA".to_vec()),
797            );
798
799            make_context(params, artifacts)
800        }
801
802        // From dnssec_signing/dnssec_signing.rite.yaml
803        #[test]
804        fn dnssec_hash_verification_full_pipeline() {
805            let ctx = ceremony_context();
806
807            // actual: "${artifact.ksr | sha256 | hex | substr(0, 16)}"
808            let expr = rite_model::expression::parse_expression(
809                "${artifact.ksr | sha256 | hex | substr(0, 16)}",
810            )
811            .unwrap();
812            let result = evaluate(&expr, &ctx).unwrap();
813
814            if let Value::String(s) = result {
815                assert_eq!(s.len(), 16); // First 16 hex chars
816                assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
817            } else {
818                panic!("Expected String");
819            }
820        }
821
822        #[test]
823        fn dnssec_param_comparison() {
824            let ctx = ceremony_context();
825
826            // expected: "${param.expected_ksr_hash}"
827            let expr =
828                rite_model::expression::parse_expression("${param.expected_ksr_hash}").unwrap();
829            let result = evaluate(&expr, &ctx).unwrap();
830
831            assert_eq!(result, Value::String("a3b8f4e91c2d6078".to_string()));
832        }
833
834        #[test]
835        fn hsm_fingerprint_computation() {
836            let ctx = ceremony_context();
837
838            // value: "${artifact.master_public_key | sha256 | hex}"
839            let expr = rite_model::expression::parse_expression(
840                "${artifact.master_public_key | sha256 | hex}",
841            )
842            .unwrap();
843            let result = evaluate(&expr, &ctx).unwrap();
844
845            if let Value::String(s) = result {
846                assert_eq!(s.len(), 64); // Full SHA-256 hex
847            } else {
848                panic!("Expected String");
849            }
850        }
851    }
852}