hashema 0.0.2 → 0.1.0
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.
- data/README.md +173 -4
- data/lib/hashema.rb +4 -6
- data/lib/hashema/compiler.rb +38 -0
- data/lib/hashema/conform_to_schema.rb +63 -16
- data/lib/hashema/schema.rb +259 -0
- data/lib/hashema/validator.rb +10 -101
- data/lib/hashema/version.rb +1 -1
- metadata +16 -11
- checksums.yaml +0 -7
data/README.md
CHANGED
@@ -2,9 +2,141 @@
|
|
2
2
|
|
3
3
|
Hashema lets you validate JSONable objects (hashes and arrays) against a schema, and assert their validity in your RSpec examples.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Installation
|
6
6
|
|
7
|
-
|
7
|
+
```bash
|
8
|
+
gem install hashema
|
9
|
+
```
|
10
|
+
|
11
|
+
Or, if you're using [bundler](https://rubygems.org/gems/bundler), put this in your `Gemfile`:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
gem 'hashema'
|
15
|
+
```
|
16
|
+
|
17
|
+
Hashema hooks into your RSpec config to provide the `conform_to_schema` matcher. If `rspec` is listed in your `Gemfile`, you should be able to use `conform_to_schema` in your tests with no further setup.
|
18
|
+
|
19
|
+
## RSpec usage
|
20
|
+
|
21
|
+
With hashema and RSpec, it's easy to ensure your JSON APIs return the data your clients expect.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
describe BlogSerializer do
|
25
|
+
before { @serializer = BlogSerializer.new(Blog.create) }
|
26
|
+
|
27
|
+
describe '#as_json' do
|
28
|
+
subject { @serializer.as_json }
|
29
|
+
|
30
|
+
SCHEMA = {
|
31
|
+
url: /^https?:\/\/.+/,
|
32
|
+
posts: [
|
33
|
+
{ title: String,
|
34
|
+
published: [true, false]
|
35
|
+
}
|
36
|
+
]
|
37
|
+
}
|
38
|
+
|
39
|
+
it { is_expected_to conform_to_schema SCHEMA }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## The Schema DSL by example
|
45
|
+
|
46
|
+
### Allowing any value
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
expect(
|
50
|
+
Rotation.new('squirrel')
|
51
|
+
).to conform_to_schema Object
|
52
|
+
```
|
53
|
+
|
54
|
+
### Checking for an exact match
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
expect(
|
58
|
+
{error: 'not found'}
|
59
|
+
).to conform_to_schema({error: 'not found'})
|
60
|
+
```
|
61
|
+
|
62
|
+
### Checking for membership in a class
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
expect(
|
66
|
+
{berzerker: 'pasta'}
|
67
|
+
).to conform_to_schema Hash
|
68
|
+
```
|
69
|
+
|
70
|
+
### Checking that a string value matches a regular expression
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
expect(
|
74
|
+
'Hello! My name is Fridge.'
|
75
|
+
).to conform_to_schema /^Hello! My name is \w+\.$/
|
76
|
+
```
|
77
|
+
|
78
|
+
### Checking for inclusion in a set of alternatives
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
expect(
|
82
|
+
{is_awesome: true}
|
83
|
+
).to conform_to_schema({is_awesome: [true, false]})
|
84
|
+
```
|
85
|
+
|
86
|
+
### Checking for inclusion in a range of legal values
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
expect(
|
90
|
+
{kyu_rank: 17}
|
91
|
+
).to conform_to_schema({kyu_rank: 1..30})
|
92
|
+
```
|
93
|
+
|
94
|
+
### Checking that all elements of an array share a schema
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
expect(
|
98
|
+
[{name: 'Melody'}, {name: 'Elias'}, {name: 'Yoda'}]
|
99
|
+
).to conform_to_schema [{name: String}]
|
100
|
+
```
|
101
|
+
|
102
|
+
### Matching an array of items that may have different schemas
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
expect(
|
106
|
+
[{cash: '12.33'}, {credit: '28.95'}, {cash: '40.70'}]
|
107
|
+
).to conform_to_schema(
|
108
|
+
[
|
109
|
+
[{cash: /^\d+\.\d\d$/}, {credit: /^\d+\.\d\d$/}]
|
110
|
+
]
|
111
|
+
)
|
112
|
+
```
|
113
|
+
|
114
|
+
## RSpec matcher options
|
115
|
+
|
116
|
+
### with_indifferent_access
|
117
|
+
|
118
|
+
Rails fans will be familiar with ActiveSupport's `HashWithIndifferentAccess`, which treats symbol and string keys as interchangeable. Calling `with_indifferent_access` on the `conform_to_schema` matcher will make the matcher similarly tolerant, allowing you to match a hash with string keys against a schema with symbol keys. This is especially useful when writing schemas for JSON, since parsed objects will always have string keys.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
get :show
|
122
|
+
|
123
|
+
schema = {
|
124
|
+
url: /^https?:\/\/.+/,
|
125
|
+
posts: [
|
126
|
+
{ title: String,
|
127
|
+
published: [true, false]
|
128
|
+
}
|
129
|
+
]
|
130
|
+
}
|
131
|
+
|
132
|
+
expect(JSON(response.body)).to conform_to_schema(schema).with_indifferent_access
|
133
|
+
```
|
134
|
+
|
135
|
+
## Hashema without RSpec
|
136
|
+
|
137
|
+
There are times when you want to validate the structure of a data object in your production code. For example, if your program parses data from a user-created file, you might want to check that the data you read in match the schema you expect. For such situations, you can use `Hashema::Validator`.
|
138
|
+
|
139
|
+
The API of `Hashema::Validator` consists of an initializer and two instance methods: `valid?` and `failure_message`. The initializer takes an object to validate and a schema, in that order. `valid?` will return `true` iff the object conforms to the schema.
|
8
140
|
|
9
141
|
```ruby
|
10
142
|
validator = Hashema::Validator.new(
|
@@ -37,8 +169,45 @@ validator = Hashema::Validator.new(
|
|
37
169
|
validator.valid? # true
|
38
170
|
```
|
39
171
|
|
40
|
-
|
172
|
+
If `valid?` is `false`, `failure_message` will return a human-readable description of the failure, which includes, at a minimum:
|
173
|
+
|
174
|
+
- the path through the data structure to the point where the first mismatch occurred
|
175
|
+
- the expected value at that point
|
176
|
+
- the actual value
|
41
177
|
|
42
178
|
```ruby
|
43
|
-
|
179
|
+
validator = Hashema::Validator.new(
|
180
|
+
# the object to validate
|
181
|
+
{ blog:
|
182
|
+
{ url: 'http://www.blagoblag.com',
|
183
|
+
posts: [
|
184
|
+
{ title: 'hello',
|
185
|
+
published: true
|
186
|
+
},
|
187
|
+
{ title: 123,
|
188
|
+
published: false
|
189
|
+
}
|
190
|
+
]
|
191
|
+
}
|
192
|
+
},
|
193
|
+
|
194
|
+
# the schema
|
195
|
+
{ blog:
|
196
|
+
{ url: /^https?:\/\//,
|
197
|
+
posts: [
|
198
|
+
{ title: String,
|
199
|
+
published: [true, false]
|
200
|
+
}
|
201
|
+
]
|
202
|
+
}
|
203
|
+
}
|
204
|
+
)
|
205
|
+
|
206
|
+
validator.valid? # false
|
207
|
+
puts validator.failure_message
|
208
|
+
# prints:
|
209
|
+
# expected /blog/posts/1/title to match
|
210
|
+
# String
|
211
|
+
# but got
|
212
|
+
# 123
|
44
213
|
```
|
data/lib/hashema.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative './schema'
|
2
|
+
|
3
|
+
module Hashema
|
4
|
+
class Compiler
|
5
|
+
def self.compile(thing, options={})
|
6
|
+
new(options).compile(thing)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile(thing)
|
14
|
+
case thing
|
15
|
+
when ::Array
|
16
|
+
if thing.size == 1
|
17
|
+
Hashema::Array.new(compile(thing[0]))
|
18
|
+
else
|
19
|
+
Hashema::Alternatives.new(*(thing.map { |element| compile element }))
|
20
|
+
end
|
21
|
+
when ::Hash
|
22
|
+
compile_hash(thing)
|
23
|
+
else
|
24
|
+
Hashema::Atom.new(thing)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def compile_hash(hash)
|
31
|
+
with_compiled_values = ::Hash[hash.map { |k, v| [k, compile(v)]}]
|
32
|
+
klass = @options[:indifferent_access] ?
|
33
|
+
Hashema::HashWithIndifferentAccess :
|
34
|
+
Hashema::Hash
|
35
|
+
klass.new(with_compiled_values)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,24 +1,71 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
@validator = Hashema::Validator.new(actual, schema)
|
6
|
-
@validator.valid?
|
7
|
-
end
|
1
|
+
begin
|
2
|
+
require 'rspec'
|
3
|
+
rescue LoadError => e
|
4
|
+
end
|
8
5
|
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
module Hashema
|
7
|
+
module RSpecMatchers
|
8
|
+
class ConformToSchema
|
9
|
+
def initialize(schema)
|
10
|
+
@schema = schema
|
11
|
+
@with_indifferent_access = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_indifferent_access
|
15
|
+
@with_indifferent_access = true
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def matches?(actual)
|
20
|
+
@validator = Hashema::Validator.new(actual, @schema, validator_options)
|
21
|
+
@validator.valid?
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message
|
25
|
+
@validator.failure_message
|
26
|
+
end
|
27
|
+
|
28
|
+
def failure_message_for_should
|
29
|
+
failure_message
|
30
|
+
end
|
12
31
|
|
13
|
-
|
14
|
-
|
32
|
+
def failure_message_when_negated
|
33
|
+
# TODO: @actual is nil here. This probably doesn't work.
|
34
|
+
"expected\n#{@actual.inspect}\nnot to match schema\n#{@schema.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def failure_message_for_should_not
|
38
|
+
failure_message_when_negated
|
39
|
+
end
|
40
|
+
|
41
|
+
def description
|
42
|
+
"match schema\n#{@schema.inspect}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def validator_options
|
46
|
+
{indifferent_access: @with_indifferent_access}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def conform_to_schema(schema)
|
51
|
+
ConformToSchema.new(schema)
|
52
|
+
end
|
15
53
|
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if Kernel.const_defined? 'RSpec'
|
57
|
+
class Hashema::RSpecMatchers::ConformToSchema
|
58
|
+
include RSpec::Matchers::Composable
|
16
59
|
|
17
|
-
|
18
|
-
|
60
|
+
RSpec::Matchers.alias_matcher(
|
61
|
+
:an_object_conforming_to_schema,
|
62
|
+
:conform_to_schema
|
63
|
+
) do |description|
|
64
|
+
description.sub("match schema\n", 'an object conforming to schema ')
|
65
|
+
end
|
19
66
|
end
|
20
67
|
|
21
|
-
|
22
|
-
|
68
|
+
RSpec.configure do |config|
|
69
|
+
config.include Hashema::RSpecMatchers
|
23
70
|
end
|
24
71
|
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
module Hashema
|
2
|
+
class Schema < Struct.new(:expected)
|
3
|
+
# A Schema is a Comparison factory.
|
4
|
+
def compare(actual)
|
5
|
+
self.class.const_get('Comparison').new(actual, expected)
|
6
|
+
end
|
7
|
+
|
8
|
+
def inspect
|
9
|
+
expected.inspect
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Comparison < Struct.new(:actual, :expected)
|
14
|
+
def match?
|
15
|
+
mismatches.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def mismatches
|
19
|
+
@mismatches ||= find_mismatches
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def find_mismatches
|
25
|
+
raise NotImplementedError.new(
|
26
|
+
"#{self.class.name} must implement find_mismatches"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Atom < Schema
|
32
|
+
class Comparison < Hashema::Comparison
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def find_mismatches
|
37
|
+
expected === actual ? [] : [Mismatch.new(actual, expected, [])]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Array < Schema
|
43
|
+
class Comparison < Hashema::Comparison
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def find_mismatches
|
48
|
+
type_mismatches || element_mismatches
|
49
|
+
end
|
50
|
+
|
51
|
+
def type_mismatches
|
52
|
+
expectation = "be an Array, but got #{actual.class}"
|
53
|
+
actual.is_a?(::Array) ? nil : [Mismatch.new(actual, ::Array, [], expectation)]
|
54
|
+
end
|
55
|
+
|
56
|
+
def element_mismatches
|
57
|
+
actual.each_with_index.flat_map do |element, i|
|
58
|
+
element_comparison = expected.compare(element)
|
59
|
+
|
60
|
+
element_comparison.mismatches.map do |mismatch|
|
61
|
+
Mismatch.at i, mismatch
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Map < Schema
|
69
|
+
class Comparison < Hashema::Comparison
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def find_mismatches
|
74
|
+
type_mismatch || (keyset_mismatches + value_mismatches)
|
75
|
+
end
|
76
|
+
|
77
|
+
def type_mismatch
|
78
|
+
expectation = "be a #{expected_class}, but got #{actual.class}"
|
79
|
+
actual.is_a?(expected_class) ?
|
80
|
+
nil :
|
81
|
+
[Mismatch.new(actual, expected_class, [], expectation)]
|
82
|
+
end
|
83
|
+
|
84
|
+
def expected_class
|
85
|
+
raise NotImplementedError.new "#{self.class.name} must implement expected_class"
|
86
|
+
end
|
87
|
+
|
88
|
+
def value_mismatches
|
89
|
+
matching_keys.flat_map do |key|
|
90
|
+
comparison = fetch(key, expected).compare(fetch(key, actual))
|
91
|
+
|
92
|
+
comparison.mismatches.map do |mismatch|
|
93
|
+
Mismatch.at key, mismatch
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def keyset_mismatches
|
99
|
+
if extra_keys.empty? && missing_keys.empty?
|
100
|
+
[]
|
101
|
+
else
|
102
|
+
missing_keys_expectation = missing_keys.any? ?
|
103
|
+
"\nmissing keys were:\n\t#{missing_keys.map(&:inspect).join("\n\t")}" :
|
104
|
+
''
|
105
|
+
|
106
|
+
extra_keys_expectation = extra_keys.any? ?
|
107
|
+
"\nextra keys were:\n\t#{extra_keys.map(&:inspect).join("\n\t")}" :
|
108
|
+
''
|
109
|
+
|
110
|
+
expectation = "have a different set of keys" +
|
111
|
+
missing_keys_expectation +
|
112
|
+
extra_keys_expectation
|
113
|
+
|
114
|
+
[Mismatch.new(actual, expected, [], expectation)]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def extra_keys
|
119
|
+
raise NotImplementedError.new "#{self.class.name} must implement extra_keys"
|
120
|
+
end
|
121
|
+
|
122
|
+
def missing_keys
|
123
|
+
raise NotImplementedError.new "#{self.class.name} must implement missing_keys"
|
124
|
+
end
|
125
|
+
|
126
|
+
def matching_keys
|
127
|
+
raise NotImplementedError.new "#{self.class.name} must implement matching_keys"
|
128
|
+
end
|
129
|
+
|
130
|
+
def fetch(key, from_map)
|
131
|
+
raise NotImplementedError.new "#{self.class.name} must implement fetch"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Hash < Schema
|
137
|
+
class Comparison < Hashema::Map::Comparison
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def expected_class
|
142
|
+
::Hash
|
143
|
+
end
|
144
|
+
|
145
|
+
def extra_keys
|
146
|
+
@extra_keys ||= actual_keys - expected_keys
|
147
|
+
end
|
148
|
+
|
149
|
+
def missing_keys
|
150
|
+
@missing_keys ||= expected_keys - actual_keys
|
151
|
+
end
|
152
|
+
|
153
|
+
def matching_keys
|
154
|
+
@matching_keys ||= expected.keys & actual.keys
|
155
|
+
end
|
156
|
+
|
157
|
+
def fetch(key, hash)
|
158
|
+
hash[key]
|
159
|
+
end
|
160
|
+
|
161
|
+
def expected_keys
|
162
|
+
@expected_keys ||= Set.new(expected.keys)
|
163
|
+
end
|
164
|
+
|
165
|
+
def actual_keys
|
166
|
+
@actual_keys ||= Set.new(actual.keys)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class Alternatives < Schema
|
172
|
+
def initialize(*args)
|
173
|
+
super(args)
|
174
|
+
end
|
175
|
+
|
176
|
+
class Comparison < Hashema::Comparison
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def find_mismatches
|
181
|
+
if expected.none? { |alternative| alternative.compare(actual).match? }
|
182
|
+
[Mismatch.new(actual, expected, [])]
|
183
|
+
else
|
184
|
+
[]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class HashWithIndifferentAccess < Schema
|
191
|
+
class Comparison < Hashema::Map::Comparison
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def expected_class
|
196
|
+
::Hash
|
197
|
+
end
|
198
|
+
|
199
|
+
def extra_keys
|
200
|
+
@extra_keys ||= actual.keys.reject do |key|
|
201
|
+
expected.has_key? symbol_to_string key or
|
202
|
+
expected.has_key? string_to_symbol key
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def missing_keys
|
207
|
+
@missing_keys ||= expected.keys.reject do |key|
|
208
|
+
actual.has_key? symbol_to_string key or
|
209
|
+
actual.has_key? string_to_symbol key
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def matching_keys
|
214
|
+
@matching_keys ||=
|
215
|
+
Set.new(expected.keys.map(&method(:symbol_to_string))) &
|
216
|
+
Set.new(actual.keys.map(&method(:symbol_to_string)))
|
217
|
+
end
|
218
|
+
|
219
|
+
def fetch(key, hash)
|
220
|
+
return hash[symbol_to_string key] if hash.has_key? symbol_to_string key
|
221
|
+
return hash[string_to_symbol key] if hash.has_key? string_to_symbol key
|
222
|
+
end
|
223
|
+
|
224
|
+
def string_to_symbol(key)
|
225
|
+
key.is_a?(String) ? key.to_sym : key
|
226
|
+
end
|
227
|
+
|
228
|
+
def symbol_to_string(key)
|
229
|
+
key.is_a?(Symbol) ? key.to_s : key
|
230
|
+
end
|
231
|
+
|
232
|
+
def expected_keys
|
233
|
+
@expected_keys ||= Set.new(expected.keys.map(&method(:symbol_to_string)))
|
234
|
+
end
|
235
|
+
|
236
|
+
def actual_keys
|
237
|
+
@actual_keys ||= Set.new(actual.keys.map(&method(:symbol_to_string)))
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class Mismatch < Struct.new(:actual, :expected, :location, :verb)
|
243
|
+
def self.at(location, original)
|
244
|
+
new original.actual,
|
245
|
+
original.expected,
|
246
|
+
[location] + original.location
|
247
|
+
end
|
248
|
+
|
249
|
+
def message
|
250
|
+
"expected /#{location.join '/'} to #{verb}"
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
def verb
|
256
|
+
super || "match\n\t#{expected.inspect}\nbut got\n\t#{actual.inspect}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
data/lib/hashema/validator.rb
CHANGED
@@ -1,120 +1,29 @@
|
|
1
|
+
require_relative './compiler'
|
2
|
+
|
1
3
|
module Hashema
|
2
4
|
class Validator
|
3
|
-
def initialize(actual, schema)
|
5
|
+
def initialize(actual, schema, options={})
|
4
6
|
@actual = actual
|
5
7
|
@schema = schema
|
6
|
-
@
|
7
|
-
match! @actual, @schema
|
8
|
+
@schema = compile schema, options
|
8
9
|
end
|
9
10
|
|
10
11
|
def valid?
|
11
|
-
|
12
|
+
comparison.match?
|
12
13
|
end
|
13
14
|
|
14
15
|
def failure_message
|
15
|
-
|
16
|
+
comparison.mismatches[0].message
|
16
17
|
end
|
17
18
|
|
18
19
|
private
|
19
20
|
|
20
|
-
def
|
21
|
-
|
22
|
-
if schema.is_a? Hash
|
23
|
-
match_hash! actual, schema, path
|
24
|
-
elsif schema.is_a? Array and schema.length == 1
|
25
|
-
match_array! actual, schema, path
|
26
|
-
elsif schema.is_a? Array and schema.length > 1
|
27
|
-
match_alternatives! actual, schema, path
|
28
|
-
else
|
29
|
-
match_with_triple_equals! actual, schema, path
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def match_hash!(actual, schema, path)
|
35
|
-
if actual.is_a? Hash
|
36
|
-
if schema.keys.sort == actual.keys.sort
|
37
|
-
match_hash_with_same_keys! actual, schema, path
|
38
|
-
else
|
39
|
-
report_mismatched_key_sets! actual, schema, path
|
40
|
-
end
|
41
|
-
else
|
42
|
-
report_error "expected #{format_path path} to be a Hash, but got #{actual.class}"
|
43
|
-
false
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def match_hash_with_same_keys!(actual, schema, path)
|
48
|
-
recording_mismatches actual, schema, path do
|
49
|
-
actual.all? do |key, value|
|
50
|
-
match! value, schema[key], path + [key]
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def report_mismatched_key_sets!(actual, schema, path)
|
56
|
-
extras = actual.keys - schema.keys
|
57
|
-
missing = schema.keys - actual.keys
|
58
|
-
error = "expected #{format_path path} to have a different set of keys\n"
|
59
|
-
error += "the extra keys were:\n #{extras.map(&:inspect).join("\n ")}\n" if extras.any?
|
60
|
-
error += "the missing keys were:\n #{missing.map(&:inspect).join("\n ")}\n" if missing.any?
|
61
|
-
report_error error
|
62
|
-
false
|
63
|
-
end
|
64
|
-
|
65
|
-
def match_array!(actual, schema, path)
|
66
|
-
if actual.is_a? Array
|
67
|
-
recording_mismatches actual, schema, path do
|
68
|
-
actual.is_a? Array and
|
69
|
-
actual.each_with_index.all? { |elem, i| match! elem, schema[0], path + [i] }
|
70
|
-
end
|
71
|
-
else
|
72
|
-
report_error "expected #{format_path path} to be an Array, but got #{actual.class}"
|
73
|
-
false
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def match_alternatives!(actual, alternatives, path)
|
78
|
-
recording_mismatches actual, alternatives, path do
|
79
|
-
alternatives.any? do |alternative|
|
80
|
-
withholding_judgement actual, alternatives, path do
|
81
|
-
match! actual, alternative, path
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def match_with_triple_equals!(actual, expected, path)
|
88
|
-
recording_mismatches actual, expected, path do
|
89
|
-
expected === actual
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def withholding_judgement(actual, schema, path)
|
94
|
-
original_withholding_judgement = @withholding_judgement
|
95
|
-
@withholding_judgement = true
|
96
|
-
returned = yield
|
97
|
-
@withholding_judgement = original_withholding_judgement
|
98
|
-
returned
|
99
|
-
end
|
100
|
-
|
101
|
-
def recording_mismatches(actual, schema, path)
|
102
|
-
if yield
|
103
|
-
true
|
104
|
-
else
|
105
|
-
report_error "expected #{format_path path} to match\n#{schema.inspect}\nbut got\n#{actual.inspect}"
|
106
|
-
false
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def report_error(error)
|
111
|
-
unless @withholding_judgement
|
112
|
-
@mismatch ||= error
|
113
|
-
end
|
21
|
+
def compile(schema, options={})
|
22
|
+
Hashema::Compiler.compile(schema, options)
|
114
23
|
end
|
115
24
|
|
116
|
-
def
|
117
|
-
|
25
|
+
def comparison
|
26
|
+
@comparison ||= @schema.compare(@actual)
|
118
27
|
end
|
119
28
|
end
|
120
29
|
end
|
data/lib/hashema/version.rb
CHANGED
metadata
CHANGED
@@ -1,27 +1,30 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hashema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Ben Christel
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2015-05-04 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rspec
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
16
18
|
requirements:
|
17
|
-
- - '>='
|
19
|
+
- - ! '>='
|
18
20
|
- !ruby/object:Gem::Version
|
19
21
|
version: 2.0.0
|
20
22
|
type: :development
|
21
23
|
prerelease: false
|
22
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
23
26
|
requirements:
|
24
|
-
- - '>='
|
27
|
+
- - ! '>='
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: 2.0.0
|
27
30
|
description: Assert that JSONable objects conform to a schema
|
@@ -32,7 +35,9 @@ extra_rdoc_files:
|
|
32
35
|
- README.md
|
33
36
|
files:
|
34
37
|
- lib/hashema.rb
|
38
|
+
- lib/hashema/compiler.rb
|
35
39
|
- lib/hashema/conform_to_schema.rb
|
40
|
+
- lib/hashema/schema.rb
|
36
41
|
- lib/hashema/validator.rb
|
37
42
|
- lib/hashema/version.rb
|
38
43
|
- License.txt
|
@@ -40,27 +45,27 @@ files:
|
|
40
45
|
homepage: http://github.com/benchristel/hashema
|
41
46
|
licenses:
|
42
47
|
- MIT
|
43
|
-
metadata: {}
|
44
48
|
post_install_message:
|
45
49
|
rdoc_options:
|
46
50
|
- --charset=UTF-8
|
47
51
|
require_paths:
|
48
52
|
- lib
|
49
53
|
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
50
55
|
requirements:
|
51
|
-
- - '>='
|
56
|
+
- - ! '>='
|
52
57
|
- !ruby/object:Gem::Version
|
53
58
|
version: '0'
|
54
59
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
55
61
|
requirements:
|
56
|
-
- - '>='
|
62
|
+
- - ! '>='
|
57
63
|
- !ruby/object:Gem::Version
|
58
64
|
version: '0'
|
59
65
|
requirements: []
|
60
66
|
rubyforge_project:
|
61
|
-
rubygems_version:
|
67
|
+
rubygems_version: 1.8.23
|
62
68
|
signing_key:
|
63
|
-
specification_version:
|
64
|
-
summary: hashema-0.0
|
69
|
+
specification_version: 3
|
70
|
+
summary: hashema-0.1.0
|
65
71
|
test_files: []
|
66
|
-
has_rdoc:
|
checksums.yaml
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: 0320697b5755730fd8ffa18843b0f3b496cfd739
|
4
|
-
data.tar.gz: 8bf8bcedb3d51e880d5a13c499754c3fc604a5c0
|
5
|
-
SHA512:
|
6
|
-
metadata.gz: f8e5882479f1cbb3a9923575812eafc7d78281a69fd070f6ac488468e4b9d002ccfaca37b41717522df1d8429a2559251b07c321941f09a388f99a28aaafda12
|
7
|
-
data.tar.gz: 98a5238131421fb6fe61ff625cf2cbd71ffaaada4d2280df5079f25540709aa0e6cdb872336eb05a229d6819116ff2d894ffe3640cb5db1a03b7c35c5d154a7b
|