internal/errors/cfgerror/validate.go (240 lines of code) (raw):
package cfgerror
import (
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"golang.org/x/exp/constraints"
)
var (
// ErrNotSet should be used when the value is not set, but it is required.
ErrNotSet = errors.New("not set")
// ErrBlankOrEmpty should be used when non-blank/non-empty string is expected.
ErrBlankOrEmpty = errors.New("blank or empty")
// ErrDoesntExist should be used when resource doesn't exist.
ErrDoesntExist = errors.New("doesn't exist")
// ErrNotDir should be used when path on the file system exists, but it is not a directory.
ErrNotDir = errors.New("not a dir")
// ErrNotFile should be used when path on the file system exists, but it is not a file.
ErrNotFile = errors.New("not a file")
// ErrNotAbsolutePath should be used in case absolute path is expected, but the relative was provided.
ErrNotAbsolutePath = errors.New("not an absolute path")
// ErrNotUnique should be used when the value must be unique, but there are duplicates.
ErrNotUnique = errors.New("not unique")
// ErrBadOrder should be used when the order of the elements is wrong.
ErrBadOrder = errors.New("bad order")
// ErrNotInRange should be used when the value is not in expected range of values.
ErrNotInRange = errors.New("not in range")
// ErrUnsupportedValue should be used when the value is not supported.
ErrUnsupportedValue = errors.New("not supported")
)
// ValidationError represents an issue with provided configuration.
type ValidationError struct {
// Key represents a path to the field.
Key []string
// Cause contains a reason why validation failed.
Cause error
}
// Error to implement an error standard interface.
// The string representation can have 3 different formats:
// - when Key and Cause is set: "outer.inner: failure cause"
// - when only Key is set: "outer.inner"
// - when only Cause is set: "failure cause"
func (ve ValidationError) Error() string {
if len(ve.Key) != 0 && ve.Cause != nil {
return fmt.Sprintf("%s: %v", strings.Join(ve.Key, "."), ve.Cause)
}
if len(ve.Key) != 0 {
return strings.Join(ve.Key, ".")
}
if ve.Cause != nil {
return fmt.Sprintf("%v", ve.Cause)
}
return ""
}
// NewValidationError creates a new ValidationError with provided parameters.
func NewValidationError(err error, keys ...string) ValidationError {
return ValidationError{Key: keys, Cause: err}
}
// ValidationErrors is a list of ValidationError-s.
type ValidationErrors []ValidationError
// Append adds provided error into current list by enriching each ValidationError with the
// provided keys or if provided err is not an instance of the ValidationError it will be wrapped
// into it. In case the nil is provided nothing happens.
func (vs ValidationErrors) Append(err error, keys ...string) ValidationErrors {
if err == nil {
return vs
}
var validationErrs ValidationErrors
var validationErr ValidationError
if errors.As(err, &validationErrs) {
for _, err := range validationErrs {
vs = append(vs, ValidationError{
Key: append(keys, err.Key...),
Cause: err.Cause,
})
}
} else if errors.As(err, &validationErr) {
vs = append(vs, ValidationError{
Key: append(keys, validationErr.Key...),
Cause: validationErr.Cause,
})
} else {
vs = append(vs, ValidationError{
Key: keys,
Cause: err,
})
}
return vs
}
// AsError returns nil if there are no elements and itself if there is at least one.
func (vs ValidationErrors) AsError() error {
if len(vs) != 0 {
return vs
}
return nil
}
// Error transforms all validation errors into a single string joined by newline.
func (vs ValidationErrors) Error() string {
var buf strings.Builder
for i, ve := range vs {
if i != 0 {
buf.WriteString("\n")
}
buf.WriteString(ve.Error())
}
return buf.String()
}
// New returns uninitialized ValidationErrors object.
func New() ValidationErrors {
return nil
}
// NotEmpty checks if value is empty.
func NotEmpty(val string) error {
if val == "" {
return NewValidationError(ErrNotSet)
}
return nil
}
// NotBlank checks the value is not empty or blank.
func NotBlank(val string) error {
if strings.TrimSpace(val) == "" {
return NewValidationError(ErrBlankOrEmpty)
}
return nil
}
// DirExists checks the value points to an existing directory on the disk.
func DirExists(path string) error {
fs, err := os.Stat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return NewValidationError(fmt.Errorf("%w: %q", ErrDoesntExist, path))
}
return err
}
if !fs.IsDir() {
return NewValidationError(fmt.Errorf("%w: %q", ErrNotDir, path))
}
return nil
}
// FileExists checks the value points to an existing file on the disk.
func FileExists(path string) error {
fs, err := os.Stat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return NewValidationError(fmt.Errorf("%w: %q", ErrDoesntExist, path))
}
return err
}
if fs.IsDir() {
return NewValidationError(fmt.Errorf("%w: %q", ErrNotFile, path))
}
return nil
}
// PathIsAbs checks if provided path is an absolute path.
func PathIsAbs(path string) error {
if filepath.IsAbs(path) {
return nil
}
return NewValidationError(fmt.Errorf("%w: %q", ErrNotAbsolutePath, path))
}
// InRangeOpt represents configuration options for InRange function.
type InRangeOpt int
const (
// InRangeOptIncludeMin includes min value equality.
InRangeOptIncludeMin InRangeOpt = iota + 1
// InRangeOptIncludeMax includes max value equality.
InRangeOptIncludeMax
)
type inRangeOpts[T Numeric] []InRangeOpt
func (opts inRangeOpts[T]) lessThan(val, min T) bool {
for _, opt := range opts {
if opt == InRangeOptIncludeMin {
return val < min
}
}
return val <= min
}
func (opts inRangeOpts[T]) greaterThan(val, max T) bool {
for _, opt := range opts {
if opt == InRangeOptIncludeMax {
return val > max
}
}
return val >= max
}
func (opts inRangeOpts[T]) formatRange(min, max T) string {
return opts.formatRangeMin(min) + ", " + opts.formatRangeMax(max)
}
func (opts inRangeOpts[T]) formatRangeMin(min T) string {
for _, opt := range opts {
if opt == InRangeOptIncludeMin {
return fmt.Sprintf("[%v", min)
}
}
return fmt.Sprintf("(%v", min)
}
func (opts inRangeOpts[T]) formatRangeMax(max T) string {
for _, opt := range opts {
if opt == InRangeOptIncludeMax {
return fmt.Sprintf("%v]", max)
}
}
return fmt.Sprintf("%v)", max)
}
// Numeric includes types that can be used in the comparison operations.
type Numeric interface {
constraints.Integer | constraints.Float
}
// InRange returns an error if 'val' is less than 'min' or greater or equal to 'max'.
func InRange[T Numeric](min, max, val T, opts ...InRangeOpt) error {
if cmp := inRangeOpts[T](opts); cmp.lessThan(val, min) || cmp.greaterThan(val, max) {
return NewValidationError(fmt.Errorf("%w: %v out of %s", ErrNotInRange, val, cmp.formatRange(min, max)))
}
return nil
}
// IsSupportedValue ensures the provided 'value' is one listed as 'supportedValues'.
func IsSupportedValue[T comparable](value T, supportedValues ...T) error {
for _, supportedValue := range supportedValues {
if value == supportedValue {
return nil
}
}
if reflect.TypeOf(value).Kind() == reflect.String {
return NewValidationError(fmt.Errorf(`%w: "%v"`, ErrUnsupportedValue, value))
}
return NewValidationError(fmt.Errorf("%w: %v", ErrUnsupportedValue, value))
}
// IsNaturalNumber ensures that value is >= 0.
func IsNaturalNumber[T Numeric](value T) error {
if value < 0 {
return NewValidationError(fmt.Errorf("%w: %v", ErrUnsupportedValue, value))
}
return nil
}
type numeric[T Numeric] struct {
value T
}
// Comparable wraps value, so the method can be invoked on it.
func Comparable[T Numeric](val T) numeric[T] {
return numeric[T]{value: val}
}
// LessThan returns an error if val is less than one hold by c.
func (c numeric[T]) LessThan(val T) error {
if cmp := inRangeOpts[T](nil); cmp.lessThan(val, c.value) {
err := fmt.Errorf("%w: %v is not less than %v", ErrNotInRange, c.value, val)
return NewValidationError(err)
}
return nil
}
// GreaterThan returns an error if val is greater than one hold by c.
func (c numeric[T]) GreaterThan(val T) error {
if cmp := inRangeOpts[T](nil); cmp.greaterThan(val, c.value) {
err := fmt.Errorf("%w: %v is not greater than %v", ErrNotInRange, c.value, val)
return NewValidationError(err)
}
return nil
}
// GreaterOrEqual returns an error if val is greater than one hold by c.
func (c numeric[T]) GreaterOrEqual(val T) error {
if cmp := (inRangeOpts[T]{InRangeOptIncludeMax}); cmp.greaterThan(val, c.value) {
err := fmt.Errorf("%w: %v is not greater than or equal to %v", ErrNotInRange, c.value, val)
return NewValidationError(err)
}
return nil
}
// InRange returns an error if 'c.value' is less than 'min' or greater or equal to 'max'.
func (c numeric[T]) InRange(min, max T, opts ...InRangeOpt) error {
return InRange(min, max, c.value, opts...)
}
// NotEmptySlice returns an error if provided slice has no elements.
func NotEmptySlice[T any](slice []T) error {
if len(slice) == 0 {
return NewValidationError(ErrNotSet)
}
return nil
}
// NotEmptyMap returns an error if provided map has no elements.
func NotEmptyMap[K comparable, V any](m map[K]V) error {
if len(m) == 0 {
return NewValidationError(ErrNotSet)
}
return nil
}