Skip to main content

rite_model/
expression.rs

1//! Expression parsing for computed values in ceremony DSL.
2//!
3//! This module provides types and functions for parsing pipeline expressions
4//! that compute values at runtime, such as `${artifact.ksr | sha256 | hex}`.
5//!
6//! ## Syntax
7//!
8//! Expressions use Unix-style pipes:
9//!
10//! ```text
11//! ${artifact.ksr | sha256 | hex}
12//! ${artifact.keypair.public | fingerprint | hex | upper}
13//! ${concat(artifact.a | sha256, artifact.b | sha256) | hex}
14//! ```
15//!
16//! ## Design Principles
17//!
18//! - **Bytes-first**: Hash functions return raw bytes; encoding is explicit
19//! - **Left-to-right**: Data flows through transformations naturally
20//! - **Safe**: All functions are total and terminate (no user-defined functions)
21//!
22//! ## Grammar
23//!
24//! The formal grammar is documented in `docs/dsl/grammar.md` (not yet published).
25
26// The expression parser uses byte-index arithmetic and slice indexing throughout.
27// All indices are guarded by length checks and loop bounds; the operations cannot
28// overflow or panic in practice. Suppressed here to avoid obscuring the parser logic.
29#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
30
31use std::ops::Range;
32
33/// The type/namespace of a reference.
34///
35/// Used in full-form references like `${param.name}` or `${artifact.ksr}`.
36/// This is the canonical definition, re-exported by `reference.rs`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum RefType {
39    /// Parameter reference (runtime configuration from CLI/env/defaults)
40    Param,
41    /// Role reference (ceremony participant)
42    Role,
43    /// Artifact reference (materials, outputs, intermediate values)
44    Artifact,
45}
46
47impl std::str::FromStr for RefType {
48    type Err = String;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        match s {
52            "param" => Ok(Self::Param),
53            "role" => Ok(Self::Role),
54            "artifact" => Ok(Self::Artifact),
55            _ => Err(format!("unknown reference type: {s}")),
56        }
57    }
58}
59
60impl std::fmt::Display for RefType {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::Param => write!(f, "param"),
64            Self::Role => write!(f, "role"),
65            Self::Artifact => write!(f, "artifact"),
66        }
67    }
68}
69
70/// A runtime value in the expression system.
71#[derive(Debug, Clone, PartialEq)]
72pub enum Value {
73    /// Raw binary data (computation core)
74    Bytes(Vec<u8>),
75    /// UTF-8 text string
76    String(String),
77    /// Integer value
78    Integer(i64),
79    /// Boolean value
80    Boolean(bool),
81    /// Null/missing value
82    Null,
83}
84
85impl Value {
86    /// Try to get this value as bytes.
87    /// Strings are converted to UTF-8 bytes.
88    pub fn as_bytes(&self) -> Option<Vec<u8>> {
89        match self {
90            Value::Bytes(b) => Some(b.clone()),
91            Value::String(s) => Some(s.as_bytes().to_vec()),
92            _ => None,
93        }
94    }
95
96    /// Try to get this value as a string.
97    pub fn as_string(&self) -> Option<&str> {
98        match self {
99            Value::String(s) => Some(s),
100            _ => None,
101        }
102    }
103
104    /// Try to get this value as an integer.
105    pub fn as_integer(&self) -> Option<i64> {
106        match self {
107            Value::Integer(i) => Some(*i),
108            _ => None,
109        }
110    }
111
112    /// Check if this value is null.
113    pub fn is_null(&self) -> bool {
114        matches!(self, Value::Null)
115    }
116
117    /// Get a human-readable type name for error messages.
118    pub fn type_name(&self) -> &'static str {
119        match self {
120            Value::Bytes(_) => "bytes",
121            Value::String(_) => "string",
122            Value::Integer(_) => "integer",
123            Value::Boolean(_) => "boolean",
124            Value::Null => "null",
125        }
126    }
127}
128
129impl std::fmt::Display for Value {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
133            Value::String(s) => write!(f, "{s}"),
134            Value::Integer(i) => write!(f, "{i}"),
135            Value::Boolean(b) => write!(f, "{b}"),
136            Value::Null => write!(f, "null"),
137        }
138    }
139}
140
141/// A parsed expression from ceremony YAML.
142///
143/// Expressions can be:
144/// - Simple references: `${artifact.ksr}`
145/// - Pipelines: `${artifact.ksr | sha256 | hex}`
146/// - Function calls with arguments: `${concat(a | sha256, b | sha256) | hex}`
147#[derive(Debug, Clone, PartialEq)]
148pub enum Expression {
149    /// A simple reference to a parameter or artifact
150    Reference(Reference),
151
152    /// A literal value
153    Literal(Literal),
154
155    /// A pipeline of transformations
156    Pipeline {
157        /// The starting value (reference or literal)
158        source: Box<Expression>,
159        /// The transformation stages
160        stages: Vec<PipeStage>,
161    },
162}
163
164/// A reference to a ceremony value: `${type.name}` or `${type.name.property}`.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct Reference {
167    /// The reference type (param, artifact, role)
168    pub ref_type: RefType,
169    /// The primary identifier
170    pub name: String,
171    /// Optional sub-property (e.g., `private` in `artifact.keypair.private`)
172    pub property: Option<String>,
173}
174
175impl Reference {
176    /// Create a new reference.
177    pub fn new(ref_type: RefType, name: &str) -> Self {
178        Self {
179            ref_type,
180            name: name.to_string(),
181            property: None,
182        }
183    }
184
185    /// Create a new reference with a property.
186    pub fn with_property(ref_type: RefType, name: &str, property: &str) -> Self {
187        Self {
188            ref_type,
189            name: name.to_string(),
190            property: Some(property.to_string()),
191        }
192    }
193}
194
195impl std::fmt::Display for Reference {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        if let Some(prop) = &self.property {
198            write!(f, "{}.{}.{}", self.ref_type, self.name, prop)
199        } else {
200            write!(f, "{}.{}", self.ref_type, self.name)
201        }
202    }
203}
204
205/// A literal value in an expression.
206#[derive(Debug, Clone, PartialEq)]
207pub enum Literal {
208    /// String literal (single or double quoted)
209    String(String),
210    /// Integer literal
211    Integer(i64),
212    /// Floating-point literal
213    Float(f64),
214    /// Boolean literal
215    Boolean(bool),
216    /// Null literal
217    Null,
218}
219
220#[allow(clippy::cast_possible_truncation)]
221impl From<Literal> for Value {
222    fn from(lit: Literal) -> Self {
223        match lit {
224            Literal::String(s) => Value::String(s),
225            Literal::Integer(i) => Value::Integer(i),
226            Literal::Float(f) => Value::Integer(f as i64), // Truncate float to integer
227            Literal::Boolean(b) => Value::Boolean(b),
228            Literal::Null => Value::Null,
229        }
230    }
231}
232
233/// A stage in a pipeline.
234#[derive(Debug, Clone, PartialEq)]
235pub struct PipeStage {
236    /// The function name
237    pub function: String,
238    /// Optional arguments to the function
239    pub args: Vec<Expression>,
240}
241
242impl PipeStage {
243    /// Create a new pipe stage with just a function name (no arguments).
244    pub fn new(function: &str) -> Self {
245        Self {
246            function: function.to_string(),
247            args: Vec::new(),
248        }
249    }
250
251    /// Create a new pipe stage with arguments.
252    pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
253        Self {
254            function: function.to_string(),
255            args,
256        }
257    }
258}
259
260/// An expression with its location in the source string.
261#[derive(Debug, Clone, PartialEq)]
262pub struct LocatedExpression {
263    /// The parsed expression
264    pub expression: Expression,
265    /// Byte range in the source string
266    pub range: Range<usize>,
267}
268
269/// A value that may contain deferred expressions (parsed, not yet evaluated).
270///
271/// This is the bridge between the resolver and runtime: the resolver parses
272/// all `${...}` patterns into structured expressions, and the runtime evaluates
273/// them without any string parsing.
274#[derive(Debug, Clone, PartialEq)]
275pub enum ExprValue {
276    /// Fully resolved literal (no expressions)
277    Literal(Literal),
278    /// Single expression (entire value is one expression)
279    Expr(Expression),
280    /// String with embedded expressions: `"Hash: ${artifact.x | sha256}"`
281    Interpolated(Vec<StringPart>),
282    /// Structured object with potentially deferred fields
283    Object(std::collections::HashMap<String, ExprValue>),
284    /// Array of values
285    Array(Vec<ExprValue>),
286}
287
288impl ExprValue {
289    /// Get the value as a literal string, if it is one.
290    pub fn as_literal_string(&self) -> Option<&str> {
291        match self {
292            ExprValue::Literal(Literal::String(s)) => Some(s),
293            _ => None,
294        }
295    }
296
297    /// Get a field from an Object variant.
298    pub fn get(&self, key: &str) -> Option<&ExprValue> {
299        match self {
300            ExprValue::Object(map) => map.get(key),
301            _ => None,
302        }
303    }
304
305    /// Convert to a display string (for script generation, not evaluation).
306    ///
307    /// For literals, returns the literal value.
308    /// For expressions, returns the expression syntax like `"${artifact.ksr | sha256}"`.
309    pub fn to_display_string(&self) -> String {
310        match self {
311            ExprValue::Literal(Literal::String(s)) => s.clone(),
312            ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
313            ExprValue::Literal(Literal::Float(f)) => f.to_string(),
314            ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
315            ExprValue::Literal(Literal::Null) => "null".to_string(),
316            ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
317            ExprValue::Interpolated(parts) => parts
318                .iter()
319                .map(|p| match p {
320                    StringPart::Literal(s) => s.clone(),
321                    StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
322                })
323                .collect(),
324            ExprValue::Object(_) => "[object]".to_string(),
325            ExprValue::Array(_) => "[array]".to_string(),
326        }
327    }
328}
329
330/// Convert an `Expression` back to a string representation.
331fn expr_to_string(expr: &Expression) -> String {
332    match expr {
333        Expression::Reference(r) => r.to_string(),
334        Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
335        Expression::Literal(Literal::Integer(i)) => i.to_string(),
336        Expression::Literal(Literal::Float(f)) => f.to_string(),
337        Expression::Literal(Literal::Boolean(b)) => b.to_string(),
338        Expression::Literal(Literal::Null) => "null".to_string(),
339        Expression::Pipeline { source, stages } => {
340            let mut result = expr_to_string(source);
341            for stage in stages {
342                result.push_str(" | ");
343                result.push_str(&stage.function);
344                if !stage.args.is_empty() {
345                    result.push('(');
346                    let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
347                    result.push_str(&args.join(", "));
348                    result.push(')');
349                }
350            }
351            result
352        }
353    }
354}
355
356/// A part of an interpolated string.
357#[derive(Debug, Clone, PartialEq)]
358pub enum StringPart {
359    /// Literal text
360    Literal(String),
361    /// Embedded expression
362    Expr(Expression),
363}
364
365/// Parse an expression string.
366///
367/// The string should be in the form `${...}` containing a pipeline expression.
368///
369/// # Examples
370///
371/// ```
372/// use rite_model::expression::{parse_expression, Expression, Reference, RefType, PipeStage};
373///
374/// // Simple reference
375/// let expr = parse_expression("${artifact.ksr}").unwrap();
376/// assert!(matches!(expr, Expression::Reference(_)));
377///
378/// // Pipeline
379/// let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
380/// if let Expression::Pipeline { stages, .. } = expr {
381///     assert_eq!(stages.len(), 2);
382///     assert_eq!(stages[0].function, "sha256");
383///     assert_eq!(stages[1].function, "hex");
384/// }
385/// ```
386pub fn parse_expression(s: &str) -> Option<Expression> {
387    let s = s.trim();
388
389    // Must be wrapped in ${ }
390    let inner = s.strip_prefix("${").and_then(|s| s.strip_suffix('}'))?;
391    let inner = inner.trim();
392
393    if inner.is_empty() {
394        return None;
395    }
396
397    parse_pipeline(inner)
398}
399
400/// Parse a pipeline expression (without the `${ }` wrapper).
401fn parse_pipeline(s: &str) -> Option<Expression> {
402    let s = s.trim();
403
404    // Split on pipe operator, being careful about nested parentheses.
405    let parts = split_on_pipes(s);
406
407    if parts.is_empty() {
408        return None;
409    }
410
411    // First part is the source.
412    let source = parse_source(parts.first()?.trim())?;
413
414    if parts.len() == 1 {
415        // No pipeline, just a source.
416        return Some(source);
417    }
418
419    // Rest are pipe stages.
420    let mut stages = Vec::new();
421    for part in parts.iter().skip(1) {
422        let stage = parse_pipe_stage(part.trim())?;
423        stages.push(stage);
424    }
425
426    Some(Expression::Pipeline {
427        source: Box::new(source),
428        stages,
429    })
430}
431
432/// Split a string on pipe operators, respecting parentheses.
433fn split_on_pipes(s: &str) -> Vec<&str> {
434    let mut parts = Vec::new();
435    let mut start = 0;
436    let mut paren_depth: u32 = 0;
437    let bytes = s.as_bytes();
438
439    for (i, &b) in bytes.iter().enumerate() {
440        match b {
441            b'(' => paren_depth += 1,
442            b')' => paren_depth = paren_depth.saturating_sub(1),
443            b'|' if paren_depth == 0 => {
444                parts.push(&s[start..i]);
445                start = i + 1;
446            }
447            _ => {}
448        }
449    }
450
451    // Add the last part.
452    if start < s.len() {
453        parts.push(&s[start..]);
454    }
455
456    parts
457}
458
459/// Parse a source expression (reference, literal, or function call).
460fn parse_source(s: &str) -> Option<Expression> {
461    let s = s.trim();
462
463    // Try to parse as a function call (for concat, etc.)
464    if let Some(expr) = parse_function_call(s) {
465        return Some(expr);
466    }
467
468    // Try to parse as a reference (namespace.name.property).
469    if let Some(expr_ref) = parse_ref(s) {
470        return Some(Expression::Reference(expr_ref));
471    }
472
473    // Try to parse as a literal.
474    if let Some(lit) = parse_literal(s) {
475        return Some(Expression::Literal(lit));
476    }
477
478    None
479}
480
481/// Parse a reference: `namespace.name` or `namespace.name.property`.
482fn parse_ref(s: &str) -> Option<Reference> {
483    let parts: Vec<&str> = s.splitn(3, '.').collect();
484
485    if parts.len() < 2 {
486        return None;
487    }
488
489    let namespace = parts[0];
490    if !is_valid_identifier(namespace) {
491        return None;
492    }
493
494    // Parse and validate namespace.
495    let ref_type: RefType = namespace.parse().ok()?;
496
497    let name = parts[1];
498    if !is_valid_identifier(name) {
499        return None;
500    }
501
502    let property = if parts.len() == 3 {
503        let prop = parts[2];
504        if !is_valid_identifier(prop) {
505            return None;
506        }
507        Some(prop.to_string())
508    } else {
509        None
510    };
511
512    Some(Reference {
513        ref_type,
514        name: name.to_string(),
515        property,
516    })
517}
518
519/// Parse a literal value.
520fn parse_literal(s: &str) -> Option<Literal> {
521    let s = s.trim();
522
523    // String literal (double or single quoted).
524    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
525        let inner = &s[1..s.len() - 1];
526        return Some(Literal::String(inner.to_string()));
527    }
528
529    // Integer literal.
530    if let Ok(i) = s.parse::<i64>() {
531        return Some(Literal::Integer(i));
532    }
533
534    None
535}
536
537/// Parse a function call: `name(arg1, arg2, ...)`
538fn parse_function_call(s: &str) -> Option<Expression> {
539    let s = s.trim();
540
541    // Find the opening paren.
542    let paren_pos = s.find('(')?;
543    let name = &s[..paren_pos];
544
545    if !is_valid_identifier(name) {
546        return None;
547    }
548
549    // Must end with closing paren.
550    if !s.ends_with(')') {
551        return None;
552    }
553
554    let args_str = &s[paren_pos + 1..s.len() - 1];
555
556    // Parse arguments.
557    let args = parse_function_args(args_str)?;
558
559    // For functions like concat, the args ARE the sources, represented as a Pipeline
560    // with a placeholder source and the function as the first stage.
561    Some(Expression::Pipeline {
562        source: Box::new(Expression::Literal(Literal::Integer(0))), // Placeholder
563        stages: vec![PipeStage::with_args(name, args)],
564    })
565}
566
567/// Parse function arguments, handling nested expressions.
568fn parse_function_args(s: &str) -> Option<Vec<Expression>> {
569    let s = s.trim();
570
571    if s.is_empty() {
572        return Some(Vec::new());
573    }
574
575    let mut args = Vec::new();
576    let mut current_start = 0;
577    let mut paren_depth: u32 = 0;
578    let bytes = s.as_bytes();
579
580    for (i, &b) in bytes.iter().enumerate() {
581        match b {
582            b'(' => paren_depth += 1,
583            b')' => paren_depth = paren_depth.saturating_sub(1),
584            b',' if paren_depth == 0 => {
585                let arg = &s[current_start..i];
586                let expr = parse_pipeline(arg.trim())?;
587                args.push(expr);
588                current_start = i + 1;
589            }
590            _ => {}
591        }
592    }
593
594    // Add the last argument.
595    if current_start < s.len() {
596        let arg = &s[current_start..];
597        let expr = parse_pipeline(arg.trim())?;
598        args.push(expr);
599    }
600
601    Some(args)
602}
603
604/// Parse a pipe stage: `function` or `function(args)`.
605fn parse_pipe_stage(s: &str) -> Option<PipeStage> {
606    let s = s.trim();
607
608    // Check for function with arguments.
609    if let Some(paren_pos) = s.find('(') {
610        let name = &s[..paren_pos];
611        if !is_valid_identifier(name) {
612            return None;
613        }
614
615        if !s.ends_with(')') {
616            return None;
617        }
618
619        let args_str = &s[paren_pos + 1..s.len() - 1];
620        let args = parse_function_args(args_str)?;
621
622        return Some(PipeStage::with_args(name, args));
623    }
624
625    // Simple function name.
626    if is_valid_identifier(s) {
627        return Some(PipeStage::new(s));
628    }
629
630    None
631}
632
633/// Check if a string is a valid identifier (ASCII alphanumeric or underscore, not starting
634/// with a digit).
635fn is_valid_identifier(s: &str) -> bool {
636    let mut chars = s.chars();
637    match chars.next() {
638        None => false,
639        Some(first) => {
640            (first.is_ascii_alphabetic() || first == '_')
641                && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
642        }
643    }
644}
645
646/// Find all expressions in a string.
647///
648/// Returns expressions with their locations for LSP support.
649pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
650    let mut results = Vec::new();
651    let bytes = s.as_bytes();
652    let mut i = 0;
653
654    while i < bytes.len() {
655        if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
656            // Find matching closing brace.
657            let mut depth = 1;
658            let mut j = i + 2;
659            while j < bytes.len() && depth > 0 {
660                match bytes[j] {
661                    b'{' => depth += 1,
662                    b'}' => depth -= 1,
663                    _ => {}
664                }
665                j += 1;
666            }
667
668            if depth == 0 {
669                let expr_str = &s[i..j];
670                if let Some(expression) = parse_expression(expr_str) {
671                    results.push(LocatedExpression {
672                        expression,
673                        range: i..j,
674                    });
675                }
676                i = j;
677                continue;
678            }
679        }
680        i += 1;
681    }
682
683    results
684}
685
686/// Parse a JSON value into an `ExprValue`, extracting all `${...}` expressions.
687///
688/// This function recursively converts JSON into `ExprValue`:
689/// - Strings containing `${...}` become `Expr` (if entire string) or `Interpolated` (if mixed)
690/// - Plain strings become `Literal(String)`
691/// - Numbers, bools, null become their `Literal` equivalents
692/// - Objects and arrays are recursed into
693///
694/// # Examples
695///
696/// ```
697/// use rite_model::expression::{parse_expr_value, ExprValue, Literal};
698///
699/// // Plain string → Literal
700/// let json = serde_json::json!("hello");
701/// let expr = parse_expr_value(&json);
702/// assert!(matches!(expr, ExprValue::Literal(Literal::String(_))));
703///
704/// // Expression string → Expr
705/// let json = serde_json::json!("${artifact.ksr | sha256}");
706/// let expr = parse_expr_value(&json);
707/// assert!(matches!(expr, ExprValue::Expr(_)));
708/// ```
709pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
710    match json {
711        serde_json::Value::String(s) => parse_string_to_expr_value(s),
712        serde_json::Value::Number(n) => {
713            if let Some(i) = n.as_i64() {
714                ExprValue::Literal(Literal::Integer(i))
715            } else if let Some(f) = n.as_f64() {
716                ExprValue::Literal(Literal::Float(f))
717            } else {
718                // Fallback for very large numbers: truncate to 0.
719                ExprValue::Literal(Literal::Integer(0))
720            }
721        }
722        serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
723        serde_json::Value::Null => ExprValue::Literal(Literal::Null),
724        serde_json::Value::Object(map) => {
725            let mut result = std::collections::HashMap::new();
726            for (k, v) in map {
727                result.insert(k.clone(), parse_expr_value(v));
728            }
729            ExprValue::Object(result)
730        }
731        serde_json::Value::Array(arr) => {
732            let result: Vec<_> = arr.iter().map(parse_expr_value).collect();
733            ExprValue::Array(result)
734        }
735    }
736}
737
738/// Parse a string into an `ExprValue`.
739///
740/// Handles three cases:
741/// 1. Entire string is one expression: `${...}` → `ExprValue::Expr`
742/// 2. String contains expressions: `"Hash: ${...}"` → `ExprValue::Interpolated`
743/// 3. Plain string: `"hello"` → `ExprValue::Literal`
744fn parse_string_to_expr_value(s: &str) -> ExprValue {
745    let trimmed = s.trim();
746
747    // Case 1: Entire string is a single expression.
748    if trimmed.starts_with("${")
749        && trimmed.ends_with('}')
750        && !trimmed[2..trimmed.len() - 1].contains("${")
751        && let Some(expr) = parse_expression(trimmed)
752    {
753        return ExprValue::Expr(expr);
754    }
755
756    // Case 2: String contains embedded expressions.
757    if s.contains("${") {
758        let parts = parse_interpolated_string(s);
759        if parts.len() == 1 {
760            // Single literal part.
761            if let StringPart::Literal(lit) = &parts[0] {
762                return ExprValue::Literal(Literal::String(lit.clone()));
763            }
764        }
765        return ExprValue::Interpolated(parts);
766    }
767
768    // Case 3: Plain string.
769    ExprValue::Literal(Literal::String(s.to_string()))
770}
771
772/// Parse an interpolated string into parts.
773///
774/// `"Hello ${param.name}!"` → `[Literal("Hello "), Expr(...), Literal("!")]`
775fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
776    let mut parts = Vec::new();
777    let bytes = s.as_bytes();
778    let mut last_end = 0;
779    let mut i = 0;
780
781    while i < bytes.len() {
782        if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
783            // Push literal part before this expression.
784            if i > last_end {
785                parts.push(StringPart::Literal(s[last_end..i].to_string()));
786            }
787
788            // Find matching closing brace.
789            let mut depth = 1;
790            let mut j = i + 2;
791            while j < bytes.len() && depth > 0 {
792                match bytes[j] {
793                    b'{' => depth += 1,
794                    b'}' => depth -= 1,
795                    _ => {}
796                }
797                j += 1;
798            }
799
800            if depth == 0 {
801                let expr_str = &s[i..j];
802                if let Some(expr) = parse_expression(expr_str) {
803                    parts.push(StringPart::Expr(expr));
804                } else {
805                    // Failed to parse: keep as literal.
806                    parts.push(StringPart::Literal(expr_str.to_string()));
807                }
808                last_end = j;
809                i = j;
810                continue;
811            }
812        }
813        i += 1;
814    }
815
816    // Push remaining literal part.
817    if last_end < s.len() {
818        parts.push(StringPart::Literal(s[last_end..].to_string()));
819    }
820
821    parts
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827
828    #[test]
829    fn parse_simple_reference() {
830        let expr = parse_expression("${artifact.ksr}").unwrap();
831        match expr {
832            Expression::Reference(r) => {
833                assert_eq!(r.ref_type, RefType::Artifact);
834                assert_eq!(r.name, "ksr");
835                assert!(r.property.is_none());
836            }
837            _ => panic!("Expected Reference"),
838        }
839    }
840
841    #[test]
842    fn parse_reference_with_property() {
843        let expr = parse_expression("${artifact.keypair.private}").unwrap();
844        match expr {
845            Expression::Reference(r) => {
846                assert_eq!(r.ref_type, RefType::Artifact);
847                assert_eq!(r.name, "keypair");
848                assert_eq!(r.property, Some("private".to_string()));
849            }
850            _ => panic!("Expected Reference"),
851        }
852    }
853
854    #[test]
855    fn parse_param_reference() {
856        let expr = parse_expression("${param.key_label}").unwrap();
857        match expr {
858            Expression::Reference(r) => {
859                assert_eq!(r.ref_type, RefType::Param);
860                assert_eq!(r.name, "key_label");
861            }
862            _ => panic!("Expected Reference"),
863        }
864    }
865
866    #[test]
867    fn parse_simple_pipeline() {
868        let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
869        match expr {
870            Expression::Pipeline { source, stages } => {
871                assert!(matches!(*source, Expression::Reference(_)));
872                assert_eq!(stages.len(), 1);
873                assert_eq!(stages[0].function, "sha256");
874                assert!(stages[0].args.is_empty());
875            }
876            _ => panic!("Expected Pipeline"),
877        }
878    }
879
880    #[test]
881    fn parse_multi_stage_pipeline() {
882        let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
883        match expr {
884            Expression::Pipeline { stages, .. } => {
885                assert_eq!(stages.len(), 2);
886                assert_eq!(stages[0].function, "sha256");
887                assert_eq!(stages[1].function, "hex");
888            }
889            _ => panic!("Expected Pipeline"),
890        }
891    }
892
893    #[test]
894    fn parse_pipeline_with_function_args() {
895        let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
896        match expr {
897            Expression::Pipeline { stages, .. } => {
898                assert_eq!(stages.len(), 1);
899                assert_eq!(stages[0].function, "substr");
900                assert_eq!(stages[0].args.len(), 2);
901            }
902            _ => panic!("Expected Pipeline"),
903        }
904    }
905
906    #[test]
907    fn parse_complex_pipeline() {
908        let expr =
909            parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
910        match expr {
911            Expression::Pipeline { source, stages } => {
912                if let Expression::Reference(r) = *source {
913                    assert_eq!(r.name, "keypair");
914                    assert_eq!(r.property, Some("public".to_string()));
915                }
916                assert_eq!(stages.len(), 3);
917                assert_eq!(stages[0].function, "fingerprint");
918                assert_eq!(stages[1].function, "hex");
919                assert_eq!(stages[2].function, "upper");
920            }
921            _ => panic!("Expected Pipeline"),
922        }
923    }
924
925    #[test]
926    fn parse_invalid_expression() {
927        assert!(parse_expression("not an expression").is_none());
928        assert!(parse_expression("${").is_none());
929        assert!(parse_expression("${}").is_none());
930        assert!(parse_expression("${invalid}").is_none()); // No namespace
931    }
932
933    #[test]
934    fn find_expressions_in_string() {
935        let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
936        assert_eq!(exprs.len(), 1);
937        assert_eq!(exprs[0].range.start, 6);
938    }
939
940    #[test]
941    fn find_multiple_expressions() {
942        let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
943        assert_eq!(exprs.len(), 2);
944    }
945
946    #[test]
947    fn value_type_names() {
948        assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
949        assert_eq!(Value::String(String::new()).type_name(), "string");
950        assert_eq!(Value::Integer(0).type_name(), "integer");
951        assert_eq!(Value::Boolean(true).type_name(), "boolean");
952        assert_eq!(Value::Null.type_name(), "null");
953    }
954
955    #[test]
956    fn value_as_bytes() {
957        let v = Value::Bytes(vec![1, 2, 3]);
958        assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
959
960        let v = Value::String("hello".into());
961        assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
962
963        let v = Value::Integer(42);
964        assert!(v.as_bytes().is_none());
965    }
966
967    mod ceremony_examples {
968        use super::*;
969
970        // From dnssec_signing/dnssec_signing.rite.yaml
971        #[test]
972        fn dnssec_hash_verification() {
973            // actual: "${artifact.ksr | sha256 | hex | substr(0, 16)}"
974            let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
975            match expr {
976                Expression::Pipeline { source, stages } => {
977                    if let Expression::Reference(r) = *source {
978                        assert_eq!(r.ref_type, RefType::Artifact);
979                        assert_eq!(r.name, "ksr");
980                    } else {
981                        panic!("Expected Reference source");
982                    }
983                    // 3 stages: sha256, hex, substr
984                    assert_eq!(stages.len(), 3);
985                    assert_eq!(stages[0].function, "sha256");
986                    assert_eq!(stages[1].function, "hex");
987                    assert_eq!(stages[2].function, "substr");
988                    assert_eq!(stages[2].args.len(), 2);
989                }
990                _ => panic!("Expected Pipeline"),
991            }
992        }
993
994        #[test]
995        fn dnssec_param_reference() {
996            // expected: "${param.expected_ksr_hash}"
997            let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
998            match expr {
999                Expression::Reference(r) => {
1000                    assert_eq!(r.ref_type, RefType::Param);
1001                    assert_eq!(r.name, "expected_ksr_hash");
1002                }
1003                _ => panic!("Expected Reference"),
1004            }
1005        }
1006
1007        #[test]
1008        fn dnssec_oral_readback() {
1009            // value: "${artifact.ksr | sha256 | hex}"
1010            let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1011            match expr {
1012                Expression::Pipeline { stages, .. } => {
1013                    assert_eq!(stages.len(), 2);
1014                    assert_eq!(stages[0].function, "sha256");
1015                    assert_eq!(stages[1].function, "hex");
1016                }
1017                _ => panic!("Expected Pipeline"),
1018            }
1019        }
1020
1021        // From airgapped_hsm/airgapped_hsm.rite.yaml
1022        #[test]
1023        fn hsm_artifact_reference() {
1024            // input: ${artifact.master_keypair}
1025            let expr = parse_expression("${artifact.master_keypair}").unwrap();
1026            match expr {
1027                Expression::Reference(r) => {
1028                    assert_eq!(r.ref_type, RefType::Artifact);
1029                    assert_eq!(r.name, "master_keypair");
1030                    assert!(r.property.is_none());
1031                }
1032                _ => panic!("Expected Reference"),
1033            }
1034        }
1035
1036        #[test]
1037        fn hsm_public_key_fingerprint() {
1038            // value: "${artifact.master_public_key | sha256 | hex}"
1039            let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1040            match expr {
1041                Expression::Pipeline { source, stages } => {
1042                    if let Expression::Reference(r) = *source {
1043                        assert_eq!(r.name, "master_public_key");
1044                    }
1045                    assert_eq!(stages.len(), 2);
1046                }
1047                _ => panic!("Expected Pipeline"),
1048            }
1049        }
1050
1051        // From crypto_test/crypto_test.rite.yaml
1052        #[test]
1053        fn crypto_artifact_with_property() {
1054            // key_to_wrap: ${artifact.keypair.private}
1055            let expr = parse_expression("${artifact.keypair.private}").unwrap();
1056            match expr {
1057                Expression::Reference(r) => {
1058                    assert_eq!(r.ref_type, RefType::Artifact);
1059                    assert_eq!(r.name, "keypair");
1060                    assert_eq!(r.property, Some("private".to_string()));
1061                }
1062                _ => panic!("Expected Reference"),
1063            }
1064        }
1065
1066        // String interpolation patterns
1067        #[test]
1068        fn find_embedded_expressions() {
1069            // message: "Verify serial number: ${param.hsm_serial}"
1070            let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1071            assert_eq!(exprs.len(), 1);
1072            if let Expression::Reference(r) = &exprs[0].expression {
1073                assert_eq!(r.ref_type, RefType::Param);
1074                assert_eq!(r.name, "hsm_serial");
1075            }
1076        }
1077
1078        #[test]
1079        fn find_multiple_embedded_params() {
1080            // "Key ${param.key_label} for HSM ${param.hsm_serial}"
1081            let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1082            assert_eq!(exprs.len(), 2);
1083        }
1084
1085        // Role references (namespace = "role" - used in validation, not expressions)
1086        #[test]
1087        fn role_reference_parses() {
1088            let expr = parse_expression("${role.ceremony_admin}").unwrap();
1089            match expr {
1090                Expression::Reference(r) => {
1091                    assert_eq!(r.ref_type, RefType::Role);
1092                    assert_eq!(r.name, "ceremony_admin");
1093                }
1094                _ => panic!("Expected Reference"),
1095            }
1096        }
1097    }
1098
1099    mod expr_value_tests {
1100        use super::*;
1101
1102        #[test]
1103        fn parse_literal_string() {
1104            let json = serde_json::json!("hello");
1105            let expr = parse_expr_value(&json);
1106            assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1107        }
1108
1109        #[test]
1110        fn parse_literal_integer() {
1111            let json = serde_json::json!(42);
1112            let expr = parse_expr_value(&json);
1113            assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1114        }
1115
1116        #[test]
1117        fn parse_literal_float() {
1118            let json = serde_json::json!(1.5);
1119            let expr = parse_expr_value(&json);
1120            assert!(
1121                matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1122            );
1123        }
1124
1125        #[test]
1126        fn parse_literal_boolean() {
1127            let json = serde_json::json!(true);
1128            let expr = parse_expr_value(&json);
1129            assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1130        }
1131
1132        #[test]
1133        fn parse_literal_null() {
1134            let json = serde_json::Value::Null;
1135            let expr = parse_expr_value(&json);
1136            assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1137        }
1138
1139        #[test]
1140        fn parse_single_expression() {
1141            let json = serde_json::json!("${artifact.ksr | sha256}");
1142            let expr = parse_expr_value(&json);
1143            match expr {
1144                ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1145                    assert!(matches!(*source, Expression::Reference(_)));
1146                    assert_eq!(stages.len(), 1);
1147                    assert_eq!(stages[0].function, "sha256");
1148                }
1149                _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1150            }
1151        }
1152
1153        #[test]
1154        fn parse_simple_reference() {
1155            let json = serde_json::json!("${param.name}");
1156            let expr = parse_expr_value(&json);
1157            match expr {
1158                ExprValue::Expr(Expression::Reference(r)) => {
1159                    assert_eq!(r.ref_type, RefType::Param);
1160                    assert_eq!(r.name, "name");
1161                }
1162                _ => panic!("Expected Expr(Reference), got {expr:?}"),
1163            }
1164        }
1165
1166        #[test]
1167        fn parse_interpolated_string() {
1168            let json = serde_json::json!("Hello, ${param.name}!");
1169            let expr = parse_expr_value(&json);
1170            match expr {
1171                ExprValue::Interpolated(parts) => {
1172                    assert_eq!(parts.len(), 3);
1173                    assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1174                    assert!(matches!(
1175                        &parts[1],
1176                        StringPart::Expr(Expression::Reference(_))
1177                    ));
1178                    assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1179                }
1180                _ => panic!("Expected Interpolated, got {expr:?}"),
1181            }
1182        }
1183
1184        #[test]
1185        fn parse_object_with_expressions() {
1186            let json = serde_json::json!({
1187                "actual": "${artifact.ksr | sha256 | hex}",
1188                "expected": "${param.expected_hash}",
1189                "static": "hello"
1190            });
1191            let expr = parse_expr_value(&json);
1192            match expr {
1193                ExprValue::Object(map) => {
1194                    assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1195                    assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1196                    assert!(matches!(
1197                        map.get("static"),
1198                        Some(ExprValue::Literal(Literal::String(_)))
1199                    ));
1200                }
1201                _ => panic!("Expected Object, got {expr:?}"),
1202            }
1203        }
1204
1205        #[test]
1206        fn parse_array_with_expressions() {
1207            let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1208            let expr = parse_expr_value(&json);
1209            match expr {
1210                ExprValue::Array(arr) => {
1211                    assert_eq!(arr.len(), 3);
1212                    assert!(matches!(&arr[0], ExprValue::Expr(_)));
1213                    assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1214                    assert!(matches!(&arr[2], ExprValue::Expr(_)));
1215                }
1216                _ => panic!("Expected Array, got {expr:?}"),
1217            }
1218        }
1219
1220        #[test]
1221        fn parse_multiple_expressions_in_string() {
1222            let json = serde_json::json!("${param.a} and ${param.b}");
1223            let expr = parse_expr_value(&json);
1224            match expr {
1225                ExprValue::Interpolated(parts) => {
1226                    assert_eq!(parts.len(), 3);
1227                    assert!(matches!(&parts[0], StringPart::Expr(_)));
1228                    assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1229                    assert!(matches!(&parts[2], StringPart::Expr(_)));
1230                }
1231                _ => panic!("Expected Interpolated, got {expr:?}"),
1232            }
1233        }
1234    }
1235}