Skip to content

Commit 92de722

Browse files
committed
feat: add cli cmd to validate single oas spec
1 parent 224827e commit 92de722

2 files changed

Lines changed: 180 additions & 0 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ A validation module for [libopenapi](https://github.com/pb33f/libopenapi).
2828
go get github.com/pb33f/libopenapi-validator
2929
```
3030

31+
## Validate OpenAPI Document
32+
33+
```bash
34+
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest [--regexengine] <file>
35+
```
36+
🔍 Example: Use a custom regex engine/flag (e.g., ecmascript)
37+
```bash
38+
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --regexengine=ecmascript <file>
39+
```
40+
🔧 Supported **--regexengine** flags/values
41+
- none
42+
- ignorecase
43+
- multiline
44+
- explicitcapture
45+
- compiled
46+
- singleline
47+
- ignorepatternwhitespace
48+
- righttoleft
49+
- debug
50+
- ecmascript
51+
- re2
52+
- unicode
53+
ℹ️ Default: re2
54+
3155
## Documentation
3256

3357
- [The structure of the validator](https://pb33f.io/libopenapi/validation/#the-structure-of-the-validator)

cmd/validate/main.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"flag"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
10+
"github.com/dlclark/regexp2"
11+
"github.com/pb33f/libopenapi"
12+
validator "github.com/pb33f/libopenapi-validator"
13+
"github.com/pb33f/libopenapi-validator/config"
14+
"github.com/santhosh-tekuri/jsonschema/v6"
15+
)
16+
17+
type customRegexp regexp2.Regexp
18+
19+
func (re *customRegexp) MatchString(s string) bool {
20+
matched, err := (*regexp2.Regexp)(re).MatchString(s)
21+
return err == nil && matched
22+
}
23+
24+
func (re *customRegexp) String() string {
25+
return (*regexp2.Regexp)(re).String()
26+
}
27+
28+
type regexEngine struct {
29+
runtimeOption regexp2.RegexOptions
30+
}
31+
32+
func (e *regexEngine) run(s string) (jsonschema.Regexp, error) {
33+
re, err := regexp2.Compile(s, e.runtimeOption)
34+
if err != nil {
35+
return nil, err
36+
}
37+
return (*customRegexp)(re), nil
38+
}
39+
40+
var regexParsingOptionsMap = map[string]regexp2.RegexOptions{
41+
"none": regexp2.None,
42+
"ignorecase": regexp2.IgnoreCase,
43+
"multiline": regexp2.Multiline,
44+
"explicitcapture": regexp2.ExplicitCapture,
45+
"compiled": regexp2.Compiled,
46+
"singleline": regexp2.Singleline,
47+
"ignorepatternwhitespace": regexp2.IgnorePatternWhitespace,
48+
"righttoleft": regexp2.RightToLeft,
49+
"debug": regexp2.Debug,
50+
"ecmascript": regexp2.ECMAScript,
51+
"re2": regexp2.RE2,
52+
"unicode": regexp2.Unicode,
53+
}
54+
55+
var (
56+
defaultRegexEngine = ""
57+
regexParsingOptions = flag.String("regexengine", defaultRegexEngine, `Specify the regex parsing option to use.
58+
Supported values are:
59+
Engines: re2 (default), ecmascript
60+
Flags: ignorecase, multiline, explicitcapture, compiled,
61+
singleline, ignorepatternwhitespace, righttoleft,
62+
debug, unicode
63+
If not specified, the default libopenapi option is "re2".
64+
65+
If not specified, the default libopenapi regex engine is "re2"".`)
66+
)
67+
68+
func main() {
69+
flag.Usage = func() {
70+
fmt.Fprintf(os.Stderr, `Usage: validate [OPTIONS] <file>
71+
72+
Validates an OpenAPI document using libopenapi-validator.
73+
74+
Options:
75+
--regexengine string Specify the regex parsing option to use.
76+
Supported values are:
77+
Engines: re2 (default), ecmascript
78+
Flags: ignorecase, multiline, explicitcapture, compiled,
79+
singleline, ignorepatternwhitespace, righttoleft,
80+
debug, unicode
81+
If not specified, the default libopenapi option is "re2".
82+
83+
-h, --help Show this help message and exit.
84+
`)
85+
}
86+
87+
for _, arg := range os.Args[1:] {
88+
if arg == "--help" || arg == "-h" {
89+
flag.Usage()
90+
os.Exit(0)
91+
}
92+
}
93+
94+
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
95+
flag.Parse()
96+
filename := flag.Arg(0)
97+
if len(flag.Args()) != 1 || filename == "" {
98+
logger.Error("missing file argument", slog.Any("args", os.Args))
99+
flag.Usage()
100+
os.Exit(1)
101+
}
102+
validationOpts := []config.Option{}
103+
if *regexParsingOptions != "" {
104+
regexEngineOption, ok := regexParsingOptionsMap[*regexParsingOptions]
105+
if !ok {
106+
logger.Error("unsupported regex option provided",
107+
slog.String("provided", *regexParsingOptions),
108+
slog.Any("supported", []string{
109+
"none",
110+
"ignorecase",
111+
"multiline",
112+
"explicitcapture",
113+
"compiled",
114+
"singleline",
115+
"ignorepatternwhitespace",
116+
"righttoleft",
117+
"debug",
118+
"ecmascript",
119+
"re2",
120+
"unicode",
121+
}),
122+
)
123+
os.Exit(1)
124+
}
125+
reEngine := &regexEngine{
126+
runtimeOption: regexEngineOption,
127+
}
128+
129+
validationOpts = append(validationOpts, config.WithRegexEngine(reEngine.run))
130+
}
131+
132+
data, err := os.ReadFile(filename)
133+
if err != nil {
134+
logger.Error("error reading file", slog.String("provided", filename), slog.Any("error", err))
135+
os.Exit(1)
136+
}
137+
138+
doc, err := libopenapi.NewDocument(data)
139+
if err != nil {
140+
logger.Error("error creating new libopenapi document", slog.Any("error", err))
141+
os.Exit(1)
142+
}
143+
144+
docValidator, validatorErrs := validator.NewValidator(doc, validationOpts...)
145+
if len(validatorErrs) > 0 {
146+
logger.Error("error creating a new validator", slog.Any("errors", errors.Join(validatorErrs...)))
147+
os.Exit(1)
148+
}
149+
150+
valid, validationErrs := docValidator.ValidateDocument()
151+
if !valid {
152+
logger.Error("validation errors", slog.Any("errors", validationErrs))
153+
os.Exit(1)
154+
}
155+
logger.Info("document passes all validations", slog.String("filename", filename))
156+
}

0 commit comments

Comments
 (0)