Skip to main content

converge_pack/
types.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Shared semantic value types for the public Converge contract.
5
6use serde::{Deserialize, Serialize};
7use std::borrow::Borrow;
8use std::fmt;
9use std::ops::Deref;
10
11macro_rules! string_newtype {
12    ($(#[$meta:meta])* $name:ident) => {
13        $(#[$meta])*
14        #[derive(
15            Debug,
16            Clone,
17            PartialEq,
18            Eq,
19            Hash,
20            PartialOrd,
21            Ord,
22            Serialize,
23            Deserialize,
24        )]
25        #[serde(transparent)]
26        pub struct $name(String);
27
28        impl $name {
29            /// Create a new typed string value.
30            #[must_use]
31            pub fn new(value: impl Into<String>) -> Self {
32                Self(value.into())
33            }
34
35            /// Borrow the underlying string.
36            #[must_use]
37            pub fn as_str(&self) -> &str {
38                &self.0
39            }
40        }
41
42        impl Deref for $name {
43            type Target = str;
44
45            fn deref(&self) -> &Self::Target {
46                self.as_str()
47            }
48        }
49
50        impl AsRef<str> for $name {
51            fn as_ref(&self) -> &str {
52                self.as_str()
53            }
54        }
55
56        impl Borrow<str> for $name {
57            fn borrow(&self) -> &str {
58                self.as_str()
59            }
60        }
61
62        impl fmt::Display for $name {
63            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64                f.write_str(self.as_str())
65            }
66        }
67
68        impl From<&str> for $name {
69            fn from(value: &str) -> Self {
70                Self::new(value)
71            }
72        }
73
74        impl From<String> for $name {
75            fn from(value: String) -> Self {
76                Self::new(value)
77            }
78        }
79
80        impl From<&$name> for $name {
81            fn from(value: &$name) -> Self {
82                value.clone()
83            }
84        }
85
86        impl From<$name> for String {
87            fn from(value: $name) -> Self {
88                value.0
89            }
90        }
91
92        impl From<&$name> for String {
93            fn from(value: &$name) -> Self {
94                value.as_str().to_string()
95            }
96        }
97
98        impl PartialEq<&str> for $name {
99            fn eq(&self, other: &&str) -> bool {
100                self.as_str() == *other
101            }
102        }
103
104        impl PartialEq<str> for $name {
105            fn eq(&self, other: &str) -> bool {
106                self.as_str() == other
107            }
108        }
109
110        impl PartialEq<$name> for &str {
111            fn eq(&self, other: &$name) -> bool {
112                *self == other.as_str()
113            }
114        }
115
116        impl PartialEq<$name> for str {
117            fn eq(&self, other: &$name) -> bool {
118                self == other.as_str()
119            }
120        }
121
122        impl PartialEq<String> for $name {
123            fn eq(&self, other: &String) -> bool {
124                self.as_str() == other.as_str()
125            }
126        }
127
128        impl PartialEq<$name> for String {
129            fn eq(&self, other: &$name) -> bool {
130                self.as_str() == other.as_str()
131            }
132        }
133
134        impl PartialEq<&$name> for $name {
135            fn eq(&self, other: &&$name) -> bool {
136                self == *other
137            }
138        }
139    };
140}
141
142string_newtype!(
143    /// Unique identifier for a promoted fact.
144    FactId
145);
146string_newtype!(
147    /// Unique identifier for a proposal.
148    ProposalId
149);
150string_newtype!(
151    /// Unique identifier for a raw observation.
152    ObservationId
153);
154string_newtype!(
155    /// Unique identifier for a human approval.
156    ApprovalId
157);
158string_newtype!(
159    /// Unique identifier for a derived artifact.
160    ArtifactId
161);
162string_newtype!(
163    /// Unique identifier for a promotion gate.
164    GateId
165);
166string_newtype!(
167    /// Identifier for a recorded actor.
168    ActorId
169);
170string_newtype!(
171    /// Identifier for a named validation check.
172    ValidationCheckId
173);
174string_newtype!(
175    /// Identifier for a trace.
176    TraceId
177);
178string_newtype!(
179    /// Identifier for a trace span.
180    SpanId
181);
182string_newtype!(
183    /// Identifier for an external trace system.
184    TraceSystemId
185);
186string_newtype!(
187    /// Reference into an external trace system.
188    TraceReference
189);
190string_newtype!(
191    /// Identifier for a flow-gate principal.
192    PrincipalId
193);
194string_newtype!(
195    /// Identifier for an experience event envelope.
196    EventId
197);
198string_newtype!(
199    /// Identifier for a tenant scope.
200    TenantId
201);
202string_newtype!(
203    /// Identifier for correlating related events or runs.
204    CorrelationId
205);
206string_newtype!(
207    /// Identifier for a convergence chain or run.
208    ChainId
209);
210string_newtype!(
211    /// Identifier for a stored replay trace link.
212    TraceLinkId
213);
214string_newtype!(
215    /// Identifier for a backend, provider, or adapter.
216    BackendId
217);
218string_newtype!(
219    /// Identifier for a named pack.
220    PackId
221);
222string_newtype!(
223    /// Identifier for a truth definition.
224    TruthId
225);
226string_newtype!(
227    /// Identifier for a policy definition.
228    PolicyId
229);
230string_newtype!(
231    /// Identifier for an approval point or workflow reference.
232    ApprovalPointId
233);
234string_newtype!(
235    /// Identifier for a success criterion.
236    CriterionId
237);
238string_newtype!(
239    /// Consumer-owned name for a constraint.
240    ConstraintName
241);
242string_newtype!(
243    /// Consumer-owned value for a constraint.
244    ConstraintValue
245);
246string_newtype!(
247    /// Identifier for a business or governance domain.
248    DomainId
249);
250string_newtype!(
251    /// Identifier for a bound policy version label.
252    PolicyVersionId
253);
254string_newtype!(
255    /// Identifier for a converging resource or flow.
256    ResourceId
257);
258string_newtype!(
259    /// Open identifier for a resource kind owned by the consumer domain.
260    ResourceKind
261);
262
263/// Content-addressable hash encoded as 32 raw bytes and serialized as hex.
264#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
265#[serde(transparent)]
266pub struct ContentHash(#[serde(with = "hex_bytes")] [u8; 32]);
267
268impl ContentHash {
269    /// Create a new content hash from raw bytes.
270    #[must_use]
271    pub fn new(bytes: [u8; 32]) -> Self {
272        Self(bytes)
273    }
274
275    /// Create a content hash from a hex string.
276    ///
277    /// # Panics
278    ///
279    /// Panics if the hex string is not exactly 64 hexadecimal characters.
280    #[must_use]
281    pub fn from_hex(hex: &str) -> Self {
282        let mut bytes = [0u8; 32];
283        hex::decode_to_slice(hex, &mut bytes).expect("invalid hex string");
284        Self(bytes)
285    }
286
287    /// Borrow the raw bytes.
288    #[must_use]
289    pub fn as_bytes(&self) -> &[u8; 32] {
290        &self.0
291    }
292
293    /// Convert to lowercase hex.
294    #[must_use]
295    pub fn to_hex(&self) -> String {
296        hex::encode(self.0)
297    }
298
299    /// The zero hash, useful for deterministic stubs.
300    #[must_use]
301    pub fn zero() -> Self {
302        Self([0u8; 32])
303    }
304}
305
306impl Default for ContentHash {
307    fn default() -> Self {
308        Self::zero()
309    }
310}
311
312impl fmt::Display for ContentHash {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        f.write_str(&self.to_hex())
315    }
316}
317
318mod hex_bytes {
319    use serde::{Deserialize, Deserializer, Serializer};
320
321    pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
322    where
323        S: Serializer,
324    {
325        serializer.serialize_str(&hex::encode(bytes))
326    }
327
328    pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
329    where
330        D: Deserializer<'de>,
331    {
332        let raw = String::deserialize(deserializer)?;
333        let mut bytes = [0u8; 32];
334        hex::decode_to_slice(raw, &mut bytes).map_err(serde::de::Error::custom)?;
335        Ok(bytes)
336    }
337}
338
339/// ISO-8601 timestamp string.
340#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
341#[serde(transparent)]
342pub struct Timestamp(String);
343
344impl Timestamp {
345    /// Create a new timestamp from an ISO-8601 string.
346    #[must_use]
347    pub fn new(value: impl Into<String>) -> Self {
348        Self(value.into())
349    }
350
351    /// Borrow the timestamp string.
352    #[must_use]
353    pub fn as_str(&self) -> &str {
354        &self.0
355    }
356
357    /// The Unix epoch in ISO-8601 form.
358    #[must_use]
359    pub fn epoch() -> Self {
360        Self::new("1970-01-01T00:00:00Z")
361    }
362
363    /// A best-effort timestamp for "now".
364    #[must_use]
365    pub fn now() -> Self {
366        use std::time::{SystemTime, UNIX_EPOCH};
367
368        let duration = SystemTime::now()
369            .duration_since(UNIX_EPOCH)
370            .unwrap_or_default();
371        Self(format!("{}Z", duration.as_secs()))
372    }
373}
374
375impl Deref for Timestamp {
376    type Target = str;
377
378    fn deref(&self) -> &Self::Target {
379        self.as_str()
380    }
381}
382
383impl AsRef<str> for Timestamp {
384    fn as_ref(&self) -> &str {
385        self.as_str()
386    }
387}
388
389impl fmt::Display for Timestamp {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        f.write_str(self.as_str())
392    }
393}
394
395impl From<&str> for Timestamp {
396    fn from(value: &str) -> Self {
397        Self::new(value)
398    }
399}
400
401impl From<String> for Timestamp {
402    fn from(value: String) -> Self {
403        Self::new(value)
404    }
405}
406
407impl From<Timestamp> for String {
408    fn from(value: Timestamp) -> Self {
409        value.0
410    }
411}
412
413impl From<&Timestamp> for String {
414    fn from(value: &Timestamp) -> Self {
415        value.as_str().to_string()
416    }
417}
418
419impl PartialEq<&str> for Timestamp {
420    fn eq(&self, other: &&str) -> bool {
421        self.as_str() == *other
422    }
423}
424
425impl PartialEq<str> for Timestamp {
426    fn eq(&self, other: &str) -> bool {
427        self.as_str() == other
428    }
429}
430
431impl PartialEq<Timestamp> for &str {
432    fn eq(&self, other: &Timestamp) -> bool {
433        *self == other.as_str()
434    }
435}
436
437impl PartialEq<Timestamp> for str {
438    fn eq(&self, other: &Timestamp) -> bool {
439        self == other.as_str()
440    }
441}
442
443impl PartialEq<String> for Timestamp {
444    fn eq(&self, other: &String) -> bool {
445        self.as_str() == other.as_str()
446    }
447}
448
449impl PartialEq<Timestamp> for String {
450    fn eq(&self, other: &Timestamp) -> bool {
451        self.as_str() == other.as_str()
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn string_newtypes_compare_like_strings_without_erasing_type_identity() {
461        let fact_id = FactId::new("fact-1");
462        let proposal_id = ProposalId::new("fact-1");
463
464        assert_eq!(fact_id, "fact-1");
465        assert_eq!("fact-1", fact_id);
466        assert_ne!(fact_id.to_string(), "");
467        assert_ne!(fact_id.as_str(), "");
468        assert_eq!(proposal_id.as_str(), "fact-1");
469    }
470
471    #[test]
472    fn content_hash_hex_roundtrip() {
473        let hash = ContentHash::from_hex(
474            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
475        );
476        assert_eq!(
477            hash.to_hex(),
478            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
479        );
480    }
481
482    #[test]
483    fn timestamp_is_transparent() {
484        let timestamp = Timestamp::epoch();
485        let json = serde_json::to_string(&timestamp).expect("timestamp should serialize");
486        assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
487    }
488}