-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathvalidator.go
More file actions
346 lines (310 loc) · 11.4 KB
/
validator.go
File metadata and controls
346 lines (310 loc) · 11.4 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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package validator
import (
"fmt"
"time"
"github.com/erraggy/oastools/internal/issues"
"github.com/erraggy/oastools/internal/severity"
"github.com/erraggy/oastools/parser"
)
// Severity indicates the severity level of a validation issue
type Severity = severity.Severity
const (
// SeverityError indicates a spec violation that makes the document invalid
SeverityError = severity.SeverityError
// SeverityWarning indicates a best practice violation or recommendation
SeverityWarning = severity.SeverityWarning
// SeverityInfo indicates informational messages
SeverityInfo = severity.SeverityInfo
// SeverityCritical indicates critical issues
SeverityCritical = severity.SeverityCritical
)
const (
// defaultErrorCapacity is the initial capacity for error slices
defaultErrorCapacity = 10
// defaultWarningCapacity is the initial capacity for warning slices
defaultWarningCapacity = 10
// Resource exhaustion protection
maxSchemaNestingDepth = 100 // Maximum depth for nested schemas to prevent stack overflow
)
// ValidationError represents a single validation issue
type ValidationError = issues.Issue
// ValidationResult contains the results of validating an OpenAPI specification
type ValidationResult struct {
// Valid is true if no errors were found (warnings are allowed)
Valid bool
// Version is the detected OAS version string
Version string
// OASVersion is the enumerated OAS version
OASVersion parser.OASVersion
// Errors contains all validation errors
Errors []ValidationError
// Warnings contains all validation warnings
Warnings []ValidationError
// ErrorCount is the total number of errors
ErrorCount int
// WarningCount is the total number of warnings
WarningCount int
// LoadTime is the time taken to load the source data
LoadTime time.Duration
// SourceSize is the size of the source data in bytes
SourceSize int64
// Stats contains statistical information about the document
Stats parser.DocumentStats
// Document contains the validated document (*parser.OAS2Document or *parser.OAS3Document).
// Added to enable ToParseResult() for package chaining.
Document any
// SourceFormat is the format of the source file (JSON or YAML)
SourceFormat parser.SourceFormat
// SourcePath is the original source path from the parsed document.
// Used by ToParseResult() to preserve the original path for package chaining.
SourcePath string
}
// ToParseResult converts the ValidationResult to a ParseResult for use with
// other packages like fixer, converter, joiner, and differ.
// The returned ParseResult has Document populated but Data is nil
// (consumers use Document, not Data).
// Validation errors and warnings are converted to string warnings with
// severity prefixes for programmatic filtering:
// "[error] path: message", "[warning] path: message", etc.
func (r *ValidationResult) ToParseResult() *parser.ParseResult {
// Convert validation errors/warnings to string warnings with severity prefix
warnings := make([]string, 0, len(r.Errors)+len(r.Warnings))
for _, e := range r.Errors {
warnings = append(warnings, "["+e.Severity.String()+"] "+e.String())
}
for _, w := range r.Warnings {
warnings = append(warnings, "["+w.Severity.String()+"] "+w.String())
}
// Use original source path, falling back to "validator" if not set
sourcePath := r.SourcePath
if sourcePath == "" {
sourcePath = "validator"
}
return &parser.ParseResult{
SourcePath: sourcePath,
SourceFormat: r.SourceFormat,
Version: r.Version,
OASVersion: r.OASVersion,
Document: r.Document,
Errors: make([]error, 0),
Warnings: warnings,
Stats: r.Stats,
LoadTime: r.LoadTime,
SourceSize: r.SourceSize,
}
}
// Validator handles OpenAPI specification validation
type Validator struct {
// IncludeWarnings determines whether to include best practice warnings
IncludeWarnings bool
// StrictMode enables stricter validation beyond the spec requirements
StrictMode bool
// ValidateStructure controls whether the parser performs basic structure validation.
// When true (default), the parser validates required fields and correct types.
// When false, parsing is more lenient and skips structure validation.
ValidateStructure bool
// UserAgent is the User-Agent string used when fetching URLs
// Defaults to "oastools" if not set
UserAgent string
// SourceMap provides source location lookup for validation errors.
// When set, validation errors will include Line, Column, and File fields.
SourceMap *parser.SourceMap
// refTracker tracks which operations reference which components.
// Built during ValidateParsed for populating OperationContext on issues.
refTracker *refTracker
// oasVersion is the detected OAS version for the document under validation.
// Set during ValidateParsed so version-sensitive checks (e.g., type "null"
// being illegal in OAS 3.0.x) can consult it without plumbing through every call.
oasVersion parser.OASVersion
}
// New creates a new Validator instance with default settings
func New() *Validator {
return &Validator{
IncludeWarnings: true,
StrictMode: false,
ValidateStructure: true,
}
}
// ValidateWithOptions validates an OpenAPI specification using functional options.
// This provides a flexible, extensible API that combines input source selection
// and configuration in a single function call.
//
// Example:
//
// result, err := validator.ValidateWithOptions(
// validator.WithFilePath("openapi.yaml"),
// validator.WithStrictMode(true),
// )
func ValidateWithOptions(opts ...Option) (*ValidationResult, error) {
cfg, err := applyOptions(opts...)
if err != nil {
return nil, fmt.Errorf("validator: invalid options: %w", err)
}
v := &Validator{
IncludeWarnings: cfg.includeWarnings,
StrictMode: cfg.strictMode,
ValidateStructure: cfg.validateStructure,
UserAgent: cfg.userAgent,
SourceMap: cfg.sourceMap,
}
// Route to appropriate validation method based on input source
// Parsed input is checked first as it's the preferred high-performance path
if cfg.parsed != nil {
return v.ValidateParsed(*cfg.parsed)
}
// cfg.filePath must be non-nil here (validated by applyOptions)
return v.Validate(*cfg.filePath)
}
// populateIssueLocation looks up the source location for an issue's path
// and populates the Line, Column, and File fields if found.
func (v *Validator) populateIssueLocation(issue *ValidationError) {
if v.SourceMap == nil {
return
}
// Convert validation path to JSON path format (add $ prefix if needed)
jsonPath := issue.Path
if !hasJSONPathPrefix(jsonPath) {
jsonPath = "$." + issue.Path
}
loc := v.SourceMap.Get(jsonPath)
if loc.IsKnown() {
issue.Line = loc.Line
issue.Column = loc.Column
issue.File = loc.File
}
}
// hasJSONPathPrefix returns true if the path already has a JSON path prefix.
func hasJSONPathPrefix(path string) bool {
return len(path) > 0 && path[0] == '$'
}
// populateOperationContext attaches operation context to an issue if applicable.
func (v *Validator) populateOperationContext(issue *ValidationError, doc any) {
if v.refTracker == nil {
return
}
issue.OperationContext = v.refTracker.getOperationContext(issue.Path, doc)
}
// addError appends a validation error and populates its source location.
func (v *Validator) addError(result *ValidationResult, path, message string, opts ...func(*ValidationError)) {
err := ValidationError{
Path: path,
Message: message,
Severity: SeverityError,
}
for _, opt := range opts {
opt(&err)
}
v.populateIssueLocation(&err)
v.populateOperationContext(&err, result.Document)
result.Errors = append(result.Errors, err)
}
// addWarning appends a validation warning and populates its source location.
func (v *Validator) addWarning(result *ValidationResult, path, message string, opts ...func(*ValidationError)) {
warn := ValidationError{
Path: path,
Message: message,
Severity: SeverityWarning,
}
for _, opt := range opts {
opt(&warn)
}
v.populateIssueLocation(&warn)
v.populateOperationContext(&warn, result.Document)
result.Warnings = append(result.Warnings, warn)
}
// withField sets the Field on a ValidationError.
func withField(field string) func(*ValidationError) {
return func(e *ValidationError) { e.Field = field }
}
// withValue sets the Value on a ValidationError.
func withValue(value any) func(*ValidationError) {
return func(e *ValidationError) { e.Value = value }
}
// withSpecRef sets the SpecRef on a ValidationError.
func withSpecRef(ref string) func(*ValidationError) {
return func(e *ValidationError) { e.SpecRef = ref }
}
// ValidateParsed validates an already parsed OpenAPI specification
func (v *Validator) ValidateParsed(parseResult parser.ParseResult) (*ValidationResult, error) {
result := &ValidationResult{
Version: parseResult.Version,
OASVersion: parseResult.OASVersion,
Errors: make([]ValidationError, 0, defaultErrorCapacity),
Warnings: make([]ValidationError, 0, defaultWarningCapacity),
LoadTime: parseResult.LoadTime,
SourceSize: parseResult.SourceSize,
Stats: parseResult.Stats,
Document: parseResult.Document,
SourceFormat: parseResult.SourceFormat,
SourcePath: parseResult.SourcePath,
}
// Record the OAS version for version-sensitive checks (e.g., type "null"
// being illegal in OAS 3.0.x but valid in OAS 3.1+).
v.oasVersion = parseResult.OASVersion
// Build reference tracker for operation context
switch doc := parseResult.Document.(type) {
case *parser.OAS3Document:
v.refTracker = buildRefTrackerOAS3(doc)
case *parser.OAS2Document:
v.refTracker = buildRefTrackerOAS2(doc)
}
// Add parser errors to validation result
for _, parseErr := range parseResult.Errors {
result.Errors = append(result.Errors, ValidationError{
Path: "document",
Message: parseErr.Error(),
Severity: SeverityError,
})
}
// Add parser warnings to validation result
for _, warning := range parseResult.Warnings {
result.Warnings = append(result.Warnings, ValidationError{
Path: "document",
Message: warning,
Severity: SeverityWarning,
})
}
// Perform additional validation based on OAS version
// Check both version and document type to ensure consistency
if parseResult.IsOAS2() {
if doc, ok := parseResult.OAS2Document(); ok {
v.validateOAS2(doc, result)
} else {
return nil, fmt.Errorf("validator: failed to cast document to OAS2Document")
}
} else if parseResult.IsOAS3() {
if doc, ok := parseResult.OAS3Document(); ok {
v.validateOAS3(doc, result)
} else {
return nil, fmt.Errorf("validator: failed to cast document to OAS3Document")
}
} else {
// in reality this should never happen, since the parser's `Parse` would have errored as well
return nil, fmt.Errorf("validator: unsupported OAS version: %s", parseResult.OASVersion)
}
// Calculate counts
result.ErrorCount = len(result.Errors)
result.WarningCount = len(result.Warnings)
result.Valid = result.ErrorCount == 0
// Filter warnings if not included
if !v.IncludeWarnings {
result.Warnings = nil
result.WarningCount = 0
}
return result, nil
}
// Validate validates an OpenAPI specification file
func (v *Validator) Validate(specPath string) (*ValidationResult, error) {
// Create parser and configure it
p := parser.New()
p.ValidateStructure = v.ValidateStructure
if v.UserAgent != "" {
p.UserAgent = v.UserAgent
}
// Parse the document
parseResult, err := p.Parse(specPath)
if err != nil {
return nil, fmt.Errorf("validator: failed to parse specification: %w", err)
}
return v.ValidateParsed(*parseResult)
}