1use serde::de;
7use serde::{Deserialize, Serialize};
8use std::borrow::Borrow;
9use std::fmt;
10use std::ops::Deref;
11use std::str::FromStr;
12use thiserror::Error;
13
14macro_rules! string_newtype {
15 ($(#[$meta:meta])* $name:ident) => {
16 $(#[$meta])*
17 #[derive(
18 Debug,
19 Clone,
20 PartialEq,
21 Eq,
22 Hash,
23 PartialOrd,
24 Ord,
25 Serialize,
26 Deserialize,
27 )]
28 #[serde(transparent)]
29 pub struct $name(String);
30
31 impl $name {
32 #[must_use]
34 pub fn new(value: impl Into<String>) -> Self {
35 Self(value.into())
36 }
37
38 #[must_use]
40 pub fn as_str(&self) -> &str {
41 &self.0
42 }
43 }
44
45 impl Deref for $name {
46 type Target = str;
47
48 fn deref(&self) -> &Self::Target {
49 self.as_str()
50 }
51 }
52
53 impl AsRef<str> for $name {
54 fn as_ref(&self) -> &str {
55 self.as_str()
56 }
57 }
58
59 impl Borrow<str> for $name {
60 fn borrow(&self) -> &str {
61 self.as_str()
62 }
63 }
64
65 impl fmt::Display for $name {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(self.as_str())
68 }
69 }
70
71 impl From<&str> for $name {
72 fn from(value: &str) -> Self {
73 Self::new(value)
74 }
75 }
76
77 impl From<String> for $name {
78 fn from(value: String) -> Self {
79 Self::new(value)
80 }
81 }
82
83 impl From<&$name> for $name {
84 fn from(value: &$name) -> Self {
85 value.clone()
86 }
87 }
88
89 impl From<$name> for String {
90 fn from(value: $name) -> Self {
91 value.0
92 }
93 }
94
95 impl From<&$name> for String {
96 fn from(value: &$name) -> Self {
97 value.as_str().to_string()
98 }
99 }
100
101 impl PartialEq<&str> for $name {
102 fn eq(&self, other: &&str) -> bool {
103 self.as_str() == *other
104 }
105 }
106
107 impl PartialEq<str> for $name {
108 fn eq(&self, other: &str) -> bool {
109 self.as_str() == other
110 }
111 }
112
113 impl PartialEq<$name> for &str {
114 fn eq(&self, other: &$name) -> bool {
115 *self == other.as_str()
116 }
117 }
118
119 impl PartialEq<$name> for str {
120 fn eq(&self, other: &$name) -> bool {
121 self == other.as_str()
122 }
123 }
124
125 impl PartialEq<String> for $name {
126 fn eq(&self, other: &String) -> bool {
127 self.as_str() == other.as_str()
128 }
129 }
130
131 impl PartialEq<$name> for String {
132 fn eq(&self, other: &$name) -> bool {
133 self.as_str() == other.as_str()
134 }
135 }
136
137 impl PartialEq<&$name> for $name {
138 fn eq(&self, other: &&$name) -> bool {
139 self == *other
140 }
141 }
142 };
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct UnitIntervalError;
148
149impl fmt::Display for UnitIntervalError {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.write_str("value must be finite and in the inclusive range 0.0..=1.0")
152 }
153}
154
155impl std::error::Error for UnitIntervalError {}
156
157#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
161#[serde(transparent)]
162pub struct UnitInterval(f64);
163
164impl UnitInterval {
165 pub const ZERO: Self = Self(0.0);
167 pub const ONE: Self = Self(1.0);
169
170 pub fn new(value: f64) -> Result<Self, UnitIntervalError> {
174 if value.is_finite() && (0.0..=1.0).contains(&value) {
175 Ok(Self(value))
176 } else {
177 Err(UnitIntervalError)
178 }
179 }
180
181 #[must_use]
185 pub fn clamped(value: f64) -> Self {
186 if value.is_finite() {
187 Self(value.clamp(0.0, 1.0))
188 } else {
189 Self::ZERO
190 }
191 }
192
193 #[must_use]
195 pub fn as_f64(self) -> f64 {
196 self.0
197 }
198
199 #[must_use]
201 pub fn saturating_add(self, delta: f64) -> Self {
202 Self::clamped(self.0 + delta)
203 }
204
205 #[must_use]
207 pub fn scale_by(self, factor: Self) -> Self {
208 Self(self.0 * factor.0)
209 }
210
211 #[must_use]
213 pub fn to_basis_points(self) -> u16 {
214 (self.0 * 10_000.0).round() as u16
215 }
216}
217
218impl Default for UnitInterval {
219 fn default() -> Self {
220 Self::ZERO
221 }
222}
223
224impl TryFrom<f64> for UnitInterval {
225 type Error = UnitIntervalError;
226
227 fn try_from(value: f64) -> Result<Self, Self::Error> {
228 Self::new(value)
229 }
230}
231
232impl From<UnitInterval> for f64 {
233 fn from(value: UnitInterval) -> Self {
234 value.as_f64()
235 }
236}
237
238impl<'de> Deserialize<'de> for UnitInterval {
239 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240 where
241 D: serde::Deserializer<'de>,
242 {
243 let value = f64::deserialize(deserializer)?;
244 Self::new(value).map_err(de::Error::custom)
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct BasisPointsError;
251
252impl fmt::Display for BasisPointsError {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 f.write_str("basis points must be in the inclusive range 0..=10000")
255 }
256}
257
258impl std::error::Error for BasisPointsError {}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
262#[serde(transparent)]
263pub struct BasisPoints(u16);
264
265impl BasisPoints {
266 pub const ZERO: Self = Self(0);
268 pub const FULL: Self = Self(10_000);
270
271 pub fn new(value: u16) -> Result<Self, BasisPointsError> {
273 if value <= 10_000 {
274 Ok(Self(value))
275 } else {
276 Err(BasisPointsError)
277 }
278 }
279
280 #[must_use]
282 pub fn clamped(value: u16) -> Self {
283 Self(value.min(10_000))
284 }
285
286 #[must_use]
288 pub fn get(self) -> u16 {
289 self.0
290 }
291
292 #[must_use]
294 pub fn as_unit_interval(self) -> UnitInterval {
295 UnitInterval::clamped(f64::from(self.0) / 10_000.0)
296 }
297}
298
299impl Default for BasisPoints {
300 fn default() -> Self {
301 Self::ZERO
302 }
303}
304
305impl TryFrom<u16> for BasisPoints {
306 type Error = BasisPointsError;
307
308 fn try_from(value: u16) -> Result<Self, Self::Error> {
309 Self::new(value)
310 }
311}
312
313impl From<BasisPoints> for u16 {
314 fn from(value: BasisPoints) -> Self {
315 value.get()
316 }
317}
318
319impl<'de> Deserialize<'de> for BasisPoints {
320 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
321 where
322 D: serde::Deserializer<'de>,
323 {
324 let value = u16::deserialize(deserializer)?;
325 Self::new(value).map_err(de::Error::custom)
326 }
327}
328
329string_newtype!(
330 FactId
332);
333string_newtype!(
334 ProposalId
336);
337string_newtype!(
338 ObservationId
340);
341string_newtype!(
342 ApprovalId
344);
345string_newtype!(
346 ArtifactId
348);
349string_newtype!(
350 GateId
352);
353string_newtype!(
354 ActorId
356);
357string_newtype!(
358 ValidationCheckId
360);
361string_newtype!(
362 TraceId
364);
365string_newtype!(
366 SpanId
368);
369string_newtype!(
370 TraceSystemId
372);
373string_newtype!(
374 TraceReference
376);
377string_newtype!(
378 PrincipalId
380);
381string_newtype!(
382 EventId
384);
385string_newtype!(
386 TenantId
388);
389string_newtype!(
390 CorrelationId
392);
393string_newtype!(
394 ChainId
396);
397string_newtype!(
398 TraceLinkId
400);
401string_newtype!(
402 BackendId
404);
405string_newtype!(
406 PackId
408);
409string_newtype!(
410 TruthId
412);
413string_newtype!(
414 PolicyId
416);
417string_newtype!(
418 ApprovalPointId
420);
421string_newtype!(
422 VoteId
424);
425string_newtype!(
426 VoteTopicId
428);
429string_newtype!(
430 DisagreementId
432);
433string_newtype!(
434 CriterionId
436);
437string_newtype!(
438 ConstraintName
440);
441string_newtype!(
442 ConstraintValue
444);
445string_newtype!(
446 DomainId
448);
449string_newtype!(
450 PolicyVersionId
452);
453string_newtype!(
454 ResourceId
456);
457string_newtype!(
458 ResourceKind
460);
461
462#[derive(Debug, Clone, PartialEq, Eq, Error)]
464pub enum SubjectRefError {
465 #[error("subject scheme is empty")]
467 EmptyScheme,
468 #[error("subject kind is empty")]
470 EmptyKind,
471 #[error("subject id is empty")]
473 EmptyId,
474 #[error("subject scheme contains invalid character {0:?}; allow [a-z][a-z0-9-]*")]
476 InvalidSchemeChar(char),
477 #[error("subject kind contains invalid character {0:?}; allow [a-z][a-z0-9-]*")]
479 InvalidKindChar(char),
480 #[error("subject id contains invalid character {0:?}; disallow '/' and whitespace")]
482 InvalidIdChar(char),
483 #[error("could not parse subject ref from {0:?}; expected scheme://kind/id")]
485 Malformed(String),
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
496pub struct SubjectRef {
497 scheme: String,
498 kind: String,
499 id: String,
500}
501
502impl SubjectRef {
503 pub fn new(
509 scheme: impl Into<String>,
510 kind: impl Into<String>,
511 id: impl Into<String>,
512 ) -> Result<Self, SubjectRefError> {
513 let scheme = scheme.into().to_ascii_lowercase();
514 let kind = kind.into().to_ascii_lowercase();
515 let id = id.into();
516
517 validate_subject_label(&scheme, SubjectPart::Scheme)?;
518 validate_subject_label(&kind, SubjectPart::Kind)?;
519 validate_subject_id(&id)?;
520
521 Ok(Self { scheme, kind, id })
522 }
523
524 pub fn parse(value: &str) -> Result<Self, SubjectRefError> {
526 value.parse()
527 }
528
529 #[must_use]
531 pub fn scheme(&self) -> &str {
532 &self.scheme
533 }
534
535 #[must_use]
537 pub fn kind(&self) -> &str {
538 &self.kind
539 }
540
541 #[must_use]
543 pub fn id(&self) -> &str {
544 &self.id
545 }
546
547 #[must_use]
549 pub fn to_uri(&self) -> String {
550 format!("{}://{}/{}", self.scheme, self.kind, self.id)
551 }
552}
553
554impl fmt::Display for SubjectRef {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 f.write_str(&self.to_uri())
557 }
558}
559
560impl FromStr for SubjectRef {
561 type Err = SubjectRefError;
562
563 fn from_str(value: &str) -> Result<Self, Self::Err> {
564 let Some((scheme, rest)) = value.split_once("://") else {
565 return Err(SubjectRefError::Malformed(value.to_string()));
566 };
567 let Some((kind, id)) = rest.split_once('/') else {
568 return Err(SubjectRefError::Malformed(value.to_string()));
569 };
570 Self::new(scheme, kind, id)
571 }
572}
573
574impl Serialize for SubjectRef {
575 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
576 where
577 S: serde::Serializer,
578 {
579 serializer.serialize_str(&self.to_uri())
580 }
581}
582
583impl<'de> Deserialize<'de> for SubjectRef {
584 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
585 where
586 D: serde::Deserializer<'de>,
587 {
588 let value = String::deserialize(deserializer)?;
589 Self::parse(&value).map_err(de::Error::custom)
590 }
591}
592
593enum SubjectPart {
594 Scheme,
595 Kind,
596}
597
598fn validate_subject_label(value: &str, part: SubjectPart) -> Result<(), SubjectRefError> {
599 if value.is_empty() {
600 return match part {
601 SubjectPart::Scheme => Err(SubjectRefError::EmptyScheme),
602 SubjectPart::Kind => Err(SubjectRefError::EmptyKind),
603 };
604 }
605
606 for (index, ch) in value.chars().enumerate() {
607 let valid = if index == 0 {
608 ch.is_ascii_lowercase()
609 } else {
610 ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-'
611 };
612 if !valid {
613 return match part {
614 SubjectPart::Scheme => Err(SubjectRefError::InvalidSchemeChar(ch)),
615 SubjectPart::Kind => Err(SubjectRefError::InvalidKindChar(ch)),
616 };
617 }
618 }
619
620 Ok(())
621}
622
623fn validate_subject_id(value: &str) -> Result<(), SubjectRefError> {
624 if value.is_empty() {
625 return Err(SubjectRefError::EmptyId);
626 }
627
628 for ch in value.chars() {
629 if ch == '/' || ch.is_whitespace() {
630 return Err(SubjectRefError::InvalidIdChar(ch));
631 }
632 }
633
634 Ok(())
635}
636
637#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
639#[serde(transparent)]
640pub struct ContentHash(#[serde(with = "hex_bytes")] [u8; 32]);
641
642impl ContentHash {
643 #[must_use]
645 pub fn new(bytes: [u8; 32]) -> Self {
646 Self(bytes)
647 }
648
649 #[must_use]
655 pub fn from_hex(hex: &str) -> Self {
656 let mut bytes = [0u8; 32];
657 hex::decode_to_slice(hex, &mut bytes).expect("invalid hex string");
658 Self(bytes)
659 }
660
661 #[must_use]
663 pub fn as_bytes(&self) -> &[u8; 32] {
664 &self.0
665 }
666
667 #[must_use]
669 pub fn to_hex(&self) -> String {
670 hex::encode(self.0)
671 }
672
673 #[must_use]
675 pub fn zero() -> Self {
676 Self([0u8; 32])
677 }
678}
679
680impl Default for ContentHash {
681 fn default() -> Self {
682 Self::zero()
683 }
684}
685
686impl fmt::Display for ContentHash {
687 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688 f.write_str(&self.to_hex())
689 }
690}
691
692mod hex_bytes {
693 use serde::{Deserialize, Deserializer, Serializer};
694
695 pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
696 where
697 S: Serializer,
698 {
699 serializer.serialize_str(&hex::encode(bytes))
700 }
701
702 pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
703 where
704 D: Deserializer<'de>,
705 {
706 let raw = String::deserialize(deserializer)?;
707 let mut bytes = [0u8; 32];
708 hex::decode_to_slice(raw, &mut bytes).map_err(serde::de::Error::custom)?;
709 Ok(bytes)
710 }
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
719#[serde(transparent)]
720pub struct Timestamp(String);
721
722impl Timestamp {
723 #[must_use]
725 pub fn new(value: impl Into<String>) -> Self {
726 Self(value.into())
727 }
728
729 #[must_use]
731 pub fn as_str(&self) -> &str {
732 &self.0
733 }
734
735 #[must_use]
737 pub fn epoch() -> Self {
738 Self::new("1970-01-01T00:00:00Z")
739 }
740
741 #[must_use]
743 pub fn lamport(time: u64) -> Self {
744 Self(format!("lamport:{time}"))
745 }
746
747 #[must_use]
752 pub fn now() -> Self {
753 Self::lamport(0)
754 }
755}
756
757impl Deref for Timestamp {
758 type Target = str;
759
760 fn deref(&self) -> &Self::Target {
761 self.as_str()
762 }
763}
764
765impl AsRef<str> for Timestamp {
766 fn as_ref(&self) -> &str {
767 self.as_str()
768 }
769}
770
771impl fmt::Display for Timestamp {
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 f.write_str(self.as_str())
774 }
775}
776
777impl From<&str> for Timestamp {
778 fn from(value: &str) -> Self {
779 Self::new(value)
780 }
781}
782
783impl From<String> for Timestamp {
784 fn from(value: String) -> Self {
785 Self::new(value)
786 }
787}
788
789impl From<Timestamp> for String {
790 fn from(value: Timestamp) -> Self {
791 value.0
792 }
793}
794
795impl From<&Timestamp> for String {
796 fn from(value: &Timestamp) -> Self {
797 value.as_str().to_string()
798 }
799}
800
801impl PartialEq<&str> for Timestamp {
802 fn eq(&self, other: &&str) -> bool {
803 self.as_str() == *other
804 }
805}
806
807impl PartialEq<str> for Timestamp {
808 fn eq(&self, other: &str) -> bool {
809 self.as_str() == other
810 }
811}
812
813impl PartialEq<Timestamp> for &str {
814 fn eq(&self, other: &Timestamp) -> bool {
815 *self == other.as_str()
816 }
817}
818
819impl PartialEq<Timestamp> for str {
820 fn eq(&self, other: &Timestamp) -> bool {
821 self == other.as_str()
822 }
823}
824
825impl PartialEq<String> for Timestamp {
826 fn eq(&self, other: &String) -> bool {
827 self.as_str() == other.as_str()
828 }
829}
830
831impl PartialEq<Timestamp> for String {
832 fn eq(&self, other: &Timestamp) -> bool {
833 self.as_str() == other.as_str()
834 }
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840
841 #[test]
842 fn string_newtypes_compare_like_strings_without_erasing_type_identity() {
843 let fact_id = FactId::new("fact-1");
844 let proposal_id = ProposalId::new("fact-1");
845
846 assert_eq!(fact_id, "fact-1");
847 assert_eq!("fact-1", fact_id);
848 assert_ne!(fact_id.to_string(), "");
849 assert_ne!(fact_id.as_str(), "");
850 assert_eq!(proposal_id.as_str(), "fact-1");
851 }
852
853 #[test]
854 fn content_hash_hex_roundtrip() {
855 let hash = ContentHash::from_hex(
856 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
857 );
858 assert_eq!(
859 hash.to_hex(),
860 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
861 );
862 }
863
864 #[test]
865 fn timestamp_now_is_deterministic_logical_zero() {
866 assert_eq!(Timestamp::now().as_str(), "lamport:0");
867 }
868
869 #[test]
870 fn timestamp_can_represent_lamport_time() {
871 assert_eq!(Timestamp::lamport(42).as_str(), "lamport:42");
872 }
873
874 #[test]
875 fn unit_interval_accepts_only_finite_closed_range_values() {
876 assert_eq!(UnitInterval::new(0.0).unwrap().as_f64(), 0.0);
877 assert_eq!(UnitInterval::new(1.0).unwrap().as_f64(), 1.0);
878 assert!(UnitInterval::new(-0.1).is_err());
879 assert!(UnitInterval::new(1.1).is_err());
880 assert!(UnitInterval::new(f64::NAN).is_err());
881 }
882
883 #[test]
884 fn unit_interval_deserialization_rejects_out_of_range_values() {
885 assert!(serde_json::from_str::<UnitInterval>("0.75").is_ok());
886 assert!(serde_json::from_str::<UnitInterval>("1.75").is_err());
887 }
888
889 #[test]
890 fn basis_points_accepts_only_unit_range_basis_points() {
891 assert_eq!(BasisPoints::new(0).unwrap().get(), 0);
892 assert_eq!(BasisPoints::new(10_000).unwrap().get(), 10_000);
893 assert!(BasisPoints::new(10_001).is_err());
894 assert_eq!(BasisPoints::clamped(20_000).get(), 10_000);
895 }
896
897 #[test]
898 fn subject_ref_normalizes_scheme_and_kind_but_preserves_id() {
899 let subject = SubjectRef::new("Atlas", "Acquisition-Assets", "SharedIdentityCore")
900 .expect("valid subject");
901
902 assert_eq!(subject.scheme(), "atlas");
903 assert_eq!(subject.kind(), "acquisition-assets");
904 assert_eq!(subject.id(), "SharedIdentityCore");
905 assert_eq!(
906 subject.to_uri(),
907 "atlas://acquisition-assets/SharedIdentityCore"
908 );
909 }
910
911 #[test]
912 fn subject_ref_parses_canonical_uri() {
913 let subject = SubjectRef::parse("quorum://unresolved-questions/identity-owner-coverage")
914 .expect("valid subject");
915
916 assert_eq!(subject.scheme(), "quorum");
917 assert_eq!(subject.kind(), "unresolved-questions");
918 assert_eq!(subject.id(), "identity-owner-coverage");
919 assert_eq!(
920 subject.to_string(),
921 "quorum://unresolved-questions/identity-owner-coverage"
922 );
923 }
924
925 #[test]
926 fn subject_ref_rejects_malformed_or_ambiguous_parts() {
927 assert!(matches!(
928 SubjectRef::parse("atlas/acquisition-assets/shared"),
929 Err(SubjectRefError::Malformed(_))
930 ));
931 assert!(matches!(
932 SubjectRef::new("1atlas", "asset", "id"),
933 Err(SubjectRefError::InvalidSchemeChar('1'))
934 ));
935 assert!(matches!(
936 SubjectRef::new("atlas", "asset_kind", "id"),
937 Err(SubjectRefError::InvalidKindChar('_'))
938 ));
939 assert!(matches!(
940 SubjectRef::new("atlas", "asset", "id with space"),
941 Err(SubjectRefError::InvalidIdChar(' '))
942 ));
943 assert!(matches!(
944 SubjectRef::parse("atlas://asset/id/extra"),
945 Err(SubjectRefError::InvalidIdChar('/'))
946 ));
947 }
948
949 #[test]
950 fn subject_ref_serializes_as_canonical_string() {
951 let subject = SubjectRef::parse("warden://dd-gates/dd-evidence.identity-data-residency")
952 .expect("valid subject");
953 let json = serde_json::to_string(&subject).expect("subject should serialize");
954
955 assert_eq!(
956 json,
957 r#""warden://dd-gates/dd-evidence.identity-data-residency""#
958 );
959 assert_eq!(
960 serde_json::from_str::<SubjectRef>(&json).expect("subject should deserialize"),
961 subject
962 );
963 }
964
965 #[test]
966 fn timestamp_is_transparent() {
967 let timestamp = Timestamp::epoch();
968 let json = serde_json::to_string(×tamp).expect("timestamp should serialize");
969 assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
970 }
971}