Skip to content

[Subissue 2/7] Application Layer CreateCommand #36

@josecelano

Description

@josecelano

Overview

Implement the delivery-agnostic CreateCommand in the application layer that orchestrates environment creation business logic. This command receives clean domain objects and coordinates between domain validation and infrastructure services.

Parent EPIC: #34 - Implement Create Environment Command
Depends On: #35 (Configuration Infrastructure)
Related: Full Specification

Key Architecture Points

  • Command is synchronous (not async) following existing patterns
  • Uses existing Environment::new() directly - no create_from_config() method
  • Repository handles directory creation during save() for atomicity
  • All errors implement .help() methods with detailed troubleshooting

Goals

  • Create synchronous CreateCommand following existing patterns

    • Dependency injection: Arc<dyn EnvironmentRepository>, Arc<dyn Clock>
    • Execute signature: fn execute(...) -> Result<Environment<Created>, CreateCommandError>
    • Use Environment::new(environment_name, ssh_credentials, ssh_port) directly
    • Repository handles directory creation - don't create in command
    • Check if environment already exists before creation
    • Persist via repository.save(&environment.into_any())
    • Delivery-agnostic - works with CLI, REST API, or any delivery mechanism
  • Add command error enum with .help() methods

    • CreateCommandError following ProvisionCommandHandlerError structure
    • Actionable error messages with .help() methods
    • Source error chaining from domain/infrastructure layers
    • Include EnvironmentAlreadyExists error variant
  • Comprehensive tests following existing patterns

    • Test builders for command setup
    • Use MockClock for deterministic testing
    • Test repository save/load integration
    • Test duplicate environment detection
    • Verify repository handles directory creation

Module Structure

src/application/commands/create/
├── mod.rs                # Module exports
├── command.rs            # CreateCommand implementation
├── errors.rs             # CreateCommandError enum with .help()
└── tests/
    ├── mod.rs
    ├── builders.rs       # Test builders
    └── integration.rs    # Integration tests

Core Implementation

CreateCommand

pub struct CreateCommand {
    environment_repository: Arc<dyn EnvironmentRepository>,
    clock: Arc<dyn Clock>,
}

impl CreateCommand {
    pub fn new(
        environment_repository: Arc<dyn EnvironmentRepository>,
        clock: Arc<dyn Clock>,
    ) -> Self { /* ... */ }

    pub fn execute(
        &self, 
        config: EnvironmentCreationConfig
    ) -> Result<Environment<Created>, CreateCommandError> {
        // 1. Convert config to domain objects
        let (environment_name, ssh_credentials, ssh_port) = 
            config.to_environment_params()
                .map_err(CreateCommandError::InvalidConfiguration)?;

        // 2. Check if environment already exists
        if self.environment_repository.exists(&environment_name)? {
            return Err(CreateCommandError::EnvironmentAlreadyExists {
                name: environment_name.as_str().to_string(),
            });
        }

        // 3. Create environment using existing constructor
        let environment = Environment::new(
            environment_name, 
            ssh_credentials, 
            ssh_port
        );

        // 4. Persist - repository handles directory creation atomically
        self.environment_repository.save(&environment.into_any())?;

        Ok(environment)
    }
}

Error Types with .help()

#[derive(Debug, Error)]
pub enum CreateCommandError {
    #[error("Configuration validation failed")]
    InvalidConfiguration(#[source] CreateConfigError),

    #[error("Environment '{name}' already exists")]
    EnvironmentAlreadyExists { name: String },

    #[error("Failed to save environment")]
    RepositoryError(#[source] Box<dyn std::error::Error + Send + Sync>),
}

impl CreateCommandError {
    pub fn help(&self) -> &'static str {
        match self {
            Self::EnvironmentAlreadyExists { .. } => {
                "Environment Already Exists - Troubleshooting:

1. List existing environments: torrust-tracker-deployer list
2. Use different name or destroy existing environment
3. Or work with existing environment (no need to recreate)

See documentation on environment management."
            }
            Self::InvalidConfiguration(_) => {
                "Invalid Configuration - Troubleshooting:

1. Check JSON syntax
2. Verify required fields present
3. Ensure SSH key files exist and are readable
4. Verify environment name follows naming rules

See documentation or generate a template."
            }
            Self::RepositoryError(_) => {
                "Repository Error - Troubleshooting:

1. Check file system permissions
2. Verify disk space
3. Ensure no other process is accessing environment
4. Check for file system errors

Report with system details if persistent."
            }
        }
    }
}

Test Examples

#[test]
fn it_should_create_environment_with_valid_configuration() {
    let (command, _temp_dir) = CreateCommandTestBuilder::new().build();
    let config = create_valid_test_config();
    
    let result = command.execute(config);
    
    assert!(result.is_ok());
}

#[test]
fn it_should_fail_when_environment_already_exists() {
    let (command, _temp_dir) = CreateCommandTestBuilder::new()
        .with_existing_environment("test-env")
        .build();
    
    let result = command.execute(config);
    
    assert!(matches!(result, Err(CreateCommandError::EnvironmentAlreadyExists { .. })));
}

Acceptance Criteria

  • CreateCommand follows existing synchronous patterns
  • Uses Environment::new() directly (no new methods)
  • Checks for duplicate environments before creation
  • Repository handles directory creation atomically
  • All errors implement .help() with detailed guidance
  • Test builders follow existing patterns
  • Tests use MockClock for determinism
  • Integration tests verify repository interaction
  • Delivery-agnostic (works with any presentation layer)

Estimated Time

3-4 hours

Notes

  • Follow ProvisionCommandHandler as reference implementation
  • Keep command synchronous - no async/await
  • Repository owns directory creation for atomicity
  • All errors must be actionable with .help() methods

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions