ward 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 (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