📋 Detailed Plan
Breaking down Phase 3 into three manageable, testable subtasks.
Goal: Enable runtime handling of typed Environment<S> states through type erasure, enabling serialization, storage, and dynamic state inspection.
Why We Need This: While Environment<S> provides compile-time type safety, we need to:
- Store environments to disk (JSON files) without knowing their state at compile time
- Load environments from disk and restore their typed state
- Inspect environment state at runtime (for status commands, logging, error handling)
- Pass environments through interfaces that can't be generic (trait objects, serialization)
Approach: Create AnyEnvironmentState enum that can hold any Environment<S> at runtime, with bidirectional conversion methods.
Purpose: Create an enum that can hold any typed Environment<S> for runtime handling.
Changes:
- Add
AnyEnvironmentStateenum tosrc/domain/environment_state.rs - One variant per state type (13 total variants)
- Derive serialization traits
Implementation Details:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnyEnvironmentState {
Created(Environment<Created>),
Provisioning(Environment<Provisioning>),
Provisioned(Environment<Provisioned>),
Configuring(Environment<Configuring>),
Configured(Environment<Configured>),
Releasing(Environment<Releasing>),
Released(Environment<Released>),
Running(Environment<Running>),
ProvisionFailed(Environment<ProvisionFailed>),
ConfigureFailed(Environment<ConfigureFailed>),
ReleaseFailed(Environment<ReleaseFailed>),
RunFailed(Environment<RunFailed>),
Destroyed(Environment<Destroyed>),
}Tests to Add:
- Create
AnyEnvironmentStatefor each state type - Verify
Debugformatting works - Verify
Cloneworks - Basic serialization test (one variant to start)
Success Criteria:
- ✅ Enum compiles with all variants
- ✅ Can create instances for all state types
- ✅ Serialization derives work
- ✅ All linters pass
- ✅ All tests pass
Status: ✅ Completed
Implementation: Located in src/domain/environment/state/mod.rs (lines 147-186)
Purpose: Enable bidirectional conversion between typed Environment<S> and type-erased AnyEnvironmentState.
Changes:
- Add
into_any()method for eachEnvironment<S>state (13 implementations) - Add
try_into_<state>()methods onAnyEnvironmentState(13 methods) - Create
StateTypeErrorfor invalid conversions - Move implementations to
src/domain/environment.rs
Implementation Details:
// In src/domain/environment.rs
// Type erasure: typed -> erased
impl Environment<Created> {
pub fn into_any(self) -> AnyEnvironmentState {
AnyEnvironmentState::Created(self)
}
}
impl Environment<Provisioning> {
pub fn into_any(self) -> AnyEnvironmentState {
AnyEnvironmentState::Provisioning(self)
}
}
// ... repeat for all 13 states
// Type restoration: erased -> typed
impl AnyEnvironmentState {
pub fn try_into_created(self) -> Result<Environment<Created>, StateTypeError> {
match self {
AnyEnvironmentState::Created(env) => Ok(env),
other => Err(StateTypeError::UnexpectedState {
expected: "created",
actual: other.state_name().to_string(),
}),
}
}
pub fn try_into_provisioning(self) -> Result<Environment<Provisioning>, StateTypeError> {
match self {
AnyEnvironmentState::Provisioning(env) => Ok(env),
other => Err(StateTypeError::UnexpectedState {
expected: "provisioning",
actual: other.state_name().to_string(),
}),
}
}
// ... repeat for all 13 states
}
// In src/domain/environment_state.rs
#[derive(Debug, Error)]
pub enum StateTypeError {
#[error("Expected state '{expected}', but found '{actual}'")]
UnexpectedState {
expected: &'static str,
actual: String,
},
}Tests to Add:
- Test
into_any()for all 13 state types - Test
try_into_<state>()success for all 13 state types - Test
try_into_<state>()failure cases (wrong state type) - Test error messages are clear and helpful
- Test round-trip conversion preserves all data
Success Criteria:
- ✅ All 13
into_any()methods work correctly - ✅ All 13
try_into_<state>()methods work correctly - ✅ Invalid conversions return clear error messages
- ✅ Round-trip conversion (typed → erased → typed) preserves all data
- ✅ All linters pass
- ✅ All tests pass
Status: ✅ Completed
Implementation:
into_any()methods in individual state files (e.g.,src/domain/environment/state/provisioning.rs)try_into_<state>()methods in state files (e.g.,src/domain/environment/state/provisioning.rs)
Purpose: Provide convenient methods to inspect and work with type-erased states without pattern matching.
Changes:
- Add introspection methods to
AnyEnvironmentState - Implement
Displaytrait for user-friendly output - Add helper predicates for state categorization
Implementation Details:
// In src/domain/environment_state.rs
impl AnyEnvironmentState {
/// Get the environment name regardless of state
pub fn name(&self) -> &EnvironmentName {
match self {
AnyEnvironmentState::Created(env) => env.name(),
AnyEnvironmentState::Provisioning(env) => env.name(),
// ... repeat for all states
}
}
/// Get the state name as a string
pub fn state_name(&self) -> &'static str {
match self {
AnyEnvironmentState::Created(_) => "created",
AnyEnvironmentState::Provisioning(_) => "provisioning",
AnyEnvironmentState::Provisioned(_) => "provisioned",
AnyEnvironmentState::Configuring(_) => "configuring",
AnyEnvironmentState::Configured(_) => "configured",
AnyEnvironmentState::Releasing(_) => "releasing",
AnyEnvironmentState::Released(_) => "released",
AnyEnvironmentState::Running(_) => "running",
AnyEnvironmentState::ProvisionFailed(_) => "provision_failed",
AnyEnvironmentState::ConfigureFailed(_) => "configure_failed",
AnyEnvironmentState::ReleaseFailed(_) => "release_failed",
AnyEnvironmentState::RunFailed(_) => "run_failed",
AnyEnvironmentState::Destroyed(_) => "destroyed",
}
}
/// Check if state represents a success (non-error) state
pub fn is_success_state(&self) -> bool {
matches!(
self,
AnyEnvironmentState::Created(_)
| AnyEnvironmentState::Provisioning(_)
| AnyEnvironmentState::Provisioned(_)
| AnyEnvironmentState::Configuring(_)
| AnyEnvironmentState::Configured(_)
| AnyEnvironmentState::Releasing(_)
| AnyEnvironmentState::Released(_)
| AnyEnvironmentState::Running(_)
| AnyEnvironmentState::Destroyed(_)
)
}
/// Check if state represents an error state
pub fn is_error_state(&self) -> bool {
matches!(
self,
AnyEnvironmentState::ProvisionFailed(_)
| AnyEnvironmentState::ConfigureFailed(_)
| AnyEnvironmentState::ReleaseFailed(_)
| AnyEnvironmentState::RunFailed(_)
)
}
/// Check if state is terminal (final state, no more transitions expected)
pub fn is_terminal_state(&self) -> bool {
matches!(
self,
AnyEnvironmentState::Running(_)
| AnyEnvironmentState::Destroyed(_)
| AnyEnvironmentState::ProvisionFailed(_)
| AnyEnvironmentState::ConfigureFailed(_)
| AnyEnvironmentState::ReleaseFailed(_)
| AnyEnvironmentState::RunFailed(_)
)
}
/// Get error details if in an error state
pub fn error_details(&self) -> Option<&str> {
match self {
AnyEnvironmentState::ProvisionFailed(env) => Some(&env.state().failed_step),
AnyEnvironmentState::ConfigureFailed(env) => Some(&env.state().failed_step),
AnyEnvironmentState::ReleaseFailed(env) => Some(&env.state().failed_step),
AnyEnvironmentState::RunFailed(env) => Some(&env.state().failed_step),
_ => None,
}
}
}
impl std::fmt::Display for AnyEnvironmentState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Environment '{}' is in state: {}", self.name().as_str(), self.state_name())?;
if let Some(error_details) = self.error_details() {
write!(f, " (failed at: {})", error_details)?;
}
Ok(())
}
}Tests to Add:
- Test
name()returns correct name for all states - Test
state_name()returns correct string for all states - Test
is_success_state()returns true for success states, false for error states - Test
is_error_state()returns true for error states, false for success states - Test
is_terminal_state()correctly identifies terminal states - Test
error_details()returnsSome(...)for error states,Nonefor success states - Test
Displayformatting for success and error states - Test serialization/deserialization round-trip for all states
Success Criteria:
- ✅ All helper methods work correctly for all 13 states
- ✅
Displayoutput is clear and user-friendly - ✅ Full serialization/deserialization round-trip works
- ✅ Pattern matching is only needed for type restoration, not inspection
- ✅ All linters pass
- ✅ All tests pass
Status: ✅ Completed
Implementation: Located in src/domain/environment/state/mod.rs (lines 188-342 for introspection, Display trait)
✅ All subtasks complete! Phase 3 is fully implemented:
- ✅
AnyEnvironmentStateenum that can hold all 13 typed states - ✅ Bidirectional conversion:
Environment<S>↔AnyEnvironmentState - ✅ State introspection without pattern matching
- ✅ Full serialization/deserialization support
- ✅ Clear, actionable error messages for invalid type conversions
- ✅ User-friendly
Displayoutput - ✅ All existing functionality preserved (backward compatibility)
- ✅ All linters passing
- ✅ All tests passing (605 tests including ~100 new Phase 3 tests)
- Subtask 1: +13 tests (one per state type for basic creation)
- Subtask 2: +39 tests (13 into_any + 13 successful try_into + 13 failed try_into)
- Subtask 3: +50+ tests (introspection methods × 13 states + Display + serialization)
- Total New Tests: ~100 tests
- Total Project Tests: ~570 tests
Phase 3 builds directly on Phase 1 and Phase 2:
- Uses the state marker types from Phase 1 (
Created,Provisioning, etc.) - Uses the generic
Environment<S>from Phase 1 - Benefits from Phase 2 state transition logging for observability
- Preserves all type-safe transitions from Phase 1
- Adds runtime capability without breaking compile-time safety
Once Phase 3 is complete, Phase 4 will use AnyEnvironmentState to:
- Implement
StateRepositorytrait for persistence - Save/load environments to/from JSON files
- Handle atomic writes for data integrity
- Implement file locking mechanism with process ID tracking
- Support concurrent access with lock files
Chosen: Enum with explicit variants
Alternative: Box<dyn EnvironmentState> trait object
Rationale:
- ✅ Serialization: Enums work seamlessly with serde
- ✅ Exhaustiveness: Compiler ensures all states are handled
- ✅ Performance: No dynamic dispatch overhead
- ✅ Type Safety: Pattern matching is exhaustive-checked
- ❌ Trait objects don't support serialization without custom machinery
Chosen: Explicit methods with Result return
Alternative: Implement TryFrom trait
Rationale:
- ✅ Clarity: Method names are explicit about intent
- ✅ Error Handling: Returns descriptive errors
- ✅ Discoverability: IDE autocomplete shows all available conversions
- ✅ Flexibility: Easy to add custom validation logic per state
Chosen: Methods like is_error_state(), error_details()
Alternative: Force users to pattern match
Rationale:
- ✅ Ergonomics: Common operations don't require pattern matching
- ✅ Consistency: Same logic applied everywhere
- ✅ Maintainability: Logic centralized in one place
- ✅ Testing: Helper methods are easily unit tested
- Technical: Type Erasure Pattern - Detailed explanation of the pattern and how we use it
- Decision: Type Erasure for Environment States - Why we chose this approach
- Phase 1 Plan - Foundation with type-state pattern
- Main Feature Spec - Overall feature goals and motivation
- Error Handling Guide - Error handling principles
- Testing Conventions - Testing best practices