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.
309 lines
No EOL
8.8 KiB
Go
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
|
|
} |