1use serde::de;
7use serde::{Deserialize, Serialize};
8use std::borrow::Borrow;
9use std::fmt;
10use std::ops::Deref;
11
12macro_rules! string_newtype {
13 ($(#[$meta:meta])* $name:ident) => {
14 $(#[$meta])*
15 #[derive(
16 Debug,
17 Clone,
18 PartialEq,
19 Eq,
20 Hash,
21 PartialOrd,
22 Ord,
23 Serialize,
24 Deserialize,
25 )]
26 #[serde(transparent)]
27 pub struct $name(String);
28
29 impl $name {
30 #[must_use]
32 pub fn new(value: impl Into<String>) -> Self {
33 Self(value.into())
34 }
35
36 #[must_use]
38 pub fn as_str(&self) -> &str {
39 &self.0
40 }
41 }
42
43 impl Deref for $name {
44 type Target = str;
45
46 fn deref(&self) -> &Self::Target {
47 self.as_str()
48 }
49 }
50
51 impl AsRef<str> for $name {
52 fn as_ref(&self) -> &str {
53 self.as_str()
54 }
55 }
56
57 impl Borrow<str> for $name {
58 fn borrow(&self) -> &str {
59 self.as_str()
60 }
61 }
62
63 impl fmt::Display for $name {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(self.as_str())
66 }
67 }
68
69 impl From<&str> for $name {
70 fn from(value: &str) -> Self {
71 Self::new(value)
72 }
73 }
74
75 impl From<String> for $name {
76 fn from(value: String) -> Self {
77 Self::new(value)
78 }
79 }
80
81 impl From<&$name> for $name {
82 fn from(value: &$name) -> Self {
83 value.clone()
84 }
85 }
86
87 impl From<$name> for String {
88 fn from(value: $name) -> Self {
89 value.0
90 }
91 }
92
93 impl From<&$name> for String {
94 fn from(value: &$name) -> Self {
95 value.as_str().to_string()
96 }
97 }
98
99 impl PartialEq<&str> for $name {
100 fn eq(&self, other: &&str) -> bool {
101 self.as_str() == *other
102 }
103 }
104
105 impl PartialEq<str> for $name {
106 fn eq(&self, other: &str) -> bool {
107 self.as_str() == other
108 }
109 }
110
111 impl PartialEq<$name> for &str {
112 fn eq(&self, other: &$name) -> bool {
113 *self == other.as_str()
114 }
115 }
116
117 impl PartialEq<$name> for str {
118 fn eq(&self, other: &$name) -> bool {
119 self == other.as_str()
120 }
121 }
122
123 impl PartialEq<String> for $name {
124 fn eq(&self, other: &String) -> bool {
125 self.as_str() == other.as_str()
126 }
127 }
128
129 impl PartialEq<$name> for String {
130 fn eq(&self, other: &$name) -> bool {
131 self.as_str() == other.as_str()
132 }
133 }
134
135 impl PartialEq<&$name> for $name {
136 fn eq(&self, other: &&$name) -> bool {
137 self == *other
138 }
139 }
140 };
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct UnitIntervalError;
146
147impl fmt::Display for UnitIntervalError {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.write_str("value must be finite and in the inclusive range 0.0..=1.0")
150 }
151}
152
153impl std::error::Error for UnitIntervalError {}
154
155#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
159#[serde(transparent)]
160pub struct UnitInterval(f64);
161
162impl UnitInterval {
163 pub const ZERO: Self = Self(0.0);
165 pub const ONE: Self = Self(1.0);
167
168 pub fn new(value: f64) -> Result<Self, UnitIntervalError> {
172 if value.is_finite() && (0.0..=1.0).contains(&value) {
173 Ok(Self(value))
174 } else {
175 Err(UnitIntervalError)
176 }
177 }
178
179 #[must_use]
183 pub fn clamped(value: f64) -> Self {
184 if value.is_finite() {
185 Self(value.clamp(0.0, 1.0))
186 } else {
187 Self::ZERO
188 }
189 }
190
191 #[must_use]
193 pub fn as_f64(self) -> f64 {
194 self.0
195 }
196
197 #[must_use]
199 pub fn saturating_add(self, delta: f64) -> Self {
200 Self::clamped(self.0 + delta)
201 }
202
203 #[must_use]
205 pub fn scale_by(self, factor: Self) -> Self {
206 Self(self.0 * factor.0)
207 }
208
209 #[must_use]
211 pub fn to_basis_points(self) -> u16 {
212 (self.0 * 10_000.0).round() as u16
213 }
214}
215
216impl Default for UnitInterval {
217 fn default() -> Self {
218 Self::ZERO
219 }
220}
221
222impl TryFrom<f64> for UnitInterval {
223 type Error = UnitIntervalError;
224
225 fn try_from(value: f64) -> Result<Self, Self::Error> {
226 Self::new(value)
227 }
228}
229
230impl From<UnitInterval> for f64 {
231 fn from(value: UnitInterval) -> Self {
232 value.as_f64()
233 }
234}
235
236impl<'de> Deserialize<'de> for UnitInterval {
237 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238 where
239 D: serde::Deserializer<'de>,
240 {
241 let value = f64::deserialize(deserializer)?;
242 Self::new(value).map_err(de::Error::custom)
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub struct BasisPointsError;
249
250impl fmt::Display for BasisPointsError {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.write_str("basis points must be in the inclusive range 0..=10000")
253 }
254}
255
256impl std::error::Error for BasisPointsError {}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
260#[serde(transparent)]
261pub struct BasisPoints(u16);
262
263impl BasisPoints {
264 pub const ZERO: Self = Self(0);
266 pub const FULL: Self = Self(10_000);
268
269 pub fn new(value: u16) -> Result<Self, BasisPointsError> {
271 if value <= 10_000 {
272 Ok(Self(value))
273 } else {
274 Err(BasisPointsError)
275 }
276 }
277
278 #[must_use]
280 pub fn clamped(value: u16) -> Self {
281 Self(value.min(10_000))
282 }
283
284 #[must_use]
286 pub fn get(self) -> u16 {
287 self.0
288 }
289
290 #[must_use]
292 pub fn as_unit_interval(self) -> UnitInterval {
293 UnitInterval::clamped(f64::from(self.0) / 10_000.0)
294 }
295}
296
297impl Default for BasisPoints {
298 fn default() -> Self {
299 Self::ZERO
300 }
301}
302
303impl TryFrom<u16> for BasisPoints {
304 type Error = BasisPointsError;
305
306 fn try_from(value: u16) -> Result<Self, Self::Error> {
307 Self::new(value)
308 }
309}
310
311impl From<BasisPoints> for u16 {
312 fn from(value: BasisPoints) -> Self {
313 value.get()
314 }
315}
316
317impl<'de> Deserialize<'de> for BasisPoints {
318 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
319 where
320 D: serde::Deserializer<'de>,
321 {
322 let value = u16::deserialize(deserializer)?;
323 Self::new(value).map_err(de::Error::custom)
324 }
325}
326
327string_newtype!(
328 FactId
330);
331string_newtype!(
332 ProposalId
334);
335string_newtype!(
336 ObservationId
338);
339string_newtype!(
340 ApprovalId
342);
343string_newtype!(
344 ArtifactId
346);
347string_newtype!(
348 GateId
350);
351string_newtype!(
352 ActorId
354);
355string_newtype!(
356 ValidationCheckId
358);
359string_newtype!(
360 TraceId
362);
363string_newtype!(
364 SpanId
366);
367string_newtype!(
368 TraceSystemId
370);
371string_newtype!(
372 TraceReference
374);
375string_newtype!(
376 PrincipalId
378);
379string_newtype!(
380 EventId
382);
383string_newtype!(
384 TenantId
386);
387string_newtype!(
388 CorrelationId
390);
391string_newtype!(
392 ChainId
394);
395string_newtype!(
396 TraceLinkId
398);
399string_newtype!(
400 BackendId
402);
403string_newtype!(
404 PackId
406);
407string_newtype!(
408 TruthId
410);
411string_newtype!(
412 PolicyId
414);
415string_newtype!(
416 ApprovalPointId
418);
419string_newtype!(
420 VoteId
422);
423string_newtype!(
424 VoteTopicId
426);
427string_newtype!(
428 DisagreementId
430);
431string_newtype!(
432 CriterionId
434);
435string_newtype!(
436 ConstraintName
438);
439string_newtype!(
440 ConstraintValue
442);
443string_newtype!(
444 DomainId
446);
447string_newtype!(
448 PolicyVersionId
450);
451string_newtype!(
452 ResourceId
454);
455string_newtype!(
456 ResourceKind
458);
459
460#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
462#[serde(transparent)]
463pub struct ContentHash(#[serde(with = "hex_bytes")] [u8; 32]);
464
465impl ContentHash {
466 #[must_use]
468 pub fn new(bytes: [u8; 32]) -> Self {
469 Self(bytes)
470 }
471
472 #[must_use]
478 pub fn from_hex(hex: &str) -> Self {
479 let mut bytes = [0u8; 32];
480 hex::decode_to_slice(hex, &mut bytes).expect("invalid hex string");
481 Self(bytes)
482 }
483
484 #[must_use]
486 pub fn as_bytes(&self) -> &[u8; 32] {
487 &self.0
488 }
489
490 #[must_use]
492 pub fn to_hex(&self) -> String {
493 hex::encode(self.0)
494 }
495
496 #[must_use]
498 pub fn zero() -> Self {
499 Self([0u8; 32])
500 }
501}
502
503impl Default for ContentHash {
504 fn default() -> Self {
505 Self::zero()
506 }
507}
508
509impl fmt::Display for ContentHash {
510 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511 f.write_str(&self.to_hex())
512 }
513}
514
515mod hex_bytes {
516 use serde::{Deserialize, Deserializer, Serializer};
517
518 pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
519 where
520 S: Serializer,
521 {
522 serializer.serialize_str(&hex::encode(bytes))
523 }
524
525 pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
526 where
527 D: Deserializer<'de>,
528 {
529 let raw = String::deserialize(deserializer)?;
530 let mut bytes = [0u8; 32];
531 hex::decode_to_slice(raw, &mut bytes).map_err(serde::de::Error::custom)?;
532 Ok(bytes)
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
538#[serde(transparent)]
539pub struct Timestamp(String);
540
541impl Timestamp {
542 #[must_use]
544 pub fn new(value: impl Into<String>) -> Self {
545 Self(value.into())
546 }
547
548 #[must_use]
550 pub fn as_str(&self) -> &str {
551 &self.0
552 }
553
554 #[must_use]
556 pub fn epoch() -> Self {
557 Self::new("1970-01-01T00:00:00Z")
558 }
559
560 #[must_use]
562 pub fn now() -> Self {
563 use std::time::{SystemTime, UNIX_EPOCH};
564
565 let duration = SystemTime::now()
566 .duration_since(UNIX_EPOCH)
567 .unwrap_or_default();
568 Self(format!("{}Z", duration.as_secs()))
569 }
570}
571
572impl Deref for Timestamp {
573 type Target = str;
574
575 fn deref(&self) -> &Self::Target {
576 self.as_str()
577 }
578}
579
580impl AsRef<str> for Timestamp {
581 fn as_ref(&self) -> &str {
582 self.as_str()
583 }
584}
585
586impl fmt::Display for Timestamp {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 f.write_str(self.as_str())
589 }
590}
591
592impl From<&str> for Timestamp {
593 fn from(value: &str) -> Self {
594 Self::new(value)
595 }
596}
597
598impl From<String> for Timestamp {
599 fn from(value: String) -> Self {
600 Self::new(value)
601 }
602}
603
604impl From<Timestamp> for String {
605 fn from(value: Timestamp) -> Self {
606 value.0
607 }
608}
609
610impl From<&Timestamp> for String {
611 fn from(value: &Timestamp) -> Self {
612 value.as_str().to_string()
613 }
614}
615
616impl PartialEq<&str> for Timestamp {
617 fn eq(&self, other: &&str) -> bool {
618 self.as_str() == *other
619 }
620}
621
622impl PartialEq<str> for Timestamp {
623 fn eq(&self, other: &str) -> bool {
624 self.as_str() == other
625 }
626}
627
628impl PartialEq<Timestamp> for &str {
629 fn eq(&self, other: &Timestamp) -> bool {
630 *self == other.as_str()
631 }
632}
633
634impl PartialEq<Timestamp> for str {
635 fn eq(&self, other: &Timestamp) -> bool {
636 self == other.as_str()
637 }
638}
639
640impl PartialEq<String> for Timestamp {
641 fn eq(&self, other: &String) -> bool {
642 self.as_str() == other.as_str()
643 }
644}
645
646impl PartialEq<Timestamp> for String {
647 fn eq(&self, other: &Timestamp) -> bool {
648 self.as_str() == other.as_str()
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn string_newtypes_compare_like_strings_without_erasing_type_identity() {
658 let fact_id = FactId::new("fact-1");
659 let proposal_id = ProposalId::new("fact-1");
660
661 assert_eq!(fact_id, "fact-1");
662 assert_eq!("fact-1", fact_id);
663 assert_ne!(fact_id.to_string(), "");
664 assert_ne!(fact_id.as_str(), "");
665 assert_eq!(proposal_id.as_str(), "fact-1");
666 }
667
668 #[test]
669 fn content_hash_hex_roundtrip() {
670 let hash = ContentHash::from_hex(
671 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
672 );
673 assert_eq!(
674 hash.to_hex(),
675 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
676 );
677 }
678
679 #[test]
680 fn unit_interval_accepts_only_finite_closed_range_values() {
681 assert_eq!(UnitInterval::new(0.0).unwrap().as_f64(), 0.0);
682 assert_eq!(UnitInterval::new(1.0).unwrap().as_f64(), 1.0);
683 assert!(UnitInterval::new(-0.1).is_err());
684 assert!(UnitInterval::new(1.1).is_err());
685 assert!(UnitInterval::new(f64::NAN).is_err());
686 }
687
688 #[test]
689 fn unit_interval_deserialization_rejects_out_of_range_values() {
690 assert!(serde_json::from_str::<UnitInterval>("0.75").is_ok());
691 assert!(serde_json::from_str::<UnitInterval>("1.75").is_err());
692 }
693
694 #[test]
695 fn basis_points_accepts_only_unit_range_basis_points() {
696 assert_eq!(BasisPoints::new(0).unwrap().get(), 0);
697 assert_eq!(BasisPoints::new(10_000).unwrap().get(), 10_000);
698 assert!(BasisPoints::new(10_001).is_err());
699 assert_eq!(BasisPoints::clamped(20_000).get(), 10_000);
700 }
701
702 #[test]
703 fn timestamp_is_transparent() {
704 let timestamp = Timestamp::epoch();
705 let json = serde_json::to_string(×tamp).expect("timestamp should serialize");
706 assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
707 }
708}