#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DocumentState {
Draft = 0,
Review = 1,
Locked = 2,
Archived = 3,
}
impl DocumentState {
pub fn from_str_name(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"DRAFT" => Some(Self::Draft),
"REVIEW" => Some(Self::Review),
"LOCKED" => Some(Self::Locked),
"ARCHIVED" => Some(Self::Archived),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Draft => "DRAFT",
Self::Review => "REVIEW",
Self::Locked => "LOCKED",
Self::Archived => "ARCHIVED",
}
}
pub fn allowed_targets(&self) -> &'static [DocumentState] {
match self {
Self::Draft => &[Self::Review, Self::Locked],
Self::Review => &[Self::Draft, Self::Locked],
Self::Locked => &[Self::Archived],
Self::Archived => &[],
}
}
}
impl std::fmt::Display for DocumentState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn is_valid_transition(from: DocumentState, to: DocumentState) -> bool {
from.allowed_targets().contains(&to)
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn is_editable(state: DocumentState) -> bool {
matches!(state, DocumentState::Draft | DocumentState::Review)
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn is_terminal(state: DocumentState) -> bool {
state.allowed_targets().is_empty()
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn is_valid_transition_str(from: &str, to: &str) -> bool {
match (
DocumentState::from_str_name(from),
DocumentState::from_str_name(to),
) {
(Some(f), Some(t)) => is_valid_transition(f, t),
_ => false,
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn is_editable_str(state: &str) -> bool {
DocumentState::from_str_name(state).is_some_and(is_editable)
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn is_terminal_str(state: &str) -> bool {
DocumentState::from_str_name(state).is_some_and(is_terminal)
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn validate_transition(from: &str, to: &str) -> String {
let from_state = match DocumentState::from_str_name(from) {
Some(s) => s,
None => {
return serde_json::json!({
"valid": false,
"reason": format!("Unknown state: {from}"),
"allowedTargets": []
})
.to_string();
}
};
let to_state = match DocumentState::from_str_name(to) {
Some(s) => s,
None => {
return serde_json::json!({
"valid": false,
"reason": format!("Unknown state: {to}"),
"allowedTargets": from_state.allowed_targets().iter().map(|s| s.as_str()).collect::<Vec<_>>()
})
.to_string();
}
};
let allowed: Vec<&str> = from_state
.allowed_targets()
.iter()
.map(|s| s.as_str())
.collect();
if from_state == to_state {
return serde_json::json!({
"valid": false,
"reason": format!("Document is already in {} state", from_state),
"allowedTargets": allowed
})
.to_string();
}
if !is_valid_transition(from_state, to_state) {
let allowed_str = if allowed.is_empty() {
"none (terminal state)".to_string()
} else {
allowed.join(", ")
};
return serde_json::json!({
"valid": false,
"reason": format!("Cannot transition from {} to {}. Allowed: {}", from_state, to_state, allowed_str),
"allowedTargets": allowed
})
.to_string();
}
serde_json::json!({
"valid": true,
"allowedTargets": allowed
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_transitions() {
assert!(is_valid_transition(
DocumentState::Draft,
DocumentState::Review
));
assert!(is_valid_transition(
DocumentState::Draft,
DocumentState::Locked
));
assert!(is_valid_transition(
DocumentState::Review,
DocumentState::Draft
));
assert!(is_valid_transition(
DocumentState::Review,
DocumentState::Locked
));
assert!(is_valid_transition(
DocumentState::Locked,
DocumentState::Archived
));
}
#[test]
fn test_invalid_transitions() {
assert!(!is_valid_transition(
DocumentState::Draft,
DocumentState::Archived
));
assert!(!is_valid_transition(
DocumentState::Review,
DocumentState::Archived
));
assert!(!is_valid_transition(
DocumentState::Locked,
DocumentState::Draft
));
assert!(!is_valid_transition(
DocumentState::Locked,
DocumentState::Review
));
assert!(!is_valid_transition(
DocumentState::Archived,
DocumentState::Draft
));
assert!(!is_valid_transition(
DocumentState::Archived,
DocumentState::Review
));
assert!(!is_valid_transition(
DocumentState::Archived,
DocumentState::Locked
));
}
#[test]
fn test_self_transitions_invalid() {
assert!(!is_valid_transition(
DocumentState::Draft,
DocumentState::Draft
));
assert!(!is_valid_transition(
DocumentState::Review,
DocumentState::Review
));
assert!(!is_valid_transition(
DocumentState::Locked,
DocumentState::Locked
));
assert!(!is_valid_transition(
DocumentState::Archived,
DocumentState::Archived
));
}
#[test]
fn test_editable() {
assert!(is_editable(DocumentState::Draft));
assert!(is_editable(DocumentState::Review));
assert!(!is_editable(DocumentState::Locked));
assert!(!is_editable(DocumentState::Archived));
}
#[test]
fn test_terminal() {
assert!(!is_terminal(DocumentState::Draft));
assert!(!is_terminal(DocumentState::Review));
assert!(!is_terminal(DocumentState::Locked));
assert!(is_terminal(DocumentState::Archived));
}
#[test]
fn test_string_helpers() {
assert!(is_valid_transition_str("DRAFT", "REVIEW"));
assert!(is_valid_transition_str("draft", "review")); assert!(!is_valid_transition_str("DRAFT", "ARCHIVED"));
assert!(!is_valid_transition_str("DRAFT", "UNKNOWN"));
assert!(!is_valid_transition_str("UNKNOWN", "DRAFT"));
}
#[test]
fn test_editable_str() {
assert!(is_editable_str("DRAFT"));
assert!(is_editable_str("REVIEW"));
assert!(!is_editable_str("LOCKED"));
assert!(!is_editable_str("ARCHIVED"));
assert!(!is_editable_str("UNKNOWN"));
}
#[test]
fn test_terminal_str() {
assert!(!is_terminal_str("DRAFT"));
assert!(is_terminal_str("ARCHIVED"));
assert!(!is_terminal_str("UNKNOWN"));
}
#[test]
fn test_validate_transition_json() {
let result: serde_json::Value =
serde_json::from_str(&validate_transition("DRAFT", "REVIEW")).unwrap();
assert_eq!(result["valid"], true);
let result: serde_json::Value =
serde_json::from_str(&validate_transition("DRAFT", "ARCHIVED")).unwrap();
assert_eq!(result["valid"], false);
assert!(
result["reason"]
.as_str()
.unwrap()
.contains("Cannot transition")
);
let result: serde_json::Value =
serde_json::from_str(&validate_transition("DRAFT", "DRAFT")).unwrap();
assert_eq!(result["valid"], false);
assert!(result["reason"].as_str().unwrap().contains("already in"));
let result: serde_json::Value =
serde_json::from_str(&validate_transition("ARCHIVED", "DRAFT")).unwrap();
assert_eq!(result["valid"], false);
assert!(result["reason"].as_str().unwrap().contains("terminal"));
}
#[test]
fn test_from_str_name() {
assert_eq!(
DocumentState::from_str_name("DRAFT"),
Some(DocumentState::Draft)
);
assert_eq!(
DocumentState::from_str_name("draft"),
Some(DocumentState::Draft)
);
assert_eq!(
DocumentState::from_str_name("Draft"),
Some(DocumentState::Draft)
);
assert_eq!(
DocumentState::from_str_name("REVIEW"),
Some(DocumentState::Review)
);
assert_eq!(
DocumentState::from_str_name("LOCKED"),
Some(DocumentState::Locked)
);
assert_eq!(
DocumentState::from_str_name("ARCHIVED"),
Some(DocumentState::Archived)
);
assert_eq!(DocumentState::from_str_name("unknown"), None);
}
}