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