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 std::str::FromStr for RefType {
48 type Err = String;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 match s {
52 "param" => Ok(Self::Param),
53 "role" => Ok(Self::Role),
54 "artifact" => Ok(Self::Artifact),
55 _ => Err(format!("unknown reference type: {s}")),
56 }
57 }
58}
59
60impl std::fmt::Display for RefType {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Param => write!(f, "param"),
64 Self::Role => write!(f, "role"),
65 Self::Artifact => write!(f, "artifact"),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub enum Value {
73 Bytes(Vec<u8>),
75 String(String),
77 Integer(i64),
79 Boolean(bool),
81 Null,
83}
84
85impl Value {
86 pub fn as_bytes(&self) -> Option<Vec<u8>> {
89 match self {
90 Value::Bytes(b) => Some(b.clone()),
91 Value::String(s) => Some(s.as_bytes().to_vec()),
92 _ => None,
93 }
94 }
95
96 pub fn as_string(&self) -> Option<&str> {
98 match self {
99 Value::String(s) => Some(s),
100 _ => None,
101 }
102 }
103
104 pub fn as_integer(&self) -> Option<i64> {
106 match self {
107 Value::Integer(i) => Some(*i),
108 _ => None,
109 }
110 }
111
112 pub fn is_null(&self) -> bool {
114 matches!(self, Value::Null)
115 }
116
117 pub fn type_name(&self) -> &'static str {
119 match self {
120 Value::Bytes(_) => "bytes",
121 Value::String(_) => "string",
122 Value::Integer(_) => "integer",
123 Value::Boolean(_) => "boolean",
124 Value::Null => "null",
125 }
126 }
127}
128
129impl std::fmt::Display for Value {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
133 Value::String(s) => write!(f, "{s}"),
134 Value::Integer(i) => write!(f, "{i}"),
135 Value::Boolean(b) => write!(f, "{b}"),
136 Value::Null => write!(f, "null"),
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq)]
148pub enum Expression {
149 Reference(Reference),
151
152 Literal(Literal),
154
155 Pipeline {
157 source: Box<Expression>,
159 stages: Vec<PipeStage>,
161 },
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct Reference {
167 pub ref_type: RefType,
169 pub name: String,
171 pub property: Option<String>,
173}
174
175impl Reference {
176 pub fn new(ref_type: RefType, name: &str) -> Self {
178 Self {
179 ref_type,
180 name: name.to_string(),
181 property: None,
182 }
183 }
184
185 pub fn with_property(ref_type: RefType, name: &str, property: &str) -> Self {
187 Self {
188 ref_type,
189 name: name.to_string(),
190 property: Some(property.to_string()),
191 }
192 }
193}
194
195impl std::fmt::Display for Reference {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 if let Some(prop) = &self.property {
198 write!(f, "{}.{}.{}", self.ref_type, self.name, prop)
199 } else {
200 write!(f, "{}.{}", self.ref_type, self.name)
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq)]
207pub enum Literal {
208 String(String),
210 Integer(i64),
212 Float(f64),
214 Boolean(bool),
216 Null,
218}
219
220#[allow(clippy::cast_possible_truncation)]
221impl From<Literal> for Value {
222 fn from(lit: Literal) -> Self {
223 match lit {
224 Literal::String(s) => Value::String(s),
225 Literal::Integer(i) => Value::Integer(i),
226 Literal::Float(f) => Value::Integer(f as i64), Literal::Boolean(b) => Value::Boolean(b),
228 Literal::Null => Value::Null,
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub struct PipeStage {
236 pub function: String,
238 pub args: Vec<Expression>,
240}
241
242impl PipeStage {
243 pub fn new(function: &str) -> Self {
245 Self {
246 function: function.to_string(),
247 args: Vec::new(),
248 }
249 }
250
251 pub fn with_args(function: &str, args: Vec<Expression>) -> Self {
253 Self {
254 function: function.to_string(),
255 args,
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
262pub struct LocatedExpression {
263 pub expression: Expression,
265 pub range: Range<usize>,
267}
268
269#[derive(Debug, Clone, PartialEq)]
275pub enum ExprValue {
276 Literal(Literal),
278 Expr(Expression),
280 Interpolated(Vec<StringPart>),
282 Object(std::collections::HashMap<String, ExprValue>),
284 Array(Vec<ExprValue>),
286}
287
288impl ExprValue {
289 pub fn as_literal_string(&self) -> Option<&str> {
291 match self {
292 ExprValue::Literal(Literal::String(s)) => Some(s),
293 _ => None,
294 }
295 }
296
297 pub fn get(&self, key: &str) -> Option<&ExprValue> {
299 match self {
300 ExprValue::Object(map) => map.get(key),
301 _ => None,
302 }
303 }
304
305 pub fn to_display_string(&self) -> String {
310 match self {
311 ExprValue::Literal(Literal::String(s)) => s.clone(),
312 ExprValue::Literal(Literal::Integer(i)) => i.to_string(),
313 ExprValue::Literal(Literal::Float(f)) => f.to_string(),
314 ExprValue::Literal(Literal::Boolean(b)) => b.to_string(),
315 ExprValue::Literal(Literal::Null) => "null".to_string(),
316 ExprValue::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
317 ExprValue::Interpolated(parts) => parts
318 .iter()
319 .map(|p| match p {
320 StringPart::Literal(s) => s.clone(),
321 StringPart::Expr(expr) => format!("${{{}}}", expr_to_string(expr)),
322 })
323 .collect(),
324 ExprValue::Object(_) => "[object]".to_string(),
325 ExprValue::Array(_) => "[array]".to_string(),
326 }
327 }
328}
329
330fn expr_to_string(expr: &Expression) -> String {
332 match expr {
333 Expression::Reference(r) => r.to_string(),
334 Expression::Literal(Literal::String(s)) => format!("\"{s}\""),
335 Expression::Literal(Literal::Integer(i)) => i.to_string(),
336 Expression::Literal(Literal::Float(f)) => f.to_string(),
337 Expression::Literal(Literal::Boolean(b)) => b.to_string(),
338 Expression::Literal(Literal::Null) => "null".to_string(),
339 Expression::Pipeline { source, stages } => {
340 let mut result = expr_to_string(source);
341 for stage in stages {
342 result.push_str(" | ");
343 result.push_str(&stage.function);
344 if !stage.args.is_empty() {
345 result.push('(');
346 let args: Vec<_> = stage.args.iter().map(expr_to_string).collect();
347 result.push_str(&args.join(", "));
348 result.push(')');
349 }
350 }
351 result
352 }
353 }
354}
355
356#[derive(Debug, Clone, PartialEq)]
358pub enum StringPart {
359 Literal(String),
361 Expr(Expression),
363}
364
365pub fn parse_expression(s: &str) -> Option<Expression> {
387 let s = s.trim();
388
389 let inner = s.strip_prefix("${").and_then(|s| s.strip_suffix('}'))?;
391 let inner = inner.trim();
392
393 if inner.is_empty() {
394 return None;
395 }
396
397 parse_pipeline(inner)
398}
399
400fn parse_pipeline(s: &str) -> Option<Expression> {
402 let s = s.trim();
403
404 let parts = split_on_pipes(s);
406
407 if parts.is_empty() {
408 return None;
409 }
410
411 let source = parse_source(parts.first()?.trim())?;
413
414 if parts.len() == 1 {
415 return Some(source);
417 }
418
419 let mut stages = Vec::new();
421 for part in parts.iter().skip(1) {
422 let stage = parse_pipe_stage(part.trim())?;
423 stages.push(stage);
424 }
425
426 Some(Expression::Pipeline {
427 source: Box::new(source),
428 stages,
429 })
430}
431
432fn split_on_pipes(s: &str) -> Vec<&str> {
434 let mut parts = Vec::new();
435 let mut start = 0;
436 let mut paren_depth: u32 = 0;
437 let bytes = s.as_bytes();
438
439 for (i, &b) in bytes.iter().enumerate() {
440 match b {
441 b'(' => paren_depth += 1,
442 b')' => paren_depth = paren_depth.saturating_sub(1),
443 b'|' if paren_depth == 0 => {
444 parts.push(&s[start..i]);
445 start = i + 1;
446 }
447 _ => {}
448 }
449 }
450
451 if start < s.len() {
453 parts.push(&s[start..]);
454 }
455
456 parts
457}
458
459fn parse_source(s: &str) -> Option<Expression> {
461 let s = s.trim();
462
463 if let Some(expr) = parse_function_call(s) {
465 return Some(expr);
466 }
467
468 if let Some(expr_ref) = parse_ref(s) {
470 return Some(Expression::Reference(expr_ref));
471 }
472
473 if let Some(lit) = parse_literal(s) {
475 return Some(Expression::Literal(lit));
476 }
477
478 None
479}
480
481fn parse_ref(s: &str) -> Option<Reference> {
483 let parts: Vec<&str> = s.splitn(3, '.').collect();
484
485 if parts.len() < 2 {
486 return None;
487 }
488
489 let namespace = parts[0];
490 if !is_valid_identifier(namespace) {
491 return None;
492 }
493
494 let ref_type: RefType = namespace.parse().ok()?;
496
497 let name = parts[1];
498 if !is_valid_identifier(name) {
499 return None;
500 }
501
502 let property = if parts.len() == 3 {
503 let prop = parts[2];
504 if !is_valid_identifier(prop) {
505 return None;
506 }
507 Some(prop.to_string())
508 } else {
509 None
510 };
511
512 Some(Reference {
513 ref_type,
514 name: name.to_string(),
515 property,
516 })
517}
518
519fn parse_literal(s: &str) -> Option<Literal> {
521 let s = s.trim();
522
523 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
525 let inner = &s[1..s.len() - 1];
526 return Some(Literal::String(inner.to_string()));
527 }
528
529 if let Ok(i) = s.parse::<i64>() {
531 return Some(Literal::Integer(i));
532 }
533
534 None
535}
536
537fn parse_function_call(s: &str) -> Option<Expression> {
539 let s = s.trim();
540
541 let paren_pos = s.find('(')?;
543 let name = &s[..paren_pos];
544
545 if !is_valid_identifier(name) {
546 return None;
547 }
548
549 if !s.ends_with(')') {
551 return None;
552 }
553
554 let args_str = &s[paren_pos + 1..s.len() - 1];
555
556 let args = parse_function_args(args_str)?;
558
559 Some(Expression::Pipeline {
562 source: Box::new(Expression::Literal(Literal::Integer(0))), stages: vec![PipeStage::with_args(name, args)],
564 })
565}
566
567fn parse_function_args(s: &str) -> Option<Vec<Expression>> {
569 let s = s.trim();
570
571 if s.is_empty() {
572 return Some(Vec::new());
573 }
574
575 let mut args = Vec::new();
576 let mut current_start = 0;
577 let mut paren_depth: u32 = 0;
578 let bytes = s.as_bytes();
579
580 for (i, &b) in bytes.iter().enumerate() {
581 match b {
582 b'(' => paren_depth += 1,
583 b')' => paren_depth = paren_depth.saturating_sub(1),
584 b',' if paren_depth == 0 => {
585 let arg = &s[current_start..i];
586 let expr = parse_pipeline(arg.trim())?;
587 args.push(expr);
588 current_start = i + 1;
589 }
590 _ => {}
591 }
592 }
593
594 if current_start < s.len() {
596 let arg = &s[current_start..];
597 let expr = parse_pipeline(arg.trim())?;
598 args.push(expr);
599 }
600
601 Some(args)
602}
603
604fn parse_pipe_stage(s: &str) -> Option<PipeStage> {
606 let s = s.trim();
607
608 if let Some(paren_pos) = s.find('(') {
610 let name = &s[..paren_pos];
611 if !is_valid_identifier(name) {
612 return None;
613 }
614
615 if !s.ends_with(')') {
616 return None;
617 }
618
619 let args_str = &s[paren_pos + 1..s.len() - 1];
620 let args = parse_function_args(args_str)?;
621
622 return Some(PipeStage::with_args(name, args));
623 }
624
625 if is_valid_identifier(s) {
627 return Some(PipeStage::new(s));
628 }
629
630 None
631}
632
633fn is_valid_identifier(s: &str) -> bool {
636 let mut chars = s.chars();
637 match chars.next() {
638 None => false,
639 Some(first) => {
640 (first.is_ascii_alphabetic() || first == '_')
641 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
642 }
643 }
644}
645
646pub fn find_expressions(s: &str) -> Vec<LocatedExpression> {
650 let mut results = Vec::new();
651 let bytes = s.as_bytes();
652 let mut i = 0;
653
654 while i < bytes.len() {
655 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
656 let mut depth = 1;
658 let mut j = i + 2;
659 while j < bytes.len() && depth > 0 {
660 match bytes[j] {
661 b'{' => depth += 1,
662 b'}' => depth -= 1,
663 _ => {}
664 }
665 j += 1;
666 }
667
668 if depth == 0 {
669 let expr_str = &s[i..j];
670 if let Some(expression) = parse_expression(expr_str) {
671 results.push(LocatedExpression {
672 expression,
673 range: i..j,
674 });
675 }
676 i = j;
677 continue;
678 }
679 }
680 i += 1;
681 }
682
683 results
684}
685
686pub fn parse_expr_value(json: &serde_json::Value) -> ExprValue {
710 match json {
711 serde_json::Value::String(s) => parse_string_to_expr_value(s),
712 serde_json::Value::Number(n) => {
713 if let Some(i) = n.as_i64() {
714 ExprValue::Literal(Literal::Integer(i))
715 } else if let Some(f) = n.as_f64() {
716 ExprValue::Literal(Literal::Float(f))
717 } else {
718 ExprValue::Literal(Literal::Integer(0))
720 }
721 }
722 serde_json::Value::Bool(b) => ExprValue::Literal(Literal::Boolean(*b)),
723 serde_json::Value::Null => ExprValue::Literal(Literal::Null),
724 serde_json::Value::Object(map) => {
725 let mut result = std::collections::HashMap::new();
726 for (k, v) in map {
727 result.insert(k.clone(), parse_expr_value(v));
728 }
729 ExprValue::Object(result)
730 }
731 serde_json::Value::Array(arr) => {
732 let result: Vec<_> = arr.iter().map(parse_expr_value).collect();
733 ExprValue::Array(result)
734 }
735 }
736}
737
738fn parse_string_to_expr_value(s: &str) -> ExprValue {
745 let trimmed = s.trim();
746
747 if trimmed.starts_with("${")
749 && trimmed.ends_with('}')
750 && !trimmed[2..trimmed.len() - 1].contains("${")
751 && let Some(expr) = parse_expression(trimmed)
752 {
753 return ExprValue::Expr(expr);
754 }
755
756 if s.contains("${") {
758 let parts = parse_interpolated_string(s);
759 if parts.len() == 1 {
760 if let StringPart::Literal(lit) = &parts[0] {
762 return ExprValue::Literal(Literal::String(lit.clone()));
763 }
764 }
765 return ExprValue::Interpolated(parts);
766 }
767
768 ExprValue::Literal(Literal::String(s.to_string()))
770}
771
772fn parse_interpolated_string(s: &str) -> Vec<StringPart> {
776 let mut parts = Vec::new();
777 let bytes = s.as_bytes();
778 let mut last_end = 0;
779 let mut i = 0;
780
781 while i < bytes.len() {
782 if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
783 if i > last_end {
785 parts.push(StringPart::Literal(s[last_end..i].to_string()));
786 }
787
788 let mut depth = 1;
790 let mut j = i + 2;
791 while j < bytes.len() && depth > 0 {
792 match bytes[j] {
793 b'{' => depth += 1,
794 b'}' => depth -= 1,
795 _ => {}
796 }
797 j += 1;
798 }
799
800 if depth == 0 {
801 let expr_str = &s[i..j];
802 if let Some(expr) = parse_expression(expr_str) {
803 parts.push(StringPart::Expr(expr));
804 } else {
805 parts.push(StringPart::Literal(expr_str.to_string()));
807 }
808 last_end = j;
809 i = j;
810 continue;
811 }
812 }
813 i += 1;
814 }
815
816 if last_end < s.len() {
818 parts.push(StringPart::Literal(s[last_end..].to_string()));
819 }
820
821 parts
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827
828 #[test]
829 fn parse_simple_reference() {
830 let expr = parse_expression("${artifact.ksr}").unwrap();
831 match expr {
832 Expression::Reference(r) => {
833 assert_eq!(r.ref_type, RefType::Artifact);
834 assert_eq!(r.name, "ksr");
835 assert!(r.property.is_none());
836 }
837 _ => panic!("Expected Reference"),
838 }
839 }
840
841 #[test]
842 fn parse_reference_with_property() {
843 let expr = parse_expression("${artifact.keypair.private}").unwrap();
844 match expr {
845 Expression::Reference(r) => {
846 assert_eq!(r.ref_type, RefType::Artifact);
847 assert_eq!(r.name, "keypair");
848 assert_eq!(r.property, Some("private".to_string()));
849 }
850 _ => panic!("Expected Reference"),
851 }
852 }
853
854 #[test]
855 fn parse_param_reference() {
856 let expr = parse_expression("${param.key_label}").unwrap();
857 match expr {
858 Expression::Reference(r) => {
859 assert_eq!(r.ref_type, RefType::Param);
860 assert_eq!(r.name, "key_label");
861 }
862 _ => panic!("Expected Reference"),
863 }
864 }
865
866 #[test]
867 fn parse_simple_pipeline() {
868 let expr = parse_expression("${artifact.ksr | sha256}").unwrap();
869 match expr {
870 Expression::Pipeline { source, stages } => {
871 assert!(matches!(*source, Expression::Reference(_)));
872 assert_eq!(stages.len(), 1);
873 assert_eq!(stages[0].function, "sha256");
874 assert!(stages[0].args.is_empty());
875 }
876 _ => panic!("Expected Pipeline"),
877 }
878 }
879
880 #[test]
881 fn parse_multi_stage_pipeline() {
882 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
883 match expr {
884 Expression::Pipeline { stages, .. } => {
885 assert_eq!(stages.len(), 2);
886 assert_eq!(stages[0].function, "sha256");
887 assert_eq!(stages[1].function, "hex");
888 }
889 _ => panic!("Expected Pipeline"),
890 }
891 }
892
893 #[test]
894 fn parse_pipeline_with_function_args() {
895 let expr = parse_expression("${artifact.data | substr(0, 16)}").unwrap();
896 match expr {
897 Expression::Pipeline { stages, .. } => {
898 assert_eq!(stages.len(), 1);
899 assert_eq!(stages[0].function, "substr");
900 assert_eq!(stages[0].args.len(), 2);
901 }
902 _ => panic!("Expected Pipeline"),
903 }
904 }
905
906 #[test]
907 fn parse_complex_pipeline() {
908 let expr =
909 parse_expression("${artifact.keypair.public | fingerprint | hex | upper}").unwrap();
910 match expr {
911 Expression::Pipeline { source, stages } => {
912 if let Expression::Reference(r) = *source {
913 assert_eq!(r.name, "keypair");
914 assert_eq!(r.property, Some("public".to_string()));
915 }
916 assert_eq!(stages.len(), 3);
917 assert_eq!(stages[0].function, "fingerprint");
918 assert_eq!(stages[1].function, "hex");
919 assert_eq!(stages[2].function, "upper");
920 }
921 _ => panic!("Expected Pipeline"),
922 }
923 }
924
925 #[test]
926 fn parse_invalid_expression() {
927 assert!(parse_expression("not an expression").is_none());
928 assert!(parse_expression("${").is_none());
929 assert!(parse_expression("${}").is_none());
930 assert!(parse_expression("${invalid}").is_none()); }
932
933 #[test]
934 fn find_expressions_in_string() {
935 let exprs = find_expressions("Hash: ${artifact.ksr | sha256 | hex}");
936 assert_eq!(exprs.len(), 1);
937 assert_eq!(exprs[0].range.start, 6);
938 }
939
940 #[test]
941 fn find_multiple_expressions() {
942 let exprs = find_expressions("${artifact.a | sha256 | hex}-${artifact.b | sha256 | hex}");
943 assert_eq!(exprs.len(), 2);
944 }
945
946 #[test]
947 fn value_type_names() {
948 assert_eq!(Value::Bytes(vec![]).type_name(), "bytes");
949 assert_eq!(Value::String(String::new()).type_name(), "string");
950 assert_eq!(Value::Integer(0).type_name(), "integer");
951 assert_eq!(Value::Boolean(true).type_name(), "boolean");
952 assert_eq!(Value::Null.type_name(), "null");
953 }
954
955 #[test]
956 fn value_as_bytes() {
957 let v = Value::Bytes(vec![1, 2, 3]);
958 assert_eq!(v.as_bytes(), Some(vec![1, 2, 3]));
959
960 let v = Value::String("hello".into());
961 assert_eq!(v.as_bytes(), Some(b"hello".to_vec()));
962
963 let v = Value::Integer(42);
964 assert!(v.as_bytes().is_none());
965 }
966
967 mod ceremony_examples {
968 use super::*;
969
970 #[test]
972 fn dnssec_hash_verification() {
973 let expr = parse_expression("${artifact.ksr | sha256 | hex | substr(0, 16)}").unwrap();
975 match expr {
976 Expression::Pipeline { source, stages } => {
977 if let Expression::Reference(r) = *source {
978 assert_eq!(r.ref_type, RefType::Artifact);
979 assert_eq!(r.name, "ksr");
980 } else {
981 panic!("Expected Reference source");
982 }
983 assert_eq!(stages.len(), 3);
985 assert_eq!(stages[0].function, "sha256");
986 assert_eq!(stages[1].function, "hex");
987 assert_eq!(stages[2].function, "substr");
988 assert_eq!(stages[2].args.len(), 2);
989 }
990 _ => panic!("Expected Pipeline"),
991 }
992 }
993
994 #[test]
995 fn dnssec_param_reference() {
996 let expr = parse_expression("${param.expected_ksr_hash}").unwrap();
998 match expr {
999 Expression::Reference(r) => {
1000 assert_eq!(r.ref_type, RefType::Param);
1001 assert_eq!(r.name, "expected_ksr_hash");
1002 }
1003 _ => panic!("Expected Reference"),
1004 }
1005 }
1006
1007 #[test]
1008 fn dnssec_oral_readback() {
1009 let expr = parse_expression("${artifact.ksr | sha256 | hex}").unwrap();
1011 match expr {
1012 Expression::Pipeline { stages, .. } => {
1013 assert_eq!(stages.len(), 2);
1014 assert_eq!(stages[0].function, "sha256");
1015 assert_eq!(stages[1].function, "hex");
1016 }
1017 _ => panic!("Expected Pipeline"),
1018 }
1019 }
1020
1021 #[test]
1023 fn hsm_artifact_reference() {
1024 let expr = parse_expression("${artifact.master_keypair}").unwrap();
1026 match expr {
1027 Expression::Reference(r) => {
1028 assert_eq!(r.ref_type, RefType::Artifact);
1029 assert_eq!(r.name, "master_keypair");
1030 assert!(r.property.is_none());
1031 }
1032 _ => panic!("Expected Reference"),
1033 }
1034 }
1035
1036 #[test]
1037 fn hsm_public_key_fingerprint() {
1038 let expr = parse_expression("${artifact.master_public_key | sha256 | hex}").unwrap();
1040 match expr {
1041 Expression::Pipeline { source, stages } => {
1042 if let Expression::Reference(r) = *source {
1043 assert_eq!(r.name, "master_public_key");
1044 }
1045 assert_eq!(stages.len(), 2);
1046 }
1047 _ => panic!("Expected Pipeline"),
1048 }
1049 }
1050
1051 #[test]
1053 fn crypto_artifact_with_property() {
1054 let expr = parse_expression("${artifact.keypair.private}").unwrap();
1056 match expr {
1057 Expression::Reference(r) => {
1058 assert_eq!(r.ref_type, RefType::Artifact);
1059 assert_eq!(r.name, "keypair");
1060 assert_eq!(r.property, Some("private".to_string()));
1061 }
1062 _ => panic!("Expected Reference"),
1063 }
1064 }
1065
1066 #[test]
1068 fn find_embedded_expressions() {
1069 let exprs = find_expressions("Verify serial number: ${param.hsm_serial}");
1071 assert_eq!(exprs.len(), 1);
1072 if let Expression::Reference(r) = &exprs[0].expression {
1073 assert_eq!(r.ref_type, RefType::Param);
1074 assert_eq!(r.name, "hsm_serial");
1075 }
1076 }
1077
1078 #[test]
1079 fn find_multiple_embedded_params() {
1080 let exprs = find_expressions("Key ${param.key_label} for HSM ${param.hsm_serial}");
1082 assert_eq!(exprs.len(), 2);
1083 }
1084
1085 #[test]
1087 fn role_reference_parses() {
1088 let expr = parse_expression("${role.ceremony_admin}").unwrap();
1089 match expr {
1090 Expression::Reference(r) => {
1091 assert_eq!(r.ref_type, RefType::Role);
1092 assert_eq!(r.name, "ceremony_admin");
1093 }
1094 _ => panic!("Expected Reference"),
1095 }
1096 }
1097 }
1098
1099 mod expr_value_tests {
1100 use super::*;
1101
1102 #[test]
1103 fn parse_literal_string() {
1104 let json = serde_json::json!("hello");
1105 let expr = parse_expr_value(&json);
1106 assert!(matches!(expr, ExprValue::Literal(Literal::String(s)) if s == "hello"));
1107 }
1108
1109 #[test]
1110 fn parse_literal_integer() {
1111 let json = serde_json::json!(42);
1112 let expr = parse_expr_value(&json);
1113 assert!(matches!(expr, ExprValue::Literal(Literal::Integer(42))));
1114 }
1115
1116 #[test]
1117 fn parse_literal_float() {
1118 let json = serde_json::json!(1.5);
1119 let expr = parse_expr_value(&json);
1120 assert!(
1121 matches!(expr, ExprValue::Literal(Literal::Float(f)) if (f - 1.5).abs() < 0.001)
1122 );
1123 }
1124
1125 #[test]
1126 fn parse_literal_boolean() {
1127 let json = serde_json::json!(true);
1128 let expr = parse_expr_value(&json);
1129 assert!(matches!(expr, ExprValue::Literal(Literal::Boolean(true))));
1130 }
1131
1132 #[test]
1133 fn parse_literal_null() {
1134 let json = serde_json::Value::Null;
1135 let expr = parse_expr_value(&json);
1136 assert!(matches!(expr, ExprValue::Literal(Literal::Null)));
1137 }
1138
1139 #[test]
1140 fn parse_single_expression() {
1141 let json = serde_json::json!("${artifact.ksr | sha256}");
1142 let expr = parse_expr_value(&json);
1143 match expr {
1144 ExprValue::Expr(Expression::Pipeline { source, stages }) => {
1145 assert!(matches!(*source, Expression::Reference(_)));
1146 assert_eq!(stages.len(), 1);
1147 assert_eq!(stages[0].function, "sha256");
1148 }
1149 _ => panic!("Expected Expr(Pipeline), got {expr:?}"),
1150 }
1151 }
1152
1153 #[test]
1154 fn parse_simple_reference() {
1155 let json = serde_json::json!("${param.name}");
1156 let expr = parse_expr_value(&json);
1157 match expr {
1158 ExprValue::Expr(Expression::Reference(r)) => {
1159 assert_eq!(r.ref_type, RefType::Param);
1160 assert_eq!(r.name, "name");
1161 }
1162 _ => panic!("Expected Expr(Reference), got {expr:?}"),
1163 }
1164 }
1165
1166 #[test]
1167 fn parse_interpolated_string() {
1168 let json = serde_json::json!("Hello, ${param.name}!");
1169 let expr = parse_expr_value(&json);
1170 match expr {
1171 ExprValue::Interpolated(parts) => {
1172 assert_eq!(parts.len(), 3);
1173 assert!(matches!(&parts[0], StringPart::Literal(s) if s == "Hello, "));
1174 assert!(matches!(
1175 &parts[1],
1176 StringPart::Expr(Expression::Reference(_))
1177 ));
1178 assert!(matches!(&parts[2], StringPart::Literal(s) if s == "!"));
1179 }
1180 _ => panic!("Expected Interpolated, got {expr:?}"),
1181 }
1182 }
1183
1184 #[test]
1185 fn parse_object_with_expressions() {
1186 let json = serde_json::json!({
1187 "actual": "${artifact.ksr | sha256 | hex}",
1188 "expected": "${param.expected_hash}",
1189 "static": "hello"
1190 });
1191 let expr = parse_expr_value(&json);
1192 match expr {
1193 ExprValue::Object(map) => {
1194 assert!(matches!(map.get("actual"), Some(ExprValue::Expr(_))));
1195 assert!(matches!(map.get("expected"), Some(ExprValue::Expr(_))));
1196 assert!(matches!(
1197 map.get("static"),
1198 Some(ExprValue::Literal(Literal::String(_)))
1199 ));
1200 }
1201 _ => panic!("Expected Object, got {expr:?}"),
1202 }
1203 }
1204
1205 #[test]
1206 fn parse_array_with_expressions() {
1207 let json = serde_json::json!(["${param.a}", "static", "${param.b}"]);
1208 let expr = parse_expr_value(&json);
1209 match expr {
1210 ExprValue::Array(arr) => {
1211 assert_eq!(arr.len(), 3);
1212 assert!(matches!(&arr[0], ExprValue::Expr(_)));
1213 assert!(matches!(&arr[1], ExprValue::Literal(Literal::String(_))));
1214 assert!(matches!(&arr[2], ExprValue::Expr(_)));
1215 }
1216 _ => panic!("Expected Array, got {expr:?}"),
1217 }
1218 }
1219
1220 #[test]
1221 fn parse_multiple_expressions_in_string() {
1222 let json = serde_json::json!("${param.a} and ${param.b}");
1223 let expr = parse_expr_value(&json);
1224 match expr {
1225 ExprValue::Interpolated(parts) => {
1226 assert_eq!(parts.len(), 3);
1227 assert!(matches!(&parts[0], StringPart::Expr(_)));
1228 assert!(matches!(&parts[1], StringPart::Literal(s) if s == " and "));
1229 assert!(matches!(&parts[2], StringPart::Expr(_)));
1230 }
1231 _ => panic!("Expected Interpolated, got {expr:?}"),
1232 }
1233 }
1234 }
1235}