object-inspector 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/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +45 -0
- data/README.md +335 -0
- data/Rakefile +13 -0
- data/examples/example.rb +131 -0
- data/features/error_display.feature +80 -0
- data/features/support/env.rb +1 -0
- data/features/syntax.feature +223 -0
- data/inspector.gemspec +24 -0
- data/lib/inspector.rb +27 -0
- data/lib/inspector/attribute_metadata.rb +19 -0
- data/lib/inspector/constraint.rb +18 -0
- data/lib/inspector/constraint/validators.rb +8 -0
- data/lib/inspector/constraint/validators/simple.rb +13 -0
- data/lib/inspector/constraint/validators/validity.rb +24 -0
- data/lib/inspector/constraint/violation.rb +34 -0
- data/lib/inspector/constraint/violation/list.rb +93 -0
- data/lib/inspector/constraints.rb +61 -0
- data/lib/inspector/constraints/email.rb +57 -0
- data/lib/inspector/constraints/empty.rb +21 -0
- data/lib/inspector/constraints/eq.rb +23 -0
- data/lib/inspector/constraints/false.rb +19 -0
- data/lib/inspector/constraints/have.rb +85 -0
- data/lib/inspector/constraints/predicate.rb +38 -0
- data/lib/inspector/constraints/true.rb +19 -0
- data/lib/inspector/constraints/valid.rb +31 -0
- data/lib/inspector/metadata.rb +75 -0
- data/lib/inspector/metadata/map.rb +24 -0
- data/lib/inspector/metadata/walker.rb +46 -0
- data/lib/inspector/property_metadata.rb +19 -0
- data/lib/inspector/type_metadata.rb +5 -0
- data/lib/inspector/validator.rb +26 -0
- data/lib/inspector/version.rb +3 -0
- data/lib/object_inspector.rb +1 -0
- data/spec/inspector/attribute_metadata_spec.rb +8 -0
- data/spec/inspector/constraint/violation/list_spec.rb +45 -0
- data/spec/inspector/constraints/false_spec.rb +18 -0
- data/spec/inspector/constraints_spec.rb +15 -0
- data/spec/inspector/metadata/map_spec.rb +38 -0
- data/spec/inspector/metadata/walker_spec.rb +7 -0
- data/spec/inspector/property_metadata_spec.rb +8 -0
- data/spec/inspector/type_metadata_spec.rb +7 -0
- data/spec/inspector/validator_spec.rb +65 -0
- data/spec/inspector_spec.rb +22 -0
- data/spec/shared_examples/metadata.rb +33 -0
- data/spec/spec_helper.rb +3 -0
- metadata +175 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
object-inspector (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
aruba (0.4.11)
|
10
|
+
childprocess (>= 0.2.3)
|
11
|
+
cucumber (>= 1.1.1)
|
12
|
+
ffi (>= 1.0.11)
|
13
|
+
rspec (>= 2.7.0)
|
14
|
+
builder (3.0.0)
|
15
|
+
childprocess (0.3.2)
|
16
|
+
ffi (~> 1.0.6)
|
17
|
+
cucumber (1.2.1)
|
18
|
+
builder (>= 2.1.2)
|
19
|
+
diff-lcs (>= 1.1.3)
|
20
|
+
gherkin (~> 2.11.0)
|
21
|
+
json (>= 1.4.6)
|
22
|
+
diff-lcs (1.1.3)
|
23
|
+
ffi (1.0.11)
|
24
|
+
gherkin (2.11.2)
|
25
|
+
json (>= 1.4.6)
|
26
|
+
json (1.7.5)
|
27
|
+
rake (0.9.2.2)
|
28
|
+
rspec (2.11.0)
|
29
|
+
rspec-core (~> 2.11.0)
|
30
|
+
rspec-expectations (~> 2.11.0)
|
31
|
+
rspec-mocks (~> 2.11.0)
|
32
|
+
rspec-core (2.11.1)
|
33
|
+
rspec-expectations (2.11.2)
|
34
|
+
diff-lcs (~> 1.1.3)
|
35
|
+
rspec-mocks (2.11.2)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
aruba (~> 0.4)
|
42
|
+
cucumber (~> 1.2)
|
43
|
+
object-inspector!
|
44
|
+
rake (~> 0.9)
|
45
|
+
rspec (~> 2.6)
|
data/README.md
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
# Inspector
|
2
|
+
|
3
|
+
a ruby validation library
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```shell
|
8
|
+
gem install object-inspector
|
9
|
+
```
|
10
|
+
|
11
|
+
## Background
|
12
|
+
|
13
|
+
We often need to validate data. And a lot of the time we're forced to put those validation rules on our so-called models. I think this is making too many assumptions about the styles of applications that we're writing and doesn't give us enough flexibility to implement something outside of this box.
|
14
|
+
|
15
|
+
Validating models is great, sure. Very often I find myself needing to validate hashes or arrays, before I even hydrate that data on my models. Other times, I don't have the luxury of using a traditional ORM - maybe I store my data in XML files or maybe I don't even store it anywhere at all.
|
16
|
+
|
17
|
+
Inspector is designed to avoid those assumptions and give the developer flexibility and power of object validation dressed in a nice DSL. The actual validations definition syntax takes inspiration from RSpec's powerful matchers. And with nested validations your validation rules are guaranteed to be ever so concise and readable.
|
18
|
+
|
19
|
+
Read through quick start to get basic idea of what I'm talking about.
|
20
|
+
|
21
|
+
## Quick start
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'inspector'
|
25
|
+
|
26
|
+
Post = Struct.new(:title, :body, :author)
|
27
|
+
Author = Struct.new(:email, :first_name, :last_name)
|
28
|
+
|
29
|
+
Inspector.valid(Post) do
|
30
|
+
attribute(:title) do
|
31
|
+
should_not be_empty
|
32
|
+
should be_kind_of(String)
|
33
|
+
should have_at_least(3).characters
|
34
|
+
end
|
35
|
+
|
36
|
+
attribute(:body) do
|
37
|
+
should_not be_empty
|
38
|
+
should be_kind_of(String)
|
39
|
+
should have_at_least(3).characters
|
40
|
+
end
|
41
|
+
|
42
|
+
attribute(:author).should validate(:as => Author)
|
43
|
+
end
|
44
|
+
|
45
|
+
Inspector.valid(Author) do
|
46
|
+
attribute(:email) do
|
47
|
+
should_not be_empty
|
48
|
+
should be_an_email
|
49
|
+
end
|
50
|
+
|
51
|
+
attribute(:first_name) do
|
52
|
+
should_not be_empty
|
53
|
+
should be_kind_of(String)
|
54
|
+
should have_at_least(1).character
|
55
|
+
should have_at_most(32).characters
|
56
|
+
end
|
57
|
+
|
58
|
+
attribute(:last_name) do
|
59
|
+
should_not be_empty
|
60
|
+
should be_kind_of(String)
|
61
|
+
should have_at_least(1).character
|
62
|
+
should have_at_most(32).characters
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
author = Author.new("not an email", "John", "Smith")
|
67
|
+
post = Post.new(123, nil, author)
|
68
|
+
|
69
|
+
violations = Inspector.validate(post)
|
70
|
+
|
71
|
+
if violations.empty?
|
72
|
+
puts "post #{post.inspect} is valid"
|
73
|
+
else
|
74
|
+
puts "invalid post #{post.inspect}:"
|
75
|
+
puts violations.to_s.split("\n").map { |line| " #{line}" }.join("\n")
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
Above code will result in the following:
|
80
|
+
|
81
|
+
invalid post #<struct Post title=123, body=nil, author=#<struct Author email="not an email", first_name="John", last_name="Smith">>:
|
82
|
+
title:
|
83
|
+
should.be_kind_of
|
84
|
+
body:
|
85
|
+
should_not.be_empty
|
86
|
+
should.be_kind_of
|
87
|
+
should.have_at_least
|
88
|
+
author:
|
89
|
+
email:
|
90
|
+
should.be_an_email
|
91
|
+
|
92
|
+
The above example is fairly simplistic, yet demonstrates several important features:
|
93
|
+
|
94
|
+
* Validation constraints can be negated (the use of should_not enforces this)
|
95
|
+
* It is possible to nest validations (`author` attribute in `Post` validation rules)
|
96
|
+
* Validations are not tied to error messages
|
97
|
+
|
98
|
+
## Usage
|
99
|
+
|
100
|
+
The quick start above highlighted basic usage scenario. However, this is definitely not everything Inspector can do.
|
101
|
+
|
102
|
+
### Validating hashes
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
require 'inspector'
|
106
|
+
|
107
|
+
Inspector.valid("request parameters") do
|
108
|
+
property("title") do
|
109
|
+
should_not be_empty
|
110
|
+
should be_kind_of(String)
|
111
|
+
should have_at_least(3).characters
|
112
|
+
end
|
113
|
+
|
114
|
+
property("body") do
|
115
|
+
should_not be_empty
|
116
|
+
should be_kind_of(String)
|
117
|
+
should have_at_least(3).characters
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
violations = Inspector.validate({
|
122
|
+
"title" => 123,
|
123
|
+
"body" => nil
|
124
|
+
}, :as => "requests parameters")
|
125
|
+
|
126
|
+
puts violations unless violations.empty?
|
127
|
+
```
|
128
|
+
|
129
|
+
The code above will result in the following:
|
130
|
+
|
131
|
+
[title]:
|
132
|
+
should.be_kind_of
|
133
|
+
[body]:
|
134
|
+
should_not.be_empty
|
135
|
+
should.be_kind_of
|
136
|
+
should.have_at_least
|
137
|
+
|
138
|
+
### Validating arrays
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
require 'inspector'
|
142
|
+
|
143
|
+
Inspector.valid("emails") do
|
144
|
+
each_item.should be_an_email
|
145
|
+
end
|
146
|
+
|
147
|
+
puts Inspector.validate(["not an email", "username@example.com"], :as => "emails")
|
148
|
+
```
|
149
|
+
|
150
|
+
Above code produces:
|
151
|
+
|
152
|
+
[0]:
|
153
|
+
should.be_an_email
|
154
|
+
|
155
|
+
### DRYing validations
|
156
|
+
|
157
|
+
Sometimes we end up with almost exactly same validations on different attributes or properties. It is quite easy to remove the duplication by using `validate` constraint:
|
158
|
+
|
159
|
+
|
160
|
+
The validations above seem a little too verbose, but we can simplify them:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
require 'inspector'
|
164
|
+
|
165
|
+
Post = Struct.new(:title, :body, :author)
|
166
|
+
Author = Struct.new(:email, :first_name, :last_name)
|
167
|
+
|
168
|
+
Inspector.valid("required string") do
|
169
|
+
should_not be_empty
|
170
|
+
should be_kind_of(String)
|
171
|
+
should have_at_least(3).characters
|
172
|
+
end
|
173
|
+
|
174
|
+
Inspector.valid("required short string") do
|
175
|
+
should_not be_empty
|
176
|
+
should be_kind_of(String)
|
177
|
+
should have_at_least(1).character
|
178
|
+
should have_at_most(32).characters
|
179
|
+
end
|
180
|
+
|
181
|
+
Inspector.valid(Post) do
|
182
|
+
attribute(:title).should validate :as => "required string"
|
183
|
+
attribute(:body).should validate :as => "required string"
|
184
|
+
attribute(:author).should validate :as => Author
|
185
|
+
end
|
186
|
+
|
187
|
+
Inspector.valid(Author) do
|
188
|
+
attribute(:email) do
|
189
|
+
should_not be_empty
|
190
|
+
should be_an_email
|
191
|
+
end
|
192
|
+
|
193
|
+
attribute(:first_name).should validate :as => "required short string"
|
194
|
+
attribute(:last_name).should validate :as => "required short string"
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
### Built-in constraints
|
199
|
+
|
200
|
+
Inspector ships with some built-in constraints. Most of them are inspired by RSpec's matchers.
|
201
|
+
|
202
|
+
#### `be_false`
|
203
|
+
|
204
|
+
validate falsiness of a value.
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
attribute(:attribute) do
|
208
|
+
should be_false
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
#### `be_true`
|
213
|
+
|
214
|
+
validate truthyness of a value.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
attribute(:attribute) do
|
218
|
+
should be_true
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
#### `validate`
|
223
|
+
|
224
|
+
validate an object as a valid type (defaults to its class):
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
attribute(:attribute) do
|
228
|
+
should validate
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
attribute(:attribute) do
|
234
|
+
should validate(:as => 'validation metadata')
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
#### `be_email`/`be_an_email`
|
239
|
+
|
240
|
+
validate value as email.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
attribute(:attribute) do
|
244
|
+
should be_email
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
attribute(:attribute) do
|
250
|
+
should be_an_email
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
#### `have`/`have_exactly`
|
255
|
+
|
256
|
+
validate collection length.
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
attribute(:attribute) do
|
260
|
+
should have(5).characters
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
attribute(:attribute) do
|
266
|
+
should have_exactly(5).characters
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
#### `have_at_least`
|
271
|
+
|
272
|
+
validate collection minimum length.
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
attribute(:attribute) do
|
276
|
+
should have_at_least(5).characters
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
#### `have_at_most`
|
281
|
+
|
282
|
+
validate collection maximum length.
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
attribute(:attribute) do
|
286
|
+
should have_at_most(5).characters
|
287
|
+
end
|
288
|
+
```
|
289
|
+
|
290
|
+
#### `be_*`
|
291
|
+
|
292
|
+
validate using predicate method.
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
attribute(:attribute) do
|
296
|
+
should be_valid # passes of attribute.valid? is true
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
### Defining simple validations (TODO)
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
Inspector.define_constraint(:have_properties) do |*properties|
|
304
|
+
valid? do |object|
|
305
|
+
properties.all? { |property| object.has_key?(property) }
|
306
|
+
end
|
307
|
+
end
|
308
|
+
```
|
309
|
+
|
310
|
+
### Defining custom validations (TODO)
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
|
314
|
+
class HavePropertiesValidator
|
315
|
+
def validate(value, constraint, violations_list)
|
316
|
+
valid = constraint.properties.all? { |property| object.has_key?(property) }
|
317
|
+
|
318
|
+
if valid ^ constraint.positive?
|
319
|
+
violations_list << Inspector::Constraint::Violation.new(constraint)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
class HavePropertiesConstraint
|
325
|
+
include Inspector::Constraint
|
326
|
+
|
327
|
+
def validator
|
328
|
+
:have_properties
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
Inspector.validators[:have_properties] = HavePropertiesValidator.new
|
333
|
+
|
334
|
+
Inspector.define_constraint(:have_properties, HavePropertiesConstraint)
|
335
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'cucumber/rake/task'
|
6
|
+
|
7
|
+
$LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
10
|
+
t.rspec_opts = "-c -f d"
|
11
|
+
end
|
12
|
+
|
13
|
+
Cucumber::Rake::Task.new
|
data/examples/example.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# class constraints
|
2
|
+
# attribute constraints
|
3
|
+
# property constraints
|
4
|
+
|
5
|
+
Inspector.valid(Address) do
|
6
|
+
attribute(:street) do
|
7
|
+
should_not be_empty
|
8
|
+
end
|
9
|
+
|
10
|
+
attribute(:zip_code) do
|
11
|
+
should_not be_empty
|
12
|
+
should have_at_most(5).characters
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# {
|
17
|
+
# "[name]" => ["not.be_empty", "be_kind_of", "have_at_least", "have_at_most"]
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
Inspector.valid("request parameters") do
|
21
|
+
should have_properties("name", "address")
|
22
|
+
|
23
|
+
property("name") do
|
24
|
+
should_not be_empty
|
25
|
+
should be_kind_of(String)
|
26
|
+
should have_at_least(3).characters
|
27
|
+
should have_at_most(255).characters
|
28
|
+
should have_only_letters_and_numbers
|
29
|
+
end
|
30
|
+
|
31
|
+
property("address") do
|
32
|
+
should have_properties("recipient", "street", "street2", "city", "state", "zip")
|
33
|
+
|
34
|
+
property("recipient") do
|
35
|
+
should_not be_empty
|
36
|
+
# should be_kind_of(String)
|
37
|
+
should have_at_least(3).characters
|
38
|
+
should have_at_most(255).characters
|
39
|
+
should have_only_letters
|
40
|
+
end
|
41
|
+
|
42
|
+
property("street") do
|
43
|
+
should_not be_empty
|
44
|
+
# should be_kind_of(String)
|
45
|
+
should have_at_least(3).characters
|
46
|
+
should have_at_most(255).characters
|
47
|
+
should have_only_letters_and_numbers
|
48
|
+
end
|
49
|
+
|
50
|
+
property("street2") do
|
51
|
+
# should be_kind_of(String)
|
52
|
+
should have_at_least(3).characters
|
53
|
+
should have_at_most(255).characters
|
54
|
+
should have_only_letters_and_numbers
|
55
|
+
end
|
56
|
+
|
57
|
+
property("city") do
|
58
|
+
should_not be_empty
|
59
|
+
# should be_kind_of(String)
|
60
|
+
should have_at_least(3).characters
|
61
|
+
should have_at_most(255).characters
|
62
|
+
should have_only_letters_and_numbers
|
63
|
+
end
|
64
|
+
|
65
|
+
property("state") do
|
66
|
+
should_not be_empty
|
67
|
+
# should be_kind_of(String)
|
68
|
+
should have_at_least(3).characters
|
69
|
+
should have_at_most(255).characters
|
70
|
+
should have_only_letters
|
71
|
+
end
|
72
|
+
|
73
|
+
property("zip") do
|
74
|
+
should_not be_empty
|
75
|
+
# should be_kind_of(String)
|
76
|
+
should have_at_most(5).characters
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
result = Inspector.validate({:some_key => 'some value'}, :as => "request parameters")
|
82
|
+
result.valid?
|
83
|
+
result.errors
|
84
|
+
# errors = Inspector.validator_for("request parameters").validate({:some_key => 'some value'})
|
85
|
+
|
86
|
+
Inspector.validator.describe(Author) do
|
87
|
+
should have_unique(:email)
|
88
|
+
|
89
|
+
its.attribute(:first_name) do
|
90
|
+
should_not be_empty
|
91
|
+
should be_kind_of(String)
|
92
|
+
|
93
|
+
should have_at_least(4).characters
|
94
|
+
should have_at_most(5).characters
|
95
|
+
# or
|
96
|
+
# its.attribute(:length) do
|
97
|
+
# should be >= 4
|
98
|
+
# should be <= 5
|
99
|
+
# end
|
100
|
+
end
|
101
|
+
|
102
|
+
its.attribute(:last_name) do
|
103
|
+
should_not be_empty
|
104
|
+
should be_kind_of(String)
|
105
|
+
end
|
106
|
+
|
107
|
+
its.attribute(:address).should be_valid(Address)
|
108
|
+
|
109
|
+
its.attribute(:email).should be_an_email
|
110
|
+
end
|
111
|
+
|
112
|
+
errors = Inspector.validator.validate(Author.new)
|
113
|
+
|
114
|
+
errors.each do |property, violations|
|
115
|
+
property.name
|
116
|
+
property.path
|
117
|
+
end
|
118
|
+
|
119
|
+
{
|
120
|
+
:violations => []
|
121
|
+
:children => {
|
122
|
+
:email => {
|
123
|
+
:violations => ['is_unique'],
|
124
|
+
:children => {
|
125
|
+
:length => {
|
126
|
+
:violations
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|