#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RefType {
Param,
Role,
Artifact,
}
impl std::str::FromStr for RefType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"param" => Ok(Self::Param),
"role" => Ok(Self::Role),
"artifact" => Ok(Self::Artifact),
_ => Err(format!("unknown reference type: {s}")),
}
}
}
impl std::fmt::Display for RefType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Param => write!(f, "param"),
Self::Role => write!(f, "role"),
Self::Artifact => write!(f, "artifact"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Bytes(Vec<u8>),
String(String),
Integer(i64),
Boolean(bool),
Null,
}
impl Value {
pub fn as_bytes(&self) -> Option<Vec<u8>> {
match self {
Value::Bytes(b) => Some(b.clone()),
Value::String(s) => Some(s.as_bytes().to_vec()),
_ => None,
}
}
pub fn as_string(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
Value::Integer(i) => Some(*i),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn type_name(&self) -> &'static str {
match self {
Value::Bytes(_) => "bytes",
Value::String(_) => "string",
Value::Integer(_) => "integer",
Value::Boolean(_) => "boolean",
Value::Null => "null",
}
}
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
Value::String(s) => write!(f, "{s}"),
Value::Integer(i) => write!(f, "{i}"),
Value::Boolean(b) => write!(f, "{b}"),
Value::Null => write!(f, "null"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expression {
Reference(Reference),
Literal(Literal),
Pipeline {
source: Box<Expression>,
stages: Vec<PipeStage>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
pub ref_type: RefType,
pub name: String,
pub property: Option<String>,
}
impl Reference {
pub fn new(ref_type: RefType, name: &str) -> Self {
Self {
ref_type,
name: name.to_string(),
property: None,
}
}
pub fn with_property(ref_type: RefType, name: &str, property: &str) -> Self {
Self {
ref_type,
name: name.to_string(),
property: Some(property.to_string()),
}
}
}
impl std::fmt::Display for Reference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(prop) = &self.property {
write!(f, "{}.{}.{}", self.ref_type, self.name, prop)
} else {
write!(f, "{}.{}", self.ref_type, self.name)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Null,
}
#[allow(clippy::cast_possible_truncation)]
impl From<Literal> for Value {
fn from(lit: Literal) -> Self {
match lit {
Literal::String(s) => Value::String(s),
Literal::Integer(i) => Value::Integer(i),
Literal::Float(f) => Value::Integer(f as i64), Literal::Boolean(b) => Value::Boolean(b),
Literal::Null => Value::Null,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PipeStage {
pub function: String,
pub args: Vec<Expression>,
}
impl PipeStage {
pub fn new(function: &str) -> Self {
Self {
function: function.to_string(),
args: Vec::new(),
}
}
pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
Self {
function: function.to_string(),
args,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LocatedExpression {
pub expression: Expression,
pub range: Range<usize>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExprValue {
Literal(Literal),
Expr(Expression),
Interpolated(Vec<StringPart>),
Object(std::collections::HashMap<String, ExprValue>),
Array(Vec<ExprValue>),
}
impl ExprValue {
pub fn as_literal_string(&self) -> Option<&str> {
match self {
ExprValue::Literal(Literal::String(s)) => Some(s),
_ => None,
}
}
pub fn get(&self, key: &str) -> Option<&ExprValue> {
match self {
ExprValue::Object(map) => map.get(key),
_ => None,
}
}
pub fn to_display_string(&self) -> String {
match self {
ExprValue::Literal(Literal::String(s)) => s.clone(),
ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
ExprValue::Literal(Literal::Float(f)) => f.to_string(),
ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
ExprValue::Literal(Literal::Null) => "null".to_string(),
ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
ExprValue::Interpolated(parts) => parts
.iter()
.map(|p| match p {
StringPart::Literal(s) => s.clone(),
StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
})
.collect(),
ExprValue::Object(_) => "[object]".to_string(),
ExprValue::Array(_) => "[array]".to_string(),
}
}
}
fn expr_to_string(expr: &Expression) -> String {
match expr {
Expression::Reference(r) => r.to_string(),
Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
Expression::Literal(Literal::Integer(i)) => i.to_string(),
Expression::Literal(Literal::Float(f)) => f.to_string(),
Expression::Literal(Literal::Boolean(b)) => b.to_string(),
Expression::Literal(Literal::Null) => "null".to_string(),
Expression::Pipeline { source, stages } => {
let mut result = expr_to_string(source);
for stage in stages {
result.push_str(" | ");
result.push_str(&stage.function);
if !stage.args.is_empty() {
result.push('(');
let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
result.push_str(&args.join(", "));
result.push(')');
}
}
result
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StringPart {
Literal(String),
Expr(Expression),
}
pub fn parse_expression(s: &str) -> Option<Expression> {
let s = s.trim();
let inner = s.strip_prefix("${").and_then(|s| s.strip_suffix('}'))?;
let inner = inner.trim();
if inner.is_empty() {
return None;
}
parse_pipeline(inner)
}
fn parse_pipeline(s: &str) -> Option<Expression> {
let s = s.trim();
let parts = split_on_pipes(s);
if parts.is_empty() {
return None;
}
let source = parse_source(parts.first()?.trim())?;
if parts.len() == 1 {
return Some(source);
}
let mut stages = Vec::new();
for part in parts.iter().skip(1) {
let stage = parse_pipe_stage(part.trim())?;
stages.push(stage);
}
Some(Expression::Pipeline {
source: Box::new(source),
stages,
})
}
fn split_on_pipes(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let mut paren_depth: u32 = 0;
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => paren_depth += 1,
b')' => paren_depth = paren_depth.saturating_sub(1),
b'|' if paren_depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < s.len() {
parts.push(&s[start..]);
}
parts
}
fn parse_source(s: &str) -> Option<Expression> {
let s = s.trim();
if let Some(expr) = parse_function_call(s) {
return Some(expr);
}
if let Some(expr_ref) = parse_ref(s) {
return Some(Expression::Reference(expr_ref));
}
if let Some(lit) = parse_literal(s) {
return Some(Expression::Literal(lit));
}
None
}
fn parse_ref(s: &str) -> Option<Reference> {
let parts: Vec<&str> = s.splitn(3, '.').collect();
if parts.len() < 2 {
return None;
}
let namespace = parts[0];
if !is_valid_identifier(namespace) {
return None;
}
let ref_type: RefType = namespace.parse().ok()?;
let name = parts[1];
if !is_valid_identifier(name) {
return None;
}
let property = if parts.len() == 3 {
let prop = parts[2];
if !is_valid_identifier(prop) {
return None;
}
Some(prop.to_string())
} else {
None
};
Some(Reference {
ref_type,
name: name.to_string(),
property,
})
}
fn parse_literal(s: &str) -> Option<Literal> {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
let inner = &s[1..s.len() - 1];
return Some(Literal::String(inner.to_string()));
}
if let Ok(i) = s.parse::<i64>() {
return Some(Literal::Integer(i));
}
None
}
fn parse_function_call(s: &str) -> Option<Expression> {
let s = s.trim();
let paren_pos = s.find('(')?;
let name = &s[..paren_pos];
if !is_valid_identifier(name) {
return None;
}
if !s.ends_with(')') {
return None;
}
let args_str = &s[paren_pos + 1..s.len() - 1];
let args = parse_function_args(args_str)?;
Some(Expression::Pipeline {
source: Box::new(Expression::Literal(Literal::Integer(0))), stages: vec![PipeStage::with_args(name, args)],
})
}
fn parse_function_args(s: &str) -> Option<Vec<Expression>> {
let s = s.trim();
if s.is_empty() {
return Some(Vec::new());
}
let mut args = Vec::new();
let mut current_start = 0;
let mut paren_depth: u32 = 0;
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => paren_depth += 1,
b')' => paren_depth = paren_depth.saturating_sub(1),
b',' if paren_depth == 0 => {
let arg = &s[current_start..i];
let expr = parse_pipeline(arg.trim())?;
args.push(expr);
current_start = i + 1;
}
_ => {}
}
}
if current_start < s.len() {
let arg = &s[current_start..];
let expr = parse_pipeline(arg.trim())?;
args.push(expr);
}
Some(args)
}
fn parse_pipe_stage(s: &str) -> Option<PipeStage> {
let s = s.trim();
if let Some(paren_pos) = s.find('(') {
let name = &s[..paren_pos];
if !is_valid_identifier(name) {
return None;
}
if !s.ends_with(')') {
return None;
}
let args_str = &s[paren_pos + 1..s.len() - 1];
let args = parse_function_args(args_str)?;
return Some(PipeStage::with_args(name, args));
}
if is_valid_identifier(s) {
return Some(PipeStage::new(s));
}
None
}
fn is_valid_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
None => false,
Some(first) => {
(first.is_ascii_alphabetic() || first == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
}
}
pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
let mut results = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
let mut depth = 1;
let mut j = i + 2;
while j < bytes.len() && depth > 0 {
match bytes[j] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
j += 1;
}
if depth == 0 {
let expr_str = &s[i..j];
if let Some(expression) = parse_expression(expr_str) {
results.push(LocatedExpression {
expression,
range: i..j,
});
}
i = j;
continue;
}
}
i += 1;
}
results
}
pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
match json {
serde_json::Value::String(s) => parse_string_to_expr_value(s),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
ExprValue::Literal(Literal::Integer(i))
} else if let Some(f) = n.as_f64() {
ExprValue::Literal(Literal::Float(f))
} else {
ExprValue::Literal(Literal::Integer(0))
}
}
serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
serde_json::Value::Null => ExprValue::Literal(Literal::Null),
serde_json::Value::Object(map) => {
let mut result = std::collections::HashMap::new();
for (k, v) in map {
result.insert(k.clone(), parse_expr_value(v));
}
ExprValue::Object(result)
}
serde_json::Value::Array(arr) => {
let result: Vec<_> = arr.iter().map(parse_expr_value).collect();
ExprValue::Array(result)
}
}
}
fn parse_string_to_expr_value(s: &str) -> ExprValue {
let trimmed = s.trim();
if trimmed.starts_with("${")
&& trimmed.ends_with('}')
&& !trimmed[2..trimmed.len() - 1].contains("${")
&& let Some(expr) = parse_expression(trimmed)
{
return ExprValue::Expr(expr);
}
if s.contains("${") {
let parts = parse_interpolated_string(s);
if parts.len() == 1 {
if let StringPart::Literal(lit) = &parts[0] {
return ExprValue::Literal(Literal::String(lit.clone()));
}
}
return ExprValue::Interpolated(parts);
}
ExprValue::Literal(Literal::String(s.to_string()))
}
fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
let mut parts = Vec::new();
let bytes = s.as_bytes();
let mut last_end = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
if i > last_end {
parts.push(StringPart::Literal(s[last_end..i].to_string()));
}
let mut depth = 1;
let mut j = i + 2;
while j < bytes.len() && depth > 0 {
match bytes[j] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
j += 1;
}
if depth == 0 {
let expr_str = &s[i..j];
if let Some(expr) = parse_expression(expr_str) {
parts.push(StringPart::Expr(expr));
} else {
parts.push(StringPart::Literal(expr_str.to_string()));
}
last_end = j;
i = j;
continue;
}
}
i += 1;
}
if last_end < s.len() {
parts.push(StringPart::Literal(s[last_end..].to_string()));
}
parts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_reference() {
let expr = parse_expression("${artifact.ksr}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Artifact);
assert_eq!(r.name, "ksr");
assert!(r.property.is_none());
}
_ => panic!("Expected Reference"),
}
}
#[test]
fn parse_reference_with_property() {
let expr = parse_expression("${artifact.keypair.private}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Artifact);
assert_eq!(r.name, "keypair");
assert_eq!(r.property, Some("private".to_string()));
}
_ => panic!("Expected Reference"),
}
}
#[test]
fn parse_param_reference() {
let expr = parse_expression("${param.key_label}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Param);
assert_eq!(r.name, "key_label");
}
_ => panic!("Expected Reference"),
}
}
#[test]
fn parse_simple_pipeline() {
let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
match expr {
Expression::Pipeline { source, stages } => {
assert!(matches!(*source, Expression::Reference(_)));
assert_eq!(stages.len(), 1);
assert_eq!(stages[0].function, "sha256");
assert!(stages[0].args.is_empty());
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn parse_multi_stage_pipeline() {
let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
match expr {
Expression::Pipeline { stages, .. } => {
assert_eq!(stages.len(), 2);
assert_eq!(stages[0].function, "sha256");
assert_eq!(stages[1].function, "hex");
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn parse_pipeline_with_function_args() {
let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
match expr {
Expression::Pipeline { stages, .. } => {
assert_eq!(stages.len(), 1);
assert_eq!(stages[0].function, "substr");
assert_eq!(stages[0].args.len(), 2);
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn parse_complex_pipeline() {
let expr =
parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
match expr {
Expression::Pipeline { source, stages } => {
if let Expression::Reference(r) = *source {
assert_eq!(r.name, "keypair");
assert_eq!(r.property, Some("public".to_string()));
}
assert_eq!(stages.len(), 3);
assert_eq!(stages[0].function, "fingerprint");
assert_eq!(stages[1].function, "hex");
assert_eq!(stages[2].function, "upper");
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn parse_invalid_expression() {
assert!(parse_expression("not an expression").is_none());
assert!(parse_expression("${").is_none());
assert!(parse_expression("${}").is_none());
assert!(parse_expression("${invalid}").is_none()); }
#[test]
fn find_expressions_in_string() {
let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
assert_eq!(exprs.len(), 1);
assert_eq!(exprs[0].range.start, 6);
}
#[test]
fn find_multiple_expressions() {
let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
assert_eq!(exprs.len(), 2);
}
#[test]
fn value_type_names() {
assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
assert_eq!(Value::String(String::new()).type_name(), "string");
assert_eq!(Value::Integer(0).type_name(), "integer");
assert_eq!(Value::Boolean(true).type_name(), "boolean");
assert_eq!(Value::Null.type_name(), "null");
}
#[test]
fn value_as_bytes() {
let v = Value::Bytes(vec![1, 2, 3]);
assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
let v = Value::String("hello".into());
assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
let v = Value::Integer(42);
assert!(v.as_bytes().is_none());
}
mod ceremony_examples {
use super::*;
#[test]
fn dnssec_hash_verification() {
let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
match expr {
Expression::Pipeline { source, stages } => {
if let Expression::Reference(r) = *source {
assert_eq!(r.ref_type, RefType::Artifact);
assert_eq!(r.name, "ksr");
} else {
panic!("Expected Reference source");
}
assert_eq!(stages.len(), 3);
assert_eq!(stages[0].function, "sha256");
assert_eq!(stages[1].function, "hex");
assert_eq!(stages[2].function, "substr");
assert_eq!(stages[2].args.len(), 2);
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn dnssec_param_reference() {
let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Param);
assert_eq!(r.name, "expected_ksr_hash");
}
_ => panic!("Expected Reference"),
}
}
#[test]
fn dnssec_oral_readback() {
let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
match expr {
Expression::Pipeline { stages, .. } => {
assert_eq!(stages.len(), 2);
assert_eq!(stages[0].function, "sha256");
assert_eq!(stages[1].function, "hex");
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn hsm_artifact_reference() {
let expr = parse_expression("${artifact.master_keypair}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Artifact);
assert_eq!(r.name, "master_keypair");
assert!(r.property.is_none());
}
_ => panic!("Expected Reference"),
}
}
#[test]
fn hsm_public_key_fingerprint() {
let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
match expr {
Expression::Pipeline { source, stages } => {
if let Expression::Reference(r) = *source {
assert_eq!(r.name, "master_public_key");
}
assert_eq!(stages.len(), 2);
}
_ => panic!("Expected Pipeline"),
}
}
#[test]
fn crypto_artifact_with_property() {
let expr = parse_expression("${artifact.keypair.private}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Artifact);
assert_eq!(r.name, "keypair");
assert_eq!(r.property, Some("private".to_string()));
}
_ => panic!("Expected Reference"),
}
}
#[test]
fn find_embedded_expressions() {
let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
assert_eq!(exprs.len(), 1);
if let Expression::Reference(r) = &exprs[0].expression {
assert_eq!(r.ref_type, RefType::Param);
assert_eq!(r.name, "hsm_serial");
}
}
#[test]
fn find_multiple_embedded_params() {
let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
assert_eq!(exprs.len(), 2);
}
#[test]
fn role_reference_parses() {
let expr = parse_expression("${role.ceremony_admin}").unwrap();
match expr {
Expression::Reference(r) => {
assert_eq!(r.ref_type, RefType::Role);
assert_eq!(r.name, "ceremony_admin");
}
_ => panic!("Expected Reference"),
}
}
}
mod expr_value_tests {
use super::*;
#[test]
fn parse_literal_string() {
let json = serde_json::json!("hello");
let expr = parse_expr_value(&json);
assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
}
#[test]
fn parse_literal_integer() {
let json = serde_json::json!(42);
let expr = parse_expr_value(&json);
assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
}
#[test]
fn parse_literal_float() {
let json = serde_json::json!(1.5);
let expr = parse_expr_value(&json);
assert!(
matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
);
}
#[test]
fn parse_literal_boolean() {
let json = serde_json::json!(true);
let expr = parse_expr_value(&json);
assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
}
#[test]
fn parse_literal_null() {
let json = serde_json::Value::Null;
let expr = parse_expr_value(&json);
assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
}
#[test]
fn parse_single_expression() {
let json = serde_json::json!("${artifact.ksr | sha256}");
let expr = parse_expr_value(&json);
match expr {
ExprValue::Expr(Expression::Pipeline { source, stages }) => {
assert!(matches!(*source, Expression::Reference(_)));
assert_eq!(stages.len(), 1);
assert_eq!(stages[0].function, "sha256");
}
_ => panic!("Expected Expr(Pipeline), got {expr:?}"),
}
}
#[test]
fn parse_simple_reference() {
let json = serde_json::json!("${param.name}");
let expr = parse_expr_value(&json);
match expr {
ExprValue::Expr(Expression::Reference(r)) => {
assert_eq!(r.ref_type, RefType::Param);
assert_eq!(r.name, "name");
}
_ => panic!("Expected Expr(Reference), got {expr:?}"),
}
}
#[test]
fn parse_interpolated_string() {
let json = serde_json::json!("Hello, ${param.name}!");
let expr = parse_expr_value(&json);
match expr {
ExprValue::Interpolated(parts) => {
assert_eq!(parts.len(), 3);
assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
assert!(matches!(
&parts[1],
StringPart::Expr(Expression::Reference(_))
));
assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
}
_ => panic!("Expected Interpolated, got {expr:?}"),
}
}
#[test]
fn parse_object_with_expressions() {
let json = serde_json::json!({
"actual": "${artifact.ksr | sha256 | hex}",
"expected": "${param.expected_hash}",
"static": "hello"
});
let expr = parse_expr_value(&json);
match expr {
ExprValue::Object(map) => {
assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
assert!(matches!(
map.get("static"),
Some(ExprValue::Literal(Literal::String(_)))
));
}
_ => panic!("Expected Object, got {expr:?}"),
}
}
#[test]
fn parse_array_with_expressions() {
let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
let expr = parse_expr_value(&json);
match expr {
ExprValue::Array(arr) => {
assert_eq!(arr.len(), 3);
assert!(matches!(&arr[0], ExprValue::Expr(_)));
assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
assert!(matches!(&arr[2], ExprValue::Expr(_)));
}
_ => panic!("Expected Array, got {expr:?}"),
}
}
#[test]
fn parse_multiple_expressions_in_string() {
let json = serde_json::json!("${param.a} and ${param.b}");
let expr = parse_expr_value(&json);
match expr {
ExprValue::Interpolated(parts) => {
assert_eq!(parts.len(), 3);
assert!(matches!(&parts[0], StringPart::Expr(_)));
assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
assert!(matches!(&parts[2], StringPart::Expr(_)));
}
_ => panic!("Expected Interpolated, got {expr:?}"),
}
}
}
}