1use std::collections::BTreeMap;
12
13use crate::error::DomainError;
14use crate::value_objects::{
15 CeremonyDescription, CeremonyGuard, CeremonyInputDefinition, CeremonyName,
16 CeremonyOutputDefinition, CeremonyRole, CeremonyState, CeremonyStep, CeremonyTransition,
17 CeremonyValidationFinding, CeremonyValidationLocus, CeremonyValidationReport, CeremonyVersion,
18};
19
20use super::ceremony_definition_analysis::CeremonyDefinitionParts;
21use super::CeremonyDefinition;
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct CeremonyDefinitionDraft {
25 name: CeremonyName,
26 version: CeremonyVersion,
27 description: Option<CeremonyDescription>,
28 inputs: Vec<CeremonyInputDefinition>,
29 outputs: Vec<CeremonyOutputDefinition>,
30 states: Vec<CeremonyState>,
31 transitions: Vec<CeremonyTransition>,
32 steps: Vec<CeremonyStep>,
33 guards: Vec<CeremonyGuard>,
34 roles: Vec<CeremonyRole>,
35}
36
37impl CeremonyDefinitionDraft {
38 #[allow(clippy::too_many_arguments)]
43 #[must_use]
44 pub fn new(
45 name: CeremonyName,
46 version: CeremonyVersion,
47 description: Option<CeremonyDescription>,
48 inputs: impl IntoIterator<Item = CeremonyInputDefinition>,
49 outputs: impl IntoIterator<Item = CeremonyOutputDefinition>,
50 states: impl IntoIterator<Item = CeremonyState>,
51 transitions: impl IntoIterator<Item = CeremonyTransition>,
52 steps: impl IntoIterator<Item = CeremonyStep>,
53 guards: impl IntoIterator<Item = CeremonyGuard>,
54 roles: impl IntoIterator<Item = CeremonyRole>,
55 ) -> Self {
56 Self {
57 name,
58 version,
59 description,
60 inputs: inputs.into_iter().collect(),
61 outputs: outputs.into_iter().collect(),
62 states: states.into_iter().collect(),
63 transitions: transitions.into_iter().collect(),
64 steps: steps.into_iter().collect(),
65 guards: guards.into_iter().collect(),
66 roles: roles.into_iter().collect(),
67 }
68 }
69
70 #[must_use]
71 pub fn name(&self) -> &CeremonyName {
72 &self.name
73 }
74
75 #[must_use]
76 pub fn version(&self) -> &CeremonyVersion {
77 &self.version
78 }
79
80 #[must_use]
81 pub fn description(&self) -> Option<&CeremonyDescription> {
82 self.description.as_ref()
83 }
84
85 #[must_use]
86 pub fn states(&self) -> &[CeremonyState] {
87 &self.states
88 }
89
90 #[must_use]
91 pub fn transitions(&self) -> &[CeremonyTransition] {
92 &self.transitions
93 }
94
95 #[must_use]
96 pub fn steps(&self) -> &[CeremonyStep] {
97 &self.steps
98 }
99
100 #[must_use]
101 pub fn guards(&self) -> &[CeremonyGuard] {
102 &self.guards
103 }
104
105 #[must_use]
106 pub fn roles(&self) -> &[CeremonyRole] {
107 &self.roles
108 }
109
110 #[must_use]
118 pub fn analyze(&self) -> CeremonyValidationReport {
119 let mut findings = Vec::new();
120
121 let (_, duplicate_inputs) = index(&self.inputs, CeremonyInputDefinition::name);
122 push_duplicates(
123 &mut findings,
124 duplicate_inputs,
125 "ceremony_input",
126 CeremonyValidationLocus::input,
127 );
128
129 let (_, duplicate_outputs) = index(&self.outputs, CeremonyOutputDefinition::name);
130 push_duplicates(
131 &mut findings,
132 duplicate_outputs,
133 "ceremony_output",
134 CeremonyValidationLocus::output,
135 );
136
137 let (states, duplicate_states) = index(&self.states, CeremonyState::id);
138 push_duplicates(
139 &mut findings,
140 duplicate_states,
141 "ceremony_state",
142 CeremonyValidationLocus::state,
143 );
144
145 let (steps, duplicate_steps) = index(&self.steps, CeremonyStep::id);
146 push_duplicates(
147 &mut findings,
148 duplicate_steps,
149 "ceremony_step",
150 CeremonyValidationLocus::step,
151 );
152
153 let (guards, duplicate_guards) = index(&self.guards, CeremonyGuard::name);
154 push_duplicates(
155 &mut findings,
156 duplicate_guards,
157 "ceremony_guard",
158 CeremonyValidationLocus::guard,
159 );
160
161 let (roles, duplicate_roles) = index(&self.roles, CeremonyRole::id);
162 push_duplicates(
163 &mut findings,
164 duplicate_roles,
165 "ceremony_role",
166 CeremonyValidationLocus::role,
167 );
168
169 CeremonyDefinitionParts {
170 states: &states,
171 transitions: &self.transitions,
172 steps: &steps,
173 guards: &guards,
174 roles: &roles,
175 }
176 .collect_findings(&mut findings);
177
178 CeremonyValidationReport::new(findings)
179 }
180
181 pub fn publish(self) -> Result<CeremonyDefinition, DomainError> {
186 CeremonyDefinition::new(
187 self.name,
188 self.version,
189 self.description,
190 self.inputs,
191 self.outputs,
192 self.states,
193 self.transitions,
194 self.steps,
195 self.guards,
196 self.roles,
197 )
198 }
199}
200
201fn index<'a, T, K>(items: &'a [T], key: impl Fn(&'a T) -> &'a K) -> (BTreeMap<K, T>, Vec<K>)
204where
205 T: Clone,
206 K: Clone + Ord + 'a,
207{
208 let mut indexed = BTreeMap::new();
209 let mut duplicates = Vec::new();
210 for item in items {
211 let item_key = key(item).clone();
212 if indexed.contains_key(&item_key) {
213 duplicates.push(item_key);
214 continue;
215 }
216 indexed.insert(item_key, item.clone());
217 }
218 (indexed, duplicates)
219}
220
221fn push_duplicates<K>(
222 findings: &mut Vec<CeremonyValidationFinding>,
223 duplicates: Vec<K>,
224 what: &'static str,
225 locus: impl Fn(K) -> CeremonyValidationLocus,
226) {
227 for key in duplicates {
228 findings.push(CeremonyValidationFinding::error(
229 locus(key),
230 DomainError::AlreadyExists { what },
231 ));
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::value_objects::{
239 GuardCondition, RetryPolicy, RoleAction, RoleId, StateId, StepHandlerConfig,
240 StepHandlerKind, StepId, StepStatus, TransitionTrigger,
241 };
242
243 fn state_id(raw: &str) -> StateId {
244 StateId::new(raw).unwrap()
245 }
246
247 fn step_id(raw: &str) -> StepId {
248 StepId::new(raw).unwrap()
249 }
250
251 fn trigger(raw: &str) -> TransitionTrigger {
252 TransitionTrigger::new(raw).unwrap()
253 }
254
255 fn step(raw_step_id: &str, raw_state_id: &str) -> CeremonyStep {
256 CeremonyStep::new(
257 step_id(raw_step_id),
258 state_id(raw_state_id),
259 StepHandlerKind::new("manual_review").unwrap(),
260 StepHandlerConfig::empty(),
261 RetryPolicy::single_attempt(),
262 None,
263 )
264 }
265
266 fn draft(
267 states: Vec<CeremonyState>,
268 transitions: Vec<CeremonyTransition>,
269 steps: Vec<CeremonyStep>,
270 guards: Vec<CeremonyGuard>,
271 roles: Vec<CeremonyRole>,
272 ) -> CeremonyDefinitionDraft {
273 CeremonyDefinitionDraft::new(
274 CeremonyName::new("planning_ceremony").unwrap(),
275 CeremonyVersion::v1(),
276 None,
277 Vec::new(),
278 Vec::new(),
279 states,
280 transitions,
281 steps,
282 guards,
283 roles,
284 )
285 }
286
287 fn three_defect_draft() -> CeremonyDefinitionDraft {
288 draft(
289 vec![
290 CeremonyState::initial(state_id("drafting")),
291 CeremonyState::terminal(state_id("done")),
292 ],
293 vec![CeremonyTransition::new(
294 state_id("drafting"),
295 state_id("nowhere"),
296 trigger("finish"),
297 Vec::new(),
298 )
299 .unwrap()],
300 Vec::new(),
301 vec![CeremonyGuard::new(
302 crate::value_objects::GuardName::new("plan_done").unwrap(),
303 GuardCondition::StepStatus {
304 step_id: step_id("missing"),
305 status: StepStatus::Completed,
306 },
307 )],
308 vec![CeremonyRole::new(
309 RoleId::new("facilitator").unwrap(),
310 vec![RoleAction::step(step_id("missing"))],
311 )
312 .unwrap()],
313 )
314 }
315
316 #[test]
317 fn a_draft_reports_every_defect_at_once() {
318 let report = three_defect_draft().analyze();
319 let errors = report.errors().collect::<Vec<_>>();
320
321 assert!(!report.is_valid());
322 assert_eq!(errors.len(), 3, "found: {errors:?}");
323 assert_eq!(
324 errors
325 .iter()
326 .map(|finding| finding.defect().clone())
327 .collect::<Vec<_>>(),
328 vec![
329 DomainError::NotFound {
330 what: "ceremony_transition.to_state"
331 },
332 DomainError::NotFound {
333 what: "ceremony_guard.step"
334 },
335 DomainError::NotFound {
336 what: "ceremony_role.step_action"
337 },
338 ]
339 );
340 }
341
342 #[test]
343 fn duplicate_declarations_are_reported_instead_of_aborting_the_analysis() {
344 let report = draft(
345 vec![
346 CeremonyState::initial(state_id("drafting")),
347 CeremonyState::terminal(state_id("done")),
348 ],
349 vec![CeremonyTransition::new(
350 state_id("drafting"),
351 state_id("done"),
352 trigger("finish"),
353 Vec::new(),
354 )
355 .unwrap()],
356 vec![step("plan", "drafting"), step("plan", "drafting")],
357 Vec::new(),
358 Vec::new(),
359 )
360 .analyze();
361 let errors = report.errors().collect::<Vec<_>>();
362
363 assert_eq!(errors.len(), 1);
364 assert_eq!(
365 errors[0].defect(),
366 &DomainError::AlreadyExists {
367 what: "ceremony_step"
368 }
369 );
370 assert_eq!(
371 errors[0].locus(),
372 &CeremonyValidationLocus::step(step_id("plan"))
373 );
374 }
375
376 #[test]
377 fn publishing_fails_with_exactly_the_first_blocking_finding() {
378 let draft = three_defect_draft();
379 let expected = draft
380 .analyze()
381 .first_error()
382 .expect("a blocking finding")
383 .defect()
384 .clone();
385
386 let error = draft.publish().unwrap_err();
387
388 assert_eq!(error, expected);
389 }
390
391 #[test]
392 fn a_clean_draft_publishes() {
393 let draft = draft(
394 vec![
395 CeremonyState::initial(state_id("drafting")),
396 CeremonyState::terminal(state_id("done")),
397 ],
398 vec![CeremonyTransition::new(
399 state_id("drafting"),
400 state_id("done"),
401 trigger("finish"),
402 Vec::new(),
403 )
404 .unwrap()],
405 vec![step("plan", "drafting")],
406 Vec::new(),
407 Vec::new(),
408 );
409
410 assert!(draft.analyze().is_valid());
411
412 let definition = draft.publish().expect("a clean draft must publish");
413
414 assert_eq!(definition.initial_state_id(), &state_id("drafting"));
415 assert!(definition.analyze().findings().is_empty());
416 }
417}