Skip to main content

made_core/entities/ceremony_instance/decisions/
start.rs

1use time::{Duration, OffsetDateTime};
2
3use crate::entities::ceremony_events::{CeremonyInstanceStarted, MemoryRecalled};
4use crate::entities::{
5    CeremonyDefinition, CeremonyEvent, CeremonyInstance, PublishedCeremonyDefinition,
6};
7use crate::error::DomainError;
8use crate::value_objects::{
9    BudgetAccountId, CeremonyContext, CeremonyDeadline, CeremonyDefinitionDigest, CeremonyId,
10    CeremonyLineage, SessionRecollection, StateDeadline, StateVisit,
11};
12
13impl CeremonyInstance {
14    /// The opening of a ceremony run from a definition supplied for it.
15    ///
16    /// A constructor rather than a command: there is no instance yet
17    /// to decide against. Required inputs are checked against this run's
18    /// context before any opening event is built. The event carries everything
19    /// [`Self::from_started`] needs to open the same instance without
20    /// the definition in hand.
21    ///
22    /// A batch rather than one event, because an opening is sometimes
23    /// two facts: what was started, and what it was told. See
24    /// [`Self::opening_batch`].
25    pub fn decide_start(
26        id: CeremonyId,
27        definition: &CeremonyDefinition,
28        context: CeremonyContext,
29        recollection: Option<SessionRecollection>,
30        now: OffsetDateTime,
31    ) -> Result<Vec<CeremonyEvent>, DomainError> {
32        Ok(Self::opening_batch(
33            Self::opening(id, definition, context, now, None, None, None)?,
34            recollection,
35            now,
36        ))
37    }
38
39    /// The opening of a ceremony bound to a published definition, its
40    /// digest recorded so a later reader can check which one ran.
41    pub fn decide_start_bound(
42        id: CeremonyId,
43        published: &PublishedCeremonyDefinition,
44        context: CeremonyContext,
45        recollection: Option<SessionRecollection>,
46        now: OffsetDateTime,
47    ) -> Result<Vec<CeremonyEvent>, DomainError> {
48        Ok(Self::opening_batch(
49            Self::opening(
50                id,
51                published.definition(),
52                context,
53                now,
54                Some(published.digest()),
55                None,
56                None,
57            )?,
58            recollection,
59            now,
60        ))
61    }
62
63    /// Open a published child using the exact recollection and lineage sealed by its parent plan.
64    pub fn decide_start_bound_child(
65        id: CeremonyId,
66        published: &PublishedCeremonyDefinition,
67        context: CeremonyContext,
68        lineage: CeremonyLineage,
69        recollection: Option<SessionRecollection>,
70        now: OffsetDateTime,
71    ) -> Result<Vec<CeremonyEvent>, DomainError> {
72        Ok(Self::opening_batch(
73            Self::opening(
74                id,
75                published.definition(),
76                context,
77                now,
78                Some(published.digest()),
79                Some(lineage),
80                None,
81            )?,
82            recollection,
83            now,
84        ))
85    }
86
87    /// Open a root ceremony and seal the shared ledger account for its whole tree.
88    pub fn decide_start_bound_budgeted(
89        id: CeremonyId,
90        published: &PublishedCeremonyDefinition,
91        context: CeremonyContext,
92        budget_account_id: BudgetAccountId,
93        recollection: Option<SessionRecollection>,
94        now: OffsetDateTime,
95    ) -> Result<Vec<CeremonyEvent>, DomainError> {
96        Ok(Self::opening_batch(
97            Self::opening(
98                id,
99                published.definition(),
100                context,
101                now,
102                Some(published.digest()),
103                None,
104                Some(budget_account_id),
105            )?,
106            recollection,
107            now,
108        ))
109    }
110
111    /// Open a child with the exact shared ledger account sealed by its parent plan.
112    pub fn decide_start_bound_budgeted_child(
113        id: CeremonyId,
114        published: &PublishedCeremonyDefinition,
115        context: CeremonyContext,
116        lineage: CeremonyLineage,
117        budget_account_id: BudgetAccountId,
118        recollection: Option<SessionRecollection>,
119        now: OffsetDateTime,
120    ) -> Result<Vec<CeremonyEvent>, DomainError> {
121        Ok(Self::opening_batch(
122            Self::opening(
123                id,
124                published.definition(),
125                context,
126                now,
127                Some(published.digest()),
128                Some(lineage),
129                Some(budget_account_id),
130            )?,
131            recollection,
132            now,
133        ))
134    }
135
136    /// The opening, and the recollection when there was one.
137    ///
138    /// The recollection is decided outside — reading memory is IO, and
139    /// `decide` stays pure — and handed in. What is decided here is
140    /// whether it becomes a fact: a recollection that came back empty
141    /// appends nothing, so a session with nothing to recall, which is
142    /// every session that declares no scope, has exactly the stream it
143    /// had before memory could be read at all.
144    fn opening_batch(
145        opening: CeremonyInstanceStarted,
146        recollection: Option<SessionRecollection>,
147        now: OffsetDateTime,
148    ) -> Vec<CeremonyEvent> {
149        let mut events = vec![CeremonyEvent::CeremonyInstanceStarted(opening)];
150        if let Some(recollection) = recollection.filter(|recalled| !recalled.is_empty()) {
151            events.push(CeremonyEvent::MemoryRecalled(MemoryRecalled {
152                recollection,
153                recalled_at: now,
154            }));
155        }
156        events
157    }
158
159    /// What starting derives from the definition: the initial state
160    /// and the steps that get a pending record.
161    pub(in crate::entities::ceremony_instance) fn opening(
162        id: CeremonyId,
163        definition: &CeremonyDefinition,
164        context: CeremonyContext,
165        now: OffsetDateTime,
166        bound_definition: Option<CeremonyDefinitionDigest>,
167        lineage: Option<CeremonyLineage>,
168        budget_account_id: Option<BudgetAccountId>,
169    ) -> Result<CeremonyInstanceStarted, DomainError> {
170        let missing = definition
171            .inputs()
172            .values()
173            .filter(|input| input.requirement().is_required())
174            .filter(|input| context.attributes().get(input.name().as_str()).is_none())
175            .map(|input| input.name().as_str())
176            .collect::<Vec<_>>();
177        if !missing.is_empty() {
178            return Err(DomainError::InvalidDocument {
179                reason: format!("missing required ceremony inputs: {}", missing.join(", ")),
180            });
181        }
182        let ceremony_deadline = definition
183            .ceremony_timeout()
184            .map(|timeout| checked_deadline(now, timeout.duration(), "ceremony_deadline"))
185            .transpose()?
186            .map(CeremonyDeadline::new);
187        let state_deadline = definition
188            .state_timeout()
189            .map(|timeout| checked_deadline(now, timeout.duration(), "state_deadline"))
190            .transpose()?
191            .map(|at| {
192                StateDeadline::new(definition.initial_state_id().clone(), StateVisit::FIRST, at)
193            });
194        Ok(CeremonyInstanceStarted {
195            ceremony_id: id,
196            definition_name: definition.name().clone(),
197            definition_version: definition.version().clone(),
198            initial_state: definition.initial_state_id().clone(),
199            step_ids: definition.steps().keys().cloned().collect(),
200            context,
201            bound_definition,
202            lineage,
203            budget_account_id,
204            ceremony_deadline,
205            state_deadline,
206            created_at: now,
207        })
208    }
209}
210
211pub(crate) fn checked_deadline(
212    now: OffsetDateTime,
213    duration: crate::value_objects::DurationMs,
214    field: &'static str,
215) -> Result<OffsetDateTime, DomainError> {
216    let millis = i64::try_from(duration.get()).map_err(|_| DomainError::OutOfRange {
217        field,
218        value: duration.get() as f64,
219        min: 0.0,
220        max: i64::MAX as f64,
221    })?;
222    now.checked_add(Duration::milliseconds(millis))
223        .ok_or(DomainError::OutOfRange {
224            field,
225            value: duration.get() as f64,
226            min: 0.0,
227            max: i64::MAX as f64,
228        })
229}