-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathvalidate.go
More file actions
320 lines (277 loc) · 9.48 KB
/
validate.go
File metadata and controls
320 lines (277 loc) · 9.48 KB
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// Copyright 2025 The Rivaas Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"context"
"fmt"
"reflect"
"sync"
)
// DefaultEngine is the [Engine] used by package-level [Validate] and [ValidatePartial].
// It is lazily initialized on first use (thread-safe).
//
// For test isolation or multiple engines in the same process, create an Engine with
// [New] or [MustNew] and use [Engine.Validate] instead of [Validate]. Tests may
// replace DefaultEngine and restore it in a defer to customize the default (avoid
// doing so in parallel tests that share the default).
var DefaultEngine *Engine
var defaultEngineOnce sync.Once
// getDefaultEngine returns the default [Engine], creating it if necessary.
func getDefaultEngine() *Engine {
defaultEngineOnce.Do(func() {
DefaultEngine = MustNew()
})
return DefaultEngine
}
// Validate validates a value using the default [Engine].
// For customized validation, create an Engine with [New] or [MustNew].
//
// Validate returns nil if validation passes, or an [*Error] if validation fails.
// The error can be type-asserted to *Error for structured field errors.
//
// Parameters:
// - ctx: Context passed to [ValidatorWithContext] implementations
// - v: The value to validate (typically a pointer to a struct)
// - opts: Optional per-call configuration (see [Option])
//
// Example:
//
// var req CreateUserRequest
// if err := validation.Validate(ctx, &req); err != nil {
// var verr *validation.Error
// if errors.As(err, &verr) {
// // Handle structured validation errors
// }
// }
//
// With options:
//
// if err := validation.Validate(ctx, &req,
// validation.WithStrategy(StrategyTags),
// validation.WithPartial(true),
// validation.WithPresence(presenceMap),
// ); err != nil {
// // Handle validation error
// }
func Validate(ctx context.Context, v any, opts ...Option) error {
return getDefaultEngine().Validate(ctx, v, opts...)
}
// ValidatePartial validates only fields present in the [PresenceMap] using the default [Engine].
// ValidatePartial is useful for PATCH requests where only provided fields should be validated.
// Use [ComputePresence] to create a PresenceMap from raw JSON.
func ValidatePartial(ctx context.Context, v any, pm PresenceMap, opts ...Option) error {
return getDefaultEngine().ValidatePartial(ctx, v, pm, opts...)
}
// Validate validates a value using this validator's configuration.
//
// Validate returns nil if validation passes, or an [*Error] if validation fails.
// The error can be type-asserted to *Error for structured field errors.
// Per-call options override the validator's base configuration.
//
// Parameters:
// - ctx: Context passed to [ValidatorWithContext] implementations
// - val: The value to validate (typically a pointer to a struct)
// - opts: Optional per-call configuration overrides (see [Option])
//
// Example:
//
// engine := validation.MustNew(validation.WithMaxErrors(10))
//
// if err := engine.Validate(ctx, &req); err != nil {
// var verr *validation.Error
// if errors.As(err, &verr) {
// // Handle structured validation errors
// }
// }
//
//nolint:contextcheck // intentional: WithContext option allows explicit context override
func (v *Engine) Validate(ctx context.Context, val any, opts ...Option) error {
if val == nil {
return &Error{Fields: []FieldError{{Code: "nil", Message: ErrCannotValidateNilValue.Error()}}}
}
// Apply per-call options on top of validator's base config
cfg := applyOptions(v.cfg, opts...)
// Use context from config if explicitly set via WithContext, otherwise use the ctx parameter
if cfg.ctx != nil {
ctx = cfg.ctx
}
// Handle nil pointers and invalid values
rv := reflect.ValueOf(val)
if !rv.IsValid() {
return &Error{Fields: []FieldError{{Code: "invalid", Message: ErrCannotValidateInvalidValue.Error()}}}
}
// Check for nil pointers (but preserve pointer for interface validation)
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return &Error{Fields: []FieldError{{Code: "nil_pointer", Message: "cannot validate nil pointer"}}}
}
rv = rv.Elem()
}
// Get the concrete value (dereferenced) for custom validator
concreteV := rv.Interface()
// Custom validator runs first (on dereferenced value)
if cfg.customValidator != nil {
if err := cfg.customValidator(concreteV); err != nil {
return v.coerceToValidationErrors(err, cfg)
}
}
// Run all strategies if requested (use original val to preserve pointer)
if cfg.runAll {
return v.validateAll(ctx, val, cfg)
}
// Determine strategy (use original val to check interfaces)
strategy := cfg.strategy
if strategy == StrategyAuto {
strategy = v.determineStrategy(ctx, val, cfg)
}
// Run single strategy (use original val to preserve pointer for interface validation)
return v.validateByStrategy(ctx, val, strategy, cfg)
}
// ValidatePartial validates only fields present in the [PresenceMap].
// It is useful for PATCH requests where only provided fields should be validated.
// Use [ComputePresence] to create a PresenceMap from raw JSON.
func (v *Engine) ValidatePartial(ctx context.Context, val any, pm PresenceMap, opts ...Option) error {
opts = append([]Option{WithPresence(pm), WithPartial(true)}, opts...)
return v.Validate(ctx, val, opts...)
}
// validateAll runs all applicable validation strategies and aggregates errors into an [*Error].
func (v *Engine) validateAll(ctx context.Context, val any, cfg *config) error {
var all Error
strategies := []Strategy{StrategyInterface, StrategyTags, StrategyJSONSchema}
applied := 0
for _, strategy := range strategies {
if !v.isApplicable(ctx, val, strategy, cfg) {
continue
}
applied++
if err := v.validateByStrategy(ctx, val, strategy, cfg); err != nil {
all.AddError(err)
// Check max errors
if cfg.maxErrors > 0 && len(all.Fields) >= cfg.maxErrors {
all.Truncated = true
break
}
}
}
// If requireAny is true, at least one strategy must have passed
if cfg.requireAny && applied > 0 && len(all.Fields) == 0 {
return nil
}
if all.HasErrors() {
all.Sort()
return &all
}
return nil
}
// isApplicable checks if a validation [Strategy] can apply to the value.
func (v *Engine) isApplicable(ctx context.Context, val any, strategy Strategy, cfg *config) bool {
switch strategy {
case StrategyInterface:
// Check if value implements Validator or ValidatorWithContext (type assertion only).
// When the method is on a pointer receiver, callers must pass a pointer.
if _, ok := val.(Validator); ok {
return true
}
if ctx != nil {
if _, ok := val.(ValidatorWithContext); ok {
return true
}
}
return false
case StrategyTags:
// Tags require a struct type with actual validation tags
rv := reflect.ValueOf(val)
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return false
}
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return false
}
// Check if struct has any validation tags
rt := rv.Type()
for i := range rt.NumField() {
field := rt.Field(i)
if field.Tag.Get("validate") != "" {
return true
}
}
return false
case StrategyJSONSchema:
// JSON Schema requires a schema to be available
if cfg.customSchema != "" {
return true
}
if _, ok := val.(JSONSchemaProvider); ok {
return true
}
return false
default:
return false
}
}
// determineStrategy automatically determines the best validation strategy.
func (v *Engine) determineStrategy(ctx context.Context, val any, cfg *config) Strategy {
// Priority order:
// 1. Interface validation (Validate/ValidateContext)
// 2. Tag validation (struct tags)
// 3. JSON Schema
if v.isApplicable(ctx, val, StrategyInterface, cfg) {
return StrategyInterface
}
if v.isApplicable(ctx, val, StrategyTags, cfg) {
return StrategyTags
}
if v.isApplicable(ctx, val, StrategyJSONSchema, cfg) {
return StrategyJSONSchema
}
// Default to tags if it's a struct
rv := reflect.ValueOf(val)
for rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return StrategyTags
}
rv = rv.Elem()
}
if rv.Kind() == reflect.Struct {
return StrategyTags
}
return StrategyTags
}
// validateByStrategy dispatches to the appropriate validation function based on [Strategy].
func (v *Engine) validateByStrategy(ctx context.Context, val any, strategy Strategy, cfg *config) error {
switch strategy {
case StrategyInterface:
// Use original value (may be pointer) for interface validation
return v.validateWithInterface(ctx, val, cfg)
case StrategyTags:
// Dereference for tag validation (tags work on struct values)
rv := reflect.ValueOf(val)
for rv.Kind() == reflect.Pointer && !rv.IsNil() {
rv = rv.Elem()
}
return v.validateWithTags(rv.Interface(), cfg)
case StrategyJSONSchema:
// Dereference for schema validation
rv := reflect.ValueOf(val)
for rv.Kind() == reflect.Pointer && !rv.IsNil() {
rv = rv.Elem()
}
return v.validateWithSchema(ctx, rv.Interface(), cfg)
default:
return &Error{Fields: []FieldError{{Code: "unknown_strategy", Message: fmt.Sprintf("%v: %d", ErrUnknownValidationStrategy, strategy)}}}
}
}