ward 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. data/.document +5 -0
  2. data/.gitignore +28 -0
  3. data/LICENSE +19 -0
  4. data/README.markdown +99 -0
  5. data/Rakefile +47 -0
  6. data/VERSION +1 -0
  7. data/features/acceptance_matcher.feature +78 -0
  8. data/features/attribute_keyword.feature +13 -0
  9. data/features/close_to_matcher.feature +130 -0
  10. data/features/context_arguments.feature +47 -0
  11. data/features/equal_to_matcher.feature +25 -0
  12. data/features/error_messages.feature +69 -0
  13. data/features/external_validation.feature +15 -0
  14. data/features/has_matcher.feature +72 -0
  15. data/features/has_matcher_initialized_with_expectation.feature +94 -0
  16. data/features/has_matcher_relativities.feature +171 -0
  17. data/features/include_matcher.feature +28 -0
  18. data/features/is_keyword.feature +42 -0
  19. data/features/is_not_keyword.feature +62 -0
  20. data/features/match_matcher.feature +49 -0
  21. data/features/multiple_validators.feature +29 -0
  22. data/features/nil_matcher.feature +25 -0
  23. data/features/predicate_matcher.feature +23 -0
  24. data/features/present_matcher.feature +59 -0
  25. data/features/satisfy_matcher.feature +80 -0
  26. data/features/scenario_validation.feature +81 -0
  27. data/features/step_definitions/external_validation_steps.rb +69 -0
  28. data/features/step_definitions/generic_validation_steps.rb +33 -0
  29. data/features/step_definitions/object_definition_steps.rb +43 -0
  30. data/features/support/env.rb +12 -0
  31. data/features/support/object_builder.rb +33 -0
  32. data/features/support/struct.rb +38 -0
  33. data/lang/en.yml +56 -0
  34. data/lib/ward.rb +26 -0
  35. data/lib/ward/context.rb +70 -0
  36. data/lib/ward/context_chain.rb +87 -0
  37. data/lib/ward/dsl.rb +7 -0
  38. data/lib/ward/dsl/validation_block.rb +73 -0
  39. data/lib/ward/dsl/validation_builder.rb +190 -0
  40. data/lib/ward/errors.rb +213 -0
  41. data/lib/ward/matchers.rb +97 -0
  42. data/lib/ward/matchers/acceptance.rb +43 -0
  43. data/lib/ward/matchers/close_to.rb +60 -0
  44. data/lib/ward/matchers/equal_to.rb +33 -0
  45. data/lib/ward/matchers/has.rb +283 -0
  46. data/lib/ward/matchers/include.rb +54 -0
  47. data/lib/ward/matchers/match.rb +29 -0
  48. data/lib/ward/matchers/matcher.rb +68 -0
  49. data/lib/ward/matchers/nil.rb +30 -0
  50. data/lib/ward/matchers/predicate.rb +31 -0
  51. data/lib/ward/matchers/present.rb +56 -0
  52. data/lib/ward/matchers/satisfy.rb +65 -0
  53. data/lib/ward/spec.rb +17 -0
  54. data/lib/ward/spec/matcher_matcher.rb +114 -0
  55. data/lib/ward/support.rb +7 -0
  56. data/lib/ward/support/basic_object.rb +55 -0
  57. data/lib/ward/support/result.rb +49 -0
  58. data/lib/ward/validator.rb +147 -0
  59. data/lib/ward/validator_set.rb +115 -0
  60. data/lib/ward/version.rb +3 -0
  61. data/spec/lib/has_matcher_relativity_examples.rb +15 -0
  62. data/spec/lib/have_public_method_defined.rb +22 -0
  63. data/spec/rcov.opts +8 -0
  64. data/spec/spec.opts +4 -0
  65. data/spec/spec_helper.rb +19 -0
  66. data/spec/ward/context_chain_spec.rb +178 -0
  67. data/spec/ward/context_spec.rb +57 -0
  68. data/spec/ward/dsl/validation_block_spec.rb +27 -0
  69. data/spec/ward/dsl/validation_builder_spec.rb +212 -0
  70. data/spec/ward/errors_spec.rb +149 -0
  71. data/spec/ward/matchers/acceptance_spec.rb +16 -0
  72. data/spec/ward/matchers/close_to_spec.rb +57 -0
  73. data/spec/ward/matchers/equal_to_spec.rb +16 -0
  74. data/spec/ward/matchers/has_spec.rb +175 -0
  75. data/spec/ward/matchers/include_spec.rb +41 -0
  76. data/spec/ward/matchers/match_spec.rb +21 -0
  77. data/spec/ward/matchers/matcher_spec.rb +54 -0
  78. data/spec/ward/matchers/nil_spec.rb +16 -0
  79. data/spec/ward/matchers/predicate_spec.rb +19 -0
  80. data/spec/ward/matchers/present_spec.rb +16 -0
  81. data/spec/ward/matchers/satisfy_spec.rb +68 -0
  82. data/spec/ward/matchers_spec.rb +51 -0
  83. data/spec/ward/spec/have_public_method_defined_spec.rb +31 -0
  84. data/spec/ward/spec/matcher_matcher_spec.rb +217 -0
  85. data/spec/ward/validator_set_spec.rb +178 -0
  86. data/spec/ward/validator_spec.rb +264 -0
  87. data/tasks/features.rake +15 -0
  88. data/tasks/rcov.rake +24 -0
  89. data/tasks/spec.rake +18 -0
  90. data/tasks/yard.rake +9 -0
  91. data/ward.gemspec +176 -0
  92. metadata +239 -0
@@ -0,0 +1,97 @@
1
+ require 'ward/matchers/matcher'
2
+ require 'ward/matchers/acceptance'
3
+ require 'ward/matchers/close_to'
4
+ require 'ward/matchers/equal_to'
5
+ require 'ward/matchers/has'
6
+ require 'ward/matchers/include'
7
+ require 'ward/matchers/match'
8
+ require 'ward/matchers/nil'
9
+ require 'ward/matchers/predicate'
10
+ require 'ward/matchers/present'
11
+ require 'ward/matchers/satisfy'
12
+
13
+ module Ward
14
+ # Matchers are used to determine whether a particular value is valid.
15
+ #
16
+ # Any class instance can be a validator so long as it responds to #matches?;
17
+ # the #matches? method should take at least one argument which will be the
18
+ # value of the object being validated. The matcher should then return a
19
+ # true-like value if the match is successful or either nil, false, or an
20
+ # array whose first member is false, if the match was not successful.
21
+ #
22
+ # In the event that your matcher returns an array, the second member will be
23
+ # used as the error message.
24
+ #
25
+ module Matchers
26
+
27
+ # Registers a matcher and it's slug.
28
+ #
29
+ # A matcher can be registered with as many slugs as desired.
30
+ #
31
+ # @param [Symbol, #to_sym] slug
32
+ # A slug which will be used by the DSL in order to assign a context to
33
+ # a matcher.
34
+ # @param [Ward::Matchers::Matcher] matcher
35
+ # The matcher to be registered.
36
+ #
37
+ # @example Registering the Acceptance handler to be used with :accepted
38
+ #
39
+ # Matchers.register(:accepted, Matchers::Acceptance)
40
+ #
41
+ # # The Acceptance matcher can now be used like so...
42
+ #
43
+ # validate do |form|
44
+ # form.acceptable_use_policy.is.accepted
45
+ # form.acceptable_use_policy.is_not.accepted
46
+ # end
47
+ #
48
+ # @example Registering the Has matcher twice.
49
+ #
50
+ # Matchers.register(:has, Matchers::Acceptance)
51
+ #
52
+ # validate do |post|
53
+ # post.has(1).author
54
+ # end
55
+ #
56
+ # # This provides access to the Has matcher, but "does_not.has" doesn't
57
+ # # make much sense. So, we register the Has matcher a second time with
58
+ # # a slug which make sense when used in the negative.
59
+ #
60
+ # Matchers.register(:have, Matchers::Acceptance)
61
+ #
62
+ # validate do |post|
63
+ # form.does_not.have(1).author
64
+ # end
65
+ #
66
+ # # Much better.
67
+ #
68
+ def self.register(slug, matcher)
69
+ matchers[slug.to_sym] = matcher
70
+ end
71
+
72
+ # Returns the registered matchers.
73
+ #
74
+ # @return [Hash{Symbol => Ward::Matchers::Matcher}]
75
+ #
76
+ def self.matchers
77
+ @matchers ||= {}
78
+ end
79
+
80
+ # Register the built-in matchers.
81
+
82
+ register :accepted, Acceptance
83
+ register :close_to, CloseTo
84
+ register :equal_to, EqualTo
85
+ register :has, Has
86
+ register :have, Has
87
+ register :included_in, Include
88
+ register :matches, Match
89
+ register :match, Match
90
+ register :nil, Nil
91
+ register :one_of, Include
92
+ register :present, Present
93
+ register :satisfies, Satisfy
94
+ register :satisfy, Satisfy
95
+
96
+ end # Matchers
97
+ end # Ward
@@ -0,0 +1,43 @@
1
+ module Ward
2
+ module Matchers
3
+ # Tests whether the validation value is accepted.
4
+ #
5
+ # An "accepted" value one which is exactly true, "t", "true", "1", "y",
6
+ # or "yes".
7
+ #
8
+ # @example
9
+ #
10
+ # class Person
11
+ # validate do |person|
12
+ # person.name.is.accepted
13
+ # end
14
+ # end
15
+ #
16
+ class Acceptance < Matcher
17
+
18
+ # Creates a new matcher instance.
19
+ #
20
+ # @param [Object] expected
21
+ # The expected value for the matcher.
22
+ #
23
+ def initialize(expected = nil, *extra_args)
24
+ expected = Array(expected || %w( t true y yes 1 ) + [1])
25
+ @include_matcher = Include.new(expected)
26
+
27
+ super(expected, *extra_args)
28
+ end
29
+
30
+ # Returns whether the given value is accepted.
31
+ #
32
+ # @param [Object] actual
33
+ # The validation value.
34
+ #
35
+ # @return [Boolean]
36
+ #
37
+ def matches?(actual)
38
+ actual == true or @include_matcher.matches?(actual)
39
+ end
40
+
41
+ end # Acceptance
42
+ end # Matchers
43
+ end # Ward
@@ -0,0 +1,60 @@
1
+ module Ward
2
+ module Matchers
3
+ # Tests whether the validation value is within the delta of the expected
4
+ # value.
5
+ #
6
+ # @example Validating that the estimate attribute is 10 (+- 5).
7
+ #
8
+ # class Price
9
+ # validate do |price|
10
+ # price.estimate.is.close_to(10, 5)
11
+ # end
12
+ # end
13
+ #
14
+ class CloseTo < Matcher
15
+
16
+ # Creates a new CloseTo matcher instance.
17
+ #
18
+ # @param [Numeric] expected
19
+ # The expected value for the matcher.
20
+ # @param [Numeric] delta
21
+ # The the acceptable range by which the actual value can deviate.
22
+ #
23
+ def initialize(expected, delta, *extra_args)
24
+ raise ArgumentError,
25
+ 'The CloseTo matcher requires that the +expected+ value ' \
26
+ 'responds to +-+' unless expected.respond_to?(:-)
27
+
28
+ raise ArgumentError,
29
+ 'The CloseTo matcher requires that a Numeric +delta+ value ' \
30
+ 'is supplied' unless delta.kind_of?(Numeric)
31
+
32
+ super(expected, *extra_args)
33
+
34
+ @delta = delta
35
+ end
36
+
37
+ # Returns whether the given value is close to the expected value.
38
+ #
39
+ # @param [Object] actual
40
+ # The validation value.
41
+ #
42
+ # @return [Boolean]
43
+ #
44
+ def matches?(actual)
45
+ (actual - @expected).abs <= @delta
46
+ end
47
+
48
+ # Adds extra information to the error message.
49
+ #
50
+ # @param [String] error
51
+ # @return [String]
52
+ #
53
+ def customise_error_values(values)
54
+ values[:delta] = @delta
55
+ values
56
+ end
57
+
58
+ end # CloseTo
59
+ end # Matchers
60
+ end # Ward
@@ -0,0 +1,33 @@
1
+ module Ward
2
+ module Matchers
3
+ # Tests whether the validation value is equal in value to -- but not
4
+ # necessarily the same object as -- the expected value.
5
+ #
6
+ # @example
7
+ #
8
+ # class Person
9
+ # validate do |person|
10
+ # person.name.is.equal_to('Michael Scarn')
11
+ # end
12
+ # end
13
+ #
14
+ # @todo
15
+ # Once the validator DSL is is available, amend the class documentation
16
+ # to provide an example of how to call +is+ and +is_not+ with a value.
17
+ #
18
+ class EqualTo < Matcher
19
+
20
+ # Returns whether the given value is equal to the expected value.
21
+ #
22
+ # @param [Object] actual
23
+ # The validation value.
24
+ #
25
+ # @return [Boolean]
26
+ #
27
+ def matches?(actual)
28
+ actual.eql?(@expected)
29
+ end
30
+
31
+ end # EqualTo
32
+ end # Matchers
33
+ end # Ward
@@ -0,0 +1,283 @@
1
+ module Ward
2
+ module Matchers
3
+ # Tests whether the actual value is a collection with the expected number
4
+ # of members, or contains a collection with the expected number of
5
+ # members.
6
+ #
7
+ # @example Setting the exact size for a collection
8
+ #
9
+ # class Author
10
+ # validate do |author|
11
+ # # All mean the same thing.
12
+ # author.has(5).posts
13
+ # author.has.exactly(5).posts
14
+ # end
15
+ # end
16
+ #
17
+ # @example Setting the minimum size for a collection
18
+ #
19
+ # class Author
20
+ # validate do |author|
21
+ # # All mean the same thing.
22
+ # author.has.at_least(5).posts
23
+ # author.has.gte(5).posts
24
+ #
25
+ # author.has.greater_than(4).posts
26
+ # author.has.more_than(4).posts
27
+ # author.has.gt(4).posts
28
+ # end
29
+ # end
30
+ #
31
+ # @example Setting the maximum size for a collection
32
+ #
33
+ # class Author
34
+ # validate do |author|
35
+ # # All mean the same thing.
36
+ # author.has.at_most(5).posts
37
+ # author.has.lte(5).posts
38
+ #
39
+ # author.has.less_than(6).posts
40
+ # author.has.fewer_than(6).posts
41
+ # author.has.lt(6).posts
42
+ # end
43
+ # end
44
+ #
45
+ # @example Setting a range of acceptable sizes for a collection
46
+ #
47
+ # class Author
48
+ # validate do |author|
49
+ # # All mean the same thing.
50
+ # author.has.between(1..5).posts
51
+ # author.has.between(1, 5).posts
52
+ # end
53
+ # end
54
+ #
55
+ class Has < Matcher
56
+
57
+ # The name of the collection whose size is being checked.
58
+ #
59
+ # @return [Symbol, nil]
60
+ # nil indicates that no collection name has been set, and the matcher
61
+ # will attempt to call +size+ or +length+ on the collection owner.
62
+ #
63
+ attr_reader :collection_name
64
+
65
+ # Returns how to test the length of the collection.
66
+ #
67
+ # @return [Symbol]
68
+ #
69
+ attr_reader :relativity
70
+
71
+ # Creates a new matcher instance.
72
+ #
73
+ # If no expected value is provided, the matcher will default to
74
+ # expecting that the collection has at_least(1).
75
+ #
76
+ # @param [Object] expected
77
+ # The expected value for the matcher.
78
+ #
79
+ def initialize(expected = nil, *extra_args)
80
+ case expected
81
+ when Range then super ; between(expected)
82
+ when Numeric then super ; eql(expected)
83
+ when :no then super ; eql(0)
84
+ else super(1, *extra_args) ; @relativity = :gte
85
+ end
86
+ end
87
+
88
+ # Returns whether the given value is satisfied by the expected block.
89
+ #
90
+ # @param [Object] collection
91
+ # The collection or the collection owner.
92
+ #
93
+ # @return [Boolean]
94
+ #
95
+ def matches?(collection)
96
+ unless @collection_name.nil?
97
+ if collection.respond_to?(@collection_name)
98
+ collection = collection.__send__(@collection_name)
99
+ elsif collection.respond_to?(@plural_collection_name)
100
+ collection = collection.__send__(@plural_collection_name)
101
+ end
102
+ end
103
+
104
+ if collection.respond_to?(:length)
105
+ actual = collection.length
106
+ elsif collection.respond_to?(:size)
107
+ actual = collection.size
108
+ else
109
+ raise RuntimeError,
110
+ 'The given value is not a collection (it does not respond to ' \
111
+ '#length or #size)'
112
+ end
113
+
114
+ result = case @relativity
115
+ when :eql, nil then actual == (@expected == :no ? 0 : @expected)
116
+ when :lte then actual <= @expected
117
+ when :gte then actual >= @expected
118
+ when :between then @expected.include?(actual)
119
+ end
120
+
121
+ [result, @relativity || :eql]
122
+ end
123
+
124
+ # Sets that the collection should be smaller than the expected value.
125
+ #
126
+ # @param [Numeric] n
127
+ # The maximum size of the collection + 1.
128
+ #
129
+ # @return [Ward::Matchers::Has]
130
+ # Returns self.
131
+ #
132
+ def lt(n)
133
+ set_relativity(:lte, n - 1)
134
+ end
135
+
136
+ alias_method :fewer_than, :lt
137
+ alias_method :less_than, :lt
138
+
139
+ # Sets that the collection should be no larger than the expected value.
140
+ #
141
+ # @param [Numeric] n
142
+ # The maximum size of the collection.
143
+ #
144
+ # @return [Ward::Matchers::Has]
145
+ # Returns self.
146
+ #
147
+ def lte(n)
148
+ set_relativity(:lte, n)
149
+ end
150
+
151
+ alias_method :at_most, :lte
152
+
153
+ # Sets that the collection should be the exact size of the expectation.
154
+ #
155
+ # @param [Numeric] n
156
+ # The exact expected size of the collection.
157
+ #
158
+ # @return [Ward::Matchers::Has]
159
+ # Returns self.
160
+ #
161
+ def eql(n)
162
+ set_relativity(:eql, n)
163
+ end
164
+
165
+ alias_method :exactly, :eql
166
+
167
+ # Sets that the collection should be no smaller than the expected value.
168
+ #
169
+ # @param [Numeric] n
170
+ # The minimum size of the collection.
171
+ #
172
+ # @return [Ward::Matchers::Has]
173
+ # Returns self.
174
+ #
175
+ def gte(n)
176
+ set_relativity(:gte, n)
177
+ end
178
+
179
+ alias_method :at_least, :gte
180
+
181
+ # Sets that the collection should be greater than the expected value.
182
+ #
183
+ # @param [Numeric] n
184
+ # The minimum size of the collection - 1.
185
+ #
186
+ # @return [Ward::Matchers::Has]
187
+ # Returns self.
188
+ #
189
+ def gt(n)
190
+ set_relativity(:gte, n + 1)
191
+ end
192
+
193
+ alias_method :greater_than, :gt
194
+ alias_method :more_than, :gt
195
+
196
+ # Sets that the collection size should be between two values.
197
+ #
198
+ # @param [Range, Numeric] n
199
+ # The lowest boundary for the collection size. Alternatively, a range
200
+ # specifying the acceptable size of the collection.
201
+ # @param [Numeric] upper
202
+ # The lowest boundary for the collection size. Omit if a range is
203
+ # supplied as the first argument.
204
+ #
205
+ # @return [Ward::Matchers::Has]
206
+ # Returns self.
207
+ #
208
+ def between(n, upper = nil)
209
+ if n.kind_of?(Range)
210
+ set_relativity(:between, n)
211
+ elsif upper.nil?
212
+ raise ArgumentError,
213
+ 'You must supply an upper boundary for the collection size'
214
+ else
215
+ set_relativity(:between, (n..upper))
216
+ end
217
+ end
218
+
219
+ # Adds extra information to the error message.
220
+ #
221
+ # @param [String] error
222
+ # @return [String]
223
+ #
224
+ def customise_error_values(values)
225
+ values[:collection] = @collection_desc || 'items'
226
+
227
+ if @relativity == :between
228
+ values[:lower] = @expected.first
229
+ values[:upper] = @expected.last
230
+ end
231
+
232
+ values
233
+ end
234
+
235
+ private
236
+
237
+ # Sets the collection name to be used by the matcher.
238
+ #
239
+ # @return [Ward::Matchers::Has]
240
+ # Returns self.
241
+ #
242
+ # @todo
243
+ # Capture args and block to be used when fetching the collection.
244
+ #
245
+ def method_missing(method, *args, &block)
246
+ @collection_desc = ActiveSupport::Inflector.humanize(method).downcase
247
+
248
+ @collection_desc = ActiveSupport::Inflector.singularize(
249
+ @collection_desc) if @expected == 1
250
+
251
+ unless method == :characters and ''.respond_to?(:characters)
252
+ @collection_name, @plural_collection_name =
253
+ method, ActiveSupport::Inflector.pluralize(method.to_s).to_sym
254
+ end
255
+
256
+ self
257
+ end
258
+
259
+ # Sets the relativity and the expected value.
260
+ #
261
+ # Also marks that a relativity has been set, preventing another one from
262
+ # being set in the future.
263
+ #
264
+ # @param [Symbol] relativity
265
+ # The relativity to set.
266
+ # @param [Numeric] expected
267
+ # The expected value to set.
268
+ #
269
+ # @return [Ward::Matchers::Has]
270
+ # Returns self.
271
+ #
272
+ def set_relativity(relativity, expected)
273
+ raise RuntimeError,
274
+ "A relativity (#{@relativity}) was already set; you cannot " \
275
+ "set another" if @relativity_set
276
+
277
+ @relativity, @expected, @relativity_set = relativity, expected, true
278
+ self
279
+ end
280
+
281
+ end # Has
282
+ end # Matchers
283
+ end # Ward