diff --git a/.gitignore b/.gitignore index 7f8d7ec42f938e9c9beccf297c47976224988fbc..e17055d92e8013bd8e77386ec18603dae0248dbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ yajsv build/ +coverage.out \ No newline at end of file diff --git a/README.md b/README.md index 9260b2944920599940230d10f55e363cd31a16d0..58a1e77a59caf293cf68e1658fa879d016e6c63c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [](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. @@ -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 ``` diff --git a/go.mod b/go.mod index 9748099f8d9e2a034b5da82e84666112420cdda4..9f96eafe8ccbbb6a6b79c11c5fe16f6abeae00ef 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 96c7825ea334dad73a4b490f1adcdcded729bb2d..5b7e5c4e3306b08d0064a4012b2ba46eaae0fc83 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/main.go b/main.go index e123f48971e180ad5ffe8f1c53e6deff13254e22..a21d14ad0f27a509f21d8ffa43f1e02b7a9c6a26 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// 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 @@ -6,6 +6,7 @@ import ( "bufio" "flag" "fmt" + "io/ioutil" "log" "os" "path/filepath" @@ -13,14 +14,13 @@ import ( "strings" "sync" + "github.com/ghodss/yaml" "github.com/mitchellh/go-homedir" "github.com/xeipuuv/gojsonschema" - "neilpa.me/go-x/fileuri" ) var ( version = "undefined" - 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") @@ -70,6 +70,7 @@ 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 { @@ -77,26 +78,41 @@ 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) + 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) @@ -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) @@ -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. @@ -189,14 +228,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 { @@ -204,14 +235,18 @@ func glob(pattern string) []string { 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 diff --git a/main_test.go b/main_test.go index dcefe3ab5773cb4da6acf9efb9fb43814df2dc3e..875ba19dc7967d15ff940e810beb2476a8f25733 100644 --- a/main_test.go +++ b/main_test.go @@ -1,4 +1,4 @@ -// +build !windows +// +build windows !windows package main @@ -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) @@ -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) @@ -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 +} diff --git a/main_windows_test.go b/main_windows_test.go deleted file mode 100644 index 2d25c02c64662ab57ebc629e14d966647e1ee269..0000000000000000000000000000000000000000 --- a/main_windows_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// +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 -} diff --git a/testdata/data-error.yml b/testdata/data-error.yml new file mode 100644 index 0000000000000000000000000000000000000000..0370438aea1d328d1285a880a63f00a436004829 --- /dev/null +++ b/testdata/data-error.yml @@ -0,0 +1 @@ +invalid: "an escaped \' single quote is not valid yaml \ No newline at end of file diff --git a/testdata/data-fail.yml b/testdata/data-fail.yml new file mode 100644 index 0000000000000000000000000000000000000000..5f2b215eed91feeeba334f55ca29a3198f404df5 --- /dev/null +++ b/testdata/data-fail.yml @@ -0,0 +1,2 @@ +--- +bar: missing foo diff --git a/testdata/data-pass.yml b/testdata/data-pass.yml new file mode 100644 index 0000000000000000000000000000000000000000..73761e387a2a94053a53efc8d363e7d473d4e9c3 --- /dev/null +++ b/testdata/data-pass.yml @@ -0,0 +1,3 @@ +--- +foo: asdf +bar: zxcv diff --git a/testdata/schema.yml b/testdata/schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..2d1e4c9606d87911a17f517e264f847e425c4cef --- /dev/null +++ b/testdata/schema.yml @@ -0,0 +1,7 @@ +--- +properties: + foo: + type: string + bar: {} +required: + - foo