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 as_literal_integer(&self) -> Option<i64> {
342 match self {
343 ExprValue::Literal(Literal::Integer(n)) => Some(*n),
344 _ => None,
345 }
346 }
347
348 pub fn get(&self, key: &str) -> Option<&ExprValue> {
350 match self {
351 ExprValue::Object(map) => map.get(key),
352 _ => None,
353 }
354 }
355
356 pub fn to_display_string(&self) -> String {
361 match self {
362 ExprValue::Literal(Literal::String(s)) => s.clone(),
363 ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
364 ExprValue::Literal(Literal::Float(f)) => f.to_string(),
365 ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
366 ExprValue::Literal(Literal::Null) => "null".to_string(),
367 ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
368 ExprValue::Interpolated(parts) => parts
369 .iter()
370 .map(|p| match p {
371 StringPart::Literal(s) => s.clone(),
372 StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
373 })
374 .collect(),
375 ExprValue::Object(_) => "[object]".to_string(),
376 ExprValue::Array(_) => "[array]".to_string(),
377 }
378 }
379}
380
381fn expr_to_string(expr: &Expression) -> String {
383 match expr {
384 Expression::Reference(r) => r.to_string(),
385 Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
386 Expression::Literal(Literal::Integer(i)) => i.to_string(),
387 Expression::Literal(Literal::Float(f)) => f.to_string(),
388 Expression::Literal(Literal::Boolean(b)) => b.to_string(),
389 Expression::Literal(Literal::Null) => "null".to_string(),
390 Expression::Pipeline { source, stages } => {
391 let mut result = expr_to_string(source);
392 for stage in stages {
393 result.push_str(" | ");
394 result.push_str(&stage.function);
395 if !stage.args.is_empty() {
396 result.push('(');
397 let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
398 result.push_str(&args.join(", "));
399 result.push(')');
400 }
401 }
402 result
403 }
404 }
405}
406
407#[derive(Debug, Clone, PartialEq)]
409pub enum StringPart {
410 Literal(String),
412 Expr(Expression),
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
422#[non_exhaustive]
423pub enum ExprError {
424 NotAnExpression,
426 Unclosed,
428 Empty,
430 MissingSource,
432 UnterminatedString {
434 quote: char,
436 },
437 InvalidFunctionName {
439 name: String,
441 },
442 UnclosedCall {
444 function: String,
446 },
447 InvalidArgument {
449 function: String,
451 position: usize,
453 cause: Box<ExprError>,
455 },
456 MissingNamespace {
458 text: String,
460 },
461 NotAReference {
463 text: String,
465 },
466 UnknownNamespace {
468 namespace: String,
470 },
471 MaterialNamespace {
473 namespace: String,
475 name: String,
477 },
478 EmptyName {
480 namespace: String,
482 },
483 InvalidName {
485 name: String,
487 },
488 InvalidProperty {
490 property: String,
492 },
493 InvalidPipeStage {
495 stage: String,
497 },
498}
499
500const IDENTIFIER_RULE: &str =
503 "names hold letters, digits, and underscores, and cannot start with a digit";
504
505impl std::fmt::Display for ExprError {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 match self {
508 Self::NotAnExpression => write!(
509 f,
510 "write it as an expression, '${{<namespace>.<name>}}', \
511 where <namespace> is one of {}",
512 namespace_list()
513 ),
514 Self::Unclosed => write!(f, "the expression is missing its closing '}}'"),
515 Self::Empty => write!(f, "the expression is empty"),
516 Self::MissingSource => write!(
517 f,
518 "nothing comes before the '|': a pipeline starts from a value"
519 ),
520 Self::UnterminatedString { quote } => {
521 write!(f, "the string is missing its closing {quote}")
522 }
523 Self::InvalidFunctionName { name } => {
524 write!(f, "'{name}' is not a usable function name")
525 }
526 Self::UnclosedCall { function } => {
527 write!(f, "the call to '{function}' is missing its closing ')'")
528 }
529 Self::InvalidArgument {
530 function,
531 position,
532 cause,
533 } => write!(
534 f,
535 "argument {position} to '{function}' is not a usable expression: {cause}"
536 ),
537 Self::MissingNamespace { text } => write!(
538 f,
539 "'{text}' names no namespace: write '<namespace>.<name>', \
540 where <namespace> is one of {}",
541 namespace_list()
542 ),
543 Self::NotAReference { text } => write!(
544 f,
545 "'{text}' is neither a literal nor a reference: a reference starts with \
546 one of {}",
547 namespace_list()
548 ),
549 Self::UnknownNamespace { namespace } => match nearest_namespace(namespace) {
550 Some(known) => {
551 write!(
552 f,
553 "unknown namespace '{namespace}': did you mean '{known}'?"
554 )
555 }
556 None => write!(
557 f,
558 "unknown namespace '{namespace}': expected one of {}",
559 namespace_list()
560 ),
561 },
562 Self::MaterialNamespace { namespace, name } => write!(
563 f,
564 "there is no '{namespace}' namespace: a material is read as 'artifact.{name}'"
565 ),
566 Self::EmptyName { namespace } => {
567 write!(f, "nothing follows '{namespace}.': expected a name")
568 }
569 Self::InvalidName { name } => {
570 write!(f, "'{name}' is not a usable name: {IDENTIFIER_RULE}")
571 }
572 Self::InvalidProperty { property } => {
573 write!(
574 f,
575 "'{property}' is not a usable property: {IDENTIFIER_RULE}"
576 )
577 }
578 Self::InvalidPipeStage { stage } => write!(
579 f,
580 "'{stage}' is not a usable pipe stage: expected a function name, \
581 with any arguments in parentheses"
582 ),
583 }
584 }
585}
586
587impl ExprError {
588 #[must_use]
593 pub fn suggestion(&self) -> Option<RefType> {
594 match self {
595 Self::UnknownNamespace { namespace } => nearest_namespace(namespace),
596 Self::MaterialNamespace { .. } => Some(RefType::Artifact),
599 Self::NotAnExpression
600 | Self::Unclosed
601 | Self::Empty
602 | Self::MissingSource
603 | Self::UnterminatedString { .. }
604 | Self::InvalidFunctionName { .. }
605 | Self::UnclosedCall { .. }
606 | Self::InvalidArgument { .. }
607 | Self::MissingNamespace { .. }
608 | Self::NotAReference { .. }
609 | Self::EmptyName { .. }
610 | Self::InvalidName { .. }
611 | Self::InvalidProperty { .. }
612 | Self::InvalidPipeStage { .. } => None,
613 }
614 }
615}
616
617impl std::error::Error for ExprError {
618 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
619 match self {
620 Self::InvalidArgument { cause, .. } => Some(cause.as_ref()),
621 Self::NotAnExpression
622 | Self::Unclosed
623 | Self::Empty
624 | Self::MissingSource
625 | Self::UnterminatedString { .. }
626 | Self::InvalidFunctionName { .. }
627 | Self::UnclosedCall { .. }
628 | Self::MissingNamespace { .. }
629 | Self::NotAReference { .. }
630 | Self::UnknownNamespace { .. }
631 | Self::MaterialNamespace { .. }
632 | Self::EmptyName { .. }
633 | Self::InvalidName { .. }
634 | Self::InvalidProperty { .. }
635 | Self::InvalidPipeStage { .. } => None,
636 }
637 }
638}
639
640fn namespace_list() -> String {
642 RefType::ALL
643 .iter()
644 .map(|ref_type| ref_type.as_str())
645 .collect::<Vec<_>>()
646 .join(", ")
647}
648
649pub fn parse_expression(s: &str) -> Option<Expression> {
673 parse_expression_detailed(s).ok()
674}
675
676pub fn parse_expression_detailed(s: &str) -> Result<Expression, ExprError> {
697 let Some(rest) = s.trim().strip_prefix("${") else {
700 return Err(ExprError::NotAnExpression);
701 };
702 let Some(inner) = rest.strip_suffix('}') else {
703 return Err(ExprError::Unclosed);
704 };
705
706 parse_pipeline(inner)
707}
708
709fn parse_pipeline(s: &str) -> Result<Expression, ExprError> {
711 let s = s.trim();
712
713 let parts = split_top_level(s, b'|');
715 let first = parts.first().copied().unwrap_or("").trim();
716
717 if first.is_empty() && parts.len() > 1 {
720 return Err(ExprError::MissingSource);
721 }
722
723 let source = parse_source(first)?;
724 if parts.len() == 1 {
725 return Ok(source);
727 }
728
729 let mut stages = Vec::with_capacity(parts.len().saturating_sub(1));
730 for part in parts.iter().skip(1) {
731 stages.push(parse_pipe_stage(part.trim())?);
732 }
733
734 Ok(Expression::Pipeline {
735 source: Box::new(source),
736 stages,
737 })
738}
739
740fn split_top_level(s: &str, delimiter: u8) -> Vec<&str> {
745 let mut parts = Vec::new();
746 let mut start = 0;
747 let mut paren_depth: u32 = 0;
748 let bytes = s.as_bytes();
749
750 for (i, &b) in bytes.iter().enumerate() {
751 match b {
752 b'(' => paren_depth += 1,
753 b')' => paren_depth = paren_depth.saturating_sub(1),
754 _ if b == delimiter && paren_depth == 0 => {
755 parts.push(&s[start..i]);
756 start = i + 1;
757 }
758 _ => {}
759 }
760 }
761
762 if start < s.len() {
764 parts.push(&s[start..]);
765 }
766
767 parts
768}
769
770fn parse_source(s: &str) -> Result<Expression, ExprError> {
778 let s = s.trim();
779
780 if s.is_empty() {
781 return Err(ExprError::Empty);
782 }
783
784 if let Some(quote) = s.chars().next().filter(|c| *c == '"' || *c == '\'') {
785 return parse_string_literal(s, quote).map(Expression::Literal);
786 }
787
788 if let Some(paren) = s.find('(')
789 && is_valid_identifier(&s[..paren])
790 {
791 return parse_call(s, paren).map(|stage| Expression::Pipeline {
793 source: Box::new(Expression::Literal(Literal::Integer(0))),
794 stages: vec![stage],
795 });
796 }
797
798 if let Some((namespace, rest)) = s.split_once('.') {
799 return parse_ref(namespace, rest).map(Expression::Reference);
800 }
801
802 if let Ok(value) = s.parse::<i64>() {
803 return Ok(Expression::Literal(Literal::Integer(value)));
804 }
805
806 if is_valid_identifier(s) {
809 return Err(ExprError::MissingNamespace {
810 text: s.to_string(),
811 });
812 }
813
814 Err(ExprError::NotAReference {
815 text: s.to_string(),
816 })
817}
818
819fn parse_ref(namespace: &str, rest: &str) -> Result<Reference, ExprError> {
821 if !is_valid_identifier(namespace) {
825 return Err(ExprError::NotAReference {
826 text: format!("{namespace}.{rest}"),
827 });
828 }
829
830 let (name, property) = match rest.split_once('.') {
831 Some((name, property)) => (name, Some(property)),
832 None => (rest, None),
833 };
834
835 let Ok(ref_type) = namespace.parse::<RefType>() else {
836 return Err(unknown_namespace(namespace, name));
837 };
838
839 if name.is_empty() {
840 return Err(ExprError::EmptyName {
841 namespace: namespace.to_string(),
842 });
843 }
844 if !is_valid_identifier(name) {
845 return Err(ExprError::InvalidName {
846 name: name.to_string(),
847 });
848 }
849
850 if let Some(property) = property
851 && !is_valid_identifier(property)
852 {
853 return Err(ExprError::InvalidProperty {
854 property: property.to_string(),
855 });
856 }
857
858 Ok(Reference {
859 ref_type,
860 name: name.to_string(),
861 property: property.map(ToString::to_string),
862 })
863}
864
865fn unknown_namespace(namespace: &str, name: &str) -> ExprError {
867 if edit_distance(namespace, "material") <= 1 {
873 return ExprError::MaterialNamespace {
874 namespace: namespace.to_string(),
875 name: name.to_string(),
876 };
877 }
878
879 ExprError::UnknownNamespace {
880 namespace: namespace.to_string(),
881 }
882}
883
884fn nearest_namespace(s: &str) -> Option<RefType> {
890 RefType::ALL
891 .iter()
892 .map(|known| (*known, edit_distance(s, known.as_str())))
893 .filter(|(_, distance)| *distance <= 2)
894 .min_by_key(|(_, distance)| *distance)
895 .map(|(known, _)| known)
896}
897
898fn edit_distance(a: &str, b: &str) -> usize {
900 let b_chars: Vec<char> = b.chars().collect();
901 let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
902 let mut current = vec![0; b_chars.len() + 1];
903
904 for (i, a_char) in a.chars().enumerate() {
905 current[0] = i + 1;
906 for (j, b_char) in b_chars.iter().enumerate() {
907 let substitution = previous[j] + usize::from(a_char != *b_char);
908 let deletion = previous[j + 1] + 1;
909 let insertion = current[j] + 1;
910 current[j + 1] = substitution.min(deletion).min(insertion);
911 }
912 std::mem::swap(&mut previous, &mut current);
913 }
914
915 previous[b_chars.len()]
916}
917
918fn parse_string_literal(s: &str, quote: char) -> Result<Literal, ExprError> {
920 if s.len() < 2 || !s.ends_with(quote) {
923 return Err(ExprError::UnterminatedString { quote });
924 }
925
926 Ok(Literal::String(s[1..s.len() - 1].to_string()))
927}
928
929fn parse_call(s: &str, paren: usize) -> Result<PipeStage, ExprError> {
934 let name = &s[..paren];
935
936 if !is_valid_identifier(name) {
937 return Err(ExprError::InvalidFunctionName {
938 name: name.to_string(),
939 });
940 }
941 if !s.ends_with(')') {
942 return Err(ExprError::UnclosedCall {
943 function: name.to_string(),
944 });
945 }
946
947 let args = split_top_level(s[paren + 1..s.len() - 1].trim(), b',')
950 .into_iter()
951 .enumerate()
952 .map(|(index, arg)| {
953 parse_pipeline(arg.trim()).map_err(|cause| ExprError::InvalidArgument {
954 function: name.to_string(),
955 position: index + 1,
956 cause: Box::new(cause),
957 })
958 })
959 .collect::<Result<Vec<_>, _>>()?;
960
961 Ok(PipeStage::with_args(name, args))
962}
963
964fn parse_pipe_stage(s: &str) -> Result<PipeStage, ExprError> {
966 let s = s.trim();
967
968 if let Some(paren) = s.find('(') {
969 return parse_call(s, paren);
970 }
971
972 if is_valid_identifier(s) {
974 return Ok(PipeStage::new(s));
975 }
976
977 Err(ExprError::InvalidPipeStage {
978 stage: s.to_string(),
979 })
980}
981
982fn is_valid_identifier(s: &str) -> bool {
985 let mut chars = s.chars();
986 match chars.next() {
987 None => false,
988 Some(first) => {
989 (first.is_ascii_alphabetic() || first == '_')
990 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
991 }
992 }
993}
994
995struct Occurrence<'a> {
997 range: Range<usize>,
998 text: &'a str,
999 parsed: Result<Expression, ExprError>,
1000}
1001
1002fn scan_occurrences(s: &str) -> Vec<Occurrence<'_>> {
1009 let mut found = Vec::new();
1010 let bytes = s.as_bytes();
1011 let mut i = 0;
1012
1013 while i < bytes.len() {
1014 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1015 let mut depth = 1;
1016 let mut j = i + 2;
1017 while j < bytes.len() && depth > 0 {
1018 match bytes[j] {
1019 b'{' => depth += 1,
1020 b'}' => depth -= 1,
1021 _ => {}
1022 }
1023 j += 1;
1024 }
1025
1026 let closed = depth == 0;
1029 let end = if closed { j } else { s.len() };
1030 let text = &s[i..end];
1031 found.push(Occurrence {
1032 range: i..end,
1033 text,
1034 parsed: if closed {
1035 parse_expression_detailed(text)
1036 } else {
1037 Err(ExprError::Unclosed)
1038 },
1039 });
1040 i = end;
1041 continue;
1042 }
1043 i += 1;
1044 }
1045
1046 found
1047}
1048
1049pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
1053 scan_occurrences(s)
1054 .into_iter()
1055 .filter_map(|occurrence| {
1056 Some(LocatedExpression {
1057 expression: occurrence.parsed.ok()?,
1058 range: occurrence.range,
1059 })
1060 })
1061 .collect()
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Eq)]
1066pub struct InvalidExpression {
1067 pub range: Range<usize>,
1069 pub text: String,
1071 pub reason: ExprError,
1073}
1074
1075pub fn find_invalid_expressions(s: &str) -> Vec<InvalidExpression> {
1093 scan_occurrences(s)
1094 .into_iter()
1095 .filter_map(|occurrence| {
1096 Some(InvalidExpression {
1097 reason: occurrence.parsed.err()?,
1098 range: occurrence.range,
1099 text: occurrence.text.to_string(),
1100 })
1101 })
1102 .collect()
1103}
1104
1105#[must_use]
1120pub fn literal_expr_value(value: &ExprValue) -> Option<serde_json::Value> {
1121 match value {
1122 ExprValue::Literal(lit) => Some(literal_to_json(lit)),
1123 ExprValue::Expr(_) | ExprValue::Interpolated(_) => None,
1124 ExprValue::Object(map) => {
1125 let mut result = serde_json::Map::new();
1126 for (k, v) in map {
1127 if let Some(json) = literal_expr_value(v) {
1128 result.insert(k.clone(), json);
1129 }
1130 }
1131 Some(serde_json::Value::Object(result))
1132 }
1133 ExprValue::Array(arr) => arr
1134 .iter()
1135 .map(literal_expr_value)
1136 .collect::<Option<Vec<_>>>()
1137 .map(serde_json::Value::Array),
1138 }
1139}
1140
1141pub fn literal_to_json(lit: &Literal) -> serde_json::Value {
1143 match lit {
1144 Literal::String(s) => serde_json::Value::String(s.clone()),
1145 Literal::Integer(i) => serde_json::Value::Number((*i).into()),
1146 Literal::Float(f) => serde_json::Number::from_f64(*f)
1147 .map_or(serde_json::Value::Null, serde_json::Value::Number),
1148 Literal::Boolean(b) => serde_json::Value::Bool(*b),
1149 Literal::Null => serde_json::Value::Null,
1150 }
1151}
1152
1153pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
1177 parse_expr_value_at(json, 0)
1178}
1179
1180const MAX_VALUE_DEPTH: usize = 64;
1185
1186fn parse_expr_value_at(json: &serde_json::Value, depth: usize) -> ExprValue {
1195 if depth >= MAX_VALUE_DEPTH {
1196 return ExprValue::Literal(Literal::Null);
1197 }
1198 let child_depth = depth.saturating_add(1);
1199 match json {
1200 serde_json::Value::String(s) => parse_string_to_expr_value(s),
1201 serde_json::Value::Number(n) => {
1202 if let Some(i) = n.as_i64() {
1203 ExprValue::Literal(Literal::Integer(i))
1204 } else if let Some(f) = n.as_f64() {
1205 ExprValue::Literal(Literal::Float(f))
1206 } else {
1207 ExprValue::Literal(Literal::Integer(0))
1209 }
1210 }
1211 serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
1212 serde_json::Value::Null => ExprValue::Literal(Literal::Null),
1213 serde_json::Value::Object(map) => {
1214 let mut result = std::collections::HashMap::new();
1215 for (k, v) in map {
1216 result.insert(k.clone(), parse_expr_value_at(v, child_depth));
1217 }
1218 ExprValue::Object(result)
1219 }
1220 serde_json::Value::Array(arr) => {
1221 let result: Vec<_> = arr
1222 .iter()
1223 .map(|v| parse_expr_value_at(v, child_depth))
1224 .collect();
1225 ExprValue::Array(result)
1226 }
1227 }
1228}
1229
1230fn parse_string_to_expr_value(s: &str) -> ExprValue {
1237 let trimmed = s.trim();
1238
1239 if trimmed.starts_with("${")
1241 && trimmed.ends_with('}')
1242 && !trimmed[2..trimmed.len() - 1].contains("${")
1243 && let Some(expr) = parse_expression(trimmed)
1244 {
1245 return ExprValue::Expr(expr);
1246 }
1247
1248 if s.contains("${") {
1250 let parts = parse_interpolated_string(s);
1251 if parts.len() == 1 {
1252 if let StringPart::Literal(lit) = &parts[0] {
1254 return ExprValue::Literal(Literal::String(lit.clone()));
1255 }
1256 }
1257 return ExprValue::Interpolated(parts);
1258 }
1259
1260 ExprValue::Literal(Literal::String(s.to_string()))
1262}
1263
1264fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
1268 let mut parts = Vec::new();
1269 let bytes = s.as_bytes();
1270 let mut last_end = 0;
1271 let mut i = 0;
1272
1273 while i < bytes.len() {
1274 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
1275 if i > last_end {
1277 parts.push(StringPart::Literal(s[last_end..i].to_string()));
1278 }
1279
1280 let mut depth = 1;
1282 let mut j = i + 2;
1283 while j < bytes.len() && depth > 0 {
1284 match bytes[j] {
1285 b'{' => depth += 1,
1286 b'}' => depth -= 1,
1287 _ => {}
1288 }
1289 j += 1;
1290 }
1291
1292 if depth == 0 {
1293 let expr_str = &s[i..j];
1294 if let Some(expr) = parse_expression(expr_str) {
1295 parts.push(StringPart::Expr(expr));
1296 } else {
1297 parts.push(StringPart::Literal(expr_str.to_string()));
1299 }
1300 last_end = j;
1301 i = j;
1302 continue;
1303 }
1304 }
1305 i += 1;
1306 }
1307
1308 if last_end < s.len() {
1310 parts.push(StringPart::Literal(s[last_end..].to_string()));
1311 }
1312
1313 parts
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318 use super::*;
1319
1320 #[test]
1321 fn parse_simple_reference() {
1322 let expr = parse_expression("${artifact.ksr}").unwrap();
1323 match expr {
1324 Expression::Reference(r) => {
1325 assert_eq!(r.ref_type, RefType::Artifact);
1326 assert_eq!(r.name, "ksr");
1327 assert!(r.property.is_none());
1328 }
1329 _ => panic!("Expected Reference"),
1330 }
1331 }
1332
1333 #[test]
1334 fn parse_reference_with_property() {
1335 let expr = parse_expression("${artifact.keypair.private}").unwrap();
1336 match expr {
1337 Expression::Reference(r) => {
1338 assert_eq!(r.ref_type, RefType::Artifact);
1339 assert_eq!(r.name, "keypair");
1340 assert_eq!(r.property, Some("private".to_string()));
1341 }
1342 _ => panic!("Expected Reference"),
1343 }
1344 }
1345
1346 #[test]
1347 fn parse_param_reference() {
1348 let expr = parse_expression("${param.key_label}").unwrap();
1349 match expr {
1350 Expression::Reference(r) => {
1351 assert_eq!(r.ref_type, RefType::Param);
1352 assert_eq!(r.name, "key_label");
1353 }
1354 _ => panic!("Expected Reference"),
1355 }
1356 }
1357
1358 #[test]
1359 fn parse_simple_pipeline() {
1360 let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
1361 match expr {
1362 Expression::Pipeline { source, stages } => {
1363 assert!(matches!(*source, Expression::Reference(_)));
1364 assert_eq!(stages.len(), 1);
1365 assert_eq!(stages[0].function, "sha256");
1366 assert!(stages[0].args.is_empty());
1367 }
1368 _ => panic!("Expected Pipeline"),
1369 }
1370 }
1371
1372 #[test]
1373 fn parse_multi_stage_pipeline() {
1374 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1375 match expr {
1376 Expression::Pipeline { stages, .. } => {
1377 assert_eq!(stages.len(), 2);
1378 assert_eq!(stages[0].function, "sha256");
1379 assert_eq!(stages[1].function, "hex");
1380 }
1381 _ => panic!("Expected Pipeline"),
1382 }
1383 }
1384
1385 #[test]
1386 fn parse_pipeline_with_function_args() {
1387 let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
1388 match expr {
1389 Expression::Pipeline { stages, .. } => {
1390 assert_eq!(stages.len(), 1);
1391 assert_eq!(stages[0].function, "substr");
1392 assert_eq!(stages[0].args.len(), 2);
1393 }
1394 _ => panic!("Expected Pipeline"),
1395 }
1396 }
1397
1398 #[test]
1399 fn parse_complex_pipeline() {
1400 let expr =
1401 parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
1402 match expr {
1403 Expression::Pipeline { source, stages } => {
1404 if let Expression::Reference(r) = *source {
1405 assert_eq!(r.name, "keypair");
1406 assert_eq!(r.property, Some("public".to_string()));
1407 }
1408 assert_eq!(stages.len(), 3);
1409 assert_eq!(stages[0].function, "fingerprint");
1410 assert_eq!(stages[1].function, "hex");
1411 assert_eq!(stages[2].function, "upper");
1412 }
1413 _ => panic!("Expected Pipeline"),
1414 }
1415 }
1416
1417 #[test]
1418 fn parse_invalid_expression() {
1419 assert!(parse_expression("not an expression").is_none());
1420 assert!(parse_expression("${").is_none());
1421 assert!(parse_expression("${}").is_none());
1422 assert!(parse_expression("${invalid}").is_none()); }
1424
1425 #[test]
1426 fn find_expressions_in_string() {
1427 let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
1428 assert_eq!(exprs.len(), 1);
1429 assert_eq!(exprs[0].range.start, 6);
1430 }
1431
1432 #[test]
1433 fn find_multiple_expressions() {
1434 let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
1435 assert_eq!(exprs.len(), 2);
1436 }
1437
1438 #[test]
1439 fn value_type_names() {
1440 assert_eq!(Value::bytes(vec![]).type_name(), "bytes");
1441 assert_eq!(Value::String(String::new()).type_name(), "string");
1442 assert_eq!(Value::Integer(0).type_name(), "integer");
1443 assert_eq!(Value::Boolean(true).type_name(), "boolean");
1444 assert_eq!(Value::Null.type_name(), "null");
1445 }
1446
1447 #[test]
1450 fn value_debug_does_not_print_bytes() {
1451 let v = Value::bytes(b"the recovery phrase".to_vec());
1452 assert_eq!(format!("{v:?}"), "Bytes(<19 bytes>)");
1453 assert_eq!(format!("{:?}", Value::String("x".into())), "String(\"x\")");
1454 }
1455
1456 #[test]
1457 fn value_as_bytes() {
1458 let v = Value::bytes(vec![1, 2, 3]);
1459 assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
1460
1461 let v = Value::String("hello".into());
1462 assert_eq!(v.as_bytes(), Some(&b"hello"[..]));
1463
1464 let v = Value::Integer(42);
1465 assert!(v.as_bytes().is_none());
1466 }
1467
1468 mod ceremony_examples {
1469 use super::*;
1470
1471 #[test]
1473 fn dnssec_hash_verification() {
1474 let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
1476 match expr {
1477 Expression::Pipeline { source, stages } => {
1478 if let Expression::Reference(r) = *source {
1479 assert_eq!(r.ref_type, RefType::Artifact);
1480 assert_eq!(r.name, "ksr");
1481 } else {
1482 panic!("Expected Reference source");
1483 }
1484 assert_eq!(stages.len(), 3);
1486 assert_eq!(stages[0].function, "sha256");
1487 assert_eq!(stages[1].function, "hex");
1488 assert_eq!(stages[2].function, "substr");
1489 assert_eq!(stages[2].args.len(), 2);
1490 }
1491 _ => panic!("Expected Pipeline"),
1492 }
1493 }
1494
1495 #[test]
1496 fn dnssec_param_reference() {
1497 let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
1499 match expr {
1500 Expression::Reference(r) => {
1501 assert_eq!(r.ref_type, RefType::Param);
1502 assert_eq!(r.name, "expected_ksr_hash");
1503 }
1504 _ => panic!("Expected Reference"),
1505 }
1506 }
1507
1508 #[test]
1509 fn dnssec_oral_readback() {
1510 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1512 match expr {
1513 Expression::Pipeline { stages, .. } => {
1514 assert_eq!(stages.len(), 2);
1515 assert_eq!(stages[0].function, "sha256");
1516 assert_eq!(stages[1].function, "hex");
1517 }
1518 _ => panic!("Expected Pipeline"),
1519 }
1520 }
1521
1522 #[test]
1524 fn hsm_artifact_reference() {
1525 let expr = parse_expression("${artifact.master_keypair}").unwrap();
1527 match expr {
1528 Expression::Reference(r) => {
1529 assert_eq!(r.ref_type, RefType::Artifact);
1530 assert_eq!(r.name, "master_keypair");
1531 assert!(r.property.is_none());
1532 }
1533 _ => panic!("Expected Reference"),
1534 }
1535 }
1536
1537 #[test]
1538 fn hsm_public_key_fingerprint() {
1539 let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1541 match expr {
1542 Expression::Pipeline { source, stages } => {
1543 if let Expression::Reference(r) = *source {
1544 assert_eq!(r.name, "master_public_key");
1545 }
1546 assert_eq!(stages.len(), 2);
1547 }
1548 _ => panic!("Expected Pipeline"),
1549 }
1550 }
1551
1552 #[test]
1554 fn crypto_artifact_with_property() {
1555 let expr = parse_expression("${artifact.keypair.private}").unwrap();
1557 match expr {
1558 Expression::Reference(r) => {
1559 assert_eq!(r.ref_type, RefType::Artifact);
1560 assert_eq!(r.name, "keypair");
1561 assert_eq!(r.property, Some("private".to_string()));
1562 }
1563 _ => panic!("Expected Reference"),
1564 }
1565 }
1566
1567 #[test]
1569 fn find_embedded_expressions() {
1570 let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1572 assert_eq!(exprs.len(), 1);
1573 if let Expression::Reference(r) = &exprs[0].expression {
1574 assert_eq!(r.ref_type, RefType::Param);
1575 assert_eq!(r.name, "hsm_serial");
1576 }
1577 }
1578
1579 #[test]
1580 fn find_multiple_embedded_params() {
1581 let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1583 assert_eq!(exprs.len(), 2);
1584 }
1585
1586 #[test]
1588 fn role_reference_parses() {
1589 let expr = parse_expression("${role.ceremony_admin}").unwrap();
1590 match expr {
1591 Expression::Reference(r) => {
1592 assert_eq!(r.ref_type, RefType::Role);
1593 assert_eq!(r.name, "ceremony_admin");
1594 }
1595 _ => panic!("Expected Reference"),
1596 }
1597 }
1598 }
1599
1600 mod expr_value_tests {
1601 use super::*;
1602
1603 #[test]
1604 fn parse_literal_string() {
1605 let json = serde_json::json!("hello");
1606 let expr = parse_expr_value(&json);
1607 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1608 }
1609
1610 #[test]
1611 fn parse_literal_integer() {
1612 let json = serde_json::json!(42);
1613 let expr = parse_expr_value(&json);
1614 assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1615 }
1616
1617 #[test]
1618 fn parse_literal_float() {
1619 let json = serde_json::json!(1.5);
1620 let expr = parse_expr_value(&json);
1621 assert!(
1622 matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1623 );
1624 }
1625
1626 #[test]
1627 fn parse_literal_boolean() {
1628 let json = serde_json::json!(true);
1629 let expr = parse_expr_value(&json);
1630 assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1631 }
1632
1633 #[test]
1634 fn parse_literal_null() {
1635 let json = serde_json::Value::Null;
1636 let expr = parse_expr_value(&json);
1637 assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1638 }
1639
1640 #[test]
1641 fn parse_single_expression() {
1642 let json = serde_json::json!("${artifact.ksr | sha256}");
1643 let expr = parse_expr_value(&json);
1644 match expr {
1645 ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1646 assert!(matches!(*source, Expression::Reference(_)));
1647 assert_eq!(stages.len(), 1);
1648 assert_eq!(stages[0].function, "sha256");
1649 }
1650 _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1651 }
1652 }
1653
1654 #[test]
1655 fn parse_simple_reference() {
1656 let json = serde_json::json!("${param.name}");
1657 let expr = parse_expr_value(&json);
1658 match expr {
1659 ExprValue::Expr(Expression::Reference(r)) => {
1660 assert_eq!(r.ref_type, RefType::Param);
1661 assert_eq!(r.name, "name");
1662 }
1663 _ => panic!("Expected Expr(Reference), got {expr:?}"),
1664 }
1665 }
1666
1667 #[test]
1668 fn parse_interpolated_string() {
1669 let json = serde_json::json!("Hello, ${param.name}!");
1670 let expr = parse_expr_value(&json);
1671 match expr {
1672 ExprValue::Interpolated(parts) => {
1673 assert_eq!(parts.len(), 3);
1674 assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1675 assert!(matches!(
1676 &parts[1],
1677 StringPart::Expr(Expression::Reference(_))
1678 ));
1679 assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1680 }
1681 _ => panic!("Expected Interpolated, got {expr:?}"),
1682 }
1683 }
1684
1685 #[test]
1686 fn parse_object_with_expressions() {
1687 let json = serde_json::json!({
1688 "actual": "${artifact.ksr | sha256 | hex}",
1689 "expected": "${param.expected_hash}",
1690 "static": "hello"
1691 });
1692 let expr = parse_expr_value(&json);
1693 match expr {
1694 ExprValue::Object(map) => {
1695 assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1696 assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1697 assert!(matches!(
1698 map.get("static"),
1699 Some(ExprValue::Literal(Literal::String(_)))
1700 ));
1701 }
1702 _ => panic!("Expected Object, got {expr:?}"),
1703 }
1704 }
1705
1706 #[test]
1707 fn parse_array_with_expressions() {
1708 let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1709 let expr = parse_expr_value(&json);
1710 match expr {
1711 ExprValue::Array(arr) => {
1712 assert_eq!(arr.len(), 3);
1713 assert!(matches!(&arr[0], ExprValue::Expr(_)));
1714 assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1715 assert!(matches!(&arr[2], ExprValue::Expr(_)));
1716 }
1717 _ => panic!("Expected Array, got {expr:?}"),
1718 }
1719 }
1720
1721 #[test]
1722 fn parse_deeply_nested_value_is_truncated_not_crashed() {
1723 let mut json = serde_json::json!("leaf");
1727 for _ in 0..200 {
1728 json = serde_json::Value::Array(vec![json]);
1729 }
1730 let mut expr = parse_expr_value(&json);
1731 let mut levels = 0;
1732 loop {
1733 match expr {
1734 ExprValue::Array(mut arr) => {
1735 assert_eq!(arr.len(), 1);
1736 expr = arr.remove(0);
1737 levels += 1;
1738 }
1739 other => {
1740 assert!(
1741 matches!(other, ExprValue::Literal(Literal::Null)),
1742 "truncated tail must be Literal(Null), got {other:?}"
1743 );
1744 break;
1745 }
1746 }
1747 }
1748 assert_eq!(levels, MAX_VALUE_DEPTH, "recursion must stop at the cap");
1749 }
1750
1751 #[test]
1752 fn parse_moderately_nested_value_is_preserved() {
1753 let mut json = serde_json::json!("leaf");
1754 for _ in 0..10 {
1755 json = serde_json::Value::Array(vec![json]);
1756 }
1757 let mut expr = parse_expr_value(&json);
1758 for _ in 0..10 {
1759 match expr {
1760 ExprValue::Array(mut arr) => {
1761 assert_eq!(arr.len(), 1);
1762 expr = arr.remove(0);
1763 }
1764 other => panic!("expected Array, got {other:?}"),
1765 }
1766 }
1767 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "leaf"));
1768 }
1769
1770 #[test]
1771 fn parse_multiple_expressions_in_string() {
1772 let json = serde_json::json!("${param.a} and ${param.b}");
1773 let expr = parse_expr_value(&json);
1774 match expr {
1775 ExprValue::Interpolated(parts) => {
1776 assert_eq!(parts.len(), 3);
1777 assert!(matches!(&parts[0], StringPart::Expr(_)));
1778 assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1779 assert!(matches!(&parts[2], StringPart::Expr(_)));
1780 }
1781 _ => panic!("Expected Interpolated, got {expr:?}"),
1782 }
1783 }
1784 }
1785
1786 mod invalid_expressions {
1787 use super::*;
1788
1789 #[test]
1792 fn all_lists_every_namespace() {
1793 for ref_type in RefType::ALL {
1794 match ref_type {
1795 RefType::Param | RefType::Role | RefType::Artifact => {}
1796 }
1797 }
1798 assert_eq!(RefType::ALL.len(), 3);
1799 assert_eq!(namespace_list(), "param, role, artifact");
1800 }
1801
1802 #[test]
1803 fn a_valid_expression_yields_nothing() {
1804 for s in [
1805 "${artifact.ksr}",
1806 "${artifact.keypair.public | sha256 | hex}",
1807 "${param.name}",
1808 "plain text with no expression",
1809 "Hash: ${artifact.a | sha256 | hex} done",
1810 "${concat(artifact.a | sha256, artifact.b) | hex}",
1811 ] {
1812 assert!(
1813 find_invalid_expressions(s).is_empty(),
1814 "{s} should be accepted"
1815 );
1816 }
1817 }
1818
1819 #[test]
1820 fn an_unknown_namespace_is_reported_with_its_range() {
1821 let found = find_invalid_expressions("sign ${nonsense.x} now");
1822 assert_eq!(found.len(), 1);
1823 assert_eq!(found[0].range, 5..18);
1824 assert_eq!(found[0].text, "${nonsense.x}");
1825 assert_eq!(
1826 found[0].reason,
1827 ExprError::UnknownNamespace {
1828 namespace: "nonsense".to_string(),
1829 }
1830 );
1831 assert_eq!(
1832 found[0].reason.to_string(),
1833 "unknown namespace 'nonsense': expected one of param, role, artifact"
1834 );
1835 }
1836
1837 #[test]
1838 fn every_occurrence_in_one_string_is_reported() {
1839 let found = find_invalid_expressions("${a.b} ${param.ok} ${c.d}");
1840 assert_eq!(found.len(), 2);
1841 assert_eq!(found[0].text, "${a.b}");
1842 assert_eq!(found[1].text, "${c.d}");
1843 }
1844
1845 #[test]
1846 fn the_material_namespace_names_the_artifact_form() {
1847 for namespace in ["material", "materials"] {
1850 let found = find_invalid_expressions(&format!("${{{namespace}.manifest}}"));
1851 assert_eq!(found.len(), 1, "for {namespace}");
1852 assert_eq!(
1853 found[0].reason,
1854 ExprError::MaterialNamespace {
1855 namespace: namespace.to_string(),
1856 name: "manifest".to_string(),
1857 }
1858 );
1859 assert_eq!(
1860 found[0].reason.to_string(),
1861 format!(
1862 "there is no '{namespace}' namespace: \
1863 a material is read as 'artifact.manifest'"
1864 )
1865 );
1866 assert_eq!(
1867 found[0].reason.suggestion(),
1868 Some(RefType::Artifact),
1869 "an editor fix points at the artifact namespace"
1870 );
1871 }
1872 }
1873
1874 #[test]
1875 fn a_pipeline_with_nothing_before_the_pipe_is_not_an_empty_expression() {
1876 assert_eq!(
1877 parse_expression_detailed("${ | sha256 }").unwrap_err(),
1878 ExprError::MissingSource
1879 );
1880 assert_eq!(
1881 parse_expression_detailed("${}").unwrap_err(),
1882 ExprError::Empty
1883 );
1884 }
1885
1886 #[test]
1887 fn an_unclosed_expression_is_reported_to_the_end_of_the_string() {
1888 let found = find_invalid_expressions("see ${artifact.k");
1889 assert_eq!(found.len(), 1);
1890 assert_eq!(found[0].text, "${artifact.k");
1891 assert_eq!(found[0].reason, ExprError::Unclosed);
1892 assert_eq!(
1893 found[0].reason.to_string(),
1894 "the expression is missing its closing '}'"
1895 );
1896 }
1897
1898 #[test]
1899 fn each_shape_of_mistake_gets_its_own_reason() {
1900 for (input, expected) in [
1901 ("${}", "the expression is empty"),
1902 (
1903 "${artifact}",
1904 "'artifact' names no namespace: write '<namespace>.<name>', \
1905 where <namespace> is one of param, role, artifact",
1906 ),
1907 (
1908 "${artifact.}",
1909 "nothing follows 'artifact.': expected a name",
1910 ),
1911 (
1912 "${artifact.9k}",
1913 "'9k' is not a usable name: names hold letters, digits, and underscores, \
1914 and cannot start with a digit",
1915 ),
1916 (
1917 "${artifact.k.9p}",
1918 "'9p' is not a usable property: names hold letters, digits, and underscores, \
1919 and cannot start with a digit",
1920 ),
1921 (
1922 "${artifact.k | 9hex}",
1923 "'9hex' is not a usable pipe stage: expected a function name, \
1924 with any arguments in parentheses",
1925 ),
1926 (
1927 "${concat(artifact.a}",
1928 "the call to 'concat' is missing its closing ')'",
1929 ),
1930 ] {
1931 let found = find_invalid_expressions(input);
1932 assert_eq!(found.len(), 1, "{input} should be rejected");
1933 assert_eq!(found[0].reason.to_string(), expected, "for {input}");
1934 }
1935 }
1936
1937 #[test]
1938 fn a_bare_name_is_told_to_use_the_wrapper() {
1939 let error = parse_expression_detailed("keypair").unwrap_err();
1940 assert_eq!(error, ExprError::NotAnExpression);
1941 assert_eq!(
1942 error.to_string(),
1943 "write it as an expression, '${<namespace>.<name>}', \
1944 where <namespace> is one of param, role, artifact"
1945 );
1946 }
1947
1948 #[test]
1949 fn a_namespace_one_or_two_edits_away_is_suggested() {
1950 for (input, expected) in [
1951 ("${paramm.region}", RefType::Param),
1952 ("${params.region}", RefType::Param),
1953 ("${roles.officer}", RefType::Role),
1954 ("${artifacts.ksr}", RefType::Artifact),
1955 ] {
1956 let error = parse_expression_detailed(input).unwrap_err();
1957 assert_eq!(error.suggestion(), Some(expected), "for {input}");
1958 assert!(
1959 error
1960 .to_string()
1961 .contains(&format!("did you mean '{expected}'")),
1962 "for {input}: {error}"
1963 );
1964 }
1965 }
1966
1967 #[test]
1968 fn a_namespace_nothing_like_a_known_one_gets_no_suggestion() {
1969 for input in ["${nonsense.x}", "${env.HOME}", "${secret.pin}"] {
1970 let error = parse_expression_detailed(input).unwrap_err();
1971 assert!(
1972 matches!(error, ExprError::UnknownNamespace { .. }),
1973 "{input} should be an unknown namespace, got {error:?}"
1974 );
1975 assert_eq!(error.suggestion(), None, "{input} should not be guessed at");
1976 }
1977 }
1978
1979 #[test]
1980 fn a_failing_argument_names_its_position_and_its_own_cause() {
1981 let error = parse_expression_detailed("${concat(artifact.a, nonsense.b)}").unwrap_err();
1982 assert_eq!(
1983 error,
1984 ExprError::InvalidArgument {
1985 function: "concat".to_string(),
1986 position: 2,
1987 cause: Box::new(ExprError::UnknownNamespace {
1988 namespace: "nonsense".to_string(),
1989 }),
1990 }
1991 );
1992 assert_eq!(
1993 error.to_string(),
1994 "argument 2 to 'concat' is not a usable expression: \
1995 unknown namespace 'nonsense': expected one of param, role, artifact"
1996 );
1997 }
1998
1999 #[test]
2000 fn a_lone_quote_is_an_unterminated_string() {
2001 for (input, quote) in [("${\"}", '"'), ("${'}", '\''), ("${\"abc}", '"')] {
2004 assert_eq!(
2005 parse_expression_detailed(input).unwrap_err(),
2006 ExprError::UnterminatedString { quote },
2007 "for {input}"
2008 );
2009 }
2010 }
2011
2012 #[test]
2013 fn a_quote_opens_a_string_even_when_it_holds_a_parenthesis() {
2014 let parsed = parse_expression_detailed("${\"a(b\"}");
2015 assert!(
2016 matches!(parsed, Ok(Expression::Literal(Literal::String(ref s))) if s == "a(b"),
2017 "the parenthesis belongs to the string, got {parsed:?}"
2018 );
2019 }
2020
2021 #[test]
2022 fn the_shape_a_source_is_read_as_comes_from_its_first_characters() {
2023 for (input, expected) in [
2026 (
2027 "${param.x(1)}",
2028 ExprError::InvalidName {
2029 name: "x(1)".to_string(),
2030 },
2031 ),
2032 (
2033 "${42abc}",
2034 ExprError::NotAReference {
2035 text: "42abc".to_string(),
2036 },
2037 ),
2038 (
2039 "${1.5}",
2040 ExprError::NotAReference {
2041 text: "1.5".to_string(),
2042 },
2043 ),
2044 ] {
2045 assert_eq!(
2046 parse_expression_detailed(input).unwrap_err(),
2047 expected,
2048 "for {input}"
2049 );
2050 }
2051 }
2052
2053 #[test]
2054 fn parses_every_accepted_form() {
2055 for input in [
2056 "${artifact.ksr}",
2057 "${artifact.keypair.public}",
2058 "${param.region | sha256 | hex}",
2059 "${\"literal\"}",
2060 "${'literal'}",
2061 "${42}",
2062 "${-42}",
2063 "${\"\"}",
2064 "${concat(artifact.a, artifact.b) | hex}",
2065 "${pad(artifact.a, 4)}",
2066 ] {
2067 assert!(
2068 parse_expression_detailed(input).is_ok(),
2069 "{input} should parse, got {:?}",
2070 parse_expression_detailed(input)
2071 );
2072 }
2073 }
2074 }
2075}
2076
2077#[cfg(test)]
2078mod literal_projection_tests {
2079 use super::{ExprValue, Literal, StringPart, literal_expr_value, parse_expression};
2080 use serde_json::json;
2081
2082 fn deferred() -> ExprValue {
2083 ExprValue::Expr(parse_expression("${param.algo}").expect("parse"))
2084 }
2085
2086 fn literal(s: &str) -> ExprValue {
2087 ExprValue::Literal(Literal::String(s.to_string()))
2088 }
2089
2090 #[test]
2091 fn keeps_literal_fields_and_drops_deferred_ones() {
2092 let value = ExprValue::Object(
2093 [
2094 ("algorithm".to_string(), literal("CMS-RSA-GCM")),
2095 ("label".to_string(), deferred()),
2096 ]
2097 .into_iter()
2098 .collect(),
2099 );
2100
2101 assert_eq!(
2102 literal_expr_value(&value),
2103 Some(json!({"algorithm": "CMS-RSA-GCM"})),
2104 );
2105 }
2106
2107 #[test]
2108 fn drops_an_interpolated_field() {
2109 let value = ExprValue::Object(
2110 [(
2111 "subject".to_string(),
2112 ExprValue::Interpolated(vec![
2113 StringPart::Literal("CN=".to_string()),
2114 StringPart::Expr(parse_expression("${param.cn}").expect("parse")),
2115 ]),
2116 )]
2117 .into_iter()
2118 .collect(),
2119 );
2120
2121 assert_eq!(literal_expr_value(&value), Some(json!({})));
2122 }
2123
2124 #[test]
2125 fn a_deferred_element_discards_the_whole_array() {
2126 let value = ExprValue::Array(vec![literal("a"), deferred()]);
2129 assert_eq!(literal_expr_value(&value), None);
2130
2131 let all_literal = ExprValue::Array(vec![literal("a"), literal("b")]);
2132 assert_eq!(literal_expr_value(&all_literal), Some(json!(["a", "b"])));
2133 }
2134
2135 #[test]
2136 fn a_fully_deferred_block_yields_nothing() {
2137 assert_eq!(literal_expr_value(&deferred()), None);
2138 }
2139}