edge-connect-client/internal/domain/errors.go
Stephan Lo 1c13c93512 refactor(arch): Relocate domain and ports packages
Moves the `domain` and `ports` packages from `internal/core` to `internal`.

This refactoring simplifies the directory structure by elevating the core architectural concepts of domain and ports to the top level of the `internal` directory. The `core` directory is now removed as its only purpose was to house these two packages.

All import paths across the project have been updated to reflect this change.
2025-10-09 01:16:31 +02:00

309 lines
No EOL
8.8 KiB
Go

// Package domain contains domain-specific error types for the EdgeConnect client
package domain
import (
"errors"
"fmt"
"strings"
)
// ErrorCode represents different types of domain errors
type ErrorCode int
const (
// Resource errors
ErrResourceNotFound ErrorCode = iota
ErrResourceAlreadyExists
ErrResourceConflict
// Validation errors
ErrValidationFailed
ErrInvalidConfiguration
ErrInvalidInput
// Business logic errors
ErrQuotaExceeded
ErrInsufficientPermissions
ErrOperationNotAllowed
// Infrastructure errors
ErrNetworkError
ErrAuthenticationFailed
ErrServiceUnavailable
ErrTimeout
// Internal errors
ErrInternalError
ErrUnknownError
)
// String returns a human-readable string representation of the error code
func (e ErrorCode) String() string {
switch e {
case ErrResourceNotFound:
return "RESOURCE_NOT_FOUND"
case ErrResourceAlreadyExists:
return "RESOURCE_ALREADY_EXISTS"
case ErrResourceConflict:
return "RESOURCE_CONFLICT"
case ErrValidationFailed:
return "VALIDATION_FAILED"
case ErrInvalidConfiguration:
return "INVALID_CONFIGURATION"
case ErrInvalidInput:
return "INVALID_INPUT"
case ErrQuotaExceeded:
return "QUOTA_EXCEEDED"
case ErrInsufficientPermissions:
return "INSUFFICIENT_PERMISSIONS"
case ErrOperationNotAllowed:
return "OPERATION_NOT_ALLOWED"
case ErrNetworkError:
return "NETWORK_ERROR"
case ErrAuthenticationFailed:
return "AUTHENTICATION_FAILED"
case ErrServiceUnavailable:
return "SERVICE_UNAVAILABLE"
case ErrTimeout:
return "TIMEOUT"
case ErrInternalError:
return "INTERNAL_ERROR"
case ErrUnknownError:
return "UNKNOWN_ERROR"
default:
return "UNDEFINED_ERROR"
}
}
// DomainError represents a domain-specific error with detailed context
type DomainError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
Cause error `json:"-"`
Context map[string]interface{} `json:"context,omitempty"`
Resource *ResourceIdentifier `json:"resource,omitempty"`
Operation string `json:"operation,omitempty"`
Retryable bool `json:"retryable"`
}
// ResourceIdentifier provides context about the resource involved in the error
type ResourceIdentifier struct {
Type string `json:"type"`
Name string `json:"name"`
Organization string `json:"organization,omitempty"`
Region string `json:"region,omitempty"`
Version string `json:"version,omitempty"`
}
// Error implements the error interface
func (e *DomainError) Error() string {
var parts []string
if e.Operation != "" {
parts = append(parts, fmt.Sprintf("operation %s failed", e.Operation))
}
if e.Resource != nil {
parts = append(parts, fmt.Sprintf("resource %s", e.resourceString()))
}
parts = append(parts, e.Message)
if e.Details != "" {
parts = append(parts, e.Details)
}
result := strings.Join(parts, ": ")
if e.Cause != nil {
result = fmt.Sprintf("%s (caused by: %v)", result, e.Cause)
}
return result
}
// Unwrap returns the underlying cause for error wrapping
func (e *DomainError) Unwrap() error {
return e.Cause
}
// Is checks if the error matches a specific error code
func (e *DomainError) Is(target error) bool {
if de, ok := target.(*DomainError); ok {
return e.Code == de.Code
}
return false
}
// IsRetryable indicates whether the operation should be retried
func (e *DomainError) IsRetryable() bool {
return e.Retryable
}
// WithContext adds context information to the error
func (e *DomainError) WithContext(key string, value interface{}) *DomainError {
if e.Context == nil {
e.Context = make(map[string]interface{})
}
e.Context[key] = value
return e
}
// WithDetails adds additional details to the error
func (e *DomainError) WithDetails(details string) *DomainError {
e.Details = details
return e
}
func (e *DomainError) resourceString() string {
if e.Resource == nil {
return ""
}
parts := []string{e.Resource.Type}
if e.Resource.Organization != "" && e.Resource.Name != "" {
parts = append(parts, fmt.Sprintf("%s/%s", e.Resource.Organization, e.Resource.Name))
} else if e.Resource.Name != "" {
parts = append(parts, e.Resource.Name)
}
if e.Resource.Version != "" {
parts = append(parts, fmt.Sprintf("version %s", e.Resource.Version))
}
if e.Resource.Region != "" {
parts = append(parts, fmt.Sprintf("in region %s", e.Resource.Region))
}
return strings.Join(parts, " ")
}
// Error creation helpers
// NewDomainError creates a new domain error with the specified code and message
func NewDomainError(code ErrorCode, message string) *DomainError {
return &DomainError{
Code: code,
Message: message,
Retryable: isRetryableByDefault(code),
}
}
// NewDomainErrorWithCause creates a new domain error with an underlying cause
func NewDomainErrorWithCause(code ErrorCode, message string, cause error) *DomainError {
return &DomainError{
Code: code,
Message: message,
Cause: cause,
Retryable: isRetryableByDefault(code),
}
}
// NewResourceError creates a domain error for resource-related operations
func NewResourceError(code ErrorCode, operation string, resource *ResourceIdentifier, message string) *DomainError {
return &DomainError{
Code: code,
Message: message,
Operation: operation,
Resource: resource,
Retryable: isRetryableByDefault(code),
}
}
func isRetryableByDefault(code ErrorCode) bool {
switch code {
case ErrNetworkError, ErrServiceUnavailable, ErrTimeout, ErrInternalError:
return true
default:
return false
}
}
// Predefined errors for common scenarios
var (
// Resource errors
ErrAppNotFound = NewDomainError(ErrResourceNotFound, "application not found")
ErrAppExists = NewDomainError(ErrResourceAlreadyExists, "application already exists")
ErrInstanceNotFound = NewDomainError(ErrResourceNotFound, "app instance not found")
ErrInstanceExists = NewDomainError(ErrResourceAlreadyExists, "app instance already exists")
ErrCloudletNotFound = NewDomainError(ErrResourceNotFound, "cloudlet not found")
// Validation errors
ErrInvalidAppKey = NewDomainError(ErrValidationFailed, "invalid application key")
ErrInvalidInstanceKey = NewDomainError(ErrValidationFailed, "invalid app instance key")
ErrInvalidCloudletKey = NewDomainError(ErrValidationFailed, "invalid cloudlet key")
ErrMissingRegion = NewDomainError(ErrValidationFailed, "region is required")
// Business logic errors
ErrDeploymentFailed = NewDomainError(ErrOperationNotAllowed, "deployment failed")
ErrRollbackFailed = NewDomainError(ErrOperationNotAllowed, "rollback failed")
ErrPlanningFailed = NewDomainError(ErrOperationNotAllowed, "deployment planning failed")
)
// Helper functions for creating specific error scenarios
// NewAppError creates an error related to application operations
func NewAppError(code ErrorCode, operation string, appKey AppKey, region string, message string) *DomainError {
resource := &ResourceIdentifier{
Type: "app",
Organization: appKey.Organization,
Name: appKey.Name,
Version: appKey.Version,
Region: region,
}
return NewResourceError(code, operation, resource, message)
}
// NewInstanceError creates an error related to app instance operations
func NewInstanceError(code ErrorCode, operation string, instanceKey AppInstanceKey, region string, message string) *DomainError {
resource := &ResourceIdentifier{
Type: "app-instance",
Organization: instanceKey.Organization,
Name: instanceKey.Name,
Region: region,
}
return NewResourceError(code, operation, resource, message)
}
// NewCloudletError creates an error related to cloudlet operations
func NewCloudletError(code ErrorCode, operation string, cloudletKey CloudletKey, region string, message string) *DomainError {
resource := &ResourceIdentifier{
Type: "cloudlet",
Organization: cloudletKey.Organization,
Name: cloudletKey.Name,
Region: region,
}
return NewResourceError(code, operation, resource, message)
}
// Error checking utilities
// IsNotFoundError checks if an error indicates a resource was not found
func IsNotFoundError(err error) bool {
var de *DomainError
return errors.As(err, &de) && de.Code == ErrResourceNotFound
}
// IsValidationError checks if an error is a validation error
func IsValidationError(err error) bool {
var de *DomainError
return errors.As(err, &de) && (de.Code == ErrValidationFailed || de.Code == ErrInvalidInput || de.Code == ErrInvalidConfiguration)
}
// IsRetryableError checks if an error is retryable
func IsRetryableError(err error) bool {
var de *DomainError
if errors.As(err, &de) {
return de.IsRetryable()
}
return false
}
// IsAuthenticationError checks if an error is authentication-related
func IsAuthenticationError(err error) bool {
var de *DomainError
return errors.As(err, &de) && de.Code == ErrAuthenticationFailed
}