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