# Hexagonal Architecture (Ports & Adapters)

rdev implements hexagonal architecture to achieve clean separation of concerns, testability, and flexibility in infrastructure choices.

## Overview

Hexagonal architecture organizes code into three layers:

1. **Domain** - Core business logic and models
2. **Ports** - Abstract interfaces defining capabilities
3. **Adapters** - Concrete implementations of ports

```
                         ┌─────────────────────────┐
                         │        Domain           │
                         │                         │
    Driving              │  ┌─────────────────┐   │              Driven
    (Primary)            │  │     Models      │   │             (Secondary)
    Adapters             │  │  Project, Cmd   │   │              Adapters
        │                │  │  APIKey, Scope  │   │                  │
        │                │  └─────────────────┘   │                  │
        │                │                         │                  │
        │                │  ┌─────────────────┐   │                  │
        │                │  │     Ports       │   │                  │
        ▼                │  │  (Interfaces)   │   │                  ▼
    ┌───────┐            │  └─────────────────┘   │            ┌───────┐
    │ HTTP  │───────────▶│                         │◀───────────│  K8s  │
    │Handler│            │  ┌─────────────────┐   │            │Adapter│
    └───────┘            │  │    Services     │   │            └───────┘
                         │  │ ProjectService  │   │
                         │  │   AuthService   │   │            ┌───────┐
                         │  └─────────────────┘   │◀───────────│  DB   │
                         │                         │            │Adapter│
                         └─────────────────────────┘            └───────┘
```

## Domain Layer

Located in `internal/domain/`.

### Models

```go
// Project represents a development environment
type Project struct {
    ID          ProjectID
    Name        string
    PodName     string
    Status      ProjectStatus
    LastSeen    time.Time
    Labels      map[string]string
    Annotations map[string]string
}

// Command represents an executable command
type Command struct {
    ID        CommandID
    ProjectID ProjectID
    Type      CommandType
    Args      []string
    Status    CommandStatus
    StartedAt time.Time
    EndedAt   *time.Time
    ExitCode  *int
}
```

### Domain Errors

```go
var (
    ErrProjectNotFound      = errors.New("project not found")
    ErrCommandNotFound      = errors.New("command not found")
    ErrInvalidCommand       = errors.New("invalid command")
    ErrCommandSanitization  = errors.New("command failed sanitization")
)
```

## Ports Layer

Located in `internal/port/`.

### Port Interfaces

```go
// ProjectRepository defines project data access
type ProjectRepository interface {
    List(ctx context.Context) ([]domain.Project, error)
    Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error)
    Exists(ctx context.Context, id domain.ProjectID) (bool, error)
    RefreshStatus(ctx context.Context) error
}

// CommandExecutor defines command execution capability
type CommandExecutor interface {
    Execute(ctx context.Context, cmd *domain.Command, podName string,
            outputFn func(domain.OutputLine)) (*domain.CommandResult, error)
    Cancel(id domain.CommandID) error
    ActiveCount() int
}
```

### Benefits of Ports

1. **Testability**: Mock implementations for unit tests
2. **Flexibility**: Swap adapters without changing business logic
3. **Documentation**: Interfaces define contracts clearly

## Adapters Layer

Located in `internal/adapter/`.

### Kubernetes Adapter

```go
// kubernetes/project_repository.go
type ProjectRepository struct {
    namespace string
    client    kubernetes.Interface
    projects  []domain.Project
    mu        sync.RWMutex
}

func (r *ProjectRepository) List(ctx context.Context) ([]domain.Project, error) {
    // Uses K8s API to discover pods with rdev labels
}

// kubernetes/executor.go
type Executor struct {
    namespace      string
    activeCommands map[domain.CommandID]context.CancelFunc
}

func (e *Executor) Execute(ctx context.Context, cmd *domain.Command,
    podName string, outputFn func(domain.OutputLine)) (*domain.CommandResult, error) {
    // Uses kubectl exec to run commands
}
```

### Caching Adapter

```go
// cached/project_repository.go
type ProjectRepository struct {
    inner         port.ProjectRepository
    ttl           time.Duration
    projectsCache []domain.Project
    lastFetch     time.Time
}

func (r *ProjectRepository) List(ctx context.Context) ([]domain.Project, error) {
    if r.isCacheFresh() {
        return r.projectsCache, nil
    }
    return r.inner.List(ctx)
}
```

## Service Layer

Located in `internal/service/`.

Services orchestrate domain logic using ports:

```go
type ProjectService struct {
    repo     port.ProjectRepository
    executor port.CommandExecutor
    streams  port.StreamManager
}

func (s *ProjectService) ExecuteClaude(ctx context.Context,
    req ExecuteClaudeRequest) (*ExecuteResult, error) {

    // 1. Validate project exists
    project, err := s.repo.Get(ctx, req.ProjectID)
    if err != nil {
        return nil, err
    }

    // 2. Sanitize command
    if err := sanitize.ClaudePrompt(req.Prompt); err != nil {
        return nil, domain.ErrCommandSanitization
    }

    // 3. Execute via port
    result, err := s.executor.Execute(ctx, cmd, project.PodName,
        s.handleOutput)

    return result, nil
}
```

## Dependency Injection

Dependencies flow inward:

```go
// cmd/rdev-api/main.go
func main() {
    // Create adapters
    k8sClient := kubernetes.NewClientset()
    projectRepo := kubernetes.NewProjectRepositoryWithClient(namespace, k8sClient)
    cachedRepo := cached.NewProjectRepository(projectRepo, 30*time.Second)
    executor := kubernetes.NewExecutor(namespace)

    // Create services with ports
    projectService := service.NewProjectService(cachedRepo, executor)

    // Create handlers with services
    projectsHandler := handlers.NewProjectsHandlerWithService(projectService)
}
```

## Testing

### Unit Tests with Mocks

```go
type mockProjectRepo struct {
    projects []domain.Project
}

func (m *mockProjectRepo) Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error) {
    for _, p := range m.projects {
        if p.ID == id {
            return &p, nil
        }
    }
    return nil, domain.ErrProjectNotFound
}

func TestProjectService_ExecuteClaude(t *testing.T) {
    repo := &mockProjectRepo{projects: testProjects}
    exec := &mockExecutor{}
    svc := service.NewProjectService(repo, exec)

    result, err := svc.ExecuteClaude(ctx, req)
    // Assert...
}
```

### Integration Tests with Real Adapters

```go
func TestKubernetesAdapter_Execute(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    executor := kubernetes.NewExecutor("test-namespace")
    // Test with real K8s...
}
```

## Trade-offs

### Benefits
- Clear separation of concerns
- Easy to test in isolation
- Flexible infrastructure choices
- Domain logic remains pure

### Costs
- More interfaces and types
- Initial setup complexity
- Some indirection overhead

## Related Patterns

- **Repository Pattern**: Abstracts data access
- **Service Layer Pattern**: Orchestrates business logic
- **Dependency Injection**: Decouples creation from usage
