dato_json_schema 0.20.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +75 -0
- data/bin/validate-schema +40 -0
- data/lib/commands/validate_schema.rb +130 -0
- data/lib/json_pointer.rb +7 -0
- data/lib/json_pointer/evaluator.rb +80 -0
- data/lib/json_reference.rb +58 -0
- data/lib/json_schema.rb +31 -0
- data/lib/json_schema/attributes.rb +117 -0
- data/lib/json_schema/configuration.rb +28 -0
- data/lib/json_schema/document_store.rb +30 -0
- data/lib/json_schema/error.rb +85 -0
- data/lib/json_schema/parser.rb +390 -0
- data/lib/json_schema/reference_expander.rb +364 -0
- data/lib/json_schema/schema.rb +295 -0
- data/lib/json_schema/validator.rb +606 -0
- data/schemas/hyper-schema.json +168 -0
- data/schemas/schema.json +150 -0
- data/test/bin_test.rb +19 -0
- data/test/commands/validate_schema_test.rb +121 -0
- data/test/data_scaffold.rb +241 -0
- data/test/json_pointer/evaluator_test.rb +69 -0
- data/test/json_reference/reference_test.rb +45 -0
- data/test/json_schema/attribute_test.rb +121 -0
- data/test/json_schema/document_store_test.rb +42 -0
- data/test/json_schema/error_test.rb +18 -0
- data/test/json_schema/parser_test.rb +362 -0
- data/test/json_schema/reference_expander_test.rb +618 -0
- data/test/json_schema/schema_test.rb +46 -0
- data/test/json_schema/validator_test.rb +1078 -0
- data/test/json_schema_test.rb +46 -0
- data/test/test_helper.rb +17 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 735c70cc15ebcd63ca8bf57f522e3530a5188edde5604477485248ec8492311f
|
4
|
+
data.tar.gz: ef9fa70ea29692a09b7b18b947305beb245060dcb67f30ecef35590d1c649fcf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da29a2be214acabeda042ab5d2a170711054b676087c56e568b41d83170cad46e63280e911cd4a01c5f59092418a9e507ef5bc2c80c7164ddd5680e065c029fe
|
7
|
+
data.tar.gz: b28c3d0fc1db24f3a11ecff03fe74c1006bfeaa97421ec2ce39bc94a03d364ea5926448a11cc13dcb58551daf2361f1bc087a18097175893d462d686a7d72336
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014-2015 Brandur and contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# json_schema
|
2
|
+
|
3
|
+
A JSON Schema V4 and Hyperschema V4 parser and validator.
|
4
|
+
|
5
|
+
Validate some data based on a JSON Schema:
|
6
|
+
|
7
|
+
```
|
8
|
+
gem install json_schema
|
9
|
+
validate-schema schema.json data.json
|
10
|
+
```
|
11
|
+
|
12
|
+
## Programmatic
|
13
|
+
|
14
|
+
``` ruby
|
15
|
+
require "json"
|
16
|
+
require "json_schema"
|
17
|
+
|
18
|
+
# parse the schema - raise SchemaError if it's invalid
|
19
|
+
schema_data = JSON.parse(File.read("schema.json"))
|
20
|
+
schema = JsonSchema.parse!(schema_data)
|
21
|
+
|
22
|
+
# expand $ref nodes - raise SchemaError if unable to resolve
|
23
|
+
schema.expand_references!
|
24
|
+
|
25
|
+
# validate some data - raise ValidationError if it doesn't conform
|
26
|
+
data = JSON.parse(File.read("data.json"))
|
27
|
+
schema.validate!(data)
|
28
|
+
|
29
|
+
# iterate through hyperschema links
|
30
|
+
schema.links.each do |link|
|
31
|
+
puts "#{link.method} #{link.href}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# abort on first error, instead of listing them all:
|
35
|
+
schema.validate!(data, fail_fast: true)
|
36
|
+
```
|
37
|
+
|
38
|
+
Errors have a `message` (for humans), and `type` (for machines).
|
39
|
+
`ValidationError`s also include a `path`, a JSON pointer to the location in
|
40
|
+
the supplied document which violated the schema. See [errors](docs/errors.md)
|
41
|
+
for more info.
|
42
|
+
|
43
|
+
Non-bang methods return a two-element array, with `true`/`false` at index 0
|
44
|
+
to indicate pass/fail, and an array of errors at index 1 (if any).
|
45
|
+
|
46
|
+
Passing `fail_fast: true` (default: `false`) will cause the validator to abort
|
47
|
+
on the first error it encounters and report just that. Even on fully valid data
|
48
|
+
this can offer some speed improvement, since it doesn't have to collect error
|
49
|
+
messages that might be later discarded (think of e.g. the `anyOf` directive).
|
50
|
+
|
51
|
+
## Development
|
52
|
+
|
53
|
+
Run the test suite with:
|
54
|
+
|
55
|
+
```
|
56
|
+
rake
|
57
|
+
```
|
58
|
+
|
59
|
+
Or run specific suites or tests with:
|
60
|
+
|
61
|
+
```
|
62
|
+
ruby -Ilib -Itest test/json_schema/validator_test.rb
|
63
|
+
ruby -Ilib -Itest test/json_schema/validator_test.rb -n /anyOf/
|
64
|
+
```
|
65
|
+
|
66
|
+
## Release
|
67
|
+
|
68
|
+
1. Update the version in `json_schema.gemspec` as appropriate for [semantic
|
69
|
+
versioning](http://semver.org) and add details to `CHANGELOG`.
|
70
|
+
2. `git commit` those changes with a message like "Bump version to x.y.z".
|
71
|
+
3. Run the `release` task:
|
72
|
+
|
73
|
+
```
|
74
|
+
bundle exec rake release
|
75
|
+
```
|
data/bin/validate-schema
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require_relative "../lib/commands/validate_schema"
|
5
|
+
|
6
|
+
def print_usage!
|
7
|
+
$stderr.puts "Usage: validate-schema <schema> <data>, ..."
|
8
|
+
$stderr.puts " validate-schema -d <data>, ..."
|
9
|
+
end
|
10
|
+
|
11
|
+
command = Commands::ValidateSchema.new
|
12
|
+
|
13
|
+
parser = OptionParser.new { |opts|
|
14
|
+
opts.on("-d", "--detect", "Detect schema from $schema") do
|
15
|
+
command.detect = true
|
16
|
+
|
17
|
+
# mix in common schemas for convenience
|
18
|
+
command.extra_schemas += ["schema.json", "hyper-schema.json"].
|
19
|
+
map { |f| File.expand_path(f, __FILE__ + "/../../schemas") }
|
20
|
+
end
|
21
|
+
opts.on("-s", "--schema SCHEMA", "Additional schema to use for references") do |s|
|
22
|
+
command.extra_schemas << s
|
23
|
+
end
|
24
|
+
opts.on("-f", "--fail-fast", "Abort after encountering the first validation error") do |s|
|
25
|
+
command.fail_fast = true
|
26
|
+
end
|
27
|
+
}
|
28
|
+
|
29
|
+
parser.parse!
|
30
|
+
success = command.run(ARGV.dup)
|
31
|
+
|
32
|
+
if success
|
33
|
+
command.messages.each { |m| $stdout.puts(m) }
|
34
|
+
elsif !command.errors.empty?
|
35
|
+
command.errors.each { |e| $stderr.puts(e) }
|
36
|
+
exit(1)
|
37
|
+
else
|
38
|
+
print_usage!
|
39
|
+
exit(1)
|
40
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require "json"
|
2
|
+
require "yaml"
|
3
|
+
require_relative "../json_schema"
|
4
|
+
|
5
|
+
module Commands
|
6
|
+
class ValidateSchema
|
7
|
+
attr_accessor :detect
|
8
|
+
attr_accessor :fail_fast
|
9
|
+
attr_accessor :extra_schemas
|
10
|
+
|
11
|
+
attr_accessor :errors
|
12
|
+
attr_accessor :messages
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@detect = false
|
16
|
+
@fail_fast = false
|
17
|
+
@extra_schemas = []
|
18
|
+
|
19
|
+
@errors = []
|
20
|
+
@messages = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(argv)
|
24
|
+
return false if !initialize_store
|
25
|
+
|
26
|
+
if !detect
|
27
|
+
return false if !(schema_file = argv.shift)
|
28
|
+
return false if !(schema = parse(schema_file))
|
29
|
+
end
|
30
|
+
|
31
|
+
# if there are no remaining files in arguments, also a problem
|
32
|
+
return false if argv.count < 1
|
33
|
+
|
34
|
+
argv.each do |data_file|
|
35
|
+
if !(data = read_file(data_file))
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
if detect
|
40
|
+
if !(schema_uri = data["$schema"])
|
41
|
+
@errors = ["#{data_file}: No $schema tag for detection."]
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
|
45
|
+
if !(schema = @store.lookup_schema(schema_uri))
|
46
|
+
@errors = ["#{data_file}: Unknown $schema, try specifying one with -s."]
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
valid, errors = schema.validate(data, fail_fast: fail_fast)
|
52
|
+
|
53
|
+
if valid
|
54
|
+
@messages += ["#{data_file} is valid."]
|
55
|
+
else
|
56
|
+
@errors = map_schema_errors(data_file, errors)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@errors.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def initialize_store
|
66
|
+
@store = JsonSchema::DocumentStore.new
|
67
|
+
extra_schemas.each do |extra_schema|
|
68
|
+
if !(extra_schema = parse(extra_schema))
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
@store.add_schema(extra_schema)
|
72
|
+
end
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Builds a JSON Reference + message like "/path/to/file#/path/to/data".
|
77
|
+
def map_schema_errors(file, errors)
|
78
|
+
errors.map { |m| "#{file}#{m}" }
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse(file)
|
82
|
+
if !(schema_data = read_file(file))
|
83
|
+
return nil
|
84
|
+
end
|
85
|
+
|
86
|
+
parser = JsonSchema::Parser.new
|
87
|
+
if !(schema = parser.parse(schema_data))
|
88
|
+
@errors = map_schema_errors(file, parser.errors)
|
89
|
+
return nil
|
90
|
+
end
|
91
|
+
|
92
|
+
expander = JsonSchema::ReferenceExpander.new
|
93
|
+
if !expander.expand(schema, store: @store)
|
94
|
+
@errors = map_schema_errors(file, expander.errors)
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
|
98
|
+
schema
|
99
|
+
end
|
100
|
+
|
101
|
+
def read_file(file)
|
102
|
+
contents = File.read(file)
|
103
|
+
|
104
|
+
# Perform an empty check because boath YAML and JSON's load will return
|
105
|
+
# `nil` in the case of an empty file, which will otherwise produce
|
106
|
+
# confusing results.
|
107
|
+
if contents.empty?
|
108
|
+
@errors = ["#{file}: File is empty."]
|
109
|
+
nil
|
110
|
+
else
|
111
|
+
if File.extname(file) == ".yaml"
|
112
|
+
YAML.load(contents)
|
113
|
+
else
|
114
|
+
JSON.load(contents)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
rescue Errno::ENOENT
|
118
|
+
@errors = ["#{file}: No such file or directory."]
|
119
|
+
nil
|
120
|
+
rescue JSON::ParserError
|
121
|
+
# Ruby's parsing exceptions aren't too helpful, just point user to
|
122
|
+
# a better tool
|
123
|
+
@errors = ["#{file}: Invalid JSON. Try to validate using `jsonlint`."]
|
124
|
+
nil
|
125
|
+
rescue Psych::SyntaxError
|
126
|
+
@errors = ["#{file}: Invalid YAML."]
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/json_pointer.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module JsonPointer
|
2
|
+
# Evaluates a JSON pointer within a JSON document.
|
3
|
+
#
|
4
|
+
# Note that this class is designed to evaluate references across a plain JSON
|
5
|
+
# data object _or_ an instance of `JsonSchema::Schema`, so the constructor's
|
6
|
+
# `data` argument can be of either type.
|
7
|
+
class Evaluator
|
8
|
+
def initialize(data)
|
9
|
+
@data = data
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(original_path)
|
13
|
+
path = original_path
|
14
|
+
|
15
|
+
# the leading # can either be included or not
|
16
|
+
path = path[1..-1] if path[0] == "#"
|
17
|
+
|
18
|
+
# special case on "" or presumably "#"
|
19
|
+
if path.empty?
|
20
|
+
return @data
|
21
|
+
end
|
22
|
+
|
23
|
+
if path[0] != "/"
|
24
|
+
raise ArgumentError, %{Path must begin with a leading "/": #{original_path}.}
|
25
|
+
end
|
26
|
+
|
27
|
+
path_parts = split(path)
|
28
|
+
evaluate_segment(@data, path_parts)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def evaluate_segment(data, path_parts)
|
34
|
+
if path_parts.empty?
|
35
|
+
data
|
36
|
+
elsif data == nil
|
37
|
+
# spec doesn't define how to handle this, so we'll return `nil`
|
38
|
+
nil
|
39
|
+
else
|
40
|
+
key = transform_key(path_parts.shift)
|
41
|
+
if data.is_a?(Array)
|
42
|
+
unless key =~ /^\d+$/
|
43
|
+
raise ArgumentError, %{Key operating on an array must be a digit or "-": #{key}.}
|
44
|
+
end
|
45
|
+
evaluate_segment(data[key.to_i], path_parts)
|
46
|
+
else
|
47
|
+
evaluate_segment(data[key], path_parts)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# custom split method to account for blank segments
|
53
|
+
def split(path)
|
54
|
+
parts = []
|
55
|
+
last_index = 0
|
56
|
+
while index = path.index("/", last_index)
|
57
|
+
if index == last_index
|
58
|
+
parts << ""
|
59
|
+
else
|
60
|
+
parts << path[last_index...index]
|
61
|
+
end
|
62
|
+
last_index = index + 1
|
63
|
+
end
|
64
|
+
# and also get that last segment
|
65
|
+
parts << path[last_index..-1]
|
66
|
+
# it should begin with a blank segment from the leading "/"; kill that
|
67
|
+
parts.shift
|
68
|
+
parts
|
69
|
+
end
|
70
|
+
|
71
|
+
def transform_key(key)
|
72
|
+
# ~ has special meaning to JSON pointer to allow keys containing "/", so
|
73
|
+
# perform some transformations first as defined by the spec
|
74
|
+
# first as defined by the spec
|
75
|
+
key = key.gsub('~1', '/')
|
76
|
+
key = key.gsub('~0', '~')
|
77
|
+
key
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "uri"
|
2
|
+
require_relative "json_pointer"
|
3
|
+
|
4
|
+
module JsonReference
|
5
|
+
def self.reference(ref)
|
6
|
+
Reference.new(ref)
|
7
|
+
end
|
8
|
+
|
9
|
+
class Reference
|
10
|
+
include Comparable
|
11
|
+
|
12
|
+
attr_accessor :pointer
|
13
|
+
attr_accessor :uri
|
14
|
+
|
15
|
+
def initialize(ref)
|
16
|
+
# Note that the #to_s of `nil` is an empty string.
|
17
|
+
@uri = nil
|
18
|
+
|
19
|
+
# given a simple fragment without '#', resolve as a JSON Pointer only as
|
20
|
+
# per spec
|
21
|
+
if ref.include?("#")
|
22
|
+
uri, @pointer = ref.split('#')
|
23
|
+
if uri && !uri.empty?
|
24
|
+
@uri = URI.parse(uri)
|
25
|
+
end
|
26
|
+
@pointer ||= ""
|
27
|
+
else
|
28
|
+
@pointer = ref
|
29
|
+
end
|
30
|
+
|
31
|
+
# normalize pointers by prepending "#" and stripping trailing "/"
|
32
|
+
@pointer = "#" + @pointer
|
33
|
+
@pointer = @pointer.chomp("/")
|
34
|
+
end
|
35
|
+
|
36
|
+
def <=>(other)
|
37
|
+
to_s <=> other.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
"\#<JsonReference::Reference #{to_s}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Given the document addressed by #uri, resolves the JSON Pointer part of
|
45
|
+
# the reference.
|
46
|
+
def resolve_pointer(data)
|
47
|
+
JsonPointer.evaluate(data, @pointer)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
if @uri
|
52
|
+
"#{@uri.to_s}#{@pointer}"
|
53
|
+
else
|
54
|
+
@pointer
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/json_schema.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative "json_schema/attributes"
|
2
|
+
require_relative "json_schema/configuration"
|
3
|
+
require_relative "json_schema/document_store"
|
4
|
+
require_relative "json_schema/error"
|
5
|
+
require_relative "json_schema/parser"
|
6
|
+
require_relative "json_schema/reference_expander"
|
7
|
+
require_relative "json_schema/schema"
|
8
|
+
require_relative "json_schema/validator"
|
9
|
+
|
10
|
+
module JsonSchema
|
11
|
+
def self.configure
|
12
|
+
yield configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configuration
|
16
|
+
@configuration ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse(data)
|
20
|
+
parser = Parser.new
|
21
|
+
if schema = parser.parse(data)
|
22
|
+
[schema, nil]
|
23
|
+
else
|
24
|
+
[nil, parser.errors]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.parse!(data)
|
29
|
+
Parser.new.parse!(data)
|
30
|
+
end
|
31
|
+
end
|