Skip to content

Latest commit

 

History

History
388 lines (300 loc) · 13.6 KB

File metadata and controls

388 lines (300 loc) · 13.6 KB

Phase 3: Serialization & Type Erasure - Implementation Plan

📋 Detailed Plan
Breaking down Phase 3 into three manageable, testable subtasks.

🎯 Phase 3 Overview

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.

📋 Implementation Subtasks

Subtask 1: Create Type Erasure Enum ✅

Purpose: Create an enum that can hold any typed Environment<S> for runtime handling.

Changes:

  • Add AnyEnvironmentState enum to src/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 AnyEnvironmentState for each state type
  • Verify Debug formatting works
  • Verify Clone works
  • 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)


Subtask 2: Implement Type Conversion Methods ✅

Purpose: Enable bidirectional conversion between typed Environment<S> and type-erased AnyEnvironmentState.

Changes:

  • Add into_any() method for each Environment<S> state (13 implementations)
  • Add try_into_<state>() methods on AnyEnvironmentState (13 methods)
  • Create StateTypeError for 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)

Subtask 3: Add State Introspection Helpers ✅

Purpose: Provide convenient methods to inspect and work with type-erased states without pattern matching.

Changes:

  • Add introspection methods to AnyEnvironmentState
  • Implement Display trait 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() returns Some(...) for error states, None for success states
  • Test Display formatting for success and error states
  • Test serialization/deserialization round-trip for all states

Success Criteria:

  • ✅ All helper methods work correctly for all 13 states
  • Display output 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)


🎯 Phase 3 Completion Criteria

All subtasks complete! Phase 3 is fully implemented:

  • AnyEnvironmentState enum 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 Display output
  • ✅ All existing functionality preserved (backward compatibility)
  • ✅ All linters passing
  • ✅ All tests passing (605 tests including ~100 new Phase 3 tests)

📊 Expected Test Coverage After Phase 3

  • 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

🔄 Integration with Phase 1 & Phase 2

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

🚀 What Comes After Phase 3

Once Phase 3 is complete, Phase 4 will use AnyEnvironmentState to:

  • Implement StateRepository trait 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

🔍 Design Decisions & Rationale

Why AnyEnvironmentState Enum Instead of Trait Objects?

Chosen: Enum with explicit variants
Alternative: Box<dyn EnvironmentState> trait object

Rationale:

  1. Serialization: Enums work seamlessly with serde
  2. Exhaustiveness: Compiler ensures all states are handled
  3. Performance: No dynamic dispatch overhead
  4. Type Safety: Pattern matching is exhaustive-checked
  5. ❌ Trait objects don't support serialization without custom machinery

Why try_into_<state>() Methods Instead of From Trait?

Chosen: Explicit methods with Result return
Alternative: Implement TryFrom trait

Rationale:

  1. Clarity: Method names are explicit about intent
  2. Error Handling: Returns descriptive errors
  3. Discoverability: IDE autocomplete shows all available conversions
  4. Flexibility: Easy to add custom validation logic per state

Why Helper Methods on AnyEnvironmentState?

Chosen: Methods like is_error_state(), error_details()
Alternative: Force users to pattern match

Rationale:

  1. Ergonomics: Common operations don't require pattern matching
  2. Consistency: Same logic applied everywhere
  3. Maintainability: Logic centralized in one place
  4. Testing: Helper methods are easily unit tested

📚 Related Documentation