object-inspector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +45 -0
  4. data/README.md +335 -0
  5. data/Rakefile +13 -0
  6. data/examples/example.rb +131 -0
  7. data/features/error_display.feature +80 -0
  8. data/features/support/env.rb +1 -0
  9. data/features/syntax.feature +223 -0
  10. data/inspector.gemspec +24 -0
  11. data/lib/inspector.rb +27 -0
  12. data/lib/inspector/attribute_metadata.rb +19 -0
  13. data/lib/inspector/constraint.rb +18 -0
  14. data/lib/inspector/constraint/validators.rb +8 -0
  15. data/lib/inspector/constraint/validators/simple.rb +13 -0
  16. data/lib/inspector/constraint/validators/validity.rb +24 -0
  17. data/lib/inspector/constraint/violation.rb +34 -0
  18. data/lib/inspector/constraint/violation/list.rb +93 -0
  19. data/lib/inspector/constraints.rb +61 -0
  20. data/lib/inspector/constraints/email.rb +57 -0
  21. data/lib/inspector/constraints/empty.rb +21 -0
  22. data/lib/inspector/constraints/eq.rb +23 -0
  23. data/lib/inspector/constraints/false.rb +19 -0
  24. data/lib/inspector/constraints/have.rb +85 -0
  25. data/lib/inspector/constraints/predicate.rb +38 -0
  26. data/lib/inspector/constraints/true.rb +19 -0
  27. data/lib/inspector/constraints/valid.rb +31 -0
  28. data/lib/inspector/metadata.rb +75 -0
  29. data/lib/inspector/metadata/map.rb +24 -0
  30. data/lib/inspector/metadata/walker.rb +46 -0
  31. data/lib/inspector/property_metadata.rb +19 -0
  32. data/lib/inspector/type_metadata.rb +5 -0
  33. data/lib/inspector/validator.rb +26 -0
  34. data/lib/inspector/version.rb +3 -0
  35. data/lib/object_inspector.rb +1 -0
  36. data/spec/inspector/attribute_metadata_spec.rb +8 -0
  37. data/spec/inspector/constraint/violation/list_spec.rb +45 -0
  38. data/spec/inspector/constraints/false_spec.rb +18 -0
  39. data/spec/inspector/constraints_spec.rb +15 -0
  40. data/spec/inspector/metadata/map_spec.rb +38 -0
  41. data/spec/inspector/metadata/walker_spec.rb +7 -0
  42. data/spec/inspector/property_metadata_spec.rb +8 -0
  43. data/spec/inspector/type_metadata_spec.rb +7 -0
  44. data/spec/inspector/validator_spec.rb +65 -0
  45. data/spec/inspector_spec.rb +22 -0
  46. data/spec/shared_examples/metadata.rb +33 -0
  47. data/spec/spec_helper.rb +3 -0
  48. metadata +175 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+
4
+ gemspec
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
@@ -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
+ }