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/// Parse a JSON value into an `ExprValue`, extracting all `${...}` expressions.
1068///
1069/// This function recursively converts JSON into `ExprValue`:
1070/// - Strings containing `${...}` become `Expr` (if entire string) or `Interpolated` (if mixed)
1071/// - Plain strings become `Literal(String)`
1072/// - Numbers, bools, null become their `Literal` equivalents
1073/// - Objects and arrays are recursed into
1074///
1075/// # Examples
1076///
1077/// ```
1078/// use rite_model::expression::{parse_expr_value, ExprValue, Literal};
1079///
1080/// // Plain string → Literal
1081/// let json = serde_json::json!("hello");
1082/// let expr = parse_expr_value(&json);
1083/// assert!(matches!(expr, ExprValue::Literal(Literal::String(_))));
1084///
1085/// // Expression string → Expr
1086/// let json = serde_json::json!("${artifact.ksr | sha256}");
1087/// let expr = parse_expr_value(&json);
1088/// assert!(matches!(expr, ExprValue::Expr(_)));
1089/// ```
1090pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
1091    parse_expr_value_at(json, 0)
1092}
1093
1094/// Maximum nesting depth [`parse_expr_value`] recurses into.
1095///
1096/// Matches the depth cap the resolver enforces on ceremony YAML, so values
1097/// that went through resolution never reach this limit.
1098const MAX_VALUE_DEPTH: usize = 64;
1099
1100/// Depth-tracking worker for [`parse_expr_value`].
1101///
1102/// Values nested deeper than [`MAX_VALUE_DEPTH`] are truncated to
1103/// `Literal(Null)`: the function returns a value rather than a `Result`, so
1104/// truncation is the least invasive way to stay total on adversarial input
1105/// without overflowing the stack. The resolver rejects ceremony files nested
1106/// past the same cap before values reach this function, so the truncation is
1107/// a defensive backstop, not user-visible behavior.
1108fn parse_expr_value_at(json: &serde_json::Value, depth: usize) -> ExprValue {
1109    if depth >= MAX_VALUE_DEPTH {
1110        return ExprValue::Literal(Literal::Null);
1111    }
1112    let child_depth = depth.saturating_add(1);
1113    match json {
1114        serde_json::Value::String(s) => parse_string_to_expr_value(s),
1115        serde_json::Value::Number(n) => {
1116            if let Some(i) = n.as_i64() {
1117                ExprValue::Literal(Literal::Integer(i))
1118            } else if let Some(f) = n.as_f64() {
1119                ExprValue::Literal(Literal::Float(f))
1120            } else {
1121                // Fallback for very large numbers: truncate to 0.
1122                ExprValue::Literal(Literal::Integer(0))
1123            }
1124        }
1125        serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
1126        serde_json::Value::Null => ExprValue::Literal(Literal::Null),
1127        serde_json::Value::Object(map) => {
1128            let mut result = std::collections::HashMap::new();
1129            for (k, v) in map {
1130                result.insert(k.clone(), parse_expr_value_at(v, child_depth));
1131            }
1132            ExprValue::Object(result)
1133        }
1134        serde_json::Value::Array(arr) => {
1135            let result: Vec<_> = arr
1136                .iter()
1137                .map(|v| parse_expr_value_at(v, child_depth))
1138                .collect();
1139            ExprValue::Array(result)
1140        }
1141    }
1142}
1143
1144/// Parse a string into an `ExprValue`.
1145///
1146/// Handles three cases:
1147/// 1. Entire string is one expression: `${...}` → `ExprValue::Expr`
1148/// 2. String contains expressions: `"Hash: ${...}"` → `ExprValue::Interpolated`
1149/// 3. Plain string: `"hello"` → `ExprValue::Literal`
1150fn parse_string_to_expr_value(s: &str) -> ExprValue {
1151    let trimmed = s.trim();
1152
1153    // Case 1: Entire string is a single expression.
1154    if trimmed.starts_with("${")
1155        && trimmed.ends_with('}')
1156        && !trimmed[2..trimmed.len() - 1].contains("${")
1157        && let Some(expr) = parse_expression(trimmed)
1158    {
1159        return ExprValue::Expr(expr);
1160    }
1161
1162    // Case 2: String contains embedded expressions.
1163    if s.contains("${") {
1164        let parts = parse_interpolated_string(s);
1165        if parts.len() == 1 {
1166            // Single literal part.
1167            if let StringPart::Literal(lit) = &parts[0] {
1168                return ExprValue::Literal(Literal::String(lit.clone()));
1169            }
1170        }
1171        return ExprValue::Interpolated(parts);
1172    }
1173
1174    // Case 3: Plain string.
1175    ExprValue::Literal(Literal::String(s.to_string()))
1176}
1177
1178/// Parse an interpolated string into parts.
1179///
1180/// `"Hello ${param.name}!"` → `[Literal("Hello "), Expr(...), Literal("!")]`
1181fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
1182    let mut parts = Vec::new();
1183    let bytes = s.as_bytes();
1184    let mut last_end = 0;
1185    let mut i = 0;
1186
1187    while i < bytes.len() {
1188        if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1189            // Push literal part before this expression.
1190            if i > last_end {
1191                parts.push(StringPart::Literal(s[last_end..i].to_string()));
1192            }
1193
1194            // Find matching closing brace.
1195            let mut depth = 1;
1196            let mut j = i + 2;
1197            while j < bytes.len() && depth > 0 {
1198                match bytes[j] {
1199                    b'{' => depth += 1,
1200                    b'}' => depth -= 1,
1201                    _ => {}
1202                }
1203                j += 1;
1204            }
1205
1206            if depth == 0 {
1207                let expr_str = &s[i..j];
1208                if let Some(expr) = parse_expression(expr_str) {
1209                    parts.push(StringPart::Expr(expr));
1210                } else {
1211                    // Failed to parse: keep as literal.
1212                    parts.push(StringPart::Literal(expr_str.to_string()));
1213                }
1214                last_end = j;
1215                i = j;
1216                continue;
1217            }
1218        }
1219        i += 1;
1220    }
1221
1222    // Push remaining literal part.
1223    if last_end < s.len() {
1224        parts.push(StringPart::Literal(s[last_end..].to_string()));
1225    }
1226
1227    parts
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233
1234    #[test]
1235    fn parse_simple_reference() {
1236        let expr = parse_expression("${artifact.ksr}").unwrap();
1237        match expr {
1238            Expression::Reference(r) => {
1239                assert_eq!(r.ref_type, RefType::Artifact);
1240                assert_eq!(r.name, "ksr");
1241                assert!(r.property.is_none());
1242            }
1243            _ => panic!("Expected Reference"),
1244        }
1245    }
1246
1247    #[test]
1248    fn parse_reference_with_property() {
1249        let expr = parse_expression("${artifact.keypair.private}").unwrap();
1250        match expr {
1251            Expression::Reference(r) => {
1252                assert_eq!(r.ref_type, RefType::Artifact);
1253                assert_eq!(r.name, "keypair");
1254                assert_eq!(r.property, Some("private".to_string()));
1255            }
1256            _ => panic!("Expected Reference"),
1257        }
1258    }
1259
1260    #[test]
1261    fn parse_param_reference() {
1262        let expr = parse_expression("${param.key_label}").unwrap();
1263        match expr {
1264            Expression::Reference(r) => {
1265                assert_eq!(r.ref_type, RefType::Param);
1266                assert_eq!(r.name, "key_label");
1267            }
1268            _ => panic!("Expected Reference"),
1269        }
1270    }
1271
1272    #[test]
1273    fn parse_simple_pipeline() {
1274        let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
1275        match expr {
1276            Expression::Pipeline { source, stages } => {
1277                assert!(matches!(*source, Expression::Reference(_)));
1278                assert_eq!(stages.len(), 1);
1279                assert_eq!(stages[0].function, "sha256");
1280                assert!(stages[0].args.is_empty());
1281            }
1282            _ => panic!("Expected Pipeline"),
1283        }
1284    }
1285
1286    #[test]
1287    fn parse_multi_stage_pipeline() {
1288        let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1289        match expr {
1290            Expression::Pipeline { stages, .. } => {
1291                assert_eq!(stages.len(), 2);
1292                assert_eq!(stages[0].function, "sha256");
1293                assert_eq!(stages[1].function, "hex");
1294            }
1295            _ => panic!("Expected Pipeline"),
1296        }
1297    }
1298
1299    #[test]
1300    fn parse_pipeline_with_function_args() {
1301        let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
1302        match expr {
1303            Expression::Pipeline { stages, .. } => {
1304                assert_eq!(stages.len(), 1);
1305                assert_eq!(stages[0].function, "substr");
1306                assert_eq!(stages[0].args.len(), 2);
1307            }
1308            _ => panic!("Expected Pipeline"),
1309        }
1310    }
1311
1312    #[test]
1313    fn parse_complex_pipeline() {
1314        let expr =
1315            parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
1316        match expr {
1317            Expression::Pipeline { source, stages } => {
1318                if let Expression::Reference(r) = *source {
1319                    assert_eq!(r.name, "keypair");
1320                    assert_eq!(r.property, Some("public".to_string()));
1321                }
1322                assert_eq!(stages.len(), 3);
1323                assert_eq!(stages[0].function, "fingerprint");
1324                assert_eq!(stages[1].function, "hex");
1325                assert_eq!(stages[2].function, "upper");
1326            }
1327            _ => panic!("Expected Pipeline"),
1328        }
1329    }
1330
1331    #[test]
1332    fn parse_invalid_expression() {
1333        assert!(parse_expression("not an expression").is_none());
1334        assert!(parse_expression("${").is_none());
1335        assert!(parse_expression("${}").is_none());
1336        assert!(parse_expression("${invalid}").is_none()); // No namespace
1337    }
1338
1339    #[test]
1340    fn find_expressions_in_string() {
1341        let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
1342        assert_eq!(exprs.len(), 1);
1343        assert_eq!(exprs[0].range.start, 6);
1344    }
1345
1346    #[test]
1347    fn find_multiple_expressions() {
1348        let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
1349        assert_eq!(exprs.len(), 2);
1350    }
1351
1352    #[test]
1353    fn value_type_names() {
1354        assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
1355        assert_eq!(Value::String(String::new()).type_name(), "string");
1356        assert_eq!(Value::Integer(0).type_name(), "integer");
1357        assert_eq!(Value::Boolean(true).type_name(), "boolean");
1358        assert_eq!(Value::Null.type_name(), "null");
1359    }
1360
1361    #[test]
1362    fn value_as_bytes() {
1363        let v = Value::Bytes(vec![1, 2, 3]);
1364        assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
1365
1366        let v = Value::String("hello".into());
1367        assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
1368
1369        let v = Value::Integer(42);
1370        assert!(v.as_bytes().is_none());
1371    }
1372
1373    mod ceremony_examples {
1374        use super::*;
1375
1376        // From dnssec_signing/dnssec_signing.rite.yaml
1377        #[test]
1378        fn dnssec_hash_verification() {
1379            // actual: "${artifact.ksr | sha256 | hex | substr(0, 16)}"
1380            let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
1381            match expr {
1382                Expression::Pipeline { source, stages } => {
1383                    if let Expression::Reference(r) = *source {
1384                        assert_eq!(r.ref_type, RefType::Artifact);
1385                        assert_eq!(r.name, "ksr");
1386                    } else {
1387                        panic!("Expected Reference source");
1388                    }
1389                    // 3 stages: sha256, hex, substr
1390                    assert_eq!(stages.len(), 3);
1391                    assert_eq!(stages[0].function, "sha256");
1392                    assert_eq!(stages[1].function, "hex");
1393                    assert_eq!(stages[2].function, "substr");
1394                    assert_eq!(stages[2].args.len(), 2);
1395                }
1396                _ => panic!("Expected Pipeline"),
1397            }
1398        }
1399
1400        #[test]
1401        fn dnssec_param_reference() {
1402            // expected: "${param.expected_ksr_hash}"
1403            let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
1404            match expr {
1405                Expression::Reference(r) => {
1406                    assert_eq!(r.ref_type, RefType::Param);
1407                    assert_eq!(r.name, "expected_ksr_hash");
1408                }
1409                _ => panic!("Expected Reference"),
1410            }
1411        }
1412
1413        #[test]
1414        fn dnssec_oral_readback() {
1415            // value: "${artifact.ksr | sha256 | hex}"
1416            let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1417            match expr {
1418                Expression::Pipeline { stages, .. } => {
1419                    assert_eq!(stages.len(), 2);
1420                    assert_eq!(stages[0].function, "sha256");
1421                    assert_eq!(stages[1].function, "hex");
1422                }
1423                _ => panic!("Expected Pipeline"),
1424            }
1425        }
1426
1427        // From airgapped_hsm/airgapped_hsm.rite.yaml
1428        #[test]
1429        fn hsm_artifact_reference() {
1430            // input: ${artifact.master_keypair}
1431            let expr = parse_expression("${artifact.master_keypair}").unwrap();
1432            match expr {
1433                Expression::Reference(r) => {
1434                    assert_eq!(r.ref_type, RefType::Artifact);
1435                    assert_eq!(r.name, "master_keypair");
1436                    assert!(r.property.is_none());
1437                }
1438                _ => panic!("Expected Reference"),
1439            }
1440        }
1441
1442        #[test]
1443        fn hsm_public_key_fingerprint() {
1444            // value: "${artifact.master_public_key | sha256 | hex}"
1445            let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1446            match expr {
1447                Expression::Pipeline { source, stages } => {
1448                    if let Expression::Reference(r) = *source {
1449                        assert_eq!(r.name, "master_public_key");
1450                    }
1451                    assert_eq!(stages.len(), 2);
1452                }
1453                _ => panic!("Expected Pipeline"),
1454            }
1455        }
1456
1457        // From crypto_test/crypto_test.rite.yaml
1458        #[test]
1459        fn crypto_artifact_with_property() {
1460            // key_to_wrap: ${artifact.keypair.private}
1461            let expr = parse_expression("${artifact.keypair.private}").unwrap();
1462            match expr {
1463                Expression::Reference(r) => {
1464                    assert_eq!(r.ref_type, RefType::Artifact);
1465                    assert_eq!(r.name, "keypair");
1466                    assert_eq!(r.property, Some("private".to_string()));
1467                }
1468                _ => panic!("Expected Reference"),
1469            }
1470        }
1471
1472        // String interpolation patterns
1473        #[test]
1474        fn find_embedded_expressions() {
1475            // message: "Verify serial number: ${param.hsm_serial}"
1476            let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1477            assert_eq!(exprs.len(), 1);
1478            if let Expression::Reference(r) = &exprs[0].expression {
1479                assert_eq!(r.ref_type, RefType::Param);
1480                assert_eq!(r.name, "hsm_serial");
1481            }
1482        }
1483
1484        #[test]
1485        fn find_multiple_embedded_params() {
1486            // "Key ${param.key_label} for HSM ${param.hsm_serial}"
1487            let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1488            assert_eq!(exprs.len(), 2);
1489        }
1490
1491        // Role references (namespace = "role" - used in validation, not expressions)
1492        #[test]
1493        fn role_reference_parses() {
1494            let expr = parse_expression("${role.ceremony_admin}").unwrap();
1495            match expr {
1496                Expression::Reference(r) => {
1497                    assert_eq!(r.ref_type, RefType::Role);
1498                    assert_eq!(r.name, "ceremony_admin");
1499                }
1500                _ => panic!("Expected Reference"),
1501            }
1502        }
1503    }
1504
1505    mod expr_value_tests {
1506        use super::*;
1507
1508        #[test]
1509        fn parse_literal_string() {
1510            let json = serde_json::json!("hello");
1511            let expr = parse_expr_value(&json);
1512            assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1513        }
1514
1515        #[test]
1516        fn parse_literal_integer() {
1517            let json = serde_json::json!(42);
1518            let expr = parse_expr_value(&json);
1519            assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1520        }
1521
1522        #[test]
1523        fn parse_literal_float() {
1524            let json = serde_json::json!(1.5);
1525            let expr = parse_expr_value(&json);
1526            assert!(
1527                matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1528            );
1529        }
1530
1531        #[test]
1532        fn parse_literal_boolean() {
1533            let json = serde_json::json!(true);
1534            let expr = parse_expr_value(&json);
1535            assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1536        }
1537
1538        #[test]
1539        fn parse_literal_null() {
1540            let json = serde_json::Value::Null;
1541            let expr = parse_expr_value(&json);
1542            assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1543        }
1544
1545        #[test]
1546        fn parse_single_expression() {
1547            let json = serde_json::json!("${artifact.ksr | sha256}");
1548            let expr = parse_expr_value(&json);
1549            match expr {
1550                ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1551                    assert!(matches!(*source, Expression::Reference(_)));
1552                    assert_eq!(stages.len(), 1);
1553                    assert_eq!(stages[0].function, "sha256");
1554                }
1555                _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1556            }
1557        }
1558
1559        #[test]
1560        fn parse_simple_reference() {
1561            let json = serde_json::json!("${param.name}");
1562            let expr = parse_expr_value(&json);
1563            match expr {
1564                ExprValue::Expr(Expression::Reference(r)) => {
1565                    assert_eq!(r.ref_type, RefType::Param);
1566                    assert_eq!(r.name, "name");
1567                }
1568                _ => panic!("Expected Expr(Reference), got {expr:?}"),
1569            }
1570        }
1571
1572        #[test]
1573        fn parse_interpolated_string() {
1574            let json = serde_json::json!("Hello, ${param.name}!");
1575            let expr = parse_expr_value(&json);
1576            match expr {
1577                ExprValue::Interpolated(parts) => {
1578                    assert_eq!(parts.len(), 3);
1579                    assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1580                    assert!(matches!(
1581                        &parts[1],
1582                        StringPart::Expr(Expression::Reference(_))
1583                    ));
1584                    assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1585                }
1586                _ => panic!("Expected Interpolated, got {expr:?}"),
1587            }
1588        }
1589
1590        #[test]
1591        fn parse_object_with_expressions() {
1592            let json = serde_json::json!({
1593                "actual": "${artifact.ksr | sha256 | hex}",
1594                "expected": "${param.expected_hash}",
1595                "static": "hello"
1596            });
1597            let expr = parse_expr_value(&json);
1598            match expr {
1599                ExprValue::Object(map) => {
1600                    assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1601                    assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1602                    assert!(matches!(
1603                        map.get("static"),
1604                        Some(ExprValue::Literal(Literal::String(_)))
1605                    ));
1606                }
1607                _ => panic!("Expected Object, got {expr:?}"),
1608            }
1609        }
1610
1611        #[test]
1612        fn parse_array_with_expressions() {
1613            let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1614            let expr = parse_expr_value(&json);
1615            match expr {
1616                ExprValue::Array(arr) => {
1617                    assert_eq!(arr.len(), 3);
1618                    assert!(matches!(&arr[0], ExprValue::Expr(_)));
1619                    assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1620                    assert!(matches!(&arr[2], ExprValue::Expr(_)));
1621                }
1622                _ => panic!("Expected Array, got {expr:?}"),
1623            }
1624        }
1625
1626        #[test]
1627        fn parse_deeply_nested_value_is_truncated_not_crashed() {
1628            // 200 levels of nesting: far past the cap. The function must
1629            // return (no stack overflow), truncating everything below
1630            // MAX_VALUE_DEPTH to Literal(Null).
1631            let mut json = serde_json::json!("leaf");
1632            for _ in 0..200 {
1633                json = serde_json::Value::Array(vec![json]);
1634            }
1635            let mut expr = parse_expr_value(&json);
1636            let mut levels = 0;
1637            loop {
1638                match expr {
1639                    ExprValue::Array(mut arr) => {
1640                        assert_eq!(arr.len(), 1);
1641                        expr = arr.remove(0);
1642                        levels += 1;
1643                    }
1644                    other => {
1645                        assert!(
1646                            matches!(other, ExprValue::Literal(Literal::Null)),
1647                            "truncated tail must be Literal(Null), got {other:?}"
1648                        );
1649                        break;
1650                    }
1651                }
1652            }
1653            assert_eq!(levels, MAX_VALUE_DEPTH, "recursion must stop at the cap");
1654        }
1655
1656        #[test]
1657        fn parse_moderately_nested_value_is_preserved() {
1658            let mut json = serde_json::json!("leaf");
1659            for _ in 0..10 {
1660                json = serde_json::Value::Array(vec![json]);
1661            }
1662            let mut expr = parse_expr_value(&json);
1663            for _ in 0..10 {
1664                match expr {
1665                    ExprValue::Array(mut arr) => {
1666                        assert_eq!(arr.len(), 1);
1667                        expr = arr.remove(0);
1668                    }
1669                    other => panic!("expected Array, got {other:?}"),
1670                }
1671            }
1672            assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "leaf"));
1673        }
1674
1675        #[test]
1676        fn parse_multiple_expressions_in_string() {
1677            let json = serde_json::json!("${param.a} and ${param.b}");
1678            let expr = parse_expr_value(&json);
1679            match expr {
1680                ExprValue::Interpolated(parts) => {
1681                    assert_eq!(parts.len(), 3);
1682                    assert!(matches!(&parts[0], StringPart::Expr(_)));
1683                    assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1684                    assert!(matches!(&parts[2], StringPart::Expr(_)));
1685                }
1686                _ => panic!("Expected Interpolated, got {expr:?}"),
1687            }
1688        }
1689    }
1690
1691    mod invalid_expressions {
1692        use super::*;
1693
1694        /// `RefType::ALL` is written by hand; this match makes a new variant a
1695        /// compile error until it is listed there.
1696        #[test]
1697        fn all_lists_every_namespace() {
1698            for ref_type in RefType::ALL {
1699                match ref_type {
1700                    RefType::Param | RefType::Role | RefType::Artifact => {}
1701                }
1702            }
1703            assert_eq!(RefType::ALL.len(), 3);
1704            assert_eq!(namespace_list(), "param, role, artifact");
1705        }
1706
1707        #[test]
1708        fn a_valid_expression_yields_nothing() {
1709            for s in [
1710                "${artifact.ksr}",
1711                "${artifact.keypair.public | sha256 | hex}",
1712                "${param.name}",
1713                "plain text with no expression",
1714                "Hash: ${artifact.a | sha256 | hex} done",
1715                "${concat(artifact.a | sha256, artifact.b) | hex}",
1716            ] {
1717                assert!(
1718                    find_invalid_expressions(s).is_empty(),
1719                    "{s} should be accepted"
1720                );
1721            }
1722        }
1723
1724        #[test]
1725        fn an_unknown_namespace_is_reported_with_its_range() {
1726            let found = find_invalid_expressions("sign ${nonsense.x} now");
1727            assert_eq!(found.len(), 1);
1728            assert_eq!(found[0].range, 5..18);
1729            assert_eq!(found[0].text, "${nonsense.x}");
1730            assert_eq!(
1731                found[0].reason,
1732                ExprError::UnknownNamespace {
1733                    namespace: "nonsense".to_string(),
1734                }
1735            );
1736            assert_eq!(
1737                found[0].reason.to_string(),
1738                "unknown namespace 'nonsense': expected one of param, role, artifact"
1739            );
1740        }
1741
1742        #[test]
1743        fn every_occurrence_in_one_string_is_reported() {
1744            let found = find_invalid_expressions("${a.b} ${param.ok} ${c.d}");
1745            assert_eq!(found.len(), 2);
1746            assert_eq!(found[0].text, "${a.b}");
1747            assert_eq!(found[1].text, "${c.d}");
1748        }
1749
1750        #[test]
1751        fn the_material_namespace_names_the_artifact_form() {
1752            // The plural is what the declaring key is called, so it lands on
1753            // the same explanation rather than the generic unknown-namespace one.
1754            for namespace in ["material", "materials"] {
1755                let found = find_invalid_expressions(&format!("${{{namespace}.manifest}}"));
1756                assert_eq!(found.len(), 1, "for {namespace}");
1757                assert_eq!(
1758                    found[0].reason,
1759                    ExprError::MaterialNamespace {
1760                        namespace: namespace.to_string(),
1761                        name: "manifest".to_string(),
1762                    }
1763                );
1764                assert_eq!(
1765                    found[0].reason.to_string(),
1766                    format!(
1767                        "there is no '{namespace}' namespace: \
1768                         a material is read as 'artifact.manifest'"
1769                    )
1770                );
1771                assert_eq!(
1772                    found[0].reason.suggestion(),
1773                    Some(RefType::Artifact),
1774                    "an editor fix points at the artifact namespace"
1775                );
1776            }
1777        }
1778
1779        #[test]
1780        fn a_pipeline_with_nothing_before_the_pipe_is_not_an_empty_expression() {
1781            assert_eq!(
1782                parse_expression_detailed("${ | sha256 }").unwrap_err(),
1783                ExprError::MissingSource
1784            );
1785            assert_eq!(
1786                parse_expression_detailed("${}").unwrap_err(),
1787                ExprError::Empty
1788            );
1789        }
1790
1791        #[test]
1792        fn an_unclosed_expression_is_reported_to_the_end_of_the_string() {
1793            let found = find_invalid_expressions("see ${artifact.k");
1794            assert_eq!(found.len(), 1);
1795            assert_eq!(found[0].text, "${artifact.k");
1796            assert_eq!(found[0].reason, ExprError::Unclosed);
1797            assert_eq!(
1798                found[0].reason.to_string(),
1799                "the expression is missing its closing '}'"
1800            );
1801        }
1802
1803        #[test]
1804        fn each_shape_of_mistake_gets_its_own_reason() {
1805            for (input, expected) in [
1806                ("${}", "the expression is empty"),
1807                (
1808                    "${artifact}",
1809                    "'artifact' names no namespace: write '<namespace>.<name>', \
1810                     where <namespace> is one of param, role, artifact",
1811                ),
1812                (
1813                    "${artifact.}",
1814                    "nothing follows 'artifact.': expected a name",
1815                ),
1816                (
1817                    "${artifact.9k}",
1818                    "'9k' is not a usable name: names hold letters, digits, and underscores, \
1819                     and cannot start with a digit",
1820                ),
1821                (
1822                    "${artifact.k.9p}",
1823                    "'9p' is not a usable property: names hold letters, digits, and underscores, \
1824                     and cannot start with a digit",
1825                ),
1826                (
1827                    "${artifact.k | 9hex}",
1828                    "'9hex' is not a usable pipe stage: expected a function name, \
1829                     with any arguments in parentheses",
1830                ),
1831                (
1832                    "${concat(artifact.a}",
1833                    "the call to 'concat' is missing its closing ')'",
1834                ),
1835            ] {
1836                let found = find_invalid_expressions(input);
1837                assert_eq!(found.len(), 1, "{input} should be rejected");
1838                assert_eq!(found[0].reason.to_string(), expected, "for {input}");
1839            }
1840        }
1841
1842        #[test]
1843        fn a_bare_name_is_told_to_use_the_wrapper() {
1844            let error = parse_expression_detailed("keypair").unwrap_err();
1845            assert_eq!(error, ExprError::NotAnExpression);
1846            assert_eq!(
1847                error.to_string(),
1848                "write it as an expression, '${<namespace>.<name>}', \
1849                 where <namespace> is one of param, role, artifact"
1850            );
1851        }
1852
1853        #[test]
1854        fn a_namespace_one_or_two_edits_away_is_suggested() {
1855            for (input, expected) in [
1856                ("${paramm.region}", RefType::Param),
1857                ("${params.region}", RefType::Param),
1858                ("${roles.officer}", RefType::Role),
1859                ("${artifacts.ksr}", RefType::Artifact),
1860            ] {
1861                let error = parse_expression_detailed(input).unwrap_err();
1862                assert_eq!(error.suggestion(), Some(expected), "for {input}");
1863                assert!(
1864                    error
1865                        .to_string()
1866                        .contains(&format!("did you mean '{expected}'")),
1867                    "for {input}: {error}"
1868                );
1869            }
1870        }
1871
1872        #[test]
1873        fn a_namespace_nothing_like_a_known_one_gets_no_suggestion() {
1874            for input in ["${nonsense.x}", "${env.HOME}", "${secret.pin}"] {
1875                let error = parse_expression_detailed(input).unwrap_err();
1876                assert!(
1877                    matches!(error, ExprError::UnknownNamespace { .. }),
1878                    "{input} should be an unknown namespace, got {error:?}"
1879                );
1880                assert_eq!(error.suggestion(), None, "{input} should not be guessed at");
1881            }
1882        }
1883
1884        #[test]
1885        fn a_failing_argument_names_its_position_and_its_own_cause() {
1886            let error = parse_expression_detailed("${concat(artifact.a, nonsense.b)}").unwrap_err();
1887            assert_eq!(
1888                error,
1889                ExprError::InvalidArgument {
1890                    function: "concat".to_string(),
1891                    position: 2,
1892                    cause: Box::new(ExprError::UnknownNamespace {
1893                        namespace: "nonsense".to_string(),
1894                    }),
1895                }
1896            );
1897            assert_eq!(
1898                error.to_string(),
1899                "argument 2 to 'concat' is not a usable expression: \
1900                 unknown namespace 'nonsense': expected one of param, role, artifact"
1901            );
1902        }
1903
1904        #[test]
1905        fn a_lone_quote_is_an_unterminated_string() {
1906            // The source is dispatched on its first character, so a quote that
1907            // opens a string is never also read as the one that closes it.
1908            for (input, quote) in [("${\"}", '"'), ("${'}", '\''), ("${\"abc}", '"')] {
1909                assert_eq!(
1910                    parse_expression_detailed(input).unwrap_err(),
1911                    ExprError::UnterminatedString { quote },
1912                    "for {input}"
1913                );
1914            }
1915        }
1916
1917        #[test]
1918        fn a_quote_opens_a_string_even_when_it_holds_a_parenthesis() {
1919            let parsed = parse_expression_detailed("${\"a(b\"}");
1920            assert!(
1921                matches!(parsed, Ok(Expression::Literal(Literal::String(ref s))) if s == "a(b"),
1922                "the parenthesis belongs to the string, got {parsed:?}"
1923            );
1924        }
1925
1926        #[test]
1927        fn the_shape_a_source_is_read_as_comes_from_its_first_characters() {
1928            // A dot means a reference, so a name with a parenthesis in it is a
1929            // broken name rather than a broken call.
1930            for (input, expected) in [
1931                (
1932                    "${param.x(1)}",
1933                    ExprError::InvalidName {
1934                        name: "x(1)".to_string(),
1935                    },
1936                ),
1937                (
1938                    "${42abc}",
1939                    ExprError::NotAReference {
1940                        text: "42abc".to_string(),
1941                    },
1942                ),
1943                (
1944                    "${1.5}",
1945                    ExprError::NotAReference {
1946                        text: "1.5".to_string(),
1947                    },
1948                ),
1949            ] {
1950                assert_eq!(
1951                    parse_expression_detailed(input).unwrap_err(),
1952                    expected,
1953                    "for {input}"
1954                );
1955            }
1956        }
1957
1958        #[test]
1959        fn parses_every_accepted_form() {
1960            for input in [
1961                "${artifact.ksr}",
1962                "${artifact.keypair.public}",
1963                "${param.region | sha256 | hex}",
1964                "${\"literal\"}",
1965                "${'literal'}",
1966                "${42}",
1967                "${-42}",
1968                "${\"\"}",
1969                "${concat(artifact.a, artifact.b) | hex}",
1970                "${pad(artifact.a, 4)}",
1971            ] {
1972                assert!(
1973                    parse_expression_detailed(input).is_ok(),
1974                    "{input} should parse, got {:?}",
1975                    parse_expression_detailed(input)
1976                );
1977            }
1978        }
1979    }
1980}