- 
                Notifications
    You must be signed in to change notification settings 
- Fork 0
Closed
Description
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 - nocreate_from_config()method
- Repository handles directory creation during save()for atomicity
- All errors implement .help()methods with detailed troubleshooting
Goals
- 
Create synchronous CreateCommandfollowing 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
 
-  Dependency injection: 
- 
Add command error enum with .help()methods-  CreateCommandErrorfollowingProvisionCommandHandlerErrorstructure
-  Actionable error messages with .help()methods
- Source error chaining from domain/infrastructure layers
-  Include EnvironmentAlreadyExistserror variant
 
-  
- 
Comprehensive tests following existing patterns - Test builders for command setup
-  Use MockClockfor 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
-  CreateCommandfollows 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 MockClockfor determinism
- Integration tests verify repository interaction
- Delivery-agnostic (works with any presentation layer)
Estimated Time
3-4 hours
Notes
- Follow ProvisionCommandHandleras reference implementation
- Keep command synchronous - no async/await
- Repository owns directory creation for atomicity
- All errors must be actionable with .help()methods
Copilot
Metadata
Metadata
Assignees
Labels
No labels