feat: user service
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
// Package shared provides shared request parsing and error normalization used
|
||||
// by the user-service application and transport layers.
|
||||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrorCodeInvalidRequest reports malformed or semantically invalid caller
|
||||
// input.
|
||||
ErrorCodeInvalidRequest = "invalid_request"
|
||||
|
||||
// ErrorCodeConflict reports that the requested mutation conflicts with the
|
||||
// current source-of-truth state.
|
||||
ErrorCodeConflict = "conflict"
|
||||
|
||||
// ErrorCodeSubjectNotFound reports that the requested user subject does not
|
||||
// exist.
|
||||
ErrorCodeSubjectNotFound = "subject_not_found"
|
||||
|
||||
// ErrorCodeServiceUnavailable reports that a required dependency is
|
||||
// temporarily unavailable.
|
||||
ErrorCodeServiceUnavailable = "service_unavailable"
|
||||
|
||||
// ErrorCodeInternalError reports that a local invariant failed unexpectedly.
|
||||
ErrorCodeInternalError = "internal_error"
|
||||
)
|
||||
|
||||
var internalErrorStatusCodes = map[string]int{
|
||||
ErrorCodeInvalidRequest: http.StatusBadRequest,
|
||||
ErrorCodeConflict: http.StatusConflict,
|
||||
ErrorCodeSubjectNotFound: http.StatusNotFound,
|
||||
ErrorCodeServiceUnavailable: http.StatusServiceUnavailable,
|
||||
ErrorCodeInternalError: http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
var internalStableMessages = map[string]string{
|
||||
ErrorCodeConflict: "request conflicts with current state",
|
||||
ErrorCodeSubjectNotFound: "subject not found",
|
||||
ErrorCodeServiceUnavailable: "service is unavailable",
|
||||
ErrorCodeInternalError: "internal server error",
|
||||
}
|
||||
|
||||
// InternalErrorProjection stores the transport-ready representation of one
|
||||
// normalized trusted-internal error.
|
||||
type InternalErrorProjection struct {
|
||||
// StatusCode stores the HTTP status returned to the trusted caller.
|
||||
StatusCode int
|
||||
|
||||
// Code stores the stable machine-readable error code written into the JSON
|
||||
// envelope.
|
||||
Code string
|
||||
|
||||
// Message stores the stable or caller-safe message written into the JSON
|
||||
// envelope.
|
||||
Message string
|
||||
}
|
||||
|
||||
// ServiceError stores one normalized application-layer failure.
|
||||
type ServiceError struct {
|
||||
// Code stores the stable machine-readable error code.
|
||||
Code string
|
||||
|
||||
// Message stores the caller-safe error message.
|
||||
Message string
|
||||
|
||||
// Err stores the wrapped underlying cause when one exists.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns the caller-safe message of ServiceError.
|
||||
func (err *ServiceError) Error() string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(err.Message) != "" {
|
||||
return err.Message
|
||||
}
|
||||
if strings.TrimSpace(err.Code) != "" {
|
||||
return err.Code
|
||||
}
|
||||
if err.Err != nil {
|
||||
return err.Err.Error()
|
||||
}
|
||||
|
||||
return ErrorCodeInternalError
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped underlying cause.
|
||||
func (err *ServiceError) Unwrap() error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// NewServiceError returns one new normalized application-layer error.
|
||||
func NewServiceError(code string, message string, err error) *ServiceError {
|
||||
return &ServiceError{
|
||||
Code: strings.TrimSpace(code),
|
||||
Message: strings.TrimSpace(message),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidRequest returns one normalized invalid-request error.
|
||||
func InvalidRequest(message string) *ServiceError {
|
||||
return NewServiceError(ErrorCodeInvalidRequest, strings.TrimSpace(message), nil)
|
||||
}
|
||||
|
||||
// Conflict returns one normalized conflict error.
|
||||
func Conflict() *ServiceError {
|
||||
return NewServiceError(ErrorCodeConflict, "", nil)
|
||||
}
|
||||
|
||||
// SubjectNotFound returns one normalized subject-not-found error.
|
||||
func SubjectNotFound() *ServiceError {
|
||||
return NewServiceError(ErrorCodeSubjectNotFound, "", nil)
|
||||
}
|
||||
|
||||
// ServiceUnavailable returns one normalized dependency-unavailable error.
|
||||
func ServiceUnavailable(err error) *ServiceError {
|
||||
return NewServiceError(ErrorCodeServiceUnavailable, "", err)
|
||||
}
|
||||
|
||||
// InternalError returns one normalized invariant-failure error.
|
||||
func InternalError(err error) *ServiceError {
|
||||
return NewServiceError(ErrorCodeInternalError, "", err)
|
||||
}
|
||||
|
||||
// CodeOf returns the normalized service error code carried by err when one is
|
||||
// available.
|
||||
func CodeOf(err error) string {
|
||||
serviceErr, ok := errors.AsType[*ServiceError](err)
|
||||
if !ok || serviceErr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return serviceErr.Code
|
||||
}
|
||||
|
||||
// ProjectInternalError normalizes err to the frozen trusted-internal HTTP
|
||||
// error surface.
|
||||
func ProjectInternalError(err error) InternalErrorProjection {
|
||||
serviceErr, ok := errors.AsType[*ServiceError](err)
|
||||
code := CodeOf(err)
|
||||
if _, exists := internalErrorStatusCodes[code]; !exists {
|
||||
return InternalErrorProjection{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Code: ErrorCodeInternalError,
|
||||
Message: internalStableMessages[ErrorCodeInternalError],
|
||||
}
|
||||
}
|
||||
|
||||
message := ""
|
||||
if ok && serviceErr != nil {
|
||||
message = serviceErr.Message
|
||||
}
|
||||
if stable, exists := internalStableMessages[code]; exists {
|
||||
message = stable
|
||||
}
|
||||
if strings.TrimSpace(message) == "" {
|
||||
message = internalStableMessages[ErrorCodeInternalError]
|
||||
}
|
||||
|
||||
return InternalErrorProjection{
|
||||
StatusCode: internalErrorStatusCodes[code],
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user