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