diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 509f870..e3a4dbe 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -13,5 +13,5 @@ pub use authority::{Authority, AuthorityKind, AuthorityRef, NewAuthority}; pub use field_definition::{FieldDefinition, FieldType, NewFieldDefinition}; pub use id::{AuthorityId, FieldDefinitionId, ObjectId, OrgId, TermId, VocabularyId}; pub use label::{LocalizedLabel, pick_label}; -pub use object::{CatalogueObject, ObjectInput, Visibility}; +pub use object::{CatalogueObject, IllegalTransition, ObjectInput, Visibility}; pub use vocabulary::{NewTerm, Term, TermRef, Vocabulary}; diff --git a/crates/domain/src/object.rs b/crates/domain/src/object.rs index b8d1f47..41b1829 100644 --- a/crates/domain/src/object.rs +++ b/crates/domain/src/object.rs @@ -35,6 +35,52 @@ impl Visibility { } } +impl Visibility { + /// Whether `self` may move directly to `target`. Legal single steps are + /// `draft↔internal` and `internal↔public`; `draft↔public` is not one step. + pub fn can_transition_to(self, target: Visibility) -> bool { + use Visibility::*; + + matches!( + (self, target), + (Draft, Internal) | (Internal, Draft) | (Internal, Public) | (Public, Internal) + ) + } + + /// Validate a stepwise transition to `target`. Setting to the current value is an + /// idempotent no-op (`Ok`). A forbidden jump returns [`IllegalTransition`]. + pub fn transition_to(self, target: Visibility) -> Result { + if self == target || self.can_transition_to(target) { + Ok(target) + } else { + Err(IllegalTransition { + from: self, + to: target, + }) + } + } +} + +/// An attempted visibility change the state machine forbids. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IllegalTransition { + pub from: Visibility, + pub to: Visibility, +} + +impl std::fmt::Display for IllegalTransition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "illegal visibility transition: {} -> {}", + self.from.as_str(), + self.to.as_str() + ) + } +} + +impl std::error::Error for IllegalTransition {} + /// The mutable inventory-minimum fields of a catalogue object. #[derive(Debug, Clone, PartialEq)] pub struct ObjectInput { @@ -107,4 +153,39 @@ mod tests { ); } } + + #[test] + fn stepwise_transitions_are_legal() { + use Visibility::*; + assert_eq!(Draft.transition_to(Internal), Ok(Internal)); + assert_eq!(Internal.transition_to(Public), Ok(Public)); + assert_eq!(Public.transition_to(Internal), Ok(Internal)); + assert_eq!(Internal.transition_to(Draft), Ok(Draft)); + } + + #[test] + fn skipping_a_step_is_illegal() { + use Visibility::*; + assert_eq!( + Draft.transition_to(Public), + Err(IllegalTransition { + from: Draft, + to: Public + }) + ); + assert_eq!( + Public.transition_to(Draft), + Err(IllegalTransition { + from: Public, + to: Draft + }) + ); + } + + #[test] + fn setting_to_current_value_is_a_noop_ok() { + for v in [Visibility::Draft, Visibility::Internal, Visibility::Public] { + assert_eq!(v.transition_to(v), Ok(v)); + } + } }