dato_json_schema 0.20.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|