1use indexmap::IndexSet;
23use jiff::Timestamp;
24use serde::{Deserialize, Serialize};
25
26use crate::fingerprint::{Fingerprint, FingerprintBuilder};
27use crate::names::{NameError, ParameterName};
28
29const TAG_INTEGER: u8 = 0x01;
34const TAG_DOUBLE: u8 = 0x02;
35const TAG_BOOLEAN: u8 = 0x03;
36const TAG_STRING: u8 = 0x04;
37const TAG_SELECTION: u8 = 0x05;
38
39const CANONICAL_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
46
47const fn canonicalise_f64(v: f64) -> f64 {
48 if v.is_nan() {
49 f64::from_bits(CANONICAL_NAN_BITS)
50 } else {
51 v
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum ValueKind {
67 Integer,
69 Double,
71 Boolean,
73 String,
75 Selection,
77}
78
79#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
89pub struct SelectionItem(String);
90
91impl SelectionItem {
92 pub fn new(candidate: impl Into<String>) -> Result<Self, NameError> {
94 let s = candidate.into();
95 if s.is_empty() {
96 return Err(NameError::Empty);
97 }
98 for (offset, ch) in s.char_indices() {
99 if ch.is_control() {
100 return Err(NameError::InvalidChar { ch, offset });
101 }
102 }
103 Ok(Self(s))
104 }
105
106 #[must_use]
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111
112 #[must_use]
114 pub fn into_inner(self) -> String {
115 self.0
116 }
117}
118
119impl std::fmt::Display for SelectionItem {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 f.write_str(&self.0)
122 }
123}
124
125impl std::fmt::Debug for SelectionItem {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(f, "SelectionItem({:?})", self.0)
128 }
129}
130
131impl AsRef<str> for SelectionItem {
132 fn as_ref(&self) -> &str {
133 &self.0
134 }
135}
136
137impl Serialize for SelectionItem {
138 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
139 s.serialize_str(&self.0)
140 }
141}
142
143impl<'de> Deserialize<'de> for SelectionItem {
144 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
145 let s = String::deserialize(deserializer)?;
146 Self::new(s).map_err(serde::de::Error::custom)
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum BoundaryKind {
158 Min,
160 Max,
162 First,
164 Last,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(tag = "kind", rename_all = "snake_case")]
175pub enum GeneratorInfo {
176 Explicit,
178 Default,
180 Boundary {
182 which: BoundaryKind,
184 },
185 Random {
187 seed: Option<u64>,
189 },
190 Derived {
192 expression: String,
194 },
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct Provenance {
208 pub parameter: ParameterName,
210 pub generated_at: Timestamp,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub generator: Option<GeneratorInfo>,
215 pub fingerprint: Fingerprint,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct IntegerValue {
226 pub value: i64,
228 pub provenance: Provenance,
230}
231
232impl IntegerValue {
233 #[must_use]
235 pub fn new(name: ParameterName, value: i64, generator: Option<GeneratorInfo>) -> Self {
236 Self::new_at(name, value, generator, Timestamp::now())
237 }
238
239 #[must_use]
241 pub fn new_at(
242 name: ParameterName,
243 value: i64,
244 generator: Option<GeneratorInfo>,
245 now: Timestamp,
246 ) -> Self {
247 let fingerprint = Self::fingerprint_of(&name, value);
248 Self {
249 value,
250 provenance: Provenance {
251 parameter: name,
252 generated_at: now,
253 generator,
254 fingerprint,
255 },
256 }
257 }
258
259 #[must_use]
261 pub fn fingerprint_of(name: &ParameterName, value: i64) -> Fingerprint {
262 FingerprintBuilder::new()
263 .byte(TAG_INTEGER)
264 .update(name.as_str().as_bytes())
265 .byte(0x00)
266 .i64_le(value)
267 .finish()
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
273pub struct DoubleValue {
274 pub value: f64,
276 pub provenance: Provenance,
278}
279
280impl DoubleValue {
281 #[must_use]
283 pub fn new(name: ParameterName, value: f64, generator: Option<GeneratorInfo>) -> Self {
284 Self::new_at(name, value, generator, Timestamp::now())
285 }
286
287 #[must_use]
289 pub fn new_at(
290 name: ParameterName,
291 value: f64,
292 generator: Option<GeneratorInfo>,
293 now: Timestamp,
294 ) -> Self {
295 let fingerprint = Self::fingerprint_of(&name, value);
296 Self {
297 value: canonicalise_f64(value),
298 provenance: Provenance {
299 parameter: name,
300 generated_at: now,
301 generator,
302 fingerprint,
303 },
304 }
305 }
306
307 #[must_use]
310 pub fn fingerprint_of(name: &ParameterName, value: f64) -> Fingerprint {
311 let canonical = canonicalise_f64(value);
312 FingerprintBuilder::new()
313 .byte(TAG_DOUBLE)
314 .update(name.as_str().as_bytes())
315 .byte(0x00)
316 .update(&canonical.to_le_bytes())
317 .finish()
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct BooleanValue {
324 pub value: bool,
326 pub provenance: Provenance,
328}
329
330impl BooleanValue {
331 #[must_use]
333 pub fn new(name: ParameterName, value: bool, generator: Option<GeneratorInfo>) -> Self {
334 Self::new_at(name, value, generator, Timestamp::now())
335 }
336
337 #[must_use]
339 pub fn new_at(
340 name: ParameterName,
341 value: bool,
342 generator: Option<GeneratorInfo>,
343 now: Timestamp,
344 ) -> Self {
345 let fingerprint = Self::fingerprint_of(&name, value);
346 Self {
347 value,
348 provenance: Provenance {
349 parameter: name,
350 generated_at: now,
351 generator,
352 fingerprint,
353 },
354 }
355 }
356
357 #[must_use]
359 pub fn fingerprint_of(name: &ParameterName, value: bool) -> Fingerprint {
360 FingerprintBuilder::new()
361 .byte(TAG_BOOLEAN)
362 .update(name.as_str().as_bytes())
363 .byte(0x00)
364 .byte(u8::from(value))
365 .finish()
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371pub struct StringValue {
372 pub value: String,
374 pub provenance: Provenance,
376}
377
378impl StringValue {
379 #[must_use]
381 pub fn new(name: ParameterName, value: impl Into<String>, generator: Option<GeneratorInfo>) -> Self {
382 Self::new_at(name, value, generator, Timestamp::now())
383 }
384
385 #[must_use]
387 pub fn new_at(
388 name: ParameterName,
389 value: impl Into<String>,
390 generator: Option<GeneratorInfo>,
391 now: Timestamp,
392 ) -> Self {
393 let value = value.into();
394 let fingerprint = Self::fingerprint_of(&name, &value);
395 Self {
396 value,
397 provenance: Provenance {
398 parameter: name,
399 generated_at: now,
400 generator,
401 fingerprint,
402 },
403 }
404 }
405
406 #[must_use]
409 pub fn fingerprint_of(name: &ParameterName, value: &str) -> Fingerprint {
410 FingerprintBuilder::new()
411 .byte(TAG_STRING)
412 .update(name.as_str().as_bytes())
413 .byte(0x00)
414 .length_prefixed_str(value)
415 .finish()
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
426pub struct SelectionValue {
427 pub items: IndexSet<SelectionItem>,
429 pub provenance: Provenance,
431}
432
433impl SelectionValue {
434 #[must_use]
436 pub fn new(
437 name: ParameterName,
438 items: IndexSet<SelectionItem>,
439 generator: Option<GeneratorInfo>,
440 ) -> Self {
441 Self::new_at(name, items, generator, Timestamp::now())
442 }
443
444 #[must_use]
446 pub fn new_at(
447 name: ParameterName,
448 items: IndexSet<SelectionItem>,
449 generator: Option<GeneratorInfo>,
450 now: Timestamp,
451 ) -> Self {
452 let fingerprint = Self::fingerprint_of(&name, &items);
453 Self {
454 items,
455 provenance: Provenance {
456 parameter: name,
457 generated_at: now,
458 generator,
459 fingerprint,
460 },
461 }
462 }
463
464 #[must_use]
467 pub fn fingerprint_of(name: &ParameterName, items: &IndexSet<SelectionItem>) -> Fingerprint {
468 let mut sorted: Vec<&str> = items.iter().map(SelectionItem::as_str).collect();
469 sorted.sort_unstable();
470 let len = u32::try_from(sorted.len()).expect("selection size fits in u32");
471 let mut builder = FingerprintBuilder::new()
472 .byte(TAG_SELECTION)
473 .update(name.as_str().as_bytes())
474 .byte(0x00)
475 .u32_le(len);
476 for item in sorted {
477 builder = builder.length_prefixed_str(item);
478 }
479 builder.finish()
480 }
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496#[serde(tag = "kind", rename_all = "snake_case")]
497pub enum Value {
498 Integer(IntegerValue),
500 Double(DoubleValue),
502 Boolean(BooleanValue),
504 String(StringValue),
506 Selection(SelectionValue),
508}
509
510impl Value {
511 #[must_use]
513 pub const fn kind(&self) -> ValueKind {
514 match self {
515 Self::Integer(_) => ValueKind::Integer,
516 Self::Double(_) => ValueKind::Double,
517 Self::Boolean(_) => ValueKind::Boolean,
518 Self::String(_) => ValueKind::String,
519 Self::Selection(_) => ValueKind::Selection,
520 }
521 }
522
523 #[must_use]
525 pub const fn provenance(&self) -> &Provenance {
526 match self {
527 Self::Integer(v) => &v.provenance,
528 Self::Double(v) => &v.provenance,
529 Self::Boolean(v) => &v.provenance,
530 Self::String(v) => &v.provenance,
531 Self::Selection(v) => &v.provenance,
532 }
533 }
534
535 #[must_use]
537 pub const fn parameter(&self) -> &ParameterName {
538 &self.provenance().parameter
539 }
540
541 #[must_use]
543 pub const fn fingerprint(&self) -> &Fingerprint {
544 &self.provenance().fingerprint
545 }
546
547 #[must_use]
549 pub const fn as_integer(&self) -> Option<i64> {
550 if let Self::Integer(v) = self {
551 Some(v.value)
552 } else {
553 None
554 }
555 }
556
557 #[must_use]
559 pub const fn as_double(&self) -> Option<f64> {
560 if let Self::Double(v) = self {
561 Some(v.value)
562 } else {
563 None
564 }
565 }
566
567 #[must_use]
569 pub const fn as_boolean(&self) -> Option<bool> {
570 if let Self::Boolean(v) = self {
571 Some(v.value)
572 } else {
573 None
574 }
575 }
576
577 #[must_use]
579 pub fn as_string(&self) -> Option<&str> {
580 if let Self::String(v) = self {
581 Some(&v.value)
582 } else {
583 None
584 }
585 }
586
587 #[must_use]
589 pub const fn as_selection(&self) -> Option<&IndexSet<SelectionItem>> {
590 if let Self::Selection(v) = self {
591 Some(&v.items)
592 } else {
593 None
594 }
595 }
596
597 #[must_use]
599 pub fn integer(name: ParameterName, value: i64, generator: Option<GeneratorInfo>) -> Self {
600 Self::Integer(IntegerValue::new(name, value, generator))
601 }
602
603 #[must_use]
605 pub fn double(name: ParameterName, value: f64, generator: Option<GeneratorInfo>) -> Self {
606 Self::Double(DoubleValue::new(name, value, generator))
607 }
608
609 #[must_use]
611 pub fn boolean(name: ParameterName, value: bool, generator: Option<GeneratorInfo>) -> Self {
612 Self::Boolean(BooleanValue::new(name, value, generator))
613 }
614
615 #[must_use]
617 pub fn string(name: ParameterName, value: impl Into<String>, generator: Option<GeneratorInfo>) -> Self {
618 Self::String(StringValue::new(name, value, generator))
619 }
620
621 #[must_use]
623 pub fn selection(
624 name: ParameterName,
625 items: IndexSet<SelectionItem>,
626 generator: Option<GeneratorInfo>,
627 ) -> Self {
628 Self::Selection(SelectionValue::new(name, items, generator))
629 }
630
631 #[must_use]
637 pub fn verify_fingerprint(&self) -> bool {
638 let expected = match self {
639 Self::Integer(v) => IntegerValue::fingerprint_of(&v.provenance.parameter, v.value),
640 Self::Double(v) => DoubleValue::fingerprint_of(&v.provenance.parameter, v.value),
641 Self::Boolean(v) => BooleanValue::fingerprint_of(&v.provenance.parameter, v.value),
642 Self::String(v) => StringValue::fingerprint_of(&v.provenance.parameter, &v.value),
643 Self::Selection(v) => SelectionValue::fingerprint_of(&v.provenance.parameter, &v.items),
644 };
645 &expected == self.fingerprint()
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 fn pname(s: &str) -> ParameterName {
654 ParameterName::new(s).unwrap()
655 }
656
657 fn epoch() -> Timestamp {
658 Timestamp::from_second(0).unwrap()
659 }
660
661 #[test]
664 fn integer_value_roundtrips_accessors() {
665 let v = Value::integer(pname("threads"), 42, None);
666 assert_eq!(v.kind(), ValueKind::Integer);
667 assert_eq!(v.as_integer(), Some(42));
668 assert_eq!(v.as_double(), None);
669 assert_eq!(v.parameter().as_str(), "threads");
670 }
671
672 #[test]
673 fn boolean_double_string_accessors() {
674 let b = Value::boolean(pname("on"), true, None);
675 let d = Value::double(pname("ratio"), 1.5, None);
676 let s = Value::string(pname("label"), "hi", None);
677 assert_eq!(b.as_boolean(), Some(true));
678 assert_eq!(d.as_double(), Some(1.5));
679 assert_eq!(s.as_string(), Some("hi"));
680 }
681
682 #[test]
683 fn selection_value_preserves_authored_order() {
684 let mut items = IndexSet::new();
685 items.insert(SelectionItem::new("gamma").unwrap());
686 items.insert(SelectionItem::new("alpha").unwrap());
687 items.insert(SelectionItem::new("beta").unwrap());
688 let v = Value::selection(pname("picks"), items, None);
689 let got: Vec<&str> = v.as_selection().unwrap().iter().map(SelectionItem::as_str).collect();
690 assert_eq!(got, vec!["gamma", "alpha", "beta"]);
691 }
692
693 #[test]
696 fn integer_fingerprint_is_deterministic() {
697 let a = IntegerValue::fingerprint_of(&pname("x"), 42);
698 let b = IntegerValue::fingerprint_of(&pname("x"), 42);
699 assert_eq!(a, b);
700 }
701
702 #[test]
703 fn integer_fingerprint_distinguishes_name_and_value() {
704 let base = IntegerValue::fingerprint_of(&pname("x"), 42);
705 assert_ne!(base, IntegerValue::fingerprint_of(&pname("y"), 42));
706 assert_ne!(base, IntegerValue::fingerprint_of(&pname("x"), 43));
707 }
708
709 #[test]
710 fn integer_fingerprint_matches_hand_built_bytes() {
711 let name = pname("threads");
714 let got = IntegerValue::fingerprint_of(&name, 42);
715 let mut bytes = vec![TAG_INTEGER];
716 bytes.extend_from_slice(name.as_str().as_bytes());
717 bytes.push(0x00);
718 bytes.extend_from_slice(&42i64.to_le_bytes());
719 let expected = Fingerprint::of(&bytes);
720 assert_eq!(got, expected);
721 }
722
723 #[test]
724 fn double_nan_normalises() {
725 let nan_a = f64::NAN;
726 let nan_b = f64::from_bits(f64::NAN.to_bits() ^ 1);
728 assert!(nan_a.is_nan() && nan_b.is_nan());
729 assert_ne!(nan_a.to_bits(), nan_b.to_bits());
730
731 let fa = DoubleValue::fingerprint_of(&pname("r"), nan_a);
732 let fb = DoubleValue::fingerprint_of(&pname("r"), nan_b);
733 assert_eq!(fa, fb, "canonical NaN must collapse all payloads");
734 }
735
736 #[test]
737 fn double_value_stores_canonical_nan() {
738 let v = DoubleValue::new_at(
739 pname("r"),
740 f64::from_bits(f64::NAN.to_bits() ^ 1),
741 None,
742 epoch(),
743 );
744 assert_eq!(v.value.to_bits(), CANONICAL_NAN_BITS);
745 }
746
747 #[test]
748 fn boolean_fingerprint_distinguishes_true_and_false() {
749 let t = BooleanValue::fingerprint_of(&pname("b"), true);
750 let f = BooleanValue::fingerprint_of(&pname("b"), false);
751 assert_ne!(t, f);
752 }
753
754 #[test]
755 fn string_fingerprint_distinguishes_content() {
756 let a = StringValue::fingerprint_of(&pname("s"), "hello");
757 let b = StringValue::fingerprint_of(&pname("s"), "hellp");
758 assert_ne!(a, b);
759 }
760
761 #[test]
762 fn selection_fingerprint_is_order_independent() {
763 let mut one = IndexSet::new();
764 one.insert(SelectionItem::new("alpha").unwrap());
765 one.insert(SelectionItem::new("beta").unwrap());
766
767 let mut two = IndexSet::new();
768 two.insert(SelectionItem::new("beta").unwrap());
769 two.insert(SelectionItem::new("alpha").unwrap());
770
771 let fa = SelectionValue::fingerprint_of(&pname("s"), &one);
772 let fb = SelectionValue::fingerprint_of(&pname("s"), &two);
773 assert_eq!(fa, fb);
774 }
775
776 #[test]
777 fn selection_fingerprint_distinguishes_contents() {
778 let mut one = IndexSet::new();
779 one.insert(SelectionItem::new("alpha").unwrap());
780 let mut two = IndexSet::new();
781 two.insert(SelectionItem::new("beta").unwrap());
782 assert_ne!(
783 SelectionValue::fingerprint_of(&pname("s"), &one),
784 SelectionValue::fingerprint_of(&pname("s"), &two),
785 );
786 }
787
788 #[test]
789 fn kind_tags_are_disjoint() {
790 let name = pname("x");
791 let i = IntegerValue::fingerprint_of(&name, 0);
792 let d = DoubleValue::fingerprint_of(&name, 0.0);
793 let b = BooleanValue::fingerprint_of(&name, false);
794 let s = StringValue::fingerprint_of(&name, "");
795 let sel = SelectionValue::fingerprint_of(&name, &IndexSet::new());
796 let all = [i, d, b, s, sel];
797 for (ai, a) in all.iter().enumerate() {
798 for (bi, b) in all.iter().enumerate() {
799 if ai != bi {
800 assert_ne!(a, b, "kinds {ai} and {bi} collided");
801 }
802 }
803 }
804 }
805
806 #[test]
809 fn verify_fingerprint_passes_for_constructed_value() {
810 let v = Value::integer(pname("x"), 7, None);
811 assert!(v.verify_fingerprint());
812 }
813
814 #[test]
815 fn verify_fingerprint_detects_mutation() {
816 let mut iv = IntegerValue::new(pname("x"), 7, None);
817 iv.value = 8; let v = Value::Integer(iv);
819 assert!(!v.verify_fingerprint());
820 }
821
822 #[test]
825 fn generator_is_preserved() {
826 let v = Value::integer(
827 pname("x"),
828 7,
829 Some(GeneratorInfo::Random { seed: Some(42) }),
830 );
831 match v.provenance().generator.as_ref().unwrap() {
832 GeneratorInfo::Random { seed } => assert_eq!(*seed, Some(42)),
833 other => panic!("wrong generator: {other:?}"),
834 }
835 }
836
837 #[test]
838 fn new_at_uses_supplied_timestamp() {
839 let ts = Timestamp::from_second(1_700_000_000).unwrap();
840 let v = IntegerValue::new_at(pname("x"), 7, None, ts);
841 assert_eq!(v.provenance.generated_at, ts);
842 }
843
844 #[test]
847 fn serde_roundtrip_integer_value() {
848 let ts = Timestamp::from_second(1_700_000_000).unwrap();
849 let v = Value::Integer(IntegerValue::new_at(pname("threads"), 42, None, ts));
850 let json = serde_json::to_string(&v).unwrap();
851 let back: Value = serde_json::from_str(&json).unwrap();
852 assert_eq!(v, back);
853 assert!(back.verify_fingerprint());
854 }
855
856 #[test]
857 fn serde_roundtrip_selection_value() {
858 let ts = Timestamp::from_second(0).unwrap();
859 let mut items = IndexSet::new();
860 items.insert(SelectionItem::new("alpha").unwrap());
861 items.insert(SelectionItem::new("beta").unwrap());
862 let v = Value::Selection(SelectionValue::new_at(pname("picks"), items, None, ts));
863 let json = serde_json::to_string(&v).unwrap();
864 let back: Value = serde_json::from_str(&json).unwrap();
865 assert_eq!(v, back);
866 assert!(back.verify_fingerprint());
867 }
868
869 #[test]
870 fn selection_item_rejects_empty_and_control_chars() {
871 assert!(SelectionItem::new("").is_err());
872 assert!(SelectionItem::new("hello\nworld").is_err());
873 assert!(SelectionItem::new("hello").is_ok());
874 }
875
876 #[test]
877 fn validation_kind_serialises_as_snake_case() {
878 let s = serde_json::to_string(&ValueKind::Selection).unwrap();
879 assert_eq!(s, "\"selection\"");
880 }
881}