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 an individual vote cast on a topic.
236    VoteId
237);
238string_newtype!(
239    /// Identifier for the topic a vote or disagreement is about.
240    VoteTopicId
241);
242string_newtype!(
243    /// Identifier for a recorded disagreement.
244    DisagreementId
245);
246string_newtype!(
247    /// Identifier for a success criterion.
248    CriterionId
249);
250string_newtype!(
251    /// Consumer-owned name for a constraint.
252    ConstraintName
253);
254string_newtype!(
255    /// Consumer-owned value for a constraint.
256    ConstraintValue
257);
258string_newtype!(
259    /// Identifier for a business or governance domain.
260    DomainId
261);
262string_newtype!(
263    /// Identifier for a bound policy version label.
264    PolicyVersionId
265);
266string_newtype!(
267    /// Identifier for a converging resource or flow.
268    ResourceId
269);
270string_newtype!(
271    /// Open identifier for a resource kind owned by the consumer domain.
272    ResourceKind
273);
274
275/// Content-addressable hash encoded as 32 raw bytes and serialized as hex.
276#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
277#[serde(transparent)]
278pub struct ContentHash(#[serde(with = "hex_bytes")] [u8; 32]);
279
280impl ContentHash {
281    /// Create a new content hash from raw bytes.
282    #[must_use]
283    pub fn new(bytes: [u8; 32]) -> Self {
284        Self(bytes)
285    }
286
287    /// Create a content hash from a hex string.
288    ///
289    /// # Panics
290    ///
291    /// Panics if the hex string is not exactly 64 hexadecimal characters.
292    #[must_use]
293    pub fn from_hex(hex: &str) -> Self {
294        let mut bytes = [0u8; 32];
295        hex::decode_to_slice(hex, &mut bytes).expect("invalid hex string");
296        Self(bytes)
297    }
298
299    /// Borrow the raw bytes.
300    #[must_use]
301    pub fn as_bytes(&self) -> &[u8; 32] {
302        &self.0
303    }
304
305    /// Convert to lowercase hex.
306    #[must_use]
307    pub fn to_hex(&self) -> String {
308        hex::encode(self.0)
309    }
310
311    /// The zero hash, useful for deterministic stubs.
312    #[must_use]
313    pub fn zero() -> Self {
314        Self([0u8; 32])
315    }
316}
317
318impl Default for ContentHash {
319    fn default() -> Self {
320        Self::zero()
321    }
322}
323
324impl fmt::Display for ContentHash {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        f.write_str(&self.to_hex())
327    }
328}
329
330mod hex_bytes {
331    use serde::{Deserialize, Deserializer, Serializer};
332
333    pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
334    where
335        S: Serializer,
336    {
337        serializer.serialize_str(&hex::encode(bytes))
338    }
339
340    pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
341    where
342        D: Deserializer<'de>,
343    {
344        let raw = String::deserialize(deserializer)?;
345        let mut bytes = [0u8; 32];
346        hex::decode_to_slice(raw, &mut bytes).map_err(serde::de::Error::custom)?;
347        Ok(bytes)
348    }
349}
350
351/// ISO-8601 timestamp string.
352#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
353#[serde(transparent)]
354pub struct Timestamp(String);
355
356impl Timestamp {
357    /// Create a new timestamp from an ISO-8601 string.
358    #[must_use]
359    pub fn new(value: impl Into<String>) -> Self {
360        Self(value.into())
361    }
362
363    /// Borrow the timestamp string.
364    #[must_use]
365    pub fn as_str(&self) -> &str {
366        &self.0
367    }
368
369    /// The Unix epoch in ISO-8601 form.
370    #[must_use]
371    pub fn epoch() -> Self {
372        Self::new("1970-01-01T00:00:00Z")
373    }
374
375    /// A best-effort timestamp for "now".
376    #[must_use]
377    pub fn now() -> Self {
378        use std::time::{SystemTime, UNIX_EPOCH};
379
380        let duration = SystemTime::now()
381            .duration_since(UNIX_EPOCH)
382            .unwrap_or_default();
383        Self(format!("{}Z", duration.as_secs()))
384    }
385}
386
387impl Deref for Timestamp {
388    type Target = str;
389
390    fn deref(&self) -> &Self::Target {
391        self.as_str()
392    }
393}
394
395impl AsRef<str> for Timestamp {
396    fn as_ref(&self) -> &str {
397        self.as_str()
398    }
399}
400
401impl fmt::Display for Timestamp {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        f.write_str(self.as_str())
404    }
405}
406
407impl From<&str> for Timestamp {
408    fn from(value: &str) -> Self {
409        Self::new(value)
410    }
411}
412
413impl From<String> for Timestamp {
414    fn from(value: String) -> Self {
415        Self::new(value)
416    }
417}
418
419impl From<Timestamp> for String {
420    fn from(value: Timestamp) -> Self {
421        value.0
422    }
423}
424
425impl From<&Timestamp> for String {
426    fn from(value: &Timestamp) -> Self {
427        value.as_str().to_string()
428    }
429}
430
431impl PartialEq<&str> for Timestamp {
432    fn eq(&self, other: &&str) -> bool {
433        self.as_str() == *other
434    }
435}
436
437impl PartialEq<str> for Timestamp {
438    fn eq(&self, other: &str) -> bool {
439        self.as_str() == other
440    }
441}
442
443impl PartialEq<Timestamp> for &str {
444    fn eq(&self, other: &Timestamp) -> bool {
445        *self == other.as_str()
446    }
447}
448
449impl PartialEq<Timestamp> for str {
450    fn eq(&self, other: &Timestamp) -> bool {
451        self == other.as_str()
452    }
453}
454
455impl PartialEq<String> for Timestamp {
456    fn eq(&self, other: &String) -> bool {
457        self.as_str() == other.as_str()
458    }
459}
460
461impl PartialEq<Timestamp> for String {
462    fn eq(&self, other: &Timestamp) -> bool {
463        self.as_str() == other.as_str()
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn string_newtypes_compare_like_strings_without_erasing_type_identity() {
473        let fact_id = FactId::new("fact-1");
474        let proposal_id = ProposalId::new("fact-1");
475
476        assert_eq!(fact_id, "fact-1");
477        assert_eq!("fact-1", fact_id);
478        assert_ne!(fact_id.to_string(), "");
479        assert_ne!(fact_id.as_str(), "");
480        assert_eq!(proposal_id.as_str(), "fact-1");
481    }
482
483    #[test]
484    fn content_hash_hex_roundtrip() {
485        let hash = ContentHash::from_hex(
486            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
487        );
488        assert_eq!(
489            hash.to_hex(),
490            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
491        );
492    }
493
494    #[test]
495    fn timestamp_is_transparent() {
496        let timestamp = Timestamp::epoch();
497        let json = serde_json::to_string(&timestamp).expect("timestamp should serialize");
498        assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
499    }
500}