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.
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
+ }