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