1#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
30
31use std::ops::Range;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum RefType {
39 Param,
41 Role,
43 Artifact,
45}
46
47impl RefType {
48 pub const ALL: &'static [Self] = &[Self::Param, Self::Role, Self::Artifact];
53
54 #[must_use]
56 pub const fn as_str(self) -> &'static str {
57 match self {
58 Self::Param => "param",
59 Self::Role => "role",
60 Self::Artifact => "artifact",
61 }
62 }
63}
64
65impl std::str::FromStr for RefType {
66 type Err = String;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 Self::ALL
70 .iter()
71 .copied()
72 .find(|ref_type| ref_type.as_str() == s)
73 .ok_or_else(|| format!("unknown reference type: {s}"))
74 }
75}
76
77impl std::fmt::Display for RefType {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.write_str(self.as_str())
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub enum Value {
86 Bytes(Vec<u8>),
88 String(String),
90 Integer(i64),
92 Boolean(bool),
94 Null,
96}
97
98impl Value {
99 pub fn as_bytes(&self) -> Option<Vec<u8>> {
102 match self {
103 Value::Bytes(b) => Some(b.clone()),
104 Value::String(s) => Some(s.as_bytes().to_vec()),
105 _ => None,
106 }
107 }
108
109 pub fn as_string(&self) -> Option<&str> {
111 match self {
112 Value::String(s) => Some(s),
113 _ => None,
114 }
115 }
116
117 pub fn as_integer(&self) -> Option<i64> {
119 match self {
120 Value::Integer(i) => Some(*i),
121 _ => None,
122 }
123 }
124
125 pub fn is_null(&self) -> bool {
127 matches!(self, Value::Null)
128 }
129
130 pub fn type_name(&self) -> &'static str {
132 match self {
133 Value::Bytes(_) => "bytes",
134 Value::String(_) => "string",
135 Value::Integer(_) => "integer",
136 Value::Boolean(_) => "boolean",
137 Value::Null => "null",
138 }
139 }
140}
141
142impl std::fmt::Display for Value {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
146 Value::String(s) => write!(f, "{s}"),
147 Value::Integer(i) => write!(f, "{i}"),
148 Value::Boolean(b) => write!(f, "{b}"),
149 Value::Null => write!(f, "null"),
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
161pub enum Expression {
162 Reference(Reference),
164
165 Literal(Literal),
167
168 Pipeline {
170 source: Box<Expression>,
172 stages: Vec<PipeStage>,
174 },
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Reference {
180 pub ref_type: RefType,
182 pub name: String,
184 pub property: Option<String>,
186}
187
188impl Reference {
189 pub fn new(ref_type: RefType, name: &str) -> Self {
191 Self {
192 ref_type,
193 name: name.to_string(),
194 property: None,
195 }
196 }
197
198 pub fn with_property(ref_type: RefType, name: &str, property: &str) -> Self {
200 Self {
201 ref_type,
202 name: name.to_string(),
203 property: Some(property.to_string()),
204 }
205 }
206}
207
208impl std::fmt::Display for Reference {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 if let Some(prop) = &self.property {
211 write!(f, "{}.{}.{}", self.ref_type, self.name, prop)
212 } else {
213 write!(f, "{}.{}", self.ref_type, self.name)
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq)]
220pub enum Literal {
221 String(String),
223 Integer(i64),
225 Float(f64),
227 Boolean(bool),
229 Null,
231}
232
233#[allow(clippy::cast_possible_truncation)]
234impl From<Literal> for Value {
235 fn from(lit: Literal) -> Self {
236 match lit {
237 Literal::String(s) => Value::String(s),
238 Literal::Integer(i) => Value::Integer(i),
239 Literal::Float(f) => Value::Integer(f as i64), Literal::Boolean(b) => Value::Boolean(b),
241 Literal::Null => Value::Null,
242 }
243 }
244}
245
246#[derive(Debug, Clone, PartialEq)]
248pub struct PipeStage {
249 pub function: String,
251 pub args: Vec<Expression>,
253}
254
255impl PipeStage {
256 pub fn new(function: &str) -> Self {
258 Self {
259 function: function.to_string(),
260 args: Vec::new(),
261 }
262 }
263
264 pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
266 Self {
267 function: function.to_string(),
268 args,
269 }
270 }
271}
272
273#[derive(Debug, Clone, PartialEq)]
275pub struct LocatedExpression {
276 pub expression: Expression,
278 pub range: Range<usize>,
280}
281
282#[derive(Debug, Clone, PartialEq)]
288pub enum ExprValue {
289 Literal(Literal),
291 Expr(Expression),
293 Interpolated(Vec<StringPart>),
295 Object(std::collections::HashMap<String, ExprValue>),
297 Array(Vec<ExprValue>),
299}
300
301impl ExprValue {
302 pub fn as_literal_string(&self) -> Option<&str> {
304 match self {
305 ExprValue::Literal(Literal::String(s)) => Some(s),
306 _ => None,
307 }
308 }
309
310 pub fn get(&self, key: &str) -> Option<&ExprValue> {
312 match self {
313 ExprValue::Object(map) => map.get(key),
314 _ => None,
315 }
316 }
317
318 pub fn to_display_string(&self) -> String {
323 match self {
324 ExprValue::Literal(Literal::String(s)) => s.clone(),
325 ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
326 ExprValue::Literal(Literal::Float(f)) => f.to_string(),
327 ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
328 ExprValue::Literal(Literal::Null) => "null".to_string(),
329 ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
330 ExprValue::Interpolated(parts) => parts
331 .iter()
332 .map(|p| match p {
333 StringPart::Literal(s) => s.clone(),
334 StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
335 })
336 .collect(),
337 ExprValue::Object(_) => "[object]".to_string(),
338 ExprValue::Array(_) => "[array]".to_string(),
339 }
340 }
341}
342
343fn expr_to_string(expr: &Expression) -> String {
345 match expr {
346 Expression::Reference(r) => r.to_string(),
347 Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
348 Expression::Literal(Literal::Integer(i)) => i.to_string(),
349 Expression::Literal(Literal::Float(f)) => f.to_string(),
350 Expression::Literal(Literal::Boolean(b)) => b.to_string(),
351 Expression::Literal(Literal::Null) => "null".to_string(),
352 Expression::Pipeline { source, stages } => {
353 let mut result = expr_to_string(source);
354 for stage in stages {
355 result.push_str(" | ");
356 result.push_str(&stage.function);
357 if !stage.args.is_empty() {
358 result.push('(');
359 let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
360 result.push_str(&args.join(", "));
361 result.push(')');
362 }
363 }
364 result
365 }
366 }
367}
368
369#[derive(Debug, Clone, PartialEq)]
371pub enum StringPart {
372 Literal(String),
374 Expr(Expression),
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
384#[non_exhaustive]
385pub enum ExprError {
386 NotAnExpression,
388 Unclosed,
390 Empty,
392 MissingSource,
394 UnterminatedString {
396 quote: char,
398 },
399 InvalidFunctionName {
401 name: String,
403 },
404 UnclosedCall {
406 function: String,
408 },
409 InvalidArgument {
411 function: String,
413 position: usize,
415 cause: Box<ExprError>,
417 },
418 MissingNamespace {
420 text: String,
422 },
423 NotAReference {
425 text: String,
427 },
428 UnknownNamespace {
430 namespace: String,
432 },
433 MaterialNamespace {
435 namespace: String,
437 name: String,
439 },
440 EmptyName {
442 namespace: String,
444 },
445 InvalidName {
447 name: String,
449 },
450 InvalidProperty {
452 property: String,
454 },
455 InvalidPipeStage {
457 stage: String,
459 },
460}
461
462const IDENTIFIER_RULE: &str =
465 "names hold letters, digits, and underscores, and cannot start with a digit";
466
467impl std::fmt::Display for ExprError {
468 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469 match self {
470 Self::NotAnExpression => write!(
471 f,
472 "write it as an expression, '${{<namespace>.<name>}}', \
473 where <namespace> is one of {}",
474 namespace_list()
475 ),
476 Self::Unclosed => write!(f, "the expression is missing its closing '}}'"),
477 Self::Empty => write!(f, "the expression is empty"),
478 Self::MissingSource => write!(
479 f,
480 "nothing comes before the '|': a pipeline starts from a value"
481 ),
482 Self::UnterminatedString { quote } => {
483 write!(f, "the string is missing its closing {quote}")
484 }
485 Self::InvalidFunctionName { name } => {
486 write!(f, "'{name}' is not a usable function name")
487 }
488 Self::UnclosedCall { function } => {
489 write!(f, "the call to '{function}' is missing its closing ')'")
490 }
491 Self::InvalidArgument {
492 function,
493 position,
494 cause,
495 } => write!(
496 f,
497 "argument {position} to '{function}' is not a usable expression: {cause}"
498 ),
499 Self::MissingNamespace { text } => write!(
500 f,
501 "'{text}' names no namespace: write '<namespace>.<name>', \
502 where <namespace> is one of {}",
503 namespace_list()
504 ),
505 Self::NotAReference { text } => write!(
506 f,
507 "'{text}' is neither a literal nor a reference: a reference starts with \
508 one of {}",
509 namespace_list()
510 ),
511 Self::UnknownNamespace { namespace } => match nearest_namespace(namespace) {
512 Some(known) => {
513 write!(
514 f,
515 "unknown namespace '{namespace}': did you mean '{known}'?"
516 )
517 }
518 None => write!(
519 f,
520 "unknown namespace '{namespace}': expected one of {}",
521 namespace_list()
522 ),
523 },
524 Self::MaterialNamespace { namespace, name } => write!(
525 f,
526 "there is no '{namespace}' namespace: a material is read as 'artifact.{name}'"
527 ),
528 Self::EmptyName { namespace } => {
529 write!(f, "nothing follows '{namespace}.': expected a name")
530 }
531 Self::InvalidName { name } => {
532 write!(f, "'{name}' is not a usable name: {IDENTIFIER_RULE}")
533 }
534 Self::InvalidProperty { property } => {
535 write!(
536 f,
537 "'{property}' is not a usable property: {IDENTIFIER_RULE}"
538 )
539 }
540 Self::InvalidPipeStage { stage } => write!(
541 f,
542 "'{stage}' is not a usable pipe stage: expected a function name, \
543 with any arguments in parentheses"
544 ),
545 }
546 }
547}
548
549impl ExprError {
550 #[must_use]
555 pub fn suggestion(&self) -> Option<RefType> {
556 match self {
557 Self::UnknownNamespace { namespace } => nearest_namespace(namespace),
558 Self::MaterialNamespace { .. } => Some(RefType::Artifact),
561 Self::NotAnExpression
562 | Self::Unclosed
563 | Self::Empty
564 | Self::MissingSource
565 | Self::UnterminatedString { .. }
566 | Self::InvalidFunctionName { .. }
567 | Self::UnclosedCall { .. }
568 | Self::InvalidArgument { .. }
569 | Self::MissingNamespace { .. }
570 | Self::NotAReference { .. }
571 | Self::EmptyName { .. }
572 | Self::InvalidName { .. }
573 | Self::InvalidProperty { .. }
574 | Self::InvalidPipeStage { .. } => None,
575 }
576 }
577}
578
579impl std::error::Error for ExprError {
580 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
581 match self {
582 Self::InvalidArgument { cause, .. } => Some(cause.as_ref()),
583 Self::NotAnExpression
584 | Self::Unclosed
585 | Self::Empty
586 | Self::MissingSource
587 | Self::UnterminatedString { .. }
588 | Self::InvalidFunctionName { .. }
589 | Self::UnclosedCall { .. }
590 | Self::MissingNamespace { .. }
591 | Self::NotAReference { .. }
592 | Self::UnknownNamespace { .. }
593 | Self::MaterialNamespace { .. }
594 | Self::EmptyName { .. }
595 | Self::InvalidName { .. }
596 | Self::InvalidProperty { .. }
597 | Self::InvalidPipeStage { .. } => None,
598 }
599 }
600}
601
602fn namespace_list() -> String {
604 RefType::ALL
605 .iter()
606 .map(|ref_type| ref_type.as_str())
607 .collect::<Vec<_>>()
608 .join(", ")
609}
610
611pub fn parse_expression(s: &str) -> Option<Expression> {
635 parse_expression_detailed(s).ok()
636}
637
638pub fn parse_expression_detailed(s: &str) -> Result<Expression, ExprError> {
659 let Some(rest) = s.trim().strip_prefix("${") else {
662 return Err(ExprError::NotAnExpression);
663 };
664 let Some(inner) = rest.strip_suffix('}') else {
665 return Err(ExprError::Unclosed);
666 };
667
668 parse_pipeline(inner)
669}
670
671fn parse_pipeline(s: &str) -> Result<Expression, ExprError> {
673 let s = s.trim();
674
675 let parts = split_top_level(s, b'|');
677 let first = parts.first().copied().unwrap_or("").trim();
678
679 if first.is_empty() && parts.len() > 1 {
682 return Err(ExprError::MissingSource);
683 }
684
685 let source = parse_source(first)?;
686 if parts.len() == 1 {
687 return Ok(source);
689 }
690
691 let mut stages = Vec::with_capacity(parts.len().saturating_sub(1));
692 for part in parts.iter().skip(1) {
693 stages.push(parse_pipe_stage(part.trim())?);
694 }
695
696 Ok(Expression::Pipeline {
697 source: Box::new(source),
698 stages,
699 })
700}
701
702fn split_top_level(s: &str, delimiter: u8) -> Vec<&str> {
707 let mut parts = Vec::new();
708 let mut start = 0;
709 let mut paren_depth: u32 = 0;
710 let bytes = s.as_bytes();
711
712 for (i, &b) in bytes.iter().enumerate() {
713 match b {
714 b'(' => paren_depth += 1,
715 b')' => paren_depth = paren_depth.saturating_sub(1),
716 _ if b == delimiter && paren_depth == 0 => {
717 parts.push(&s[start..i]);
718 start = i + 1;
719 }
720 _ => {}
721 }
722 }
723
724 if start < s.len() {
726 parts.push(&s[start..]);
727 }
728
729 parts
730}
731
732fn parse_source(s: &str) -> Result<Expression, ExprError> {
740 let s = s.trim();
741
742 if s.is_empty() {
743 return Err(ExprError::Empty);
744 }
745
746 if let Some(quote) = s.chars().next().filter(|c| *c == '"' || *c == '\'') {
747 return parse_string_literal(s, quote).map(Expression::Literal);
748 }
749
750 if let Some(paren) = s.find('(')
751 && is_valid_identifier(&s[..paren])
752 {
753 return parse_call(s, paren).map(|stage| Expression::Pipeline {
755 source: Box::new(Expression::Literal(Literal::Integer(0))),
756 stages: vec![stage],
757 });
758 }
759
760 if let Some((namespace, rest)) = s.split_once('.') {
761 return parse_ref(namespace, rest).map(Expression::Reference);
762 }
763
764 if let Ok(value) = s.parse::<i64>() {
765 return Ok(Expression::Literal(Literal::Integer(value)));
766 }
767
768 if is_valid_identifier(s) {
771 return Err(ExprError::MissingNamespace {
772 text: s.to_string(),
773 });
774 }
775
776 Err(ExprError::NotAReference {
777 text: s.to_string(),
778 })
779}
780
781fn parse_ref(namespace: &str, rest: &str) -> Result<Reference, ExprError> {
783 if !is_valid_identifier(namespace) {
787 return Err(ExprError::NotAReference {
788 text: format!("{namespace}.{rest}"),
789 });
790 }
791
792 let (name, property) = match rest.split_once('.') {
793 Some((name, property)) => (name, Some(property)),
794 None => (rest, None),
795 };
796
797 let Ok(ref_type) = namespace.parse::<RefType>() else {
798 return Err(unknown_namespace(namespace, name));
799 };
800
801 if name.is_empty() {
802 return Err(ExprError::EmptyName {
803 namespace: namespace.to_string(),
804 });
805 }
806 if !is_valid_identifier(name) {
807 return Err(ExprError::InvalidName {
808 name: name.to_string(),
809 });
810 }
811
812 if let Some(property) = property
813 && !is_valid_identifier(property)
814 {
815 return Err(ExprError::InvalidProperty {
816 property: property.to_string(),
817 });
818 }
819
820 Ok(Reference {
821 ref_type,
822 name: name.to_string(),
823 property: property.map(ToString::to_string),
824 })
825}
826
827fn unknown_namespace(namespace: &str, name: &str) -> ExprError {
829 if edit_distance(namespace, "material") <= 1 {
835 return ExprError::MaterialNamespace {
836 namespace: namespace.to_string(),
837 name: name.to_string(),
838 };
839 }
840
841 ExprError::UnknownNamespace {
842 namespace: namespace.to_string(),
843 }
844}
845
846fn nearest_namespace(s: &str) -> Option<RefType> {
852 RefType::ALL
853 .iter()
854 .map(|known| (*known, edit_distance(s, known.as_str())))
855 .filter(|(_, distance)| *distance <= 2)
856 .min_by_key(|(_, distance)| *distance)
857 .map(|(known, _)| known)
858}
859
860fn edit_distance(a: &str, b: &str) -> usize {
862 let b_chars: Vec<char> = b.chars().collect();
863 let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
864 let mut current = vec![0; b_chars.len() + 1];
865
866 for (i, a_char) in a.chars().enumerate() {
867 current[0] = i + 1;
868 for (j, b_char) in b_chars.iter().enumerate() {
869 let substitution = previous[j] + usize::from(a_char != *b_char);
870 let deletion = previous[j + 1] + 1;
871 let insertion = current[j] + 1;
872 current[j + 1] = substitution.min(deletion).min(insertion);
873 }
874 std::mem::swap(&mut previous, &mut current);
875 }
876
877 previous[b_chars.len()]
878}
879
880fn parse_string_literal(s: &str, quote: char) -> Result<Literal, ExprError> {
882 if s.len() < 2 || !s.ends_with(quote) {
885 return Err(ExprError::UnterminatedString { quote });
886 }
887
888 Ok(Literal::String(s[1..s.len() - 1].to_string()))
889}
890
891fn parse_call(s: &str, paren: usize) -> Result<PipeStage, ExprError> {
896 let name = &s[..paren];
897
898 if !is_valid_identifier(name) {
899 return Err(ExprError::InvalidFunctionName {
900 name: name.to_string(),
901 });
902 }
903 if !s.ends_with(')') {
904 return Err(ExprError::UnclosedCall {
905 function: name.to_string(),
906 });
907 }
908
909 let args = split_top_level(s[paren + 1..s.len() - 1].trim(), b',')
912 .into_iter()
913 .enumerate()
914 .map(|(index, arg)| {
915 parse_pipeline(arg.trim()).map_err(|cause| ExprError::InvalidArgument {
916 function: name.to_string(),
917 position: index + 1,
918 cause: Box::new(cause),
919 })
920 })
921 .collect::<Result<Vec<_>, _>>()?;
922
923 Ok(PipeStage::with_args(name, args))
924}
925
926fn parse_pipe_stage(s: &str) -> Result<PipeStage, ExprError> {
928 let s = s.trim();
929
930 if let Some(paren) = s.find('(') {
931 return parse_call(s, paren);
932 }
933
934 if is_valid_identifier(s) {
936 return Ok(PipeStage::new(s));
937 }
938
939 Err(ExprError::InvalidPipeStage {
940 stage: s.to_string(),
941 })
942}
943
944fn is_valid_identifier(s: &str) -> bool {
947 let mut chars = s.chars();
948 match chars.next() {
949 None => false,
950 Some(first) => {
951 (first.is_ascii_alphabetic() || first == '_')
952 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
953 }
954 }
955}
956
957struct Occurrence<'a> {
959 range: Range<usize>,
960 text: &'a str,
961 parsed: Result<Expression, ExprError>,
962}
963
964fn scan_occurrences(s: &str) -> Vec<Occurrence<'_>> {
971 let mut found = Vec::new();
972 let bytes = s.as_bytes();
973 let mut i = 0;
974
975 while i < bytes.len() {
976 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
977 let mut depth = 1;
978 let mut j = i + 2;
979 while j < bytes.len() && depth > 0 {
980 match bytes[j] {
981 b'{' => depth += 1,
982 b'}' => depth -= 1,
983 _ => {}
984 }
985 j += 1;
986 }
987
988 let closed = depth == 0;
991 let end = if closed { j } else { s.len() };
992 let text = &s[i..end];
993 found.push(Occurrence {
994 range: i..end,
995 text,
996 parsed: if closed {
997 parse_expression_detailed(text)
998 } else {
999 Err(ExprError::Unclosed)
1000 },
1001 });
1002 i = end;
1003 continue;
1004 }
1005 i += 1;
1006 }
1007
1008 found
1009}
1010
1011pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
1015 scan_occurrences(s)
1016 .into_iter()
1017 .filter_map(|occurrence| {
1018 Some(LocatedExpression {
1019 expression: occurrence.parsed.ok()?,
1020 range: occurrence.range,
1021 })
1022 })
1023 .collect()
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Eq)]
1028pub struct InvalidExpression {
1029 pub range: Range<usize>,
1031 pub text: String,
1033 pub reason: ExprError,
1035}
1036
1037pub fn find_invalid_expressions(s: &str) -> Vec<InvalidExpression> {
1055 scan_occurrences(s)
1056 .into_iter()
1057 .filter_map(|occurrence| {
1058 Some(InvalidExpression {
1059 reason: occurrence.parsed.err()?,
1060 range: occurrence.range,
1061 text: occurrence.text.to_string(),
1062 })
1063 })
1064 .collect()
1065}
1066
1067pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
1091 parse_expr_value_at(json, 0)
1092}
1093
1094const MAX_VALUE_DEPTH: usize = 64;
1099
1100fn parse_expr_value_at(json: &serde_json::Value, depth: usize) -> ExprValue {
1109 if depth >= MAX_VALUE_DEPTH {
1110 return ExprValue::Literal(Literal::Null);
1111 }
1112 let child_depth = depth.saturating_add(1);
1113 match json {
1114 serde_json::Value::String(s) => parse_string_to_expr_value(s),
1115 serde_json::Value::Number(n) => {
1116 if let Some(i) = n.as_i64() {
1117 ExprValue::Literal(Literal::Integer(i))
1118 } else if let Some(f) = n.as_f64() {
1119 ExprValue::Literal(Literal::Float(f))
1120 } else {
1121 ExprValue::Literal(Literal::Integer(0))
1123 }
1124 }
1125 serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
1126 serde_json::Value::Null => ExprValue::Literal(Literal::Null),
1127 serde_json::Value::Object(map) => {
1128 let mut result = std::collections::HashMap::new();
1129 for (k, v) in map {
1130 result.insert(k.clone(), parse_expr_value_at(v, child_depth));
1131 }
1132 ExprValue::Object(result)
1133 }
1134 serde_json::Value::Array(arr) => {
1135 let result: Vec<_> = arr
1136 .iter()
1137 .map(|v| parse_expr_value_at(v, child_depth))
1138 .collect();
1139 ExprValue::Array(result)
1140 }
1141 }
1142}
1143
1144fn parse_string_to_expr_value(s: &str) -> ExprValue {
1151 let trimmed = s.trim();
1152
1153 if trimmed.starts_with("${")
1155 && trimmed.ends_with('}')
1156 && !trimmed[2..trimmed.len() - 1].contains("${")
1157 && let Some(expr) = parse_expression(trimmed)
1158 {
1159 return ExprValue::Expr(expr);
1160 }
1161
1162 if s.contains("${") {
1164 let parts = parse_interpolated_string(s);
1165 if parts.len() == 1 {
1166 if let StringPart::Literal(lit) = &parts[0] {
1168 return ExprValue::Literal(Literal::String(lit.clone()));
1169 }
1170 }
1171 return ExprValue::Interpolated(parts);
1172 }
1173
1174 ExprValue::Literal(Literal::String(s.to_string()))
1176}
1177
1178fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
1182 let mut parts = Vec::new();
1183 let bytes = s.as_bytes();
1184 let mut last_end = 0;
1185 let mut i = 0;
1186
1187 while i < bytes.len() {
1188 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1189 if i > last_end {
1191 parts.push(StringPart::Literal(s[last_end..i].to_string()));
1192 }
1193
1194 let mut depth = 1;
1196 let mut j = i + 2;
1197 while j < bytes.len() && depth > 0 {
1198 match bytes[j] {
1199 b'{' => depth += 1,
1200 b'}' => depth -= 1,
1201 _ => {}
1202 }
1203 j += 1;
1204 }
1205
1206 if depth == 0 {
1207 let expr_str = &s[i..j];
1208 if let Some(expr) = parse_expression(expr_str) {
1209 parts.push(StringPart::Expr(expr));
1210 } else {
1211 parts.push(StringPart::Literal(expr_str.to_string()));
1213 }
1214 last_end = j;
1215 i = j;
1216 continue;
1217 }
1218 }
1219 i += 1;
1220 }
1221
1222 if last_end < s.len() {
1224 parts.push(StringPart::Literal(s[last_end..].to_string()));
1225 }
1226
1227 parts
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233
1234 #[test]
1235 fn parse_simple_reference() {
1236 let expr = parse_expression("${artifact.ksr}").unwrap();
1237 match expr {
1238 Expression::Reference(r) => {
1239 assert_eq!(r.ref_type, RefType::Artifact);
1240 assert_eq!(r.name, "ksr");
1241 assert!(r.property.is_none());
1242 }
1243 _ => panic!("Expected Reference"),
1244 }
1245 }
1246
1247 #[test]
1248 fn parse_reference_with_property() {
1249 let expr = parse_expression("${artifact.keypair.private}").unwrap();
1250 match expr {
1251 Expression::Reference(r) => {
1252 assert_eq!(r.ref_type, RefType::Artifact);
1253 assert_eq!(r.name, "keypair");
1254 assert_eq!(r.property, Some("private".to_string()));
1255 }
1256 _ => panic!("Expected Reference"),
1257 }
1258 }
1259
1260 #[test]
1261 fn parse_param_reference() {
1262 let expr = parse_expression("${param.key_label}").unwrap();
1263 match expr {
1264 Expression::Reference(r) => {
1265 assert_eq!(r.ref_type, RefType::Param);
1266 assert_eq!(r.name, "key_label");
1267 }
1268 _ => panic!("Expected Reference"),
1269 }
1270 }
1271
1272 #[test]
1273 fn parse_simple_pipeline() {
1274 let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
1275 match expr {
1276 Expression::Pipeline { source, stages } => {
1277 assert!(matches!(*source, Expression::Reference(_)));
1278 assert_eq!(stages.len(), 1);
1279 assert_eq!(stages[0].function, "sha256");
1280 assert!(stages[0].args.is_empty());
1281 }
1282 _ => panic!("Expected Pipeline"),
1283 }
1284 }
1285
1286 #[test]
1287 fn parse_multi_stage_pipeline() {
1288 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1289 match expr {
1290 Expression::Pipeline { stages, .. } => {
1291 assert_eq!(stages.len(), 2);
1292 assert_eq!(stages[0].function, "sha256");
1293 assert_eq!(stages[1].function, "hex");
1294 }
1295 _ => panic!("Expected Pipeline"),
1296 }
1297 }
1298
1299 #[test]
1300 fn parse_pipeline_with_function_args() {
1301 let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
1302 match expr {
1303 Expression::Pipeline { stages, .. } => {
1304 assert_eq!(stages.len(), 1);
1305 assert_eq!(stages[0].function, "substr");
1306 assert_eq!(stages[0].args.len(), 2);
1307 }
1308 _ => panic!("Expected Pipeline"),
1309 }
1310 }
1311
1312 #[test]
1313 fn parse_complex_pipeline() {
1314 let expr =
1315 parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
1316 match expr {
1317 Expression::Pipeline { source, stages } => {
1318 if let Expression::Reference(r) = *source {
1319 assert_eq!(r.name, "keypair");
1320 assert_eq!(r.property, Some("public".to_string()));
1321 }
1322 assert_eq!(stages.len(), 3);
1323 assert_eq!(stages[0].function, "fingerprint");
1324 assert_eq!(stages[1].function, "hex");
1325 assert_eq!(stages[2].function, "upper");
1326 }
1327 _ => panic!("Expected Pipeline"),
1328 }
1329 }
1330
1331 #[test]
1332 fn parse_invalid_expression() {
1333 assert!(parse_expression("not an expression").is_none());
1334 assert!(parse_expression("${").is_none());
1335 assert!(parse_expression("${}").is_none());
1336 assert!(parse_expression("${invalid}").is_none()); }
1338
1339 #[test]
1340 fn find_expressions_in_string() {
1341 let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
1342 assert_eq!(exprs.len(), 1);
1343 assert_eq!(exprs[0].range.start, 6);
1344 }
1345
1346 #[test]
1347 fn find_multiple_expressions() {
1348 let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
1349 assert_eq!(exprs.len(), 2);
1350 }
1351
1352 #[test]
1353 fn value_type_names() {
1354 assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
1355 assert_eq!(Value::String(String::new()).type_name(), "string");
1356 assert_eq!(Value::Integer(0).type_name(), "integer");
1357 assert_eq!(Value::Boolean(true).type_name(), "boolean");
1358 assert_eq!(Value::Null.type_name(), "null");
1359 }
1360
1361 #[test]
1362 fn value_as_bytes() {
1363 let v = Value::Bytes(vec![1, 2, 3]);
1364 assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
1365
1366 let v = Value::String("hello".into());
1367 assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
1368
1369 let v = Value::Integer(42);
1370 assert!(v.as_bytes().is_none());
1371 }
1372
1373 mod ceremony_examples {
1374 use super::*;
1375
1376 #[test]
1378 fn dnssec_hash_verification() {
1379 let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
1381 match expr {
1382 Expression::Pipeline { source, stages } => {
1383 if let Expression::Reference(r) = *source {
1384 assert_eq!(r.ref_type, RefType::Artifact);
1385 assert_eq!(r.name, "ksr");
1386 } else {
1387 panic!("Expected Reference source");
1388 }
1389 assert_eq!(stages.len(), 3);
1391 assert_eq!(stages[0].function, "sha256");
1392 assert_eq!(stages[1].function, "hex");
1393 assert_eq!(stages[2].function, "substr");
1394 assert_eq!(stages[2].args.len(), 2);
1395 }
1396 _ => panic!("Expected Pipeline"),
1397 }
1398 }
1399
1400 #[test]
1401 fn dnssec_param_reference() {
1402 let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
1404 match expr {
1405 Expression::Reference(r) => {
1406 assert_eq!(r.ref_type, RefType::Param);
1407 assert_eq!(r.name, "expected_ksr_hash");
1408 }
1409 _ => panic!("Expected Reference"),
1410 }
1411 }
1412
1413 #[test]
1414 fn dnssec_oral_readback() {
1415 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1417 match expr {
1418 Expression::Pipeline { stages, .. } => {
1419 assert_eq!(stages.len(), 2);
1420 assert_eq!(stages[0].function, "sha256");
1421 assert_eq!(stages[1].function, "hex");
1422 }
1423 _ => panic!("Expected Pipeline"),
1424 }
1425 }
1426
1427 #[test]
1429 fn hsm_artifact_reference() {
1430 let expr = parse_expression("${artifact.master_keypair}").unwrap();
1432 match expr {
1433 Expression::Reference(r) => {
1434 assert_eq!(r.ref_type, RefType::Artifact);
1435 assert_eq!(r.name, "master_keypair");
1436 assert!(r.property.is_none());
1437 }
1438 _ => panic!("Expected Reference"),
1439 }
1440 }
1441
1442 #[test]
1443 fn hsm_public_key_fingerprint() {
1444 let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1446 match expr {
1447 Expression::Pipeline { source, stages } => {
1448 if let Expression::Reference(r) = *source {
1449 assert_eq!(r.name, "master_public_key");
1450 }
1451 assert_eq!(stages.len(), 2);
1452 }
1453 _ => panic!("Expected Pipeline"),
1454 }
1455 }
1456
1457 #[test]
1459 fn crypto_artifact_with_property() {
1460 let expr = parse_expression("${artifact.keypair.private}").unwrap();
1462 match expr {
1463 Expression::Reference(r) => {
1464 assert_eq!(r.ref_type, RefType::Artifact);
1465 assert_eq!(r.name, "keypair");
1466 assert_eq!(r.property, Some("private".to_string()));
1467 }
1468 _ => panic!("Expected Reference"),
1469 }
1470 }
1471
1472 #[test]
1474 fn find_embedded_expressions() {
1475 let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1477 assert_eq!(exprs.len(), 1);
1478 if let Expression::Reference(r) = &exprs[0].expression {
1479 assert_eq!(r.ref_type, RefType::Param);
1480 assert_eq!(r.name, "hsm_serial");
1481 }
1482 }
1483
1484 #[test]
1485 fn find_multiple_embedded_params() {
1486 let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1488 assert_eq!(exprs.len(), 2);
1489 }
1490
1491 #[test]
1493 fn role_reference_parses() {
1494 let expr = parse_expression("${role.ceremony_admin}").unwrap();
1495 match expr {
1496 Expression::Reference(r) => {
1497 assert_eq!(r.ref_type, RefType::Role);
1498 assert_eq!(r.name, "ceremony_admin");
1499 }
1500 _ => panic!("Expected Reference"),
1501 }
1502 }
1503 }
1504
1505 mod expr_value_tests {
1506 use super::*;
1507
1508 #[test]
1509 fn parse_literal_string() {
1510 let json = serde_json::json!("hello");
1511 let expr = parse_expr_value(&json);
1512 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1513 }
1514
1515 #[test]
1516 fn parse_literal_integer() {
1517 let json = serde_json::json!(42);
1518 let expr = parse_expr_value(&json);
1519 assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1520 }
1521
1522 #[test]
1523 fn parse_literal_float() {
1524 let json = serde_json::json!(1.5);
1525 let expr = parse_expr_value(&json);
1526 assert!(
1527 matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1528 );
1529 }
1530
1531 #[test]
1532 fn parse_literal_boolean() {
1533 let json = serde_json::json!(true);
1534 let expr = parse_expr_value(&json);
1535 assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1536 }
1537
1538 #[test]
1539 fn parse_literal_null() {
1540 let json = serde_json::Value::Null;
1541 let expr = parse_expr_value(&json);
1542 assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1543 }
1544
1545 #[test]
1546 fn parse_single_expression() {
1547 let json = serde_json::json!("${artifact.ksr | sha256}");
1548 let expr = parse_expr_value(&json);
1549 match expr {
1550 ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1551 assert!(matches!(*source, Expression::Reference(_)));
1552 assert_eq!(stages.len(), 1);
1553 assert_eq!(stages[0].function, "sha256");
1554 }
1555 _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1556 }
1557 }
1558
1559 #[test]
1560 fn parse_simple_reference() {
1561 let json = serde_json::json!("${param.name}");
1562 let expr = parse_expr_value(&json);
1563 match expr {
1564 ExprValue::Expr(Expression::Reference(r)) => {
1565 assert_eq!(r.ref_type, RefType::Param);
1566 assert_eq!(r.name, "name");
1567 }
1568 _ => panic!("Expected Expr(Reference), got {expr:?}"),
1569 }
1570 }
1571
1572 #[test]
1573 fn parse_interpolated_string() {
1574 let json = serde_json::json!("Hello, ${param.name}!");
1575 let expr = parse_expr_value(&json);
1576 match expr {
1577 ExprValue::Interpolated(parts) => {
1578 assert_eq!(parts.len(), 3);
1579 assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1580 assert!(matches!(
1581 &parts[1],
1582 StringPart::Expr(Expression::Reference(_))
1583 ));
1584 assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1585 }
1586 _ => panic!("Expected Interpolated, got {expr:?}"),
1587 }
1588 }
1589
1590 #[test]
1591 fn parse_object_with_expressions() {
1592 let json = serde_json::json!({
1593 "actual": "${artifact.ksr | sha256 | hex}",
1594 "expected": "${param.expected_hash}",
1595 "static": "hello"
1596 });
1597 let expr = parse_expr_value(&json);
1598 match expr {
1599 ExprValue::Object(map) => {
1600 assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1601 assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1602 assert!(matches!(
1603 map.get("static"),
1604 Some(ExprValue::Literal(Literal::String(_)))
1605 ));
1606 }
1607 _ => panic!("Expected Object, got {expr:?}"),
1608 }
1609 }
1610
1611 #[test]
1612 fn parse_array_with_expressions() {
1613 let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1614 let expr = parse_expr_value(&json);
1615 match expr {
1616 ExprValue::Array(arr) => {
1617 assert_eq!(arr.len(), 3);
1618 assert!(matches!(&arr[0], ExprValue::Expr(_)));
1619 assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1620 assert!(matches!(&arr[2], ExprValue::Expr(_)));
1621 }
1622 _ => panic!("Expected Array, got {expr:?}"),
1623 }
1624 }
1625
1626 #[test]
1627 fn parse_deeply_nested_value_is_truncated_not_crashed() {
1628 let mut json = serde_json::json!("leaf");
1632 for _ in 0..200 {
1633 json = serde_json::Value::Array(vec![json]);
1634 }
1635 let mut expr = parse_expr_value(&json);
1636 let mut levels = 0;
1637 loop {
1638 match expr {
1639 ExprValue::Array(mut arr) => {
1640 assert_eq!(arr.len(), 1);
1641 expr = arr.remove(0);
1642 levels += 1;
1643 }
1644 other => {
1645 assert!(
1646 matches!(other, ExprValue::Literal(Literal::Null)),
1647 "truncated tail must be Literal(Null), got {other:?}"
1648 );
1649 break;
1650 }
1651 }
1652 }
1653 assert_eq!(levels, MAX_VALUE_DEPTH, "recursion must stop at the cap");
1654 }
1655
1656 #[test]
1657 fn parse_moderately_nested_value_is_preserved() {
1658 let mut json = serde_json::json!("leaf");
1659 for _ in 0..10 {
1660 json = serde_json::Value::Array(vec![json]);
1661 }
1662 let mut expr = parse_expr_value(&json);
1663 for _ in 0..10 {
1664 match expr {
1665 ExprValue::Array(mut arr) => {
1666 assert_eq!(arr.len(), 1);
1667 expr = arr.remove(0);
1668 }
1669 other => panic!("expected Array, got {other:?}"),
1670 }
1671 }
1672 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "leaf"));
1673 }
1674
1675 #[test]
1676 fn parse_multiple_expressions_in_string() {
1677 let json = serde_json::json!("${param.a} and ${param.b}");
1678 let expr = parse_expr_value(&json);
1679 match expr {
1680 ExprValue::Interpolated(parts) => {
1681 assert_eq!(parts.len(), 3);
1682 assert!(matches!(&parts[0], StringPart::Expr(_)));
1683 assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1684 assert!(matches!(&parts[2], StringPart::Expr(_)));
1685 }
1686 _ => panic!("Expected Interpolated, got {expr:?}"),
1687 }
1688 }
1689 }
1690
1691 mod invalid_expressions {
1692 use super::*;
1693
1694 #[test]
1697 fn all_lists_every_namespace() {
1698 for ref_type in RefType::ALL {
1699 match ref_type {
1700 RefType::Param | RefType::Role | RefType::Artifact => {}
1701 }
1702 }
1703 assert_eq!(RefType::ALL.len(), 3);
1704 assert_eq!(namespace_list(), "param, role, artifact");
1705 }
1706
1707 #[test]
1708 fn a_valid_expression_yields_nothing() {
1709 for s in [
1710 "${artifact.ksr}",
1711 "${artifact.keypair.public | sha256 | hex}",
1712 "${param.name}",
1713 "plain text with no expression",
1714 "Hash: ${artifact.a | sha256 | hex} done",
1715 "${concat(artifact.a | sha256, artifact.b) | hex}",
1716 ] {
1717 assert!(
1718 find_invalid_expressions(s).is_empty(),
1719 "{s} should be accepted"
1720 );
1721 }
1722 }
1723
1724 #[test]
1725 fn an_unknown_namespace_is_reported_with_its_range() {
1726 let found = find_invalid_expressions("sign ${nonsense.x} now");
1727 assert_eq!(found.len(), 1);
1728 assert_eq!(found[0].range, 5..18);
1729 assert_eq!(found[0].text, "${nonsense.x}");
1730 assert_eq!(
1731 found[0].reason,
1732 ExprError::UnknownNamespace {
1733 namespace: "nonsense".to_string(),
1734 }
1735 );
1736 assert_eq!(
1737 found[0].reason.to_string(),
1738 "unknown namespace 'nonsense': expected one of param, role, artifact"
1739 );
1740 }
1741
1742 #[test]
1743 fn every_occurrence_in_one_string_is_reported() {
1744 let found = find_invalid_expressions("${a.b} ${param.ok} ${c.d}");
1745 assert_eq!(found.len(), 2);
1746 assert_eq!(found[0].text, "${a.b}");
1747 assert_eq!(found[1].text, "${c.d}");
1748 }
1749
1750 #[test]
1751 fn the_material_namespace_names_the_artifact_form() {
1752 for namespace in ["material", "materials"] {
1755 let found = find_invalid_expressions(&format!("${{{namespace}.manifest}}"));
1756 assert_eq!(found.len(), 1, "for {namespace}");
1757 assert_eq!(
1758 found[0].reason,
1759 ExprError::MaterialNamespace {
1760 namespace: namespace.to_string(),
1761 name: "manifest".to_string(),
1762 }
1763 );
1764 assert_eq!(
1765 found[0].reason.to_string(),
1766 format!(
1767 "there is no '{namespace}' namespace: \
1768 a material is read as 'artifact.manifest'"
1769 )
1770 );
1771 assert_eq!(
1772 found[0].reason.suggestion(),
1773 Some(RefType::Artifact),
1774 "an editor fix points at the artifact namespace"
1775 );
1776 }
1777 }
1778
1779 #[test]
1780 fn a_pipeline_with_nothing_before_the_pipe_is_not_an_empty_expression() {
1781 assert_eq!(
1782 parse_expression_detailed("${ | sha256 }").unwrap_err(),
1783 ExprError::MissingSource
1784 );
1785 assert_eq!(
1786 parse_expression_detailed("${}").unwrap_err(),
1787 ExprError::Empty
1788 );
1789 }
1790
1791 #[test]
1792 fn an_unclosed_expression_is_reported_to_the_end_of_the_string() {
1793 let found = find_invalid_expressions("see ${artifact.k");
1794 assert_eq!(found.len(), 1);
1795 assert_eq!(found[0].text, "${artifact.k");
1796 assert_eq!(found[0].reason, ExprError::Unclosed);
1797 assert_eq!(
1798 found[0].reason.to_string(),
1799 "the expression is missing its closing '}'"
1800 );
1801 }
1802
1803 #[test]
1804 fn each_shape_of_mistake_gets_its_own_reason() {
1805 for (input, expected) in [
1806 ("${}", "the expression is empty"),
1807 (
1808 "${artifact}",
1809 "'artifact' names no namespace: write '<namespace>.<name>', \
1810 where <namespace> is one of param, role, artifact",
1811 ),
1812 (
1813 "${artifact.}",
1814 "nothing follows 'artifact.': expected a name",
1815 ),
1816 (
1817 "${artifact.9k}",
1818 "'9k' is not a usable name: names hold letters, digits, and underscores, \
1819 and cannot start with a digit",
1820 ),
1821 (
1822 "${artifact.k.9p}",
1823 "'9p' is not a usable property: names hold letters, digits, and underscores, \
1824 and cannot start with a digit",
1825 ),
1826 (
1827 "${artifact.k | 9hex}",
1828 "'9hex' is not a usable pipe stage: expected a function name, \
1829 with any arguments in parentheses",
1830 ),
1831 (
1832 "${concat(artifact.a}",
1833 "the call to 'concat' is missing its closing ')'",
1834 ),
1835 ] {
1836 let found = find_invalid_expressions(input);
1837 assert_eq!(found.len(), 1, "{input} should be rejected");
1838 assert_eq!(found[0].reason.to_string(), expected, "for {input}");
1839 }
1840 }
1841
1842 #[test]
1843 fn a_bare_name_is_told_to_use_the_wrapper() {
1844 let error = parse_expression_detailed("keypair").unwrap_err();
1845 assert_eq!(error, ExprError::NotAnExpression);
1846 assert_eq!(
1847 error.to_string(),
1848 "write it as an expression, '${<namespace>.<name>}', \
1849 where <namespace> is one of param, role, artifact"
1850 );
1851 }
1852
1853 #[test]
1854 fn a_namespace_one_or_two_edits_away_is_suggested() {
1855 for (input, expected) in [
1856 ("${paramm.region}", RefType::Param),
1857 ("${params.region}", RefType::Param),
1858 ("${roles.officer}", RefType::Role),
1859 ("${artifacts.ksr}", RefType::Artifact),
1860 ] {
1861 let error = parse_expression_detailed(input).unwrap_err();
1862 assert_eq!(error.suggestion(), Some(expected), "for {input}");
1863 assert!(
1864 error
1865 .to_string()
1866 .contains(&format!("did you mean '{expected}'")),
1867 "for {input}: {error}"
1868 );
1869 }
1870 }
1871
1872 #[test]
1873 fn a_namespace_nothing_like_a_known_one_gets_no_suggestion() {
1874 for input in ["${nonsense.x}", "${env.HOME}", "${secret.pin}"] {
1875 let error = parse_expression_detailed(input).unwrap_err();
1876 assert!(
1877 matches!(error, ExprError::UnknownNamespace { .. }),
1878 "{input} should be an unknown namespace, got {error:?}"
1879 );
1880 assert_eq!(error.suggestion(), None, "{input} should not be guessed at");
1881 }
1882 }
1883
1884 #[test]
1885 fn a_failing_argument_names_its_position_and_its_own_cause() {
1886 let error = parse_expression_detailed("${concat(artifact.a, nonsense.b)}").unwrap_err();
1887 assert_eq!(
1888 error,
1889 ExprError::InvalidArgument {
1890 function: "concat".to_string(),
1891 position: 2,
1892 cause: Box::new(ExprError::UnknownNamespace {
1893 namespace: "nonsense".to_string(),
1894 }),
1895 }
1896 );
1897 assert_eq!(
1898 error.to_string(),
1899 "argument 2 to 'concat' is not a usable expression: \
1900 unknown namespace 'nonsense': expected one of param, role, artifact"
1901 );
1902 }
1903
1904 #[test]
1905 fn a_lone_quote_is_an_unterminated_string() {
1906 for (input, quote) in [("${\"}", '"'), ("${'}", '\''), ("${\"abc}", '"')] {
1909 assert_eq!(
1910 parse_expression_detailed(input).unwrap_err(),
1911 ExprError::UnterminatedString { quote },
1912 "for {input}"
1913 );
1914 }
1915 }
1916
1917 #[test]
1918 fn a_quote_opens_a_string_even_when_it_holds_a_parenthesis() {
1919 let parsed = parse_expression_detailed("${\"a(b\"}");
1920 assert!(
1921 matches!(parsed, Ok(Expression::Literal(Literal::String(ref s))) if s == "a(b"),
1922 "the parenthesis belongs to the string, got {parsed:?}"
1923 );
1924 }
1925
1926 #[test]
1927 fn the_shape_a_source_is_read_as_comes_from_its_first_characters() {
1928 for (input, expected) in [
1931 (
1932 "${param.x(1)}",
1933 ExprError::InvalidName {
1934 name: "x(1)".to_string(),
1935 },
1936 ),
1937 (
1938 "${42abc}",
1939 ExprError::NotAReference {
1940 text: "42abc".to_string(),
1941 },
1942 ),
1943 (
1944 "${1.5}",
1945 ExprError::NotAReference {
1946 text: "1.5".to_string(),
1947 },
1948 ),
1949 ] {
1950 assert_eq!(
1951 parse_expression_detailed(input).unwrap_err(),
1952 expected,
1953 "for {input}"
1954 );
1955 }
1956 }
1957
1958 #[test]
1959 fn parses_every_accepted_form() {
1960 for input in [
1961 "${artifact.ksr}",
1962 "${artifact.keypair.public}",
1963 "${param.region | sha256 | hex}",
1964 "${\"literal\"}",
1965 "${'literal'}",
1966 "${42}",
1967 "${-42}",
1968 "${\"\"}",
1969 "${concat(artifact.a, artifact.b) | hex}",
1970 "${pad(artifact.a, 4)}",
1971 ] {
1972 assert!(
1973 parse_expression_detailed(input).is_ok(),
1974 "{input} should parse, got {:?}",
1975 parse_expression_detailed(input)
1976 );
1977 }
1978 }
1979 }
1980}