Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement yaml support for schema and target doc [READY FOR REVIEW] #6

Merged
merged 11 commits into from
Mar 22, 2020
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
yajsv
build/
coverage.out
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![CI](https://github.com/neilpa/yajsv/workflows/CI/badge.svg)](https://github.com/neilpa/yajsv/actions/)

Yet Another [JSON-Schema](https://json-schema.org) Validator. Command line tool for validating JSON documents against provided schemas.
Yet Another [JSON-Schema](https://json-schema.org) Validator. Command line tool for validating JSON and YAML documents against provided schemas.

The real credit goes to [xeipuuv/gojsonschema](https://github.com/xeipuuv/gojsonschema) which does the heavy lifting behind this CLI.

Expand All @@ -18,21 +18,34 @@ There are also pre-built static binaries for Windows, Mac and Linux on the [rele

## Usage

yajsv validates JSON documents against a schema, providing a status per document:
yajsv validates JSON and YAML documents against a schema, providing a status per document:

* pass: Document is valid relative to the schema
* fail: Document is invalid relative to the schema
* error: Document is malformed, e.g. not valid JSON
* error: Document is malformed, e.g. not valid JSON or YAML

The 'fail' status may be reported multiple times per-document, once for each schema validation failure.

Basic usage

Any combination can be used for schema and document. For example you can use a JSON schema to validate a YAML document.


Basic usage example

```
$ yajsv -s schema.json document.json
document.json: pass
```

Basic usage example with YAML schema and document:

```
$ yajsv -s schema.yml document.yml
document.yml: pass
```


With multiple schema files and docs

```
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module github.com/neilpa/yajsv
go 1.12

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ghodss/yaml v1.0.0
github.com/mitchellh/go-homedir v1.1.0
github.com/xeipuuv/gojsonschema v1.2.0
neilpa.me/go-x v0.1.0
gopkg.in/yaml.v2 v2.2.8 // indirect
)
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -13,5 +17,7 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
neilpa.me/go-x v0.1.0 h1:bry050ou4HtEhZ3vZEFRKrzqvObodseVvfcQvK/M8U4=
neilpa.me/go-x v0.1.0/go.mod h1:aIemU+pQYLLV3dygXotHKF7SantXe5HzZR6VIjzY/4g=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
93 changes: 57 additions & 36 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import (
"bufio"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"sync"

"github.com/ghodss/yaml"
"github.com/mitchellh/go-homedir"
"github.com/xeipuuv/gojsonschema"
"neilpa.me/go-x/fileuri"
)

var (
version = "undefined"
zendril marked this conversation as resolved.
Show resolved Hide resolved

schemaFlag = flag.String("s", "", "primary JSON schema to validate against, required")
quietFlag = flag.Bool("q", false, "quiet, only print validation failures and errors")
versionFlag = flag.Bool("v", false, "print version and exit")
Expand Down Expand Up @@ -77,26 +77,33 @@ func realMain(args []string) int {
}
}
if len(docs) == 0 {
return usageError("no JSON documents to validate")
return usageError("no documents to validate")
}

// Compile target schema
sl := gojsonschema.NewSchemaLoader()
schemaUri := fileUri(*schemaFlag)
schemaUri := *schemaFlag
zendril marked this conversation as resolved.
Show resolved Hide resolved
for _, ref := range refFlags {
for _, p := range glob(ref) {
uri := fileUri(p)
if uri == schemaUri {
if p == schemaUri {
zendril marked this conversation as resolved.
Show resolved Hide resolved
continue
}
loader := gojsonschema.NewReferenceLoader(uri)
err := sl.AddSchemas(loader)

loader, err := jsonLoader(p)
if err != nil {
log.Fatalf("%s: unable to load schema ref: %s\n", *schemaFlag, err)
}
addSchemaErr := sl.AddSchemas(loader)
if addSchemaErr != nil {
zendril marked this conversation as resolved.
Show resolved Hide resolved
log.Fatalf("%s: invalid schema: %s\n", p, err)
}
}
}
schemaLoader := gojsonschema.NewReferenceLoader(schemaUri)

schemaLoader, err := jsonLoader(schemaUri)
if err != nil {
log.Fatalf("%s: unable to load schema: %s\n", *schemaFlag, err)
}
schema, err := sl.Compile(schemaLoader)
if err != nil {
log.Fatalf("%s: invalid schema: %s\n", *schemaFlag, err)
Expand All @@ -115,25 +122,32 @@ func realMain(args []string) int {
sem <- 0
defer func() { <-sem }()

loader := gojsonschema.NewReferenceLoader(fileUri(path))
result, err := schema.Validate(loader)
switch {
case err != nil:
msg := fmt.Sprintf("%s: error: %s", path, err)

loader, err := jsonLoader(path)
if err != nil {
msg := fmt.Sprintf("%s: unable to load doc: %s\n", *schemaFlag, err)
zendril marked this conversation as resolved.
Show resolved Hide resolved
fmt.Println(msg)
errors = append(errors, msg)
zendril marked this conversation as resolved.
Show resolved Hide resolved

case !result.Valid():
lines := make([]string, len(result.Errors()))
for i, desc := range result.Errors() {
lines[i] = fmt.Sprintf("%s: fail: %s", path, desc)
} else {
result, err := schema.Validate(loader)
switch {
case err != nil:
msg := fmt.Sprintf("%s: error: %s", path, err)
zendril marked this conversation as resolved.
Show resolved Hide resolved
fmt.Println(msg)
errors = append(errors, msg)

case !result.Valid():
lines := make([]string, len(result.Errors()))
for i, desc := range result.Errors() {
lines[i] = fmt.Sprintf("%s: fail: %s", path, desc)
}
msg := strings.Join(lines, "\n")
fmt.Println(msg)
failures = append(failures, msg)

case !*quietFlag:
fmt.Printf("%s: pass\n", path)
}
msg := strings.Join(lines, "\n")
fmt.Println(msg)
failures = append(failures, msg)

case !*quietFlag:
fmt.Printf("%s: pass\n", path)
}
}(p)
}
Expand All @@ -160,15 +174,30 @@ func realMain(args []string) int {
return exit
}

func jsonLoader(path string) (gojsonschema.JSONLoader, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
switch filepath.Ext(path) {
case ".yml", ".yaml":
buf, err = yaml.YAMLToJSON(buf)
}
if err != nil {
return nil, err
}
return gojsonschema.NewBytesLoader(buf), nil
}

func printUsage() {
fmt.Fprintf(os.Stderr, `Usage: %s -s schema.json [options] document.json ...
fmt.Fprintf(os.Stderr, `Usage: %s -s schema.json|schema.yml [options] document.json|document.yml ...
zendril marked this conversation as resolved.
Show resolved Hide resolved

yajsv validates JSON document(s) against a schema. One of three statuses are
yajsv validates JSON and YAML document(s) against a schema. One of three statuses are
reported per document:

pass: Document is valid relative to the schema
fail: Document is invalid relative to the schema
error: Document is malformed, e.g. not valid JSON
error: Document is malformed, e.g. not valid JSON or YAML

The 'fail' status may be reported multiple times per-document, once for each
schema validation failure.
Expand All @@ -189,14 +218,6 @@ func usageError(msg string) int {
return 4
}

func fileUri(path string) string {
uri, err := fileuri.FromPath(path)
if err != nil {
log.Fatalf("%s: %s", path, err)
}
return uri
}

// glob is a wrapper that also resolves `~` since we may be skipping
// the shell expansion when single-quoting globs at the command line
func glob(pattern string) []string {
Expand Down
82 changes: 78 additions & 4 deletions main_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,25 @@ import (
"log"
)

func ExampleMain_pass() {
func ExampleMain_pass_ymlschema_ymldoc() {
exit := realMain([]string{"-s", "testdata\\schema.yml", "testdata\\data-pass.yml"})
if exit != 0 {
log.Fatalf("exit: got %d, want 0", exit)
}
// Output:
// testdata\data-pass.yml: pass
}

func ExampleMain_pass_jsonschema_ymldoc() {
exit := realMain([]string{"-s", "testdata\\schema.json", "testdata\\data-pass.yml"})
if exit != 0 {
log.Fatalf("exit: got %d, want 0", exit)
}
// Output:
// testdata\data-pass.yml: pass
}

func ExampleMain_pass_jsonschema_jsondoc() {
exit := realMain([]string{"-s", "testdata\\schema.json", "testdata\\data-pass.json"})
if exit != 0 {
log.Fatalf("exit: got %d, want 0", exit)
Expand All @@ -15,7 +33,34 @@ func ExampleMain_pass() {
// testdata\data-pass.json: pass
}

func ExampleMain_fail() {
func ExampleMain_pass_ymlschema_jsondoc() {
exit := realMain([]string{"-s", "testdata\\schema.yml", "testdata\\data-pass.json"})
if exit != 0 {
log.Fatalf("exit: got %d, want 0", exit)
}
// Output:
// testdata\data-pass.json: pass
}

func ExampleMain_fail_ymlschema_ymldoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.yml", "testdata\\data-fail.yml"})
if exit != 1 {
log.Fatalf("exit: got %d, want 1", exit)
}
// Output:
// testdata\data-fail.yml: fail: (root): foo is required
}

func ExampleMain_fail_jsonschema_ymldoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.json", "testdata\\data-fail.yml"})
if exit != 1 {
log.Fatalf("exit: got %d, want 1", exit)
}
// Output:
// testdata\data-fail.yml: fail: (root): foo is required
}

func ExampleMain_fail_jsonschema_jsondoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.json", "testdata\\data-fail.json"})
if exit != 1 {
log.Fatalf("exit: got %d, want 1", exit)
Expand All @@ -24,7 +69,16 @@ func ExampleMain_fail() {
// testdata\data-fail.json: fail: (root): foo is required
}

func ExampleMain_error() {
func ExampleMain_fail_ymlschema_jsondoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.yml", "testdata\\data-fail.json"})
if exit != 1 {
log.Fatalf("exit: got %d, want 1", exit)
}
// Output:
// testdata\data-fail.json: fail: (root): foo is required
}

func ExampleMain_error_jsonschema_jsondoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.json", "testdata\\data-error.json"})
if exit != 2 {
log.Fatalf("exit: got %d, want 2", exit)
Expand All @@ -33,7 +87,16 @@ func ExampleMain_error() {
// testdata\data-error.json: error: invalid character 'o' in literal null (expecting 'u')
}

func ExampleMain_glob() {
func ExampleMain_error_ymlschema_ymldoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.yml", "testdata\\data-error.yml"})
if exit != 2 {
log.Fatalf("exit: got %d, want 2", exit)
}
// Output:
// testdata\schema.yml: unable to load doc: yaml: found unexpected end of stream
}

func ExampleMain_glob_jsonschema_jsondoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.json", "testdata\\data-*.json"})
if exit != 3 {
log.Fatalf("exit: got %d, want 3", exit)
Expand All @@ -42,3 +105,14 @@ func ExampleMain_glob() {
// testdata\data-error.json: error: invalid character 'o' in literal null (expecting 'u')
// testdata\data-fail.json: fail: (root): foo is required
}

func ExampleMain_glob_ymlschema_ymldoc() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.yml", "testdata\\data-*.yml"})
if exit != 3 {
log.Fatalf("exit: got %d, want 3", exit)
}
// Unordered output:
// testdata\schema.yml: unable to load doc: yaml: found unexpected end of stream
//
// testdata\data-fail.yml: fail: (root): foo is required
}
1 change: 1 addition & 0 deletions testdata/data-error.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid: "an escaped \' single quote is not valid yaml
2 changes: 2 additions & 0 deletions testdata/data-fail.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
bar: missing foo
3 changes: 3 additions & 0 deletions testdata/data-pass.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
foo: asdf
bar: zxcv
7 changes: 7 additions & 0 deletions testdata/schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
properties:
foo:
type: string
bar: {}
required:
- foo