-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathchecker.go
154 lines (125 loc) · 4.12 KB
/
checker.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 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, " ")
}