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=
85 changes: 60 additions & 25 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
// yajsv is a command line tool for validating JSON documents against
// yajsv is a command line tool for validating JSON and YAML documents against
// a provided JSON Schema - https://json-schema.org/
package main

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 @@ -70,33 +70,49 @@ func realMain(args []string) int {
if !filepath.IsAbs(pattern) {
pattern = filepath.Join(dir, pattern)
}

docs = append(docs, glob(pattern)...)
}
if err := scanner.Err(); err != nil {
log.Fatalf("%s: invalid file list: %s\n", list, err)
}
}
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)
schemaPath, err := filepath.Abs(*schemaFlag)
if err != nil {
log.Fatalf("%s: unable to convert to absolute path: %s\n", *schemaFlag, err)
}
for _, ref := range refFlags {
for _, p := range glob(ref) {
uri := fileUri(p)
if uri == schemaUri {
absPath, absPathErr := filepath.Abs(p)
if absPathErr != nil {
log.Fatalf("%s: unable to convert to absolute path: %s\n", absPath, absPathErr)
}

if absPath == schemaPath {
continue
}
loader := gojsonschema.NewReferenceLoader(uri)
err := sl.AddSchemas(loader)

loader, err := jsonLoader(absPath)
if err != nil {
log.Fatalf("%s: unable to load schema ref: %s\n", *schemaFlag, err)
}

if err := sl.AddSchemas(loader); err != nil {
log.Fatalf("%s: invalid schema: %s\n", p, err)
}
}
}
schemaLoader := gojsonschema.NewReferenceLoader(schemaUri)

schemaLoader, err := jsonLoader(schemaPath)
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 @@ -109,17 +125,25 @@ func realMain(args []string) int {
failures := make([]string, 0)
errors := make([]string, 0)
for _, p := range docs {
//fmt.Println(p)
wg.Add(1)
go func(path string) {
defer wg.Done()
sem <- 0
defer func() { <-sem }()

loader := gojsonschema.NewReferenceLoader(fileUri(path))

loader, err := jsonLoader(path)
if err != nil {
msg := fmt.Sprintf("%s: error: load doc %s\n", path, err)
fmt.Println(msg)
errors = append(errors, msg)
return
}
result, err := schema.Validate(loader)
switch {
case err != nil:
msg := fmt.Sprintf("%s: error: %s", path, err)
msg := fmt.Sprintf("%s: error: validate: %s", path, err)
fmt.Println(msg)
errors = append(errors, msg)
zendril marked this conversation as resolved.
Show resolved Hide resolved

Expand Down Expand Up @@ -160,15 +184,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|yml) [options] document.(json|yml) ...

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,29 +228,25 @@ 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 {
pattern, err := homedir.Expand(pattern)
if err != nil {
log.Fatal(err)
}
universalPaths := make([]string, 0)
paths, err := filepath.Glob(pattern)
for _, mypath := range paths {
universalPaths = append(universalPaths, filepath.ToSlash(mypath))
}
if err != nil {
log.Fatal(err)
}
if len(paths) == 0 {
if len(universalPaths) == 0 {
log.Fatalf("%s: no such file or directory", pattern)
}
return paths
return universalPaths
}

type stringFlags []string
Expand Down
88 changes: 81 additions & 7 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
// +build !windows
// +build windows !windows

package main

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,21 +69,50 @@ 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)
}
// Output:
// testdata/data-error.json: error: invalid character 'o' in literal null (expecting 'u')
// testdata/data-error.json: error: validate: 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/data-error.yml: error: 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)
}
// Unordered output:
// testdata/data-error.json: error: invalid character 'o' in literal null (expecting 'u')
// testdata/data-error.json: error: validate: 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/data-fail.yml: fail: (root): foo is required
//
// testdata/data-error.yml: error: load doc yaml: found unexpected end of stream
}
Loading