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 RefType {
48    /// Every namespace, in the order messages list them.
49    ///
50    /// A test matches exhaustively over `RefType` against this list, so a new
51    /// variant fails to compile until it is added here.
52    pub const ALL: &'static [Self] = &[Self::Param, Self::Role, Self::Artifact];
53
54    /// The namespace as it is written in a ceremony.
55    #[must_use]
56    pub const fn as_str(self) -> &'static str {
57        match self {
58            Self::Param => "param",
59            Self::Role => "role",
60            Self::Artifact => "artifact",
61        }
62    }
63}
64
65impl std::str::FromStr for RefType {
66    type Err = String;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        Self::ALL
70            .iter()
71            .copied()
72            .find(|ref_type| ref_type.as_str() == s)
73            .ok_or_else(|| format!("unknown reference type: {s}"))
74    }
75}
76
77impl std::fmt::Display for RefType {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.write_str(self.as_str())
80    }
81}
82
83/// A runtime value in the expression system.
84#[derive(Debug, Clone, PartialEq)]
85pub enum Value {
86    /// Raw binary data (computation core)
87    Bytes(Vec<u8>),
88    /// UTF-8 text string
89    String(String),
90    /// Integer value
91    Integer(i64),
92    /// Boolean value
93    Boolean(bool),
94    /// Null/missing value
95    Null,
96}
97
98impl Value {
99    /// Try to get this value as bytes.
100    /// Strings are converted to UTF-8 bytes.
101    pub fn as_bytes(&self) -> Option<Vec<u8>> {
102        match self {
103            Value::Bytes(b) => Some(b.clone()),
104            Value::String(s) => Some(s.as_bytes().to_vec()),
105            _ => None,
106        }
107    }
108
109    /// Try to get this value as a string.
110    pub fn as_string(&self) -> Option<&str> {
111        match self {
112            Value::String(s) => Some(s),
113            _ => None,
114        }
115    }
116
117    /// Try to get this value as an integer.
118    pub fn as_integer(&self) -> Option<i64> {
119        match self {
120            Value::Integer(i) => Some(*i),
121            _ => None,
122        }
123    }
124
125    /// Check if this value is null.
126    pub fn is_null(&self) -> bool {
127        matches!(self, Value::Null)
128    }
129
130    /// Get a human-readable type name for error messages.
131    pub fn type_name(&self) -> &'static str {
132        match self {
133            Value::Bytes(_) => "bytes",
134            Value::String(_) => "string",
135            Value::Integer(_) => "integer",
136            Value::Boolean(_) => "boolean",
137            Value::Null => "null",
138        }
139    }
140}
141
142impl std::fmt::Display for Value {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        match self {
145            Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
146            Value::String(s) => write!(f, "{s}"),
147            Value::Integer(i) => write!(f, "{i}"),
148            Value::Boolean(b) => write!(f, "{b}"),
149            Value::Null => write!(f, "null"),
150        }
151    }
152}
153
154/// A parsed expression from ceremony YAML.
155///
156/// Expressions can be:
157/// - Simple references: `${artifact.ksr}`
158/// - Pipelines: `${artifact.ksr | sha256 | hex}`
159/// - Function calls with arguments: `${concat(a | sha256, b | sha256) | hex}`
160#[derive(Debug, Clone, PartialEq)]
161pub enum Expression {
162    /// A simple reference to a parameter or artifact
163    Reference(Reference),
164
165    /// A literal value
166    Literal(Literal),
167
168    /// A pipeline of transformations
169    Pipeline {
170        /// The starting value (reference or literal)
171        source: Box<Expression>,
172        /// The transformation stages
173        stages: Vec<PipeStage>,
174    },
175}
176
177/// A reference to a ceremony value: `${type.name}` or `${type.name.property}`.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Reference {
180    /// The reference type (param, artifact, role)
181    pub ref_type: RefType,
182    /// The primary identifier
183    pub name: String,
184    /// Optional sub-property (e.g., `private` in `artifact.keypair.private`)
185    pub property: Option<String>,
186}
187
188impl Reference {
189    /// Create a new reference.
190    pub fn new(ref_type: RefType, name: &str) -> Self {
191        Self {
192            ref_type,
193            name: name.to_string(),
194            property: None,
195        }
196    }
197
198    /// Create a new reference with a property.
199    pub fn with_property(ref_type: RefType, name: &str, property: &str) -> Self {
200        Self {
201            ref_type,
202            name: name.to_string(),
203            property: Some(property.to_string()),
204        }
205    }
206}
207
208impl std::fmt::Display for Reference {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        if let Some(prop) = &self.property {
211            write!(f, "{}.{}.{}", self.ref_type, self.name, prop)
212        } else {
213            write!(f, "{}.{}", self.ref_type, self.name)
214        }
215    }
216}
217
218/// A literal value in an expression.
219#[derive(Debug, Clone, PartialEq)]
220pub enum Literal {
221    /// String literal (single or double quoted)
222    String(String),
223    /// Integer literal
224    Integer(i64),
225    /// Floating-point literal
226    Float(f64),
227    /// Boolean literal
228    Boolean(bool),
229    /// Null literal
230    Null,
231}
232
233#[allow(clippy::cast_possible_truncation)]
234impl From<Literal> for Value {
235    fn from(lit: Literal) -> Self {
236        match lit {
237            Literal::String(s) => Value::String(s),
238            Literal::Integer(i) => Value::Integer(i),
239            Literal::Float(f) => Value::Integer(f as i64), // Truncate float to integer
240            Literal::Boolean(b) => Value::Boolean(b),
241            Literal::Null => Value::Null,
242        }
243    }
244}
245
246/// A stage in a pipeline.
247#[derive(Debug, Clone, PartialEq)]
248pub struct PipeStage {
249    /// The function name
250    pub function: String,
251    /// Optional arguments to the function
252    pub args: Vec<Expression>,
253}
254
255impl PipeStage {
256    /// Create a new pipe stage with just a function name (no arguments).
257    pub fn new(function: &str) -> Self {
258        Self {
259            function: function.to_string(),
260            args: Vec::new(),
261        }
262    }
263
264    /// Create a new pipe stage with arguments.
265    pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
266        Self {
267            function: function.to_string(),
268            args,
269        }
270    }
271}
272
273/// An expression with its location in the source string.
274#[derive(Debug, Clone, PartialEq)]
275pub struct LocatedExpression {
276    /// The parsed expression
277    pub expression: Expression,
278    /// Byte range in the source string
279    pub range: Range<usize>,
280}
281
282/// A value that may contain deferred expressions (parsed, not yet evaluated).
283///
284/// This is the bridge between the resolver and runtime: the resolver parses
285/// all `${...}` patterns into structured expressions, and the runtime evaluates
286/// them without any string parsing.
287#[derive(Debug, Clone, PartialEq)]
288pub enum ExprValue {
289    /// Fully resolved literal (no expressions)
290    Literal(Literal),
291    /// Single expression (entire value is one expression)
292    Expr(Expression),
293    /// String with embedded expressions: `"Hash: ${artifact.x | sha256}"`
294    Interpolated(Vec<StringPart>),
295    /// Structured object with potentially deferred fields
296    Object(std::collections::HashMap<String, ExprValue>),
297    /// Array of values
298    Array(Vec<ExprValue>),
299}
300
301impl ExprValue {
302    /// Get the value as a literal string, if it is one.
303    pub fn as_literal_string(&self) -> Option<&str> {
304        match self {
305            ExprValue::Literal(Literal::String(s)) => Some(s),
306            _ => None,
307        }
308    }
309
310    /// Get a field from an Object variant.
311    pub fn get(&self, key: &str) -> Option<&ExprValue> {
312        match self {
313            ExprValue::Object(map) => map.get(key),
314            _ => None,
315        }
316    }
317
318    /// Convert to a display string (for script generation, not evaluation).
319    ///
320    /// For literals, returns the literal value.
321    /// For expressions, returns the expression syntax like `"${artifact.ksr | sha256}"`.
322    pub fn to_display_string(&self) -> String {
323        match self {
324            ExprValue::Literal(Literal::String(s)) => s.clone(),
325            ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
326            ExprValue::Literal(Literal::Float(f)) => f.to_string(),
327            ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
328            ExprValue::Literal(Literal::Null) => "null".to_string(),
329            ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
330            ExprValue::Interpolated(parts) => parts
331                .iter()
332                .map(|p| match p {
333                    StringPart::Literal(s) => s.clone(),
334                    StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
335                })
336                .collect(),
337            ExprValue::Object(_) => "[object]".to_string(),
338            ExprValue::Array(_) => "[array]".to_string(),
339        }
340    }
341}
342
343/// Convert an `Expression` back to a string representation.
344fn expr_to_string(expr: &Expression) -> String {
345    match expr {
346        Expression::Reference(r) => r.to_string(),
347        Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
348        Expression::Literal(Literal::Integer(i)) => i.to_string(),
349        Expression::Literal(Literal::Float(f)) => f.to_string(),
350        Expression::Literal(Literal::Boolean(b)) => b.to_string(),
351        Expression::Literal(Literal::Null) => "null".to_string(),
352        Expression::Pipeline { source, stages } => {
353            let mut result = expr_to_string(source);
354            for stage in stages {
355                result.push_str(" | ");
356                result.push_str(&stage.function);
357                if !stage.args.is_empty() {
358                    result.push('(');
359                    let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
360                    result.push_str(&args.join(", "));
361                    result.push(')');
362                }
363            }
364            result
365        }
366    }
367}
368
369/// A part of an interpolated string.
370#[derive(Debug, Clone, PartialEq)]
371pub enum StringPart {
372    /// Literal text
373    Literal(String),
374    /// Embedded expression
375    Expr(Expression),
376}
377
378/// Why a `${...}` occurrence is not a usable expression.
379///
380/// Each variant names one decision the parser made, so a caller can tell an
381/// unknown namespace from a malformed name without reading the message text.
382/// [`Display`](std::fmt::Display) renders the sentence a diagnostic shows.
383#[derive(Debug, Clone, PartialEq, Eq)]
384#[non_exhaustive]
385pub enum ExprError {
386    /// The text is not wrapped in `${` and `}`.
387    NotAnExpression,
388    /// Nothing closes the occurrence.
389    Unclosed,
390    /// The braces hold nothing.
391    Empty,
392    /// A pipeline has stages but nothing to compute them from.
393    MissingSource,
394    /// A quoted string never closes.
395    UnterminatedString {
396        /// The quote character that opened it.
397        quote: char,
398    },
399    /// The text before a `(` is not a function name.
400    InvalidFunctionName {
401        /// The text as written.
402        name: String,
403    },
404    /// A function call never closes its parentheses.
405    UnclosedCall {
406        /// The function named.
407        function: String,
408    },
409    /// One argument to a function call is not an expression.
410    InvalidArgument {
411        /// The function named.
412        function: String,
413        /// Which argument, counting from one.
414        position: usize,
415        /// Why that argument is not an expression.
416        cause: Box<ExprError>,
417    },
418    /// A name stands on its own, with no namespace in front of it.
419    MissingNamespace {
420        /// The text as written.
421        text: String,
422    },
423    /// The text is neither a literal nor the start of a reference.
424    NotAReference {
425        /// The text as written.
426        text: String,
427    },
428    /// The namespace is not one this DSL has.
429    UnknownNamespace {
430        /// The namespace as written.
431        namespace: String,
432    },
433    /// A reference names `material`, which is not a namespace.
434    MaterialNamespace {
435        /// The namespace as written, which may be a misspelling.
436        namespace: String,
437        /// The name that followed it.
438        name: String,
439    },
440    /// Nothing follows the namespace.
441    EmptyName {
442        /// The namespace as written.
443        namespace: String,
444    },
445    /// The name is not an identifier.
446    InvalidName {
447        /// The name as written.
448        name: String,
449    },
450    /// The property is not an identifier.
451    InvalidProperty {
452        /// The property as written.
453        property: String,
454    },
455    /// A pipe stage is not a function name.
456    InvalidPipeStage {
457        /// The stage as written.
458        stage: String,
459    },
460}
461
462/// What a name inside a reference may contain, quoted in messages about one
463/// that does not qualify.
464const IDENTIFIER_RULE: &str =
465    "names hold letters, digits, and underscores, and cannot start with a digit";
466
467impl std::fmt::Display for ExprError {
468    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469        match self {
470            Self::NotAnExpression => write!(
471                f,
472                "write it as an expression, '${{<namespace>.<name>}}', \
473                 where <namespace> is one of {}",
474                namespace_list()
475            ),
476            Self::Unclosed => write!(f, "the expression is missing its closing '}}'"),
477            Self::Empty => write!(f, "the expression is empty"),
478            Self::MissingSource => write!(
479                f,
480                "nothing comes before the '|': a pipeline starts from a value"
481            ),
482            Self::UnterminatedString { quote } => {
483                write!(f, "the string is missing its closing {quote}")
484            }
485            Self::InvalidFunctionName { name } => {
486                write!(f, "'{name}' is not a usable function name")
487            }
488            Self::UnclosedCall { function } => {
489                write!(f, "the call to '{function}' is missing its closing ')'")
490            }
491            Self::InvalidArgument {
492                function,
493                position,
494                cause,
495            } => write!(
496                f,
497                "argument {position} to '{function}' is not a usable expression: {cause}"
498            ),
499            Self::MissingNamespace { text } => write!(
500                f,
501                "'{text}' names no namespace: write '<namespace>.<name>', \
502                 where <namespace> is one of {}",
503                namespace_list()
504            ),
505            Self::NotAReference { text } => write!(
506                f,
507                "'{text}' is neither a literal nor a reference: a reference starts with \
508                 one of {}",
509                namespace_list()
510            ),
511            Self::UnknownNamespace { namespace } => match nearest_namespace(namespace) {
512                Some(known) => {
513                    write!(
514                        f,
515                        "unknown namespace '{namespace}': did you mean '{known}'?"
516                    )
517                }
518                None => write!(
519                    f,
520                    "unknown namespace '{namespace}': expected one of {}",
521                    namespace_list()
522                ),
523            },
524            Self::MaterialNamespace { namespace, name } => write!(
525                f,
526                "there is no '{namespace}' namespace: a material is read as 'artifact.{name}'"
527            ),
528            Self::EmptyName { namespace } => {
529                write!(f, "nothing follows '{namespace}.': expected a name")
530            }
531            Self::InvalidName { name } => {
532                write!(f, "'{name}' is not a usable name: {IDENTIFIER_RULE}")
533            }
534            Self::InvalidProperty { property } => {
535                write!(
536                    f,
537                    "'{property}' is not a usable property: {IDENTIFIER_RULE}"
538                )
539            }
540            Self::InvalidPipeStage { stage } => write!(
541                f,
542                "'{stage}' is not a usable pipe stage: expected a function name, \
543                 with any arguments in parentheses"
544            ),
545        }
546    }
547}
548
549impl ExprError {
550    /// The namespace this failure was close enough to have been meant as.
551    ///
552    /// An editor offers it as a fix. The message says the same thing, so the
553    /// two cannot disagree.
554    #[must_use]
555    pub fn suggestion(&self) -> Option<RefType> {
556        match self {
557            Self::UnknownNamespace { namespace } => nearest_namespace(namespace),
558            // A material is read as an artifact, so the namespace to write is
559            // known outright rather than guessed at.
560            Self::MaterialNamespace { .. } => Some(RefType::Artifact),
561            Self::NotAnExpression
562            | Self::Unclosed
563            | Self::Empty
564            | Self::MissingSource
565            | Self::UnterminatedString { .. }
566            | Self::InvalidFunctionName { .. }
567            | Self::UnclosedCall { .. }
568            | Self::InvalidArgument { .. }
569            | Self::MissingNamespace { .. }
570            | Self::NotAReference { .. }
571            | Self::EmptyName { .. }
572            | Self::InvalidName { .. }
573            | Self::InvalidProperty { .. }
574            | Self::InvalidPipeStage { .. } => None,
575        }
576    }
577}
578
579impl std::error::Error for ExprError {
580    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
581        match self {
582            Self::InvalidArgument { cause, .. } => Some(cause.as_ref()),
583            Self::NotAnExpression
584            | Self::Unclosed
585            | Self::Empty
586            | Self::MissingSource
587            | Self::UnterminatedString { .. }
588            | Self::InvalidFunctionName { .. }
589            | Self::UnclosedCall { .. }
590            | Self::MissingNamespace { .. }
591            | Self::NotAReference { .. }
592            | Self::UnknownNamespace { .. }
593            | Self::MaterialNamespace { .. }
594            | Self::EmptyName { .. }
595            | Self::InvalidName { .. }
596            | Self::InvalidProperty { .. }
597            | Self::InvalidPipeStage { .. } => None,
598        }
599    }
600}
601
602/// The namespaces a reference can name, formatted for a message.
603fn namespace_list() -> String {
604    RefType::ALL
605        .iter()
606        .map(|ref_type| ref_type.as_str())
607        .collect::<Vec<_>>()
608        .join(", ")
609}
610
611/// Parse an expression string.
612///
613/// The string should be in the form `${...}` containing a pipeline expression.
614/// Returns `None` for anything else; [`parse_expression_detailed`] returns the
615/// reason instead.
616///
617/// # Examples
618///
619/// ```
620/// use rite_model::expression::{parse_expression, Expression, Reference, RefType, PipeStage};
621///
622/// // Simple reference
623/// let expr = parse_expression("${artifact.ksr}").unwrap();
624/// assert!(matches!(expr, Expression::Reference(_)));
625///
626/// // Pipeline
627/// let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
628/// if let Expression::Pipeline { stages, .. } = expr {
629///     assert_eq!(stages.len(), 2);
630///     assert_eq!(stages[0].function, "sha256");
631///     assert_eq!(stages[1].function, "hex");
632/// }
633/// ```
634pub fn parse_expression(s: &str) -> Option<Expression> {
635    parse_expression_detailed(s).ok()
636}
637
638/// Parse an expression string, reporting why it is not one.
639///
640/// The counterpart to [`parse_expression`], for callers that put the failure in
641/// front of an author. Accepts exactly the same strings.
642///
643/// # Examples
644///
645/// ```
646/// use rite_model::expression::{parse_expression_detailed, ExprError, RefType};
647///
648/// let err = parse_expression_detailed("${nonsense.x}").unwrap_err();
649/// assert!(matches!(err, ExprError::UnknownNamespace { .. }));
650///
651/// let err = parse_expression_detailed("${paramm.region}").unwrap_err();
652/// assert_eq!(err.to_string(), "unknown namespace 'paramm': did you mean 'param'?");
653/// ```
654///
655/// # Errors
656///
657/// Returns the [`ExprError`] naming the first thing that did not parse.
658pub fn parse_expression_detailed(s: &str) -> Result<Expression, ExprError> {
659    // Text that never opened an expression is a different mistake from text
660    // that opened one and did not close it, and only the wrapper knows which.
661    let Some(rest) = s.trim().strip_prefix("${") else {
662        return Err(ExprError::NotAnExpression);
663    };
664    let Some(inner) = rest.strip_suffix('}') else {
665        return Err(ExprError::Unclosed);
666    };
667
668    parse_pipeline(inner)
669}
670
671/// Parse a pipeline expression (without the `${ }` wrapper).
672fn parse_pipeline(s: &str) -> Result<Expression, ExprError> {
673    let s = s.trim();
674
675    // Split on pipe operator, being careful about nested parentheses.
676    let parts = split_top_level(s, b'|');
677    let first = parts.first().copied().unwrap_or("").trim();
678
679    // An empty source with stages after it is a pipeline that starts from
680    // nothing, which is a different mistake from an empty `${}`.
681    if first.is_empty() && parts.len() > 1 {
682        return Err(ExprError::MissingSource);
683    }
684
685    let source = parse_source(first)?;
686    if parts.len() == 1 {
687        // No pipeline, just a source.
688        return Ok(source);
689    }
690
691    let mut stages = Vec::with_capacity(parts.len().saturating_sub(1));
692    for part in parts.iter().skip(1) {
693        stages.push(parse_pipe_stage(part.trim())?);
694    }
695
696    Ok(Expression::Pipeline {
697        source: Box::new(source),
698        stages,
699    })
700}
701
702/// Split a string on `delimiter`, ignoring any inside parentheses.
703///
704/// Serves both delimiters the grammar has: `|` between pipeline stages and `,`
705/// between call arguments. An empty string splits into nothing.
706fn split_top_level(s: &str, delimiter: u8) -> Vec<&str> {
707    let mut parts = Vec::new();
708    let mut start = 0;
709    let mut paren_depth: u32 = 0;
710    let bytes = s.as_bytes();
711
712    for (i, &b) in bytes.iter().enumerate() {
713        match b {
714            b'(' => paren_depth += 1,
715            b')' => paren_depth = paren_depth.saturating_sub(1),
716            _ if b == delimiter && paren_depth == 0 => {
717                parts.push(&s[start..i]);
718                start = i + 1;
719            }
720            _ => {}
721        }
722    }
723
724    // Add the last part.
725    if start < s.len() {
726        parts.push(&s[start..]);
727    }
728
729    parts
730}
731
732/// Parse the part of a pipeline before the first `|`.
733///
734/// A source is a quoted string, an integer, a function call, or a reference.
735/// Which one is decided from the text before any of them is parsed: a quote
736/// opens a string, a name followed by `(` opens a call, and a dot separates a
737/// namespace from a name. A failure then reports the shape the author reached
738/// for.
739fn parse_source(s: &str) -> Result<Expression, ExprError> {
740    let s = s.trim();
741
742    if s.is_empty() {
743        return Err(ExprError::Empty);
744    }
745
746    if let Some(quote) = s.chars().next().filter(|c| *c == '"' || *c == '\'') {
747        return parse_string_literal(s, quote).map(Expression::Literal);
748    }
749
750    if let Some(paren) = s.find('(')
751        && is_valid_identifier(&s[..paren])
752    {
753        // The arguments are the sources, so the source slot holds a placeholder.
754        return parse_call(s, paren).map(|stage| Expression::Pipeline {
755            source: Box::new(Expression::Literal(Literal::Integer(0))),
756            stages: vec![stage],
757        });
758    }
759
760    if let Some((namespace, rest)) = s.split_once('.') {
761        return parse_ref(namespace, rest).map(Expression::Reference);
762    }
763
764    if let Ok(value) = s.parse::<i64>() {
765        return Ok(Expression::Literal(Literal::Integer(value)));
766    }
767
768    // A bare name is a reference that lost its namespace, which is a different
769    // mistake from text that was never going to be one.
770    if is_valid_identifier(s) {
771        return Err(ExprError::MissingNamespace {
772            text: s.to_string(),
773        });
774    }
775
776    Err(ExprError::NotAReference {
777        text: s.to_string(),
778    })
779}
780
781/// Parse a reference split at its first dot: `name` or `name.property`.
782fn parse_ref(namespace: &str, rest: &str) -> Result<Reference, ExprError> {
783    // Not a name at all, so it was never a reference. Says so rather than
784    // reporting an unknown namespace, which would send the author looking for
785    // the wrong mistake.
786    if !is_valid_identifier(namespace) {
787        return Err(ExprError::NotAReference {
788            text: format!("{namespace}.{rest}"),
789        });
790    }
791
792    let (name, property) = match rest.split_once('.') {
793        Some((name, property)) => (name, Some(property)),
794        None => (rest, None),
795    };
796
797    let Ok(ref_type) = namespace.parse::<RefType>() else {
798        return Err(unknown_namespace(namespace, name));
799    };
800
801    if name.is_empty() {
802        return Err(ExprError::EmptyName {
803            namespace: namespace.to_string(),
804        });
805    }
806    if !is_valid_identifier(name) {
807        return Err(ExprError::InvalidName {
808            name: name.to_string(),
809        });
810    }
811
812    if let Some(property) = property
813        && !is_valid_identifier(property)
814    {
815        return Err(ExprError::InvalidProperty {
816            property: property.to_string(),
817        });
818    }
819
820    Ok(Reference {
821        ref_type,
822        name: name.to_string(),
823        property: property.map(ToString::to_string),
824    })
825}
826
827/// Classify a namespace that is not one this DSL has.
828fn unknown_namespace(namespace: &str, name: &str) -> ExprError {
829    // `material` is the namespace an author reaches for that does not exist:
830    // materials are declared under `materials:` and read as artifacts, so the
831    // mistake is worth naming rather than guessing at. Matched within one edit
832    // so that the plural, which is what the declaring key is called, lands on
833    // the same explanation.
834    if edit_distance(namespace, "material") <= 1 {
835        return ExprError::MaterialNamespace {
836            namespace: namespace.to_string(),
837            name: name.to_string(),
838        };
839    }
840
841    ExprError::UnknownNamespace {
842        namespace: namespace.to_string(),
843    }
844}
845
846/// The namespace `s` was close enough to have been meant as, if any.
847///
848/// Close enough is an edit distance of one or two, which covers a doubled or
849/// dropped letter and a plural. Anything further is reported as unknown rather
850/// than guessed at.
851fn nearest_namespace(s: &str) -> Option<RefType> {
852    RefType::ALL
853        .iter()
854        .map(|known| (*known, edit_distance(s, known.as_str())))
855        .filter(|(_, distance)| *distance <= 2)
856        .min_by_key(|(_, distance)| *distance)
857        .map(|(known, _)| known)
858}
859
860/// Levenshtein distance between two strings.
861fn edit_distance(a: &str, b: &str) -> usize {
862    let b_chars: Vec<char> = b.chars().collect();
863    let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
864    let mut current = vec![0; b_chars.len() + 1];
865
866    for (i, a_char) in a.chars().enumerate() {
867        current[0] = i + 1;
868        for (j, b_char) in b_chars.iter().enumerate() {
869            let substitution = previous[j] + usize::from(a_char != *b_char);
870            let deletion = previous[j + 1] + 1;
871            let insertion = current[j] + 1;
872            current[j + 1] = substitution.min(deletion).min(insertion);
873        }
874        std::mem::swap(&mut previous, &mut current);
875    }
876
877    previous[b_chars.len()]
878}
879
880/// Parse a quoted string literal, given the quote character that opened it.
881fn parse_string_literal(s: &str, quote: char) -> Result<Literal, ExprError> {
882    // Both quote characters are one byte, so the closing one cannot be the
883    // opening one read twice: a lone quote is unterminated.
884    if s.len() < 2 || !s.ends_with(quote) {
885        return Err(ExprError::UnterminatedString { quote });
886    }
887
888    Ok(Literal::String(s[1..s.len() - 1].to_string()))
889}
890
891/// Parse a call `name(arg, ...)`, given the position of its `(`.
892///
893/// Serves both positions a call appears in: as a pipeline source, where the
894/// arguments are the sources, and as a pipe stage.
895fn parse_call(s: &str, paren: usize) -> Result<PipeStage, ExprError> {
896    let name = &s[..paren];
897
898    if !is_valid_identifier(name) {
899        return Err(ExprError::InvalidFunctionName {
900            name: name.to_string(),
901        });
902    }
903    if !s.ends_with(')') {
904        return Err(ExprError::UnclosedCall {
905            function: name.to_string(),
906        });
907    }
908
909    // Trimmed first so that `f(  )` splits into no arguments rather than one
910    // blank one.
911    let args = split_top_level(s[paren + 1..s.len() - 1].trim(), b',')
912        .into_iter()
913        .enumerate()
914        .map(|(index, arg)| {
915            parse_pipeline(arg.trim()).map_err(|cause| ExprError::InvalidArgument {
916                function: name.to_string(),
917                position: index + 1,
918                cause: Box::new(cause),
919            })
920        })
921        .collect::<Result<Vec<_>, _>>()?;
922
923    Ok(PipeStage::with_args(name, args))
924}
925
926/// Parse a pipe stage: `function` or `function(args)`.
927fn parse_pipe_stage(s: &str) -> Result<PipeStage, ExprError> {
928    let s = s.trim();
929
930    if let Some(paren) = s.find('(') {
931        return parse_call(s, paren);
932    }
933
934    // Simple function name.
935    if is_valid_identifier(s) {
936        return Ok(PipeStage::new(s));
937    }
938
939    Err(ExprError::InvalidPipeStage {
940        stage: s.to_string(),
941    })
942}
943
944/// Check if a string is a valid identifier (ASCII alphanumeric or underscore, not starting
945/// with a digit).
946fn is_valid_identifier(s: &str) -> bool {
947    let mut chars = s.chars();
948    match chars.next() {
949        None => false,
950        Some(first) => {
951            (first.is_ascii_alphabetic() || first == '_')
952                && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
953        }
954    }
955}
956
957/// A `${...}` occurrence located in a scalar, with the result of parsing it.
958struct Occurrence<'a> {
959    range: Range<usize>,
960    text: &'a str,
961    parsed: Result<Expression, ExprError>,
962}
963
964/// Find every `${...}` occurrence in `s`, parsed or not.
965///
966/// The one place that decides where an occurrence starts and ends. Braces are
967/// balanced rather than stopping at the first `}`, so a `}` inside the
968/// expression does not cut it short. An occurrence nothing closes runs to the
969/// end of the string and parses as nothing.
970fn scan_occurrences(s: &str) -> Vec<Occurrence<'_>> {
971    let mut found = Vec::new();
972    let bytes = s.as_bytes();
973    let mut i = 0;
974
975    while i < bytes.len() {
976        if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
977            let mut depth = 1;
978            let mut j = i + 2;
979            while j < bytes.len() && depth > 0 {
980                match bytes[j] {
981                    b'{' => depth += 1,
982                    b'}' => depth -= 1,
983                    _ => {}
984                }
985                j += 1;
986            }
987
988            // An unbalanced `${a{b}` is not closed even though its text ends in
989            // a brace, and has no closing brace to parse against.
990            let closed = depth == 0;
991            let end = if closed { j } else { s.len() };
992            let text = &s[i..end];
993            found.push(Occurrence {
994                range: i..end,
995                text,
996                parsed: if closed {
997                    parse_expression_detailed(text)
998                } else {
999                    Err(ExprError::Unclosed)
1000                },
1001            });
1002            i = end;
1003            continue;
1004        }
1005        i += 1;
1006    }
1007
1008    found
1009}
1010
1011/// Find all expressions in a string.
1012///
1013/// Returns expressions with their locations for LSP support.
1014pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
1015    scan_occurrences(s)
1016        .into_iter()
1017        .filter_map(|occurrence| {
1018            Some(LocatedExpression {
1019                expression: occurrence.parsed.ok()?,
1020                range: occurrence.range,
1021            })
1022        })
1023        .collect()
1024}
1025
1026/// A `${...}` occurrence that does not parse as an expression.
1027#[derive(Debug, Clone, PartialEq, Eq)]
1028pub struct InvalidExpression {
1029    /// Byte range of the occurrence within the scanned string.
1030    pub range: Range<usize>,
1031    /// The occurrence as written, including the `${` and the `}`.
1032    pub text: String,
1033    /// Why it is not a usable expression.
1034    pub reason: ExprError,
1035}
1036
1037/// Find every `${...}` in `s` that does not parse as an expression.
1038///
1039/// The counterpart to [`find_expressions`], which keeps the occurrences that do
1040/// parse. `${` always opens an expression, so an occurrence that fails to parse
1041/// is a mistake rather than prose, and a caller reports each one it gets back.
1042///
1043/// # Examples
1044///
1045/// ```
1046/// use rite_model::expression::find_invalid_expressions;
1047///
1048/// assert!(find_invalid_expressions("${artifact.ksr | sha256}").is_empty());
1049///
1050/// let found = find_invalid_expressions("sign ${nonsense.x} now");
1051/// assert_eq!(found.len(), 1);
1052/// assert_eq!(found[0].text, "${nonsense.x}");
1053/// ```
1054pub fn find_invalid_expressions(s: &str) -> Vec<InvalidExpression> {
1055    scan_occurrences(s)
1056        .into_iter()
1057        .filter_map(|occurrence| {
1058            Some(InvalidExpression {
1059                reason: occurrence.parsed.err()?,
1060                range: occurrence.range,
1061                text: occurrence.text.to_string(),
1062            })
1063        })
1064        .collect()
1065}
1066
1067/// Project the literal part of an `ExprValue` to JSON, dropping whatever is
1068/// only known at run time.
1069///
1070/// `rite check` has no artifacts, no backend, and no evaluated parameters, so
1071/// an `Expr` or `Interpolated` node has no value to inspect. Such a node
1072/// yields `None`, and an object field holding one is left out of the object.
1073/// An array containing one yields `None` as a whole: object fields are
1074/// addressed by name, so an absent one is visible to whoever reads the result,
1075/// while array elements are addressed by position, and silently dropping one
1076/// renumbers the rest.
1077///
1078/// The result is therefore a subset of the block the runtime's
1079/// `evaluate_expr_value` will build. A caller must not read an absent field as
1080/// unset.
1081#[must_use]
1082pub fn literal_expr_value(value: &ExprValue) -> Option<serde_json::Value> {
1083    match value {
1084        ExprValue::Literal(lit) => Some(literal_to_json(lit)),
1085        ExprValue::Expr(_) | ExprValue::Interpolated(_) => None,
1086        ExprValue::Object(map) => {
1087            let mut result = serde_json::Map::new();
1088            for (k, v) in map {
1089                if let Some(json) = literal_expr_value(v) {
1090                    result.insert(k.clone(), json);
1091                }
1092            }
1093            Some(serde_json::Value::Object(result))
1094        }
1095        ExprValue::Array(arr) => arr
1096            .iter()
1097            .map(literal_expr_value)
1098            .collect::<Option<Vec<_>>>()
1099            .map(serde_json::Value::Array),
1100    }
1101}
1102
1103/// Convert a Literal to a JSON value.
1104pub fn literal_to_json(lit: &Literal) -> serde_json::Value {
1105    match lit {
1106        Literal::String(s) => serde_json::Value::String(s.clone()),
1107        Literal::Integer(i) => serde_json::Value::Number((*i).into()),
1108        Literal::Float(f) => serde_json::Number::from_f64(*f)
1109            .map_or(serde_json::Value::Null, serde_json::Value::Number),
1110        Literal::Boolean(b) => serde_json::Value::Bool(*b),
1111        Literal::Null => serde_json::Value::Null,
1112    }
1113}
1114
1115/// Parse a JSON value into an `ExprValue`, extracting all `${...}` expressions.
1116///
1117/// This function recursively converts JSON into `ExprValue`:
1118/// - Strings containing `${...}` become `Expr` (if entire string) or `Interpolated` (if mixed)
1119/// - Plain strings become `Literal(String)`
1120/// - Numbers, bools, null become their `Literal` equivalents
1121/// - Objects and arrays are recursed into
1122///
1123/// # Examples
1124///
1125/// ```
1126/// use rite_model::expression::{parse_expr_value, ExprValue, Literal};
1127///
1128/// // Plain string → Literal
1129/// let json = serde_json::json!("hello");
1130/// let expr = parse_expr_value(&json);
1131/// assert!(matches!(expr, ExprValue::Literal(Literal::String(_))));
1132///
1133/// // Expression string → Expr
1134/// let json = serde_json::json!("${artifact.ksr | sha256}");
1135/// let expr = parse_expr_value(&json);
1136/// assert!(matches!(expr, ExprValue::Expr(_)));
1137/// ```
1138pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
1139    parse_expr_value_at(json, 0)
1140}
1141
1142/// Maximum nesting depth [`parse_expr_value`] recurses into.
1143///
1144/// Matches the depth cap the resolver enforces on ceremony YAML, so values
1145/// that went through resolution never reach this limit.
1146const MAX_VALUE_DEPTH: usize = 64;
1147
1148/// Depth-tracking worker for [`parse_expr_value`].
1149///
1150/// Values nested deeper than [`MAX_VALUE_DEPTH`] are truncated to
1151/// `Literal(Null)`: the function returns a value rather than a `Result`, so
1152/// truncation is the least invasive way to stay total on adversarial input
1153/// without overflowing the stack. The resolver rejects ceremony files nested
1154/// past the same cap before values reach this function, so the truncation is
1155/// a defensive backstop, not user-visible behavior.
1156fn parse_expr_value_at(json: &serde_json::Value, depth: usize) -> ExprValue {
1157    if depth >= MAX_VALUE_DEPTH {
1158        return ExprValue::Literal(Literal::Null);
1159    }
1160    let child_depth = depth.saturating_add(1);
1161    match json {
1162        serde_json::Value::String(s) => parse_string_to_expr_value(s),
1163        serde_json::Value::Number(n) => {
1164            if let Some(i) = n.as_i64() {
1165                ExprValue::Literal(Literal::Integer(i))
1166            } else if let Some(f) = n.as_f64() {
1167                ExprValue::Literal(Literal::Float(f))
1168            } else {
1169                // Fallback for very large numbers: truncate to 0.
1170                ExprValue::Literal(Literal::Integer(0))
1171            }
1172        }
1173        serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
1174        serde_json::Value::Null => ExprValue::Literal(Literal::Null),
1175        serde_json::Value::Object(map) => {
1176            let mut result = std::collections::HashMap::new();
1177            for (k, v) in map {
1178                result.insert(k.clone(), parse_expr_value_at(v, child_depth));
1179            }
1180            ExprValue::Object(result)
1181        }
1182        serde_json::Value::Array(arr) => {
1183            let result: Vec<_> = arr
1184                .iter()
1185                .map(|v| parse_expr_value_at(v, child_depth))
1186                .collect();
1187            ExprValue::Array(result)
1188        }
1189    }
1190}
1191
1192/// Parse a string into an `ExprValue`.
1193///
1194/// Handles three cases:
1195/// 1. Entire string is one expression: `${...}` → `ExprValue::Expr`
1196/// 2. String contains expressions: `"Hash: ${...}"` → `ExprValue::Interpolated`
1197/// 3. Plain string: `"hello"` → `ExprValue::Literal`
1198fn parse_string_to_expr_value(s: &str) -> ExprValue {
1199    let trimmed = s.trim();
1200
1201    // Case 1: Entire string is a single expression.
1202    if trimmed.starts_with("${")
1203        && trimmed.ends_with('}')
1204        && !trimmed[2..trimmed.len() - 1].contains("${")
1205        && let Some(expr) = parse_expression(trimmed)
1206    {
1207        return ExprValue::Expr(expr);
1208    }
1209
1210    // Case 2: String contains embedded expressions.
1211    if s.contains("${") {
1212        let parts = parse_interpolated_string(s);
1213        if parts.len() == 1 {
1214            // Single literal part.
1215            if let StringPart::Literal(lit) = &parts[0] {
1216                return ExprValue::Literal(Literal::String(lit.clone()));
1217            }
1218        }
1219        return ExprValue::Interpolated(parts);
1220    }
1221
1222    // Case 3: Plain string.
1223    ExprValue::Literal(Literal::String(s.to_string()))
1224}
1225
1226/// Parse an interpolated string into parts.
1227///
1228/// `"Hello ${param.name}!"` → `[Literal("Hello "), Expr(...), Literal("!")]`
1229fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
1230    let mut parts = Vec::new();
1231    let bytes = s.as_bytes();
1232    let mut last_end = 0;
1233    let mut i = 0;
1234
1235    while i < bytes.len() {
1236        if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1237            // Push literal part before this expression.
1238            if i > last_end {
1239                parts.push(StringPart::Literal(s[last_end..i].to_string()));
1240            }
1241
1242            // Find matching closing brace.
1243            let mut depth = 1;
1244            let mut j = i + 2;
1245            while j < bytes.len() && depth > 0 {
1246                match bytes[j] {
1247                    b'{' => depth += 1,
1248                    b'}' => depth -= 1,
1249                    _ => {}
1250                }
1251                j += 1;
1252            }
1253
1254            if depth == 0 {
1255                let expr_str = &s[i..j];
1256                if let Some(expr) = parse_expression(expr_str) {
1257                    parts.push(StringPart::Expr(expr));
1258                } else {
1259                    // Failed to parse: keep as literal.
1260                    parts.push(StringPart::Literal(expr_str.to_string()));
1261                }
1262                last_end = j;
1263                i = j;
1264                continue;
1265            }
1266        }
1267        i += 1;
1268    }
1269
1270    // Push remaining literal part.
1271    if last_end < s.len() {
1272        parts.push(StringPart::Literal(s[last_end..].to_string()));
1273    }
1274
1275    parts
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280    use super::*;
1281
1282    #[test]
1283    fn parse_simple_reference() {
1284        let expr = parse_expression("${artifact.ksr}").unwrap();
1285        match expr {
1286            Expression::Reference(r) => {
1287                assert_eq!(r.ref_type, RefType::Artifact);
1288                assert_eq!(r.name, "ksr");
1289                assert!(r.property.is_none());
1290            }
1291            _ => panic!("Expected Reference"),
1292        }
1293    }
1294
1295    #[test]
1296    fn parse_reference_with_property() {
1297        let expr = parse_expression("${artifact.keypair.private}").unwrap();
1298        match expr {
1299            Expression::Reference(r) => {
1300                assert_eq!(r.ref_type, RefType::Artifact);
1301                assert_eq!(r.name, "keypair");
1302                assert_eq!(r.property, Some("private".to_string()));
1303            }
1304            _ => panic!("Expected Reference"),
1305        }
1306    }
1307
1308    #[test]
1309    fn parse_param_reference() {
1310        let expr = parse_expression("${param.key_label}").unwrap();
1311        match expr {
1312            Expression::Reference(r) => {
1313                assert_eq!(r.ref_type, RefType::Param);
1314                assert_eq!(r.name, "key_label");
1315            }
1316            _ => panic!("Expected Reference"),
1317        }
1318    }
1319
1320    #[test]
1321    fn parse_simple_pipeline() {
1322        let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
1323        match expr {
1324            Expression::Pipeline { source, stages } => {
1325                assert!(matches!(*source, Expression::Reference(_)));
1326                assert_eq!(stages.len(), 1);
1327                assert_eq!(stages[0].function, "sha256");
1328                assert!(stages[0].args.is_empty());
1329            }
1330            _ => panic!("Expected Pipeline"),
1331        }
1332    }
1333
1334    #[test]
1335    fn parse_multi_stage_pipeline() {
1336        let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1337        match expr {
1338            Expression::Pipeline { stages, .. } => {
1339                assert_eq!(stages.len(), 2);
1340                assert_eq!(stages[0].function, "sha256");
1341                assert_eq!(stages[1].function, "hex");
1342            }
1343            _ => panic!("Expected Pipeline"),
1344        }
1345    }
1346
1347    #[test]
1348    fn parse_pipeline_with_function_args() {
1349        let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
1350        match expr {
1351            Expression::Pipeline { stages, .. } => {
1352                assert_eq!(stages.len(), 1);
1353                assert_eq!(stages[0].function, "substr");
1354                assert_eq!(stages[0].args.len(), 2);
1355            }
1356            _ => panic!("Expected Pipeline"),
1357        }
1358    }
1359
1360    #[test]
1361    fn parse_complex_pipeline() {
1362        let expr =
1363            parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
1364        match expr {
1365            Expression::Pipeline { source, stages } => {
1366                if let Expression::Reference(r) = *source {
1367                    assert_eq!(r.name, "keypair");
1368                    assert_eq!(r.property, Some("public".to_string()));
1369                }
1370                assert_eq!(stages.len(), 3);
1371                assert_eq!(stages[0].function, "fingerprint");
1372                assert_eq!(stages[1].function, "hex");
1373                assert_eq!(stages[2].function, "upper");
1374            }
1375            _ => panic!("Expected Pipeline"),
1376        }
1377    }
1378
1379    #[test]
1380    fn parse_invalid_expression() {
1381        assert!(parse_expression("not an expression").is_none());
1382        assert!(parse_expression("${").is_none());
1383        assert!(parse_expression("${}").is_none());
1384        assert!(parse_expression("${invalid}").is_none()); // No namespace
1385    }
1386
1387    #[test]
1388    fn find_expressions_in_string() {
1389        let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
1390        assert_eq!(exprs.len(), 1);
1391        assert_eq!(exprs[0].range.start, 6);
1392    }
1393
1394    #[test]
1395    fn find_multiple_expressions() {
1396        let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
1397        assert_eq!(exprs.len(), 2);
1398    }
1399
1400    #[test]
1401    fn value_type_names() {
1402        assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
1403        assert_eq!(Value::String(String::new()).type_name(), "string");
1404        assert_eq!(Value::Integer(0).type_name(), "integer");
1405        assert_eq!(Value::Boolean(true).type_name(), "boolean");
1406        assert_eq!(Value::Null.type_name(), "null");
1407    }
1408
1409    #[test]
1410    fn value_as_bytes() {
1411        let v = Value::Bytes(vec![1, 2, 3]);
1412        assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
1413
1414        let v = Value::String("hello".into());
1415        assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
1416
1417        let v = Value::Integer(42);
1418        assert!(v.as_bytes().is_none());
1419    }
1420
1421    mod ceremony_examples {
1422        use super::*;
1423
1424        // From dnssec_signing/dnssec_signing.rite.yaml
1425        #[test]
1426        fn dnssec_hash_verification() {
1427            // actual: "${artifact.ksr | sha256 | hex | substr(0, 16)}"
1428            let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
1429            match expr {
1430                Expression::Pipeline { source, stages } => {
1431                    if let Expression::Reference(r) = *source {
1432                        assert_eq!(r.ref_type, RefType::Artifact);
1433                        assert_eq!(r.name, "ksr");
1434                    } else {
1435                        panic!("Expected Reference source");
1436                    }
1437                    // 3 stages: sha256, hex, substr
1438                    assert_eq!(stages.len(), 3);
1439                    assert_eq!(stages[0].function, "sha256");
1440                    assert_eq!(stages[1].function, "hex");
1441                    assert_eq!(stages[2].function, "substr");
1442                    assert_eq!(stages[2].args.len(), 2);
1443                }
1444                _ => panic!("Expected Pipeline"),
1445            }
1446        }
1447
1448        #[test]
1449        fn dnssec_param_reference() {
1450            // expected: "${param.expected_ksr_hash}"
1451            let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
1452            match expr {
1453                Expression::Reference(r) => {
1454                    assert_eq!(r.ref_type, RefType::Param);
1455                    assert_eq!(r.name, "expected_ksr_hash");
1456                }
1457                _ => panic!("Expected Reference"),
1458            }
1459        }
1460
1461        #[test]
1462        fn dnssec_oral_readback() {
1463            // value: "${artifact.ksr | sha256 | hex}"
1464            let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1465            match expr {
1466                Expression::Pipeline { stages, .. } => {
1467                    assert_eq!(stages.len(), 2);
1468                    assert_eq!(stages[0].function, "sha256");
1469                    assert_eq!(stages[1].function, "hex");
1470                }
1471                _ => panic!("Expected Pipeline"),
1472            }
1473        }
1474
1475        // From airgapped_hsm/airgapped_hsm.rite.yaml
1476        #[test]
1477        fn hsm_artifact_reference() {
1478            // input: ${artifact.master_keypair}
1479            let expr = parse_expression("${artifact.master_keypair}").unwrap();
1480            match expr {
1481                Expression::Reference(r) => {
1482                    assert_eq!(r.ref_type, RefType::Artifact);
1483                    assert_eq!(r.name, "master_keypair");
1484                    assert!(r.property.is_none());
1485                }
1486                _ => panic!("Expected Reference"),
1487            }
1488        }
1489
1490        #[test]
1491        fn hsm_public_key_fingerprint() {
1492            // value: "${artifact.master_public_key | sha256 | hex}"
1493            let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1494            match expr {
1495                Expression::Pipeline { source, stages } => {
1496                    if let Expression::Reference(r) = *source {
1497                        assert_eq!(r.name, "master_public_key");
1498                    }
1499                    assert_eq!(stages.len(), 2);
1500                }
1501                _ => panic!("Expected Pipeline"),
1502            }
1503        }
1504
1505        // From crypto_test/crypto_test.rite.yaml
1506        #[test]
1507        fn crypto_artifact_with_property() {
1508            // key_to_wrap: ${artifact.keypair.private}
1509            let expr = parse_expression("${artifact.keypair.private}").unwrap();
1510            match expr {
1511                Expression::Reference(r) => {
1512                    assert_eq!(r.ref_type, RefType::Artifact);
1513                    assert_eq!(r.name, "keypair");
1514                    assert_eq!(r.property, Some("private".to_string()));
1515                }
1516                _ => panic!("Expected Reference"),
1517            }
1518        }
1519
1520        // String interpolation patterns
1521        #[test]
1522        fn find_embedded_expressions() {
1523            // message: "Verify serial number: ${param.hsm_serial}"
1524            let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1525            assert_eq!(exprs.len(), 1);
1526            if let Expression::Reference(r) = &exprs[0].expression {
1527                assert_eq!(r.ref_type, RefType::Param);
1528                assert_eq!(r.name, "hsm_serial");
1529            }
1530        }
1531
1532        #[test]
1533        fn find_multiple_embedded_params() {
1534            // "Key ${param.key_label} for HSM ${param.hsm_serial}"
1535            let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1536            assert_eq!(exprs.len(), 2);
1537        }
1538
1539        // Role references (namespace = "role" - used in validation, not expressions)
1540        #[test]
1541        fn role_reference_parses() {
1542            let expr = parse_expression("${role.ceremony_admin}").unwrap();
1543            match expr {
1544                Expression::Reference(r) => {
1545                    assert_eq!(r.ref_type, RefType::Role);
1546                    assert_eq!(r.name, "ceremony_admin");
1547                }
1548                _ => panic!("Expected Reference"),
1549            }
1550        }
1551    }
1552
1553    mod expr_value_tests {
1554        use super::*;
1555
1556        #[test]
1557        fn parse_literal_string() {
1558            let json = serde_json::json!("hello");
1559            let expr = parse_expr_value(&json);
1560            assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1561        }
1562
1563        #[test]
1564        fn parse_literal_integer() {
1565            let json = serde_json::json!(42);
1566            let expr = parse_expr_value(&json);
1567            assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1568        }
1569
1570        #[test]
1571        fn parse_literal_float() {
1572            let json = serde_json::json!(1.5);
1573            let expr = parse_expr_value(&json);
1574            assert!(
1575                matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1576            );
1577        }
1578
1579        #[test]
1580        fn parse_literal_boolean() {
1581            let json = serde_json::json!(true);
1582            let expr = parse_expr_value(&json);
1583            assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1584        }
1585
1586        #[test]
1587        fn parse_literal_null() {
1588            let json = serde_json::Value::Null;
1589            let expr = parse_expr_value(&json);
1590            assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1591        }
1592
1593        #[test]
1594        fn parse_single_expression() {
1595            let json = serde_json::json!("${artifact.ksr | sha256}");
1596            let expr = parse_expr_value(&json);
1597            match expr {
1598                ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1599                    assert!(matches!(*source, Expression::Reference(_)));
1600                    assert_eq!(stages.len(), 1);
1601                    assert_eq!(stages[0].function, "sha256");
1602                }
1603                _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1604            }
1605        }
1606
1607        #[test]
1608        fn parse_simple_reference() {
1609            let json = serde_json::json!("${param.name}");
1610            let expr = parse_expr_value(&json);
1611            match expr {
1612                ExprValue::Expr(Expression::Reference(r)) => {
1613                    assert_eq!(r.ref_type, RefType::Param);
1614                    assert_eq!(r.name, "name");
1615                }
1616                _ => panic!("Expected Expr(Reference), got {expr:?}"),
1617            }
1618        }
1619
1620        #[test]
1621        fn parse_interpolated_string() {
1622            let json = serde_json::json!("Hello, ${param.name}!");
1623            let expr = parse_expr_value(&json);
1624            match expr {
1625                ExprValue::Interpolated(parts) => {
1626                    assert_eq!(parts.len(), 3);
1627                    assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1628                    assert!(matches!(
1629                        &parts[1],
1630                        StringPart::Expr(Expression::Reference(_))
1631                    ));
1632                    assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1633                }
1634                _ => panic!("Expected Interpolated, got {expr:?}"),
1635            }
1636        }
1637
1638        #[test]
1639        fn parse_object_with_expressions() {
1640            let json = serde_json::json!({
1641                "actual": "${artifact.ksr | sha256 | hex}",
1642                "expected": "${param.expected_hash}",
1643                "static": "hello"
1644            });
1645            let expr = parse_expr_value(&json);
1646            match expr {
1647                ExprValue::Object(map) => {
1648                    assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1649                    assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1650                    assert!(matches!(
1651                        map.get("static"),
1652                        Some(ExprValue::Literal(Literal::String(_)))
1653                    ));
1654                }
1655                _ => panic!("Expected Object, got {expr:?}"),
1656            }
1657        }
1658
1659        #[test]
1660        fn parse_array_with_expressions() {
1661            let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1662            let expr = parse_expr_value(&json);
1663            match expr {
1664                ExprValue::Array(arr) => {
1665                    assert_eq!(arr.len(), 3);
1666                    assert!(matches!(&arr[0], ExprValue::Expr(_)));
1667                    assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1668                    assert!(matches!(&arr[2], ExprValue::Expr(_)));
1669                }
1670                _ => panic!("Expected Array, got {expr:?}"),
1671            }
1672        }
1673
1674        #[test]
1675        fn parse_deeply_nested_value_is_truncated_not_crashed() {
1676            // 200 levels of nesting: far past the cap. The function must
1677            // return (no stack overflow), truncating everything below
1678            // MAX_VALUE_DEPTH to Literal(Null).
1679            let mut json = serde_json::json!("leaf");
1680            for _ in 0..200 {
1681                json = serde_json::Value::Array(vec![json]);
1682            }
1683            let mut expr = parse_expr_value(&json);
1684            let mut levels = 0;
1685            loop {
1686                match expr {
1687                    ExprValue::Array(mut arr) => {
1688                        assert_eq!(arr.len(), 1);
1689                        expr = arr.remove(0);
1690                        levels += 1;
1691                    }
1692                    other => {
1693                        assert!(
1694                            matches!(other, ExprValue::Literal(Literal::Null)),
1695                            "truncated tail must be Literal(Null), got {other:?}"
1696                        );
1697                        break;
1698                    }
1699                }
1700            }
1701            assert_eq!(levels, MAX_VALUE_DEPTH, "recursion must stop at the cap");
1702        }
1703
1704        #[test]
1705        fn parse_moderately_nested_value_is_preserved() {
1706            let mut json = serde_json::json!("leaf");
1707            for _ in 0..10 {
1708                json = serde_json::Value::Array(vec![json]);
1709            }
1710            let mut expr = parse_expr_value(&json);
1711            for _ in 0..10 {
1712                match expr {
1713                    ExprValue::Array(mut arr) => {
1714                        assert_eq!(arr.len(), 1);
1715                        expr = arr.remove(0);
1716                    }
1717                    other => panic!("expected Array, got {other:?}"),
1718                }
1719            }
1720            assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "leaf"));
1721        }
1722
1723        #[test]
1724        fn parse_multiple_expressions_in_string() {
1725            let json = serde_json::json!("${param.a} and ${param.b}");
1726            let expr = parse_expr_value(&json);
1727            match expr {
1728                ExprValue::Interpolated(parts) => {
1729                    assert_eq!(parts.len(), 3);
1730                    assert!(matches!(&parts[0], StringPart::Expr(_)));
1731                    assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1732                    assert!(matches!(&parts[2], StringPart::Expr(_)));
1733                }
1734                _ => panic!("Expected Interpolated, got {expr:?}"),
1735            }
1736        }
1737    }
1738
1739    mod invalid_expressions {
1740        use super::*;
1741
1742        /// `RefType::ALL` is written by hand; this match makes a new variant a
1743        /// compile error until it is listed there.
1744        #[test]
1745        fn all_lists_every_namespace() {
1746            for ref_type in RefType::ALL {
1747                match ref_type {
1748                    RefType::Param | RefType::Role | RefType::Artifact => {}
1749                }
1750            }
1751            assert_eq!(RefType::ALL.len(), 3);
1752            assert_eq!(namespace_list(), "param, role, artifact");
1753        }
1754
1755        #[test]
1756        fn a_valid_expression_yields_nothing() {
1757            for s in [
1758                "${artifact.ksr}",
1759                "${artifact.keypair.public | sha256 | hex}",
1760                "${param.name}",
1761                "plain text with no expression",
1762                "Hash: ${artifact.a | sha256 | hex} done",
1763                "${concat(artifact.a | sha256, artifact.b) | hex}",
1764            ] {
1765                assert!(
1766                    find_invalid_expressions(s).is_empty(),
1767                    "{s} should be accepted"
1768                );
1769            }
1770        }
1771
1772        #[test]
1773        fn an_unknown_namespace_is_reported_with_its_range() {
1774            let found = find_invalid_expressions("sign ${nonsense.x} now");
1775            assert_eq!(found.len(), 1);
1776            assert_eq!(found[0].range, 5..18);
1777            assert_eq!(found[0].text, "${nonsense.x}");
1778            assert_eq!(
1779                found[0].reason,
1780                ExprError::UnknownNamespace {
1781                    namespace: "nonsense".to_string(),
1782                }
1783            );
1784            assert_eq!(
1785                found[0].reason.to_string(),
1786                "unknown namespace 'nonsense': expected one of param, role, artifact"
1787            );
1788        }
1789
1790        #[test]
1791        fn every_occurrence_in_one_string_is_reported() {
1792            let found = find_invalid_expressions("${a.b} ${param.ok} ${c.d}");
1793            assert_eq!(found.len(), 2);
1794            assert_eq!(found[0].text, "${a.b}");
1795            assert_eq!(found[1].text, "${c.d}");
1796        }
1797
1798        #[test]
1799        fn the_material_namespace_names_the_artifact_form() {
1800            // The plural is what the declaring key is called, so it lands on
1801            // the same explanation rather than the generic unknown-namespace one.
1802            for namespace in ["material", "materials"] {
1803                let found = find_invalid_expressions(&format!("${{{namespace}.manifest}}"));
1804                assert_eq!(found.len(), 1, "for {namespace}");
1805                assert_eq!(
1806                    found[0].reason,
1807                    ExprError::MaterialNamespace {
1808                        namespace: namespace.to_string(),
1809                        name: "manifest".to_string(),
1810                    }
1811                );
1812                assert_eq!(
1813                    found[0].reason.to_string(),
1814                    format!(
1815                        "there is no '{namespace}' namespace: \
1816                         a material is read as 'artifact.manifest'"
1817                    )
1818                );
1819                assert_eq!(
1820                    found[0].reason.suggestion(),
1821                    Some(RefType::Artifact),
1822                    "an editor fix points at the artifact namespace"
1823                );
1824            }
1825        }
1826
1827        #[test]
1828        fn a_pipeline_with_nothing_before_the_pipe_is_not_an_empty_expression() {
1829            assert_eq!(
1830                parse_expression_detailed("${ | sha256 }").unwrap_err(),
1831                ExprError::MissingSource
1832            );
1833            assert_eq!(
1834                parse_expression_detailed("${}").unwrap_err(),
1835                ExprError::Empty
1836            );
1837        }
1838
1839        #[test]
1840        fn an_unclosed_expression_is_reported_to_the_end_of_the_string() {
1841            let found = find_invalid_expressions("see ${artifact.k");
1842            assert_eq!(found.len(), 1);
1843            assert_eq!(found[0].text, "${artifact.k");
1844            assert_eq!(found[0].reason, ExprError::Unclosed);
1845            assert_eq!(
1846                found[0].reason.to_string(),
1847                "the expression is missing its closing '}'"
1848            );
1849        }
1850
1851        #[test]
1852        fn each_shape_of_mistake_gets_its_own_reason() {
1853            for (input, expected) in [
1854                ("${}", "the expression is empty"),
1855                (
1856                    "${artifact}",
1857                    "'artifact' names no namespace: write '<namespace>.<name>', \
1858                     where <namespace> is one of param, role, artifact",
1859                ),
1860                (
1861                    "${artifact.}",
1862                    "nothing follows 'artifact.': expected a name",
1863                ),
1864                (
1865                    "${artifact.9k}",
1866                    "'9k' is not a usable name: names hold letters, digits, and underscores, \
1867                     and cannot start with a digit",
1868                ),
1869                (
1870                    "${artifact.k.9p}",
1871                    "'9p' is not a usable property: names hold letters, digits, and underscores, \
1872                     and cannot start with a digit",
1873                ),
1874                (
1875                    "${artifact.k | 9hex}",
1876                    "'9hex' is not a usable pipe stage: expected a function name, \
1877                     with any arguments in parentheses",
1878                ),
1879                (
1880                    "${concat(artifact.a}",
1881                    "the call to 'concat' is missing its closing ')'",
1882                ),
1883            ] {
1884                let found = find_invalid_expressions(input);
1885                assert_eq!(found.len(), 1, "{input} should be rejected");
1886                assert_eq!(found[0].reason.to_string(), expected, "for {input}");
1887            }
1888        }
1889
1890        #[test]
1891        fn a_bare_name_is_told_to_use_the_wrapper() {
1892            let error = parse_expression_detailed("keypair").unwrap_err();
1893            assert_eq!(error, ExprError::NotAnExpression);
1894            assert_eq!(
1895                error.to_string(),
1896                "write it as an expression, '${<namespace>.<name>}', \
1897                 where <namespace> is one of param, role, artifact"
1898            );
1899        }
1900
1901        #[test]
1902        fn a_namespace_one_or_two_edits_away_is_suggested() {
1903            for (input, expected) in [
1904                ("${paramm.region}", RefType::Param),
1905                ("${params.region}", RefType::Param),
1906                ("${roles.officer}", RefType::Role),
1907                ("${artifacts.ksr}", RefType::Artifact),
1908            ] {
1909                let error = parse_expression_detailed(input).unwrap_err();
1910                assert_eq!(error.suggestion(), Some(expected), "for {input}");
1911                assert!(
1912                    error
1913                        .to_string()
1914                        .contains(&format!("did you mean '{expected}'")),
1915                    "for {input}: {error}"
1916                );
1917            }
1918        }
1919
1920        #[test]
1921        fn a_namespace_nothing_like_a_known_one_gets_no_suggestion() {
1922            for input in ["${nonsense.x}", "${env.HOME}", "${secret.pin}"] {
1923                let error = parse_expression_detailed(input).unwrap_err();
1924                assert!(
1925                    matches!(error, ExprError::UnknownNamespace { .. }),
1926                    "{input} should be an unknown namespace, got {error:?}"
1927                );
1928                assert_eq!(error.suggestion(), None, "{input} should not be guessed at");
1929            }
1930        }
1931
1932        #[test]
1933        fn a_failing_argument_names_its_position_and_its_own_cause() {
1934            let error = parse_expression_detailed("${concat(artifact.a, nonsense.b)}").unwrap_err();
1935            assert_eq!(
1936                error,
1937                ExprError::InvalidArgument {
1938                    function: "concat".to_string(),
1939                    position: 2,
1940                    cause: Box::new(ExprError::UnknownNamespace {
1941                        namespace: "nonsense".to_string(),
1942                    }),
1943                }
1944            );
1945            assert_eq!(
1946                error.to_string(),
1947                "argument 2 to 'concat' is not a usable expression: \
1948                 unknown namespace 'nonsense': expected one of param, role, artifact"
1949            );
1950        }
1951
1952        #[test]
1953        fn a_lone_quote_is_an_unterminated_string() {
1954            // The source is dispatched on its first character, so a quote that
1955            // opens a string is never also read as the one that closes it.
1956            for (input, quote) in [("${\"}", '"'), ("${'}", '\''), ("${\"abc}", '"')] {
1957                assert_eq!(
1958                    parse_expression_detailed(input).unwrap_err(),
1959                    ExprError::UnterminatedString { quote },
1960                    "for {input}"
1961                );
1962            }
1963        }
1964
1965        #[test]
1966        fn a_quote_opens_a_string_even_when_it_holds_a_parenthesis() {
1967            let parsed = parse_expression_detailed("${\"a(b\"}");
1968            assert!(
1969                matches!(parsed, Ok(Expression::Literal(Literal::String(ref s))) if s == "a(b"),
1970                "the parenthesis belongs to the string, got {parsed:?}"
1971            );
1972        }
1973
1974        #[test]
1975        fn the_shape_a_source_is_read_as_comes_from_its_first_characters() {
1976            // A dot means a reference, so a name with a parenthesis in it is a
1977            // broken name rather than a broken call.
1978            for (input, expected) in [
1979                (
1980                    "${param.x(1)}",
1981                    ExprError::InvalidName {
1982                        name: "x(1)".to_string(),
1983                    },
1984                ),
1985                (
1986                    "${42abc}",
1987                    ExprError::NotAReference {
1988                        text: "42abc".to_string(),
1989                    },
1990                ),
1991                (
1992                    "${1.5}",
1993                    ExprError::NotAReference {
1994                        text: "1.5".to_string(),
1995                    },
1996                ),
1997            ] {
1998                assert_eq!(
1999                    parse_expression_detailed(input).unwrap_err(),
2000                    expected,
2001                    "for {input}"
2002                );
2003            }
2004        }
2005
2006        #[test]
2007        fn parses_every_accepted_form() {
2008            for input in [
2009                "${artifact.ksr}",
2010                "${artifact.keypair.public}",
2011                "${param.region | sha256 | hex}",
2012                "${\"literal\"}",
2013                "${'literal'}",
2014                "${42}",
2015                "${-42}",
2016                "${\"\"}",
2017                "${concat(artifact.a, artifact.b) | hex}",
2018                "${pad(artifact.a, 4)}",
2019            ] {
2020                assert!(
2021                    parse_expression_detailed(input).is_ok(),
2022                    "{input} should parse, got {:?}",
2023                    parse_expression_detailed(input)
2024                );
2025            }
2026        }
2027    }
2028}
2029
2030#[cfg(test)]
2031mod literal_projection_tests {
2032    use super::{ExprValue, Literal, StringPart, literal_expr_value, parse_expression};
2033    use serde_json::json;
2034
2035    fn deferred() -> ExprValue {
2036        ExprValue::Expr(parse_expression("${param.algo}").expect("parse"))
2037    }
2038
2039    fn literal(s: &str) -> ExprValue {
2040        ExprValue::Literal(Literal::String(s.to_string()))
2041    }
2042
2043    #[test]
2044    fn keeps_literal_fields_and_drops_deferred_ones() {
2045        let value = ExprValue::Object(
2046            [
2047                ("algorithm".to_string(), literal("CMS-RSA-GCM")),
2048                ("label".to_string(), deferred()),
2049            ]
2050            .into_iter()
2051            .collect(),
2052        );
2053
2054        assert_eq!(
2055            literal_expr_value(&value),
2056            Some(json!({"algorithm": "CMS-RSA-GCM"})),
2057        );
2058    }
2059
2060    #[test]
2061    fn drops_an_interpolated_field() {
2062        let value = ExprValue::Object(
2063            [(
2064                "subject".to_string(),
2065                ExprValue::Interpolated(vec![
2066                    StringPart::Literal("CN=".to_string()),
2067                    StringPart::Expr(parse_expression("${param.cn}").expect("parse")),
2068                ]),
2069            )]
2070            .into_iter()
2071            .collect(),
2072        );
2073
2074        assert_eq!(literal_expr_value(&value), Some(json!({})));
2075    }
2076
2077    #[test]
2078    fn a_deferred_element_discards_the_whole_array() {
2079        // Elements are addressed by position, so dropping one would renumber
2080        // the rest and change what a length or index check sees.
2081        let value = ExprValue::Array(vec![literal("a"), deferred()]);
2082        assert_eq!(literal_expr_value(&value), None);
2083
2084        let all_literal = ExprValue::Array(vec![literal("a"), literal("b")]);
2085        assert_eq!(literal_expr_value(&all_literal), Some(json!(["a", "b"])));
2086    }
2087
2088    #[test]
2089    fn a_fully_deferred_block_yields_nothing() {
2090        assert_eq!(literal_expr_value(&deferred()), None);
2091    }
2092}