diff --git a/README.md b/README.md index 9e057a66..35102e04 100644 --- a/README.md +++ b/README.md @@ -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] +``` +🔍 Example: Use a custom regex engine/flag (e.g., ecmascript) +```bash +go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --regexengine=ecmascript +``` +🔧 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) diff --git a/cmd/validate/main.go b/cmd/validate/main.go new file mode 100644 index 00000000..58ffcf47 --- /dev/null +++ b/cmd/validate/main.go @@ -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 +} + +func (re *customRegexp) String() string { + return (*regexp2.Regexp)(re).String() +} + +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 +} + +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] + +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. +`) + } + + for _, arg := range os.Args[1:] { + if arg == "--help" || arg == "-h" { + flag.Usage() + os.Exit(0) + } + } + + 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 := ®exEngine{ + runtimeOption: regexEngineOption, + } + + validationOpts = append(validationOpts, config.WithRegexEngine(reEngine.run)) + } + + data, err := os.ReadFile(filename) + if err != nil { + logger.Error("error reading file", slog.String("provided", filename), slog.Any("error", err)) + os.Exit(1) + } + + doc, err := libopenapi.NewDocument(data) + if err != nil { + logger.Error("error creating new libopenapi document", slog.Any("error", err)) + os.Exit(1) + } + + 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) + } + + 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)) +}