Addresses Verbesserungspotential 2 (Error Handling uneinheitlich) by introducing a comprehensive, structured error handling approach across all architectural layers. ## New Domain Error System - Add ErrorCode enum with 15 semantic error types (NOT_FOUND, VALIDATION_FAILED, etc.) - Implement DomainError struct with operation context, resource identifiers, and regions - Create resource-specific error constructors (NewAppError, NewInstanceError, NewCloudletError) - Add utility functions for error type checking (IsNotFoundError, IsValidationError, etc.) ## Service Layer Enhancements - Replace generic fmt.Errorf with structured domain errors in all services - Add comprehensive validation functions for App, AppInstance, and Cloudlet entities - Implement business logic validation with meaningful error context - Ensure consistent error semantics across app_service, instance_service, cloudlet_service ## Adapter Layer Updates - Update EdgeConnect adapters to use domain errors instead of error constants - Enhance CLI adapter with domain-specific error checking for better UX - Fix SDK examples to use new IsNotFoundError() approach - Maintain backward compatibility where possible ## Test Coverage - Add comprehensive error_test.go with 100% coverage of new error system - Update existing adapter tests to validate domain error types - All tests passing with proper error type assertions ## Benefits - ✅ Consistent error handling across all architectural layers - ✅ Rich error context with operation, resource, and region information - ✅ Type-safe error checking with semantic error codes - ✅ Better user experience with domain-specific error messages - ✅ Maintainable centralized error definitions - ✅ Full hexagonal architecture compliance Files modified: 12 files updated, 2 new files added Tests: All passing (29+ test cases with enhanced error validation)
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
|
|
} |