Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ A validation module for [libopenapi](https://github.com/pb33f/libopenapi).
go get github.com/pb33f/libopenapi-validator
```

## Validate OpenAPI Document

```bash
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest [--regexengine] <file>
```
🔍 Example: Use a custom regex engine/flag (e.g., ecmascript)
```bash
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --regexengine=ecmascript <file>
```
🔧 Supported **--regexengine** flags/values (ℹ️ Default: re2)
- none
- ignorecase
- multiline
- explicitcapture
- compiled
- singleline
- ignorepatternwhitespace
- righttoleft
- debug
- ecmascript
- re2
- unicode

## Documentation

- [The structure of the validator](https://pb33f.io/libopenapi/validation/#the-structure-of-the-validator)
Expand Down
177 changes: 177 additions & 0 deletions cmd/validate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package main

import (
"errors"
"flag"
"fmt"
"log/slog"
"os"

"github.com/dlclark/regexp2"
"github.com/pb33f/libopenapi"
"github.com/santhosh-tekuri/jsonschema/v6"

validator "github.com/pb33f/libopenapi-validator"
"github.com/pb33f/libopenapi-validator/config"
)

type customRegexp regexp2.Regexp

func (re *customRegexp) MatchString(s string) bool {
matched, err := (*regexp2.Regexp)(re).MatchString(s)
return err == nil && matched

Check warning on line 22 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L20-L22

Added lines #L20 - L22 were not covered by tests
}

func (re *customRegexp) String() string {
return (*regexp2.Regexp)(re).String()

Check warning on line 26 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L25-L26

Added lines #L25 - L26 were not covered by tests
}

type regexEngine struct {
runtimeOption regexp2.RegexOptions
}

func (e *regexEngine) run(s string) (jsonschema.Regexp, error) {
re, err := regexp2.Compile(s, e.runtimeOption)
if err != nil {
return nil, err
}
return (*customRegexp)(re), nil

Check warning on line 38 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L33-L38

Added lines #L33 - L38 were not covered by tests
}

var regexParsingOptionsMap = map[string]regexp2.RegexOptions{
"none": regexp2.None,
"ignorecase": regexp2.IgnoreCase,
"multiline": regexp2.Multiline,
"explicitcapture": regexp2.ExplicitCapture,
"compiled": regexp2.Compiled,
"singleline": regexp2.Singleline,
"ignorepatternwhitespace": regexp2.IgnorePatternWhitespace,
"righttoleft": regexp2.RightToLeft,
"debug": regexp2.Debug,
"ecmascript": regexp2.ECMAScript,
"re2": regexp2.RE2,
"unicode": regexp2.Unicode,
}

var (
defaultRegexEngine = ""
regexParsingOptions = flag.String("regexengine", defaultRegexEngine, `Specify the regex parsing option to use.
Supported values are:
Engines: re2 (default), ecmascript
Flags: ignorecase, multiline, explicitcapture, compiled,
singleline, ignorepatternwhitespace, righttoleft,
debug, unicode
If not specified, the default libopenapi option is "re2".

If not specified, the default libopenapi regex engine is "re2"".`)
)

// main is the entry point for validating an OpenAPI Specification (OAS) document.
// It uses the libopenapi-validator library to check if the provided OAS document
// conforms to the OpenAPI specification.
//
// This tool accepts a single input file (YAML or JSON) and provides an optional
// `--regexengine` flag to customize the regex engine used during validation.
// This is useful for cases where the spec uses regex patterns that require engines
// like ECMAScript or RE2.
//
// Supported regex options include:
// - Engines: re2 (default), ecmascript
// - Flags: ignorecase, multiline, explicitcapture, compiled, singleline,
// ignorepatternwhitespace, righttoleft, debug, unicode
//
// Example usage:
//
// go run main.go --regexengine=ecmascript ./my-api-spec.yaml
//
// If validation passes, the tool logs a success message.
// If the document is invalid or there is a processing error, it logs details and exits non-zero.
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: validate [OPTIONS] <file>

Validates an OpenAPI document using libopenapi-validator.

Options:
--regexengine string Specify the regex parsing option to use.
Supported values are:
Engines: re2 (default), ecmascript
Flags: ignorecase, multiline, explicitcapture, compiled,
singleline, ignorepatternwhitespace, righttoleft,
debug, unicode
If not specified, the default libopenapi option is "re2".

-h, --help Show this help message and exit.
`)
}

Check warning on line 106 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L89-L106

Added lines #L89 - L106 were not covered by tests

for _, arg := range os.Args[1:] {
if arg == "--help" || arg == "-h" {
flag.Usage()
os.Exit(0)
}

Check warning on line 112 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L108-L112

Added lines #L108 - L112 were not covered by tests
}

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
flag.Parse()
filename := flag.Arg(0)
if len(flag.Args()) != 1 || filename == "" {
logger.Error("missing file argument", slog.Any("args", os.Args))
flag.Usage()
os.Exit(1)
}
validationOpts := []config.Option{}
if *regexParsingOptions != "" {
regexEngineOption, ok := regexParsingOptionsMap[*regexParsingOptions]
if !ok {
logger.Error("unsupported regex option provided",
slog.String("provided", *regexParsingOptions),
slog.Any("supported", []string{
"none",
"ignorecase",
"multiline",
"explicitcapture",
"compiled",
"singleline",
"ignorepatternwhitespace",
"righttoleft",
"debug",
"ecmascript",
"re2",
"unicode",
}),
)
os.Exit(1)
}
reEngine := &regexEngine{
runtimeOption: regexEngineOption,
}

validationOpts = append(validationOpts, config.WithRegexEngine(reEngine.run))

Check warning on line 150 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L115-L150

Added lines #L115 - L150 were not covered by tests
}

data, err := os.ReadFile(filename)
if err != nil {
logger.Error("error reading file", slog.String("provided", filename), slog.Any("error", err))
os.Exit(1)
}

Check warning on line 157 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L153-L157

Added lines #L153 - L157 were not covered by tests

doc, err := libopenapi.NewDocument(data)
if err != nil {
logger.Error("error creating new libopenapi document", slog.Any("error", err))
os.Exit(1)
}

Check warning on line 163 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L159-L163

Added lines #L159 - L163 were not covered by tests

docValidator, validatorErrs := validator.NewValidator(doc, validationOpts...)
if len(validatorErrs) > 0 {
logger.Error("error creating a new validator", slog.Any("errors", errors.Join(validatorErrs...)))
os.Exit(1)
}

Check warning on line 169 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L165-L169

Added lines #L165 - L169 were not covered by tests

valid, validationErrs := docValidator.ValidateDocument()
if !valid {
logger.Error("validation errors", slog.Any("errors", validationErrs))
os.Exit(1)
}
logger.Info("document passes all validations", slog.String("filename", filename))

Check warning on line 176 in cmd/validate/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/main.go#L171-L176

Added lines #L171 - L176 were not covered by tests
}
Loading