// Package errs defines the application's error type (AppError) and helpers // to map domain error codes to HTTP status codes. Service layers wrap // underlying errors with errs.Wrap / errs.New; transport layers use errs.As // + errs.HTTPStatus to translate them into Google API style responses // ({code, message, fields, request_id}). package errs import ( "errors" "fmt" "net/http" ) // Code is a stable, machine-readable identifier for a class of error. // Codes are part of the public API contract surfaced by HTTP responses. type Code string // Canonical error codes. Keep this list in sync with HTTPStatus and any // downstream consumers (e.g., HTTP transport, OpenAPI schemas). const ( CodeInvalidInput Code = "invalid_input" CodeUnauthorized Code = "unauthorized" CodeForbidden Code = "forbidden" CodeNotFound Code = "not_found" CodeConflict Code = "conflict" CodeRateLimited Code = "rate_limited" CodeUpstream Code = "upstream_error" CodeUnavailable Code = "unavailable" CodeInternal Code = "internal" // Project 模块专用细粒度 code(HTTP 仍 409,但前端可据此区分提示)。 CodeProjectSlugTaken Code = "project_slug_taken" CodeProjectArchived Code = "project_archived" CodeRequirementClosed Code = "requirement_closed" // Workspace 模块细粒度 code CodeWorkspaceSlugTaken Code = "workspace_slug_taken" CodeWorkspaceCloneFailed Code = "workspace_clone_failed" CodeWorkspaceSyncInProgress Code = "workspace_sync_in_progress" CodeWorkspaceRemoteInvalid Code = "workspace_remote_invalid" CodeWorktreeBranchConflict Code = "worktree_branch_conflict" CodeWorktreeAlreadyActive Code = "worktree_already_active" CodeWorktreeNotHeldByCaller Code = "worktree_not_held_by_caller" CodeWorktreePathInvalid Code = "worktree_path_invalid" CodeGitCredentialInvalid Code = "git_credential_invalid" CodeGitCmdFailed Code = "git_cmd_failed" ) // AppError is the application's structured error type. It carries a stable // Code, a human-readable Message, an optional underlying Cause, and an // optional Fields map for per-field validation details. type AppError struct { Code Code Message string Cause error Fields map[string]any } // Error implements the error interface. When a Cause is present the format // is ": "; otherwise just the message is returned. func (e *AppError) Error() string { if e.Cause != nil { return fmt.Sprintf("%s: %s", e.Message, e.Cause.Error()) } return e.Message } // Unwrap exposes the underlying cause so errors.Is / errors.As can traverse // the chain. func (e *AppError) Unwrap() error { return e.Cause } // WithFields attaches per-field validation details and returns the receiver // for chaining. func (e *AppError) WithFields(f map[string]any) *AppError { e.Fields = f return e } // New constructs a new AppError with the given code and message. func New(code Code, msg string) *AppError { return &AppError{Code: code, Message: msg} } // Wrap wraps an existing error with an AppError carrying the given code and // message. The original error is preserved as Cause and remains discoverable // via errors.Is / errors.As. func Wrap(cause error, code Code, msg string) *AppError { return &AppError{Code: code, Message: msg, Cause: cause} } // As is a convenience wrapper around errors.As that extracts the first // *AppError in err's chain. It returns (nil, false) when err is nil or no // AppError is found. func As(err error) (*AppError, bool) { if err == nil { return nil, false } var ae *AppError if errors.As(err, &ae) { return ae, true } return nil, false } // HTTPStatus maps an error Code to its canonical HTTP status code. Unknown // codes default to 500 Internal Server Error. func HTTPStatus(code Code) int { switch code { case CodeInvalidInput: return http.StatusBadRequest case CodeUnauthorized: return http.StatusUnauthorized case CodeForbidden: return http.StatusForbidden case CodeNotFound: return http.StatusNotFound case CodeConflict, CodeProjectSlugTaken, CodeProjectArchived, CodeRequirementClosed, CodeWorkspaceSlugTaken, CodeWorkspaceSyncInProgress, CodeWorktreeBranchConflict, CodeWorktreeAlreadyActive: return http.StatusConflict case CodeRateLimited: return http.StatusTooManyRequests case CodeUpstream: return http.StatusBadGateway case CodeUnavailable: return http.StatusServiceUnavailable case CodeWorkspaceCloneFailed, CodeGitCmdFailed: return http.StatusBadGateway case CodeWorkspaceRemoteInvalid, CodeWorktreePathInvalid, CodeGitCredentialInvalid: return http.StatusBadRequest case CodeWorktreeNotHeldByCaller: return http.StatusForbidden default: return http.StatusInternalServerError } }