1#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
30
31use std::ops::Range;
32
33use zeroize::Zeroizing;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum RefType {
41 Param,
43 Role,
45 Artifact,
47}
48
49impl RefType {
50 pub const ALL: &'static [Self] = &[Self::Param, Self::Role, Self::Artifact];
55
56 #[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#[derive(Clone, PartialEq)]
91pub enum Value {
92 Bytes(Zeroizing<Vec<u8>>),
98 String(String),
100 Integer(i64),
102 Boolean(bool),
104 Null,
106}
107
108impl Value {
109 pub fn bytes(bytes: Vec<u8>) -> Self {
111 Value::Bytes(Zeroizing::new(bytes))
112 }
113
114 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 pub fn as_string(&self) -> Option<&str> {
129 match self {
130 Value::String(s) => Some(s),
131 _ => None,
132 }
133 }
134
135 pub fn as_integer(&self) -> Option<i64> {
137 match self {
138 Value::Integer(i) => Some(*i),
139 _ => None,
140 }
141 }
142
143 pub fn is_null(&self) -> bool {
145 matches!(self, Value::Null)
146 }
147
148 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#[derive(Debug, Clone, PartialEq)]
191pub enum Expression {
192 Reference(Reference),
194
195 Literal(Literal),
197
198 Pipeline {
200 source: Box<Expression>,
202 stages: Vec<PipeStage>,
204 },
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct Reference {
210 pub ref_type: RefType,
212 pub name: String,
214 pub property: Option<String>,
216}
217
218impl Reference {
219 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 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#[derive(Debug, Clone, PartialEq)]
250pub enum Literal {
251 String(String),
253 Integer(i64),
255 Float(f64),
257 Boolean(bool),
259 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), Literal::Boolean(b) => Value::Boolean(b),
271 Literal::Null => Value::Null,
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq)]
278pub struct PipeStage {
279 pub function: String,
281 pub args: Vec<Expression>,
283}
284
285impl PipeStage {
286 pub fn new(function: &str) -> Self {
288 Self {
289 function: function.to_string(),
290 args: Vec::new(),
291 }
292 }
293
294 pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
296 Self {
297 function: function.to_string(),
298 args,
299 }
300 }
301}
302
303#[derive(Debug, Clone, PartialEq)]
305pub struct LocatedExpression {
306 pub expression: Expression,
308 pub range: Range<usize>,
310}
311
312#[derive(Debug, Clone, PartialEq)]
318pub enum ExprValue {
319 Literal(Literal),
321 Expr(Expression),
323 Interpolated(Vec<StringPart>),
325 Object(std::collections::HashMap<String, ExprValue>),
327 Array(Vec<ExprValue>),
329}
330
331impl ExprValue {
332 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 pub fn get(&self, key: &str) -> Option<&ExprValue> {
342 match self {
343 ExprValue::Object(map) => map.get(key),
344 _ => None,
345 }
346 }
347
348 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
373fn 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#[derive(Debug, Clone, PartialEq)]
401pub enum StringPart {
402 Literal(String),
404 Expr(Expression),
406}
407
408#[derive(Debug, Clone, PartialEq, Eq)]
414#[non_exhaustive]
415pub enum ExprError {
416 NotAnExpression,
418 Unclosed,
420 Empty,
422 MissingSource,
424 UnterminatedString {
426 quote: char,
428 },
429 InvalidFunctionName {
431 name: String,
433 },
434 UnclosedCall {
436 function: String,
438 },
439 InvalidArgument {
441 function: String,
443 position: usize,
445 cause: Box<ExprError>,
447 },
448 MissingNamespace {
450 text: String,
452 },
453 NotAReference {
455 text: String,
457 },
458 UnknownNamespace {
460 namespace: String,
462 },
463 MaterialNamespace {
465 namespace: String,
467 name: String,
469 },
470 EmptyName {
472 namespace: String,
474 },
475 InvalidName {
477 name: String,
479 },
480 InvalidProperty {
482 property: String,
484 },
485 InvalidPipeStage {
487 stage: String,
489 },
490}
491
492const 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 #[must_use]
585 pub fn suggestion(&self) -> Option<RefType> {
586 match self {
587 Self::UnknownNamespace { namespace } => nearest_namespace(namespace),
588 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
632fn namespace_list() -> String {
634 RefType::ALL
635 .iter()
636 .map(|ref_type| ref_type.as_str())
637 .collect::<Vec<_>>()
638 .join(", ")
639}
640
641pub fn parse_expression(s: &str) -> Option<Expression> {
665 parse_expression_detailed(s).ok()
666}
667
668pub fn parse_expression_detailed(s: &str) -> Result<Expression, ExprError> {
689 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
701fn parse_pipeline(s: &str) -> Result<Expression, ExprError> {
703 let s = s.trim();
704
705 let parts = split_top_level(s, b'|');
707 let first = parts.first().copied().unwrap_or("").trim();
708
709 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 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
732fn 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 if start < s.len() {
756 parts.push(&s[start..]);
757 }
758
759 parts
760}
761
762fn 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 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 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
811fn parse_ref(namespace: &str, rest: &str) -> Result<Reference, ExprError> {
813 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
857fn unknown_namespace(namespace: &str, name: &str) -> ExprError {
859 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
876fn 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
890fn 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
910fn parse_string_literal(s: &str, quote: char) -> Result<Literal, ExprError> {
912 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
921fn 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 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
956fn 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 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
974fn 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
987struct Occurrence<'a> {
989 range: Range<usize>,
990 text: &'a str,
991 parsed: Result<Expression, ExprError>,
992}
993
994fn 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 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
1041pub 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#[derive(Debug, Clone, PartialEq, Eq)]
1058pub struct InvalidExpression {
1059 pub range: Range<usize>,
1061 pub text: String,
1063 pub reason: ExprError,
1065}
1066
1067pub 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#[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
1133pub 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
1145pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
1169 parse_expr_value_at(json, 0)
1170}
1171
1172const MAX_VALUE_DEPTH: usize = 64;
1177
1178fn 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 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
1222fn parse_string_to_expr_value(s: &str) -> ExprValue {
1229 let trimmed = s.trim();
1230
1231 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 if s.contains("${") {
1242 let parts = parse_interpolated_string(s);
1243 if parts.len() == 1 {
1244 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 ExprValue::Literal(Literal::String(s.to_string()))
1254}
1255
1256fn 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 if i > last_end {
1269 parts.push(StringPart::Literal(s[last_end..i].to_string()));
1270 }
1271
1272 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 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 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()); }
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 #[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 #[test]
1465 fn dnssec_hash_verification() {
1466 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 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 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 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 #[test]
1516 fn hsm_artifact_reference() {
1517 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 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 #[test]
1546 fn crypto_artifact_with_property() {
1547 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 #[test]
1561 fn find_embedded_expressions() {
1562 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 let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1575 assert_eq!(exprs.len(), 2);
1576 }
1577
1578 #[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 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 #[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 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 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 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 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}