Skip to main content

distributed/domain_event/
descriptor.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use super::{DOMAIN_EVENT_BODY_CODEC, DOMAIN_EVENT_BODY_CODEC_VERSION};
7
8/// How a public domain-event body was derived.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum DomainEventBodyKind {
12    /// A deliberately public post-transition aggregate state.
13    State,
14    /// A sparse or explicitly adapted outward event.
15    Event,
16    /// A stable identity and incarnation for physical deletion.
17    Deletion,
18}
19
20/// Versioned schema and codec identity for one domain-event body.
21#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct DomainEventBodyDescriptor {
23    /// Whether the body is state, a sparse event, or deletion identity.
24    pub kind: DomainEventBodyKind,
25    /// Stable body type name used by generated schemas.
26    pub type_name: Cow<'static, str>,
27    /// Independently evolving body schema version.
28    pub version: u64,
29    /// Canonical schema identifier generated for this body.
30    pub schema: Cow<'static, str>,
31    /// Lowercase `sha256:` fingerprint of the canonical body schema.
32    pub fingerprint: Cow<'static, str>,
33    /// Canonical body codec.
34    pub codec: Cow<'static, str>,
35    /// Canonical body codec version.
36    pub codec_version: u16,
37}
38
39impl DomainEventBodyDescriptor {
40    /// Declare a version-one Distributed JSON body descriptor.
41    pub const fn distributed_json(
42        kind: DomainEventBodyKind,
43        type_name: &'static str,
44        version: u64,
45        schema: &'static str,
46        fingerprint: &'static str,
47    ) -> Self {
48        Self {
49            kind,
50            type_name: Cow::Borrowed(type_name),
51            version,
52            schema: Cow::Borrowed(schema),
53            fingerprint: Cow::Borrowed(fingerprint),
54            codec: Cow::Borrowed(DOMAIN_EVENT_BODY_CODEC),
55            codec_version: DOMAIN_EVENT_BODY_CODEC_VERSION,
56        }
57    }
58}
59
60/// Independently versioned public post-transition state descriptor.
61#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
62pub struct DomainStateDescriptor {
63    /// Stable state type name used by generated schemas.
64    pub type_name: Cow<'static, str>,
65    /// Public state schema version.
66    pub version: u64,
67    /// Canonical public state schema identifier.
68    pub schema: Cow<'static, str>,
69    /// Lowercase `sha256:` fingerprint of the public state schema.
70    pub fingerprint: Cow<'static, str>,
71    /// Canonical public state codec.
72    pub codec: Cow<'static, str>,
73    /// Canonical public state codec version.
74    pub codec_version: u16,
75}
76
77impl DomainStateDescriptor {
78    /// Declare a version-one Distributed JSON domain-state descriptor.
79    pub const fn distributed_json(
80        type_name: &'static str,
81        version: u64,
82        schema: &'static str,
83        fingerprint: &'static str,
84    ) -> Self {
85        Self {
86            type_name: Cow::Borrowed(type_name),
87            version,
88            schema: Cow::Borrowed(schema),
89            fingerprint: Cow::Borrowed(fingerprint),
90            codec: Cow::Borrowed(DOMAIN_EVENT_BODY_CODEC),
91            codec_version: DOMAIN_EVENT_BODY_CODEC_VERSION,
92        }
93    }
94
95    /// Use this state schema as the body of a semantic domain event.
96    pub fn event(self, name: impl Into<Cow<'static, str>>, version: u64) -> DomainEventDescriptor {
97        DomainEventDescriptor {
98            name: name.into(),
99            version,
100            body: self.into(),
101        }
102    }
103}
104
105impl From<DomainStateDescriptor> for DomainEventBodyDescriptor {
106    fn from(state: DomainStateDescriptor) -> Self {
107        Self {
108            kind: DomainEventBodyKind::State,
109            type_name: state.type_name,
110            version: state.version,
111            schema: state.schema,
112            fingerprint: state.fingerprint,
113            codec: state.codec,
114            codec_version: state.codec_version,
115        }
116    }
117}
118
119/// Semantic domain-event name and independently versioned body contract.
120#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
121pub struct DomainEventDescriptor {
122    /// Stable semantic event name, such as `todo.completed`.
123    pub name: Cow<'static, str>,
124    /// Semantic event version, independent of its body schema version.
125    pub version: u64,
126    /// Typed body schema and codec.
127    pub body: DomainEventBodyDescriptor,
128}
129
130impl DomainEventDescriptor {
131    /// Construct a descriptor for a state-capture event.
132    pub fn state<S: DomainState>(name: impl Into<Cow<'static, str>>, version: u64) -> Self {
133        S::DESCRIPTOR.clone().event(name, version)
134    }
135}
136
137/// A deliberately public post-transition state DTO.
138///
139/// This is separate from `Snapshot`: snapshot fields and codecs are private
140/// replay-acceleration details, while this descriptor is a durable public
141/// contract.
142pub trait DomainState: Serialize {
143    /// Schema identity of the public state body.
144    const DESCRIPTOR: DomainStateDescriptor;
145}
146
147/// A sparse or explicitly adapted outward domain event.
148pub trait DomainEvent: Serialize {
149    /// Semantic event and body contract.
150    const DESCRIPTOR: DomainEventDescriptor;
151}
152
153/// Exact typed contract for one outward event a command may declare.
154///
155/// Unlike [`DomainEvent`], the contract type does not have to be the value
156/// serialized on the wire. Sourced state and deletion transitions therefore
157/// use uninhabited marker types whose `Body` is the actual state/deletion DTO.
158#[doc(hidden)]
159pub trait DomainEventContract {
160    /// Stable semantic event name.
161    const EVENT_NAME: &'static str;
162
163    /// Independently versioned semantic event version.
164    const EVENT_VERSION: u64;
165
166    /// Exact semantic event and body descriptor.
167    fn descriptor() -> DomainEventDescriptor;
168}
169
170/// Compile-time witness tying one exact event contract to its serialized body.
171///
172/// This separate generic witness preserves typed equality without exposing a
173/// private state DTO as an associated type on a public generated marker.
174#[doc(hidden)]
175pub trait DomainEventBodyContract<B: Serialize>: DomainEventContract {}
176
177/// Stable deletion body used when no live post-state exists.
178#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
179pub struct DomainDeletion<K> {
180    /// Logical aggregate/read-model key being deleted.
181    pub key: K,
182    /// Non-zero incarnation being deleted.
183    pub incarnation: u64,
184}
185
186impl<K> DomainDeletion<K> {
187    /// Construct a deletion body for a known live incarnation.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`DomainDeletionError`] when `incarnation` is zero.
192    pub fn new(key: K, incarnation: u64) -> Result<Self, DomainDeletionError> {
193        if incarnation == 0 {
194            return Err(DomainDeletionError);
195        }
196        Ok(Self { key, incarnation })
197    }
198}
199
200/// A deletion incarnation must be non-zero.
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub struct DomainDeletionError;
203
204impl fmt::Display for DomainDeletionError {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        formatter.write_str("domain deletion incarnation must be non-zero")
207    }
208}
209
210impl std::error::Error for DomainDeletionError {}