Skip to content
Snippets Groups Projects
Unverified Commit 506f5ad5 authored by Neil Pankey's avatar Neil Pankey Committed by GitHub
Browse files

Merge pull request #6 from zendril/master

Implement yaml support for schema and target doc
parents a0568993 717589cb
No related branches found
No related tags found
No related merge requests found
yajsv yajsv
build/ build/
coverage.out
\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
[![CI](https://github.com/neilpa/yajsv/workflows/CI/badge.svg)](https://github.com/neilpa/yajsv/actions/) [![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. The real credit goes to [xeipuuv/gojsonschema](https://github.com/xeipuuv/gojsonschema) which does the heavy lifting behind this CLI.
...@@ -18,21 +18,34 @@ There are also pre-built static binaries for Windows, Mac and Linux on the [rele ...@@ -18,21 +18,34 @@ There are also pre-built static binaries for Windows, Mac and Linux on the [rele
## Usage ## 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 * pass: Document is valid relative to the schema
* fail: Document is invalid 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. The 'fail' status may be reported multiple times per-document, once for each schema validation failure.
Basic usage 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 $ yajsv -s schema.json document.json
document.json: pass 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 With multiple schema files and docs
``` ```
......
...@@ -3,7 +3,9 @@ module github.com/neilpa/yajsv ...@@ -3,7 +3,9 @@ module github.com/neilpa/yajsv
go 1.12 go 1.12
require ( 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/mitchellh/go-homedir v1.1.0
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
neilpa.me/go-x v0.1.0 gopkg.in/yaml.v2 v2.2.8 // indirect
) )
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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.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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
...@@ -13,5 +17,7 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo ...@@ -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/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 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
neilpa.me/go-x v0.1.0 h1:bry050ou4HtEhZ3vZEFRKrzqvObodseVvfcQvK/M8U4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
neilpa.me/go-x v0.1.0/go.mod h1:aIemU+pQYLLV3dygXotHKF7SantXe5HzZR6VIjzY/4g= 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=
// 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/ // a provided JSON Schema - https://json-schema.org/
package main package main
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"bufio" "bufio"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
...@@ -13,14 +14,13 @@ import ( ...@@ -13,14 +14,13 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/ghodss/yaml"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/xeipuuv/gojsonschema" "github.com/xeipuuv/gojsonschema"
"neilpa.me/go-x/fileuri"
) )
var ( var (
version = "undefined" version = "undefined"
schemaFlag = flag.String("s", "", "primary JSON schema to validate against, required") schemaFlag = flag.String("s", "", "primary JSON schema to validate against, required")
quietFlag = flag.Bool("q", false, "quiet, only print validation failures and errors") quietFlag = flag.Bool("q", false, "quiet, only print validation failures and errors")
versionFlag = flag.Bool("v", false, "print version and exit") versionFlag = flag.Bool("v", false, "print version and exit")
...@@ -70,6 +70,7 @@ func realMain(args []string) int { ...@@ -70,6 +70,7 @@ func realMain(args []string) int {
if !filepath.IsAbs(pattern) { if !filepath.IsAbs(pattern) {
pattern = filepath.Join(dir, pattern) pattern = filepath.Join(dir, pattern)
} }
docs = append(docs, glob(pattern)...) docs = append(docs, glob(pattern)...)
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
...@@ -77,26 +78,41 @@ func realMain(args []string) int { ...@@ -77,26 +78,41 @@ func realMain(args []string) int {
} }
} }
if len(docs) == 0 { if len(docs) == 0 {
return usageError("no JSON documents to validate") return usageError("no documents to validate")
} }
// Compile target schema // Compile target schema
sl := gojsonschema.NewSchemaLoader() 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 _, ref := range refFlags {
for _, p := range glob(ref) { for _, p := range glob(ref) {
uri := fileUri(p) absPath, absPathErr := filepath.Abs(p)
if uri == schemaUri { if absPathErr != nil {
log.Fatalf("%s: unable to convert to absolute path: %s\n", absPath, absPathErr)
}
if absPath == schemaPath {
continue continue
} }
loader := gojsonschema.NewReferenceLoader(uri)
err := sl.AddSchemas(loader) loader, err := jsonLoader(absPath)
if err != nil { 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) 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) schema, err := sl.Compile(schemaLoader)
if err != nil { if err != nil {
log.Fatalf("%s: invalid schema: %s\n", *schemaFlag, err) log.Fatalf("%s: invalid schema: %s\n", *schemaFlag, err)
...@@ -109,17 +125,25 @@ func realMain(args []string) int { ...@@ -109,17 +125,25 @@ func realMain(args []string) int {
failures := make([]string, 0) failures := make([]string, 0)
errors := make([]string, 0) errors := make([]string, 0)
for _, p := range docs { for _, p := range docs {
//fmt.Println(p)
wg.Add(1) wg.Add(1)
go func(path string) { go func(path string) {
defer wg.Done() defer wg.Done()
sem <- 0 sem <- 0
defer func() { <-sem }() 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) result, err := schema.Validate(loader)
switch { switch {
case err != nil: case err != nil:
msg := fmt.Sprintf("%s: error: %s", path, err) msg := fmt.Sprintf("%s: error: validate: %s", path, err)
fmt.Println(msg) fmt.Println(msg)
errors = append(errors, msg) errors = append(errors, msg)
...@@ -160,15 +184,30 @@ func realMain(args []string) int { ...@@ -160,15 +184,30 @@ func realMain(args []string) int {
return exit 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() { 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: reported per document:
pass: Document is valid relative to the schema pass: Document is valid relative to the schema
fail: Document is invalid 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 The 'fail' status may be reported multiple times per-document, once for each
schema validation failure. schema validation failure.
...@@ -189,14 +228,6 @@ func usageError(msg string) int { ...@@ -189,14 +228,6 @@ func usageError(msg string) int {
return 4 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 // glob is a wrapper that also resolves `~` since we may be skipping
// the shell expansion when single-quoting globs at the command line // the shell expansion when single-quoting globs at the command line
func glob(pattern string) []string { func glob(pattern string) []string {
...@@ -204,14 +235,18 @@ func glob(pattern string) []string { ...@@ -204,14 +235,18 @@ func glob(pattern string) []string {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
universalPaths := make([]string, 0)
paths, err := filepath.Glob(pattern) paths, err := filepath.Glob(pattern)
for _, mypath := range paths {
universalPaths = append(universalPaths, filepath.ToSlash(mypath))
}
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(paths) == 0 { if len(universalPaths) == 0 {
log.Fatalf("%s: no such file or directory", pattern) log.Fatalf("%s: no such file or directory", pattern)
} }
return paths return universalPaths
} }
type stringFlags []string type stringFlags []string
......
// +build !windows // +build windows !windows
package main package main
...@@ -6,7 +6,25 @@ import ( ...@@ -6,7 +6,25 @@ import (
"log" "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"}) exit := realMain([]string{"-s", "testdata/schema.json", "testdata/data-pass.json"})
if exit != 0 { if exit != 0 {
log.Fatalf("exit: got %d, want 0", exit) log.Fatalf("exit: got %d, want 0", exit)
...@@ -15,7 +33,34 @@ func ExampleMain_pass() { ...@@ -15,7 +33,34 @@ func ExampleMain_pass() {
// testdata/data-pass.json: 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"}) exit := realMain([]string{"-q", "-s", "testdata/schema.json", "testdata/data-fail.json"})
if exit != 1 { if exit != 1 {
log.Fatalf("exit: got %d, want 1", exit) log.Fatalf("exit: got %d, want 1", exit)
...@@ -24,21 +69,50 @@ func ExampleMain_fail() { ...@@ -24,21 +69,50 @@ func ExampleMain_fail() {
// testdata/data-fail.json: fail: (root): foo is required // 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"}) exit := realMain([]string{"-q", "-s", "testdata/schema.json", "testdata/data-error.json"})
if exit != 2 { if exit != 2 {
log.Fatalf("exit: got %d, want 2", exit) log.Fatalf("exit: got %d, want 2", exit)
} }
// Output: // 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"}) exit := realMain([]string{"-q", "-s", "testdata/schema.json", "testdata/data-*.json"})
if exit != 3 { if exit != 3 {
log.Fatalf("exit: got %d, want 3", exit) log.Fatalf("exit: got %d, want 3", exit)
} }
// Unordered output: // 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 // 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
}
// +build windows
package main
import (
"log"
)
func ExampleMain_pass() {
exit := realMain([]string{"-s", "testdata\\schema.json", "testdata\\data-pass.json"})
if exit != 0 {
log.Fatalf("exit: got %d, want 0", exit)
}
// Output:
// testdata\data-pass.json: pass
}
func ExampleMain_fail() {
exit := realMain([]string{"-q", "-s", "testdata\\schema.json", "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() {
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')
}
func ExampleMain_glob() {
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-fail.json: fail: (root): foo is required
}
invalid: "an escaped \' single quote is not valid yaml
\ No newline at end of file
---
bar: missing foo
---
foo: asdf
bar: zxcv
---
properties:
foo:
type: string
bar: {}
required:
- foo
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment