Download cloud-forge/crates/cloud-lifecycle/src/lib.rs from Snapkitty/tlm-jxcl-forge: direct link, hf CLI and curl.
- Browser
- Download file 4.8 kB
-
https://huggingface.co/Snapkitty/tlm-jxcl-forge/resolve/main/cloud-forge/crates/cloud-lifecycle/src/lib.rs
- Command line
-
hf download hf://Snapkitty/tlm-jxcl-forge/cloud-forge/crates/cloud-lifecycle/src/lib.rs
-
curl -L -o lib.rs https://huggingface.co/Snapkitty/tlm-jxcl-forge/resolve/main/cloud-forge/crates/cloud-lifecycle/src/lib.rs
4.8 kB
| // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE | |
| // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial | |
| // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms. | |
| //! The resource lifecycle state machine every `cloud-forge` resource | |
| //! goes through, and the transition table that governs it. | |
| //! | |
| //! This directly implements the roadmap's invariant | |
| //! **CLOUD-I003: deleted resources cannot silently reappear** -- | |
| //! [`Lifecycle::Deleted`] has no outgoing transitions at all, checked | |
| //! exhaustively below rather than left to convention. | |
| use cloud_errors::CloudError; | |
| /// A resource's lifecycle state. | |
| pub enum Lifecycle { | |
| Creating, | |
| Active, | |
| Updating, | |
| Deleting, | |
| Deleted, | |
| Failed, | |
| } | |
| impl Lifecycle { | |
| fn name(self) -> &'static str { | |
| match self { | |
| Lifecycle::Creating => "Creating", | |
| Lifecycle::Active => "Active", | |
| Lifecycle::Updating => "Updating", | |
| Lifecycle::Deleting => "Deleting", | |
| Lifecycle::Deleted => "Deleted", | |
| Lifecycle::Failed => "Failed", | |
| } | |
| } | |
| /// Whether moving from `self` to `to` is allowed. | |
| /// | |
| /// The table, spelled out rather than computed, so it is the | |
| /// single place anyone auditing this crate needs to read: | |
| /// | |
| /// - `Creating` -> `Active`, `Failed` | |
| /// - `Active` -> `Updating`, `Deleting`, `Failed` | |
| /// - `Updating` -> `Active`, `Failed` | |
| /// - `Deleting` -> `Deleted`, `Failed` | |
| /// - `Failed` -> `Deleting` (so a failed resource can still be torn down) | |
| /// - `Deleted` -> nothing; terminal. | |
| pub fn can_transition_to(self, to: Lifecycle) -> bool { | |
| use Lifecycle::*; | |
| matches!( | |
| (self, to), | |
| (Creating, Active) | |
| | (Creating, Failed) | |
| | (Active, Updating) | |
| | (Active, Deleting) | |
| | (Active, Failed) | |
| | (Updating, Active) | |
| | (Updating, Failed) | |
| | (Deleting, Deleted) | |
| | (Deleting, Failed) | |
| | (Failed, Deleting) | |
| ) | |
| } | |
| /// Attempts the transition, returning [`CloudError::InvalidTransition`] | |
| /// if it isn't allowed. | |
| pub fn transition_to(self, to: Lifecycle) -> Result<Lifecycle, CloudError> { | |
| if self.can_transition_to(to) { | |
| Ok(to) | |
| } else { | |
| Err(CloudError::InvalidTransition { | |
| what: "lifecycle", | |
| from: self.name().to_string(), | |
| to: to.name().to_string(), | |
| }) | |
| } | |
| } | |
| /// A terminal state has no outgoing transitions at all. | |
| pub fn is_terminal(self) -> bool { | |
| self == Lifecycle::Deleted | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| use Lifecycle::*; | |
| const ALL: [Lifecycle; 6] = [Creating, Active, Updating, Deleting, Deleted, Failed]; | |
| fn deleted_is_the_only_terminal_state() { | |
| for state in ALL { | |
| assert_eq!(state.is_terminal(), state == Deleted); | |
| } | |
| } | |
| fn deleted_has_no_outgoing_transitions() { | |
| for target in ALL { | |
| assert!( | |
| !Deleted.can_transition_to(target), | |
| "Deleted -> {target:?} should be rejected" | |
| ); | |
| } | |
| } | |
| fn the_happy_path_creating_through_deleted_is_allowed() { | |
| let mut state = Creating; | |
| for next in [Active, Updating, Active, Deleting, Deleted] { | |
| state = state.transition_to(next).unwrap(); | |
| } | |
| assert_eq!(state, Deleted); | |
| } | |
| fn a_failed_resource_can_still_be_torn_down() { | |
| assert!(Creating.can_transition_to(Failed)); | |
| assert!(Failed.can_transition_to(Deleting)); | |
| assert!(Deleting.can_transition_to(Deleted)); | |
| } | |
| fn creating_cannot_skip_straight_to_deleting_or_deleted() { | |
| assert!(!Creating.can_transition_to(Deleting)); | |
| assert!(!Creating.can_transition_to(Deleted)); | |
| } | |
| fn active_cannot_go_back_to_creating() { | |
| assert!(!Active.can_transition_to(Creating)); | |
| } | |
| fn transition_to_returns_a_descriptive_error_on_rejection() { | |
| let err = Deleted.transition_to(Active).unwrap_err(); | |
| let message = err.to_string(); | |
| assert!(message.contains("Deleted")); | |
| assert!(message.contains("Active")); | |
| } | |
| fn no_state_can_transition_to_itself() { | |
| for state in ALL { | |
| assert!( | |
| !state.can_transition_to(state), | |
| "{state:?} -> itself should not be a no-op transition" | |
| ); | |
| } | |
| } | |
| } | |