154 lines
4.1 KiB
Go
154 lines
4.1 KiB
Go
// Copyright (c) 2023-2024 Onur Cinar.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
// https://github.com/cinar/checker
|
|
|
|
// Package v2 Checker is a Go library for validating user input through checker rules provided in struct tags.
|
|
package v2
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// checkerTag is the name of the field tag used for checker.
|
|
checkerTag = "checkers"
|
|
|
|
// sliceConfigPrefix is the prefix used to distinguish slice-level checks from item-level checks.
|
|
sliceConfigPrefix = "@"
|
|
)
|
|
|
|
// checkStructJob defines a check strcut job.
|
|
type checkStructJob struct {
|
|
Name string
|
|
Value reflect.Value
|
|
Config string
|
|
}
|
|
|
|
// Check applies the given check functions to a value sequentially.
|
|
// It returns the final value and the first encountered error, if any.
|
|
func Check[T any](value T, checks ...CheckFunc[T]) (T, error) {
|
|
var err error
|
|
|
|
for _, check := range checks {
|
|
value, err = check(value)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return value, err
|
|
}
|
|
|
|
// CheckWithConfig applies the check functions specified by the config string to the given value.
|
|
// It returns the modified value and the first encountered error, if any.
|
|
func CheckWithConfig[T any](value T, config string) (T, error) {
|
|
newValue, err := ReflectCheckWithConfig(reflect.Indirect(reflect.ValueOf(value)), config)
|
|
return newValue.Interface().(T), err
|
|
}
|
|
|
|
// ReflectCheckWithConfig applies the check functions specified by the config string
|
|
// to the given reflect.Value. It returns the modified reflect.Value and the first
|
|
// encountered error, if any.
|
|
func ReflectCheckWithConfig(value reflect.Value, config string) (reflect.Value, error) {
|
|
return Check(value, makeChecks(config)...)
|
|
}
|
|
|
|
// CheckStruct checks the given struct based on the validation rules specified in the
|
|
// "checker" tag of each struct field. It returns a map of field names to their
|
|
// corresponding errors, and a boolean indicating if all checks passed.
|
|
func CheckStruct(st any) (map[string]error, bool) {
|
|
errs := make(map[string]error)
|
|
|
|
jobs := []*checkStructJob{
|
|
{
|
|
Name: "",
|
|
Value: reflect.Indirect(reflect.ValueOf(st)),
|
|
},
|
|
}
|
|
|
|
for len(jobs) > 0 {
|
|
job := jobs[0]
|
|
jobs = jobs[1:]
|
|
|
|
switch job.Value.Kind() {
|
|
case reflect.Struct:
|
|
for i := 0; i < job.Value.NumField(); i++ {
|
|
field := job.Value.Type().Field(i)
|
|
|
|
name := fieldName(job.Name, field)
|
|
value := reflect.Indirect(job.Value.FieldByIndex(field.Index))
|
|
|
|
jobs = append(jobs, &checkStructJob{
|
|
Name: name,
|
|
Value: value,
|
|
Config: field.Tag.Get(checkerTag),
|
|
})
|
|
}
|
|
|
|
case reflect.Slice:
|
|
sliceConfig, itemConfig := splitSliceConfig(job.Config)
|
|
job.Config = sliceConfig
|
|
|
|
for i := 0; i < job.Value.Len(); i++ {
|
|
name := fmt.Sprintf("%s[%d]", job.Name, i)
|
|
value := reflect.Indirect(job.Value.Index(i))
|
|
|
|
jobs = append(jobs, &checkStructJob{
|
|
Name: name,
|
|
Value: value,
|
|
Config: itemConfig,
|
|
})
|
|
}
|
|
}
|
|
|
|
if job.Config != "" {
|
|
newValue, err := ReflectCheckWithConfig(job.Value, job.Config)
|
|
if err != nil {
|
|
errs[job.Name] = err
|
|
}
|
|
|
|
job.Value.Set(newValue)
|
|
}
|
|
}
|
|
|
|
return errs, len(errs) == 0
|
|
}
|
|
|
|
// fieldName returns the field name. If a "json" tag is present, it uses the
|
|
// tag value instead. It also prepends the parent struct's name (if any) to
|
|
// create a fully qualified field name.
|
|
func fieldName(prefix string, field reflect.StructField) string {
|
|
// Default to field name
|
|
name := field.Name
|
|
|
|
// Use json tag if present
|
|
if jsonTag, ok := field.Tag.Lookup("json"); ok {
|
|
name = jsonTag
|
|
}
|
|
|
|
// Prepend parent name
|
|
if prefix != "" {
|
|
name = prefix + "." + name
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
// splitSliceConfig splits config string into slice and item-level configurations.
|
|
func splitSliceConfig(config string) (string, string) {
|
|
sliceFileds := make([]string, 0)
|
|
itemFields := make([]string, 0)
|
|
|
|
for _, configField := range strings.Fields(config) {
|
|
if strings.HasPrefix(configField, sliceConfigPrefix) {
|
|
sliceFileds = append(sliceFileds, strings.TrimPrefix(configField, sliceConfigPrefix))
|
|
} else {
|
|
itemFields = append(itemFields, configField)
|
|
}
|
|
}
|
|
|
|
return strings.Join(sliceFileds, " "), strings.Join(itemFields, " ")
|
|
}
|