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,7 @@
1
+ require 'ward/dsl/validation_block'
2
+ require 'ward/dsl/validation_builder'
3
+
4
+ module Ward
5
+ # Classes which provide the Ward validator DSL.
6
+ module DSL; end
7
+ end # Ward
@@ -0,0 +1,73 @@
1
+ module Ward
2
+ module DSL
3
+ # Creates one or more validators using a block.
4
+ #
5
+ # @see Ward::ValidatorSet.build
6
+ #
7
+ # @example
8
+ #
9
+ # # Builds a validation set with two validators
10
+ # #
11
+ # # * One for "author.name" and the EqualTo matcher.
12
+ # # * One for "title" with the Match matcher.
13
+ #
14
+ # ValidationBlock.new do |object|
15
+ # object.author.name.is.equal_to('Michael Scarn')
16
+ # object.title.match(/something/)
17
+ # end
18
+ #
19
+ class ValidationBlock < Support::BasicObject
20
+
21
+ # Creates a new ValidationBlock instance.
22
+ #
23
+ # NOTE: Providing an existing ValidatorSet will result in a copy of that
24
+ # set being mutated; the original will not be changed.
25
+ #
26
+ # @param [Ward::ValidatorSet] set
27
+ # A ValidatorSet to which the built validators should be added.
28
+ #
29
+ def initialize(set = nil, &block)
30
+ @set = if set.nil? then Ward::ValidatorSet.new else set.dup end
31
+ run(&block) if block
32
+ end
33
+
34
+ # Returns the ValidatorSet created by the DSL.
35
+ #
36
+ # @return [Ward::ValidatorSet]
37
+ #
38
+ def to_validator_set
39
+ @set
40
+ end
41
+
42
+ private
43
+
44
+ # Runs a block, creating the appropriate validators.
45
+ #
46
+ # @todo Ensure that each matcher was correctly set up.
47
+ #
48
+ def run
49
+ @builders = []
50
+ yield self
51
+ @set.merge!(@builders.map { |builder| builder.to_validator })
52
+ @builders = nil
53
+ end
54
+
55
+ # Provides the DSL.
56
+ #
57
+ # Will take the given message and creates a new ValidationBuilder.
58
+ #
59
+ # @return [Ward::DSL::ValidationBuilder]
60
+ # Returns the created builder.
61
+ #
62
+ def method_missing(method, *extra_args, &block)
63
+ raise 'ValidationBlock can only be used when provided ' \
64
+ 'with a block' if @builders.nil?
65
+
66
+ builder = ValidationBuilder.new.__send__(method, *extra_args, &block)
67
+ @builders.push(builder)
68
+ builder
69
+ end
70
+
71
+ end # ValidationBlock
72
+ end # DSL
73
+ end # Ward
@@ -0,0 +1,190 @@
1
+ module Ward
2
+ module DSL
3
+ # Creates a single {Validator}. Any message received which doesn't
4
+ # correspond with a matcher will be assumed to be part of the context.
5
+ #
6
+ # @example
7
+ #
8
+ # # Builds a validation whose context is "author.name" and uses the
9
+ # # EqualTo matcher to ensure that the "author.name" is "Michel Scarn".
10
+ #
11
+ # ValidationBuilder.new.author.name.is.equal_to('Michael Scarn')
12
+ #
13
+ class ValidationBuilder < Support::BasicObject
14
+
15
+ # Creates a new ValidationBuilder instance.
16
+ #
17
+ def initialize
18
+ @context = Ward::ContextChain.new
19
+ @matcher, @message, @scenarios, @negative = nil, nil, nil, false
20
+ end
21
+
22
+ # Sets the error message to be used if the validation fails.
23
+ #
24
+ # This can be one of three possibilities:
25
+ #
26
+ # * A Hash. If the matcher used returns a different error state in
27
+ # order to provide more details about what failed (such as the Has
28
+ # matcher), you may provide a hash with custom error messages for
29
+ # each error state.
30
+ #
31
+ # * A String which will be used whenever the validation fails,
32
+ # regardless of what went wrong.
33
+ #
34
+ # * nil (default). The validation will use the default error message
35
+ # for the matcher.
36
+ #
37
+ # @param [Hash{Symbol => String}, String, nil] message
38
+ #
39
+ # @return [Ward::DSL::ValidatorBuilder]
40
+ # Returns self.
41
+ #
42
+ # @example Setting an explicit error message.
43
+ #
44
+ # validate do |person|
45
+ # person.name.is.present.message('You must enter a name!')
46
+ # end
47
+ #
48
+ # @example Setting an explicit error message with a Hash.
49
+ #
50
+ # validate do |person|
51
+ # person.name.length.is(1..50).message(
52
+ # :too_short => "Your name must be at least 1 character long",
53
+ # :too_long => "That's an interesting name!"
54
+ # )
55
+ # end
56
+ #
57
+ def message(message)
58
+ @message = message
59
+ self
60
+ end
61
+
62
+ # Sets the name to be used for the context in error messages.
63
+ #
64
+ # When Ward generates error messages for you, it determines the
65
+ # 'context name' by joining the method names; for example 'name.length'
66
+ # becomes 'name length'.
67
+ #
68
+ # This isn't much use if you want to support languages other than
69
+ # English in your application, so the 'context' method allows you to set
70
+ # a custom string to be used. You may provide a String, in which case it
71
+ # will be used literally, a Hash of +language => String+, or a Symbol
72
+ # identifying a string to be used from a language file.
73
+ #
74
+ # See the localisation documentation for more examples.
75
+ #
76
+ def context(name)
77
+ @context_name = name
78
+ self
79
+ end
80
+
81
+ # Sets the scenarios under which the built validator should run.
82
+ #
83
+ # @param [Symbol, ...] scenarios
84
+ # The scenarios as Symbols.
85
+ #
86
+ # @return [Ward::DSL::ValidatorBuilder]
87
+ # Returns self.
88
+ #
89
+ def scenarios(*scenarios)
90
+ @scenarios = scenarios.flatten
91
+ self
92
+ end
93
+
94
+ alias_method :scenario, :scenarios
95
+
96
+ # Adds an attribute to the context chain.
97
+ #
98
+ # Useful your classes have attribute which you want to validate, and
99
+ # their name conflicts with a method on the validator DSL (e.g.
100
+ # "message").
101
+ #
102
+ # @param [Symbol] attribute
103
+ # The attribute to be validated.
104
+ #
105
+ # @return [Ward::DSL::ValidatorBuilder]
106
+ # Returns self.
107
+ #
108
+ def attribute(attribute, *args, &block)
109
+ @context << Ward::Context.new(attribute, *args, &block)
110
+ self
111
+ end
112
+
113
+ # Set this as a positive expectation. Can be omitted.
114
+ #
115
+ # @example
116
+ # object.name.is.blank
117
+ #
118
+ # @return [Ward::DSL::ValidatorBuilder]
119
+ # Returns self.
120
+ #
121
+ def is(*args)
122
+ equal_to(*args) unless args.empty?
123
+ self
124
+ end
125
+
126
+ # Set this as a negative expectation.
127
+ #
128
+ # @example
129
+ # object.name.is_not.blank
130
+ #
131
+ # @return [Ward::DSL::ValidatorBuilder]
132
+ # Returns self.
133
+ #
134
+ def is_not(*args)
135
+ @negative = true
136
+ is(*args)
137
+ end
138
+
139
+ alias_method :does_not, :is_not
140
+
141
+ # Provides the DSL.
142
+ #
143
+ # Will take the given message and use it to customise the matcher (if
144
+ # one is set), set a matcher, or extend the context.
145
+ #
146
+ # @return [Ward::DSL::ValidatorBuilder]
147
+ # Returns self.
148
+ #
149
+ def method_missing(method, *args, &block)
150
+ # I'd normally shy away from using method_missing, but there's no
151
+ # good alternative here since a user may register their own matchers
152
+ # later in the load process.
153
+
154
+ if @matcher
155
+ @matcher.__send__(method, *args, &block)
156
+ elsif Ward::Matchers.matchers.has_key?(method)
157
+ @matcher = Ward::Matchers.matchers[method].new(*args, &block)
158
+ elsif method.to_s =~ /\?$/
159
+ @matcher = Ward::Matchers::Predicate.new(method, *args, &block)
160
+ else
161
+ attribute(method, *args, &block)
162
+ end
163
+
164
+ self
165
+ end
166
+
167
+ # Converts the builder to a Validator instance.
168
+ #
169
+ # @return [Ward::Validator]
170
+ #
171
+ # @raise [IncompleteValidation]
172
+ # An IncompleteValidationError will be raised if builder does not have
173
+ # all of the needed information in order to create the necessary
174
+ # validation (for example, if no matcher has been set).
175
+ #
176
+ # @todo
177
+ # More descriptive error messages.
178
+ #
179
+ def to_validator
180
+ raise Ward::IncompleteValidator,
181
+ 'Validator was missing a matcher' if @matcher.nil?
182
+
183
+ Ward::Validator.new(@context, @matcher, :message => @message,
184
+ :scenarios => @scenarios, :negative => @negative,
185
+ :context_name => @context_name)
186
+ end
187
+
188
+ end # Validate
189
+ end # DSL
190
+ end # Ward
@@ -0,0 +1,213 @@
1
+ module Ward
2
+ # Holds errors associated with a valid? call.
3
+ #
4
+ class Errors
5
+
6
+ include Enumerable
7
+
8
+ class << self
9
+
10
+ # Returns a localisation message.
11
+ #
12
+ # @param [keys] String
13
+ # The key of the message to be returned from the current language
14
+ # file.
15
+ #
16
+ # @return [String, nil]
17
+ # Returns the message or nil if it couldn't be found.
18
+ #
19
+ # @example
20
+ #
21
+ # Ward::Errors.message('has.eql.positive')
22
+ # # => '%{context} should have %{expected} %{collection}'
23
+ #
24
+ # @example Passing multiple keys as fallbacks.
25
+ #
26
+ # Ward::Errors.message(
27
+ # 'does.not.exist', 'has.eql.negative', 'has.eql.positive')
28
+ #
29
+ # # => '%{context} should not have %{expected} %{collection}'
30
+ #
31
+ def message(*keys)
32
+ messages[ keys.detect { |key| messages.has_key?(key) } ]
33
+ end
34
+
35
+ # Returns the unformatted error message for a matcher.
36
+ #
37
+ # @param [Ward::Matchers::Matcher] matcher
38
+ # The matcher.
39
+ # @param [Boolean] negative
40
+ # Whether to return a negative message, rather than a positive.
41
+ # @param [nil, Symbol, String] key
42
+ # If a string is supplied, +error_for+ will assume that the string
43
+ # should be used as the error. A symbol will be assumed to be a 'key'
44
+ # from the language file, while nil will result in the validator using
45
+ # the default error message for the matcher.
46
+ #
47
+ # @return [String]
48
+ #
49
+ def error_for(matcher, negative, key = nil)
50
+ return key if key.is_a?(String)
51
+
52
+ language_key = if key.nil?
53
+ "#{matcher.class.error_id}."
54
+ else
55
+ "#{matcher.class.error_id}.#{key}."
56
+ end
57
+
58
+ language_key << (negative ? 'negative' : 'positive')
59
+
60
+ message(language_key) || '%{context} is invalid'
61
+ end
62
+
63
+ # Receives an array and formats it nicely, assuming that only one value
64
+ # is expected.
65
+ #
66
+ # @example One member
67
+ # format_exclusive_list([1]) # => '1'
68
+ #
69
+ # @example Two members
70
+ # format_exclusive_list([1, 2]) # => '1 or 2'
71
+ #
72
+ # @example Many members
73
+ # format_exclusive_list([1, 2, 3]) # => '1, 2, or 3'
74
+ #
75
+ # @param [Enumerable] list
76
+ # The list to be formatted.
77
+ #
78
+ # @return [String]
79
+ #
80
+ def format_exclusive_list(list)
81
+ format_list(list, message('generic.exclusive_conjunction'))
82
+ end
83
+
84
+ # Receives an array and formats it nicely, assuming that all values are
85
+ # expected.
86
+ #
87
+ # @example One member
88
+ # format_inclusive_list([1]) # => '1'
89
+ #
90
+ # @example Two members
91
+ # format_inclusive_list([1, 2]) # => '1 and 2'
92
+ #
93
+ # @example Many members
94
+ # format_inclusive_list([1, 2, 3]) # => '1, 2, and 3'
95
+ #
96
+ # @param [Enumerable] list
97
+ # The list to be formatted.
98
+ #
99
+ # @return [String]
100
+ #
101
+ def format_inclusive_list(list)
102
+ format_list(list, message('generic.inclusive_conjunction'))
103
+ end
104
+
105
+ private
106
+
107
+ # Formats a list.
108
+ #
109
+ # @see Ward::Errors.format_exclusive_list
110
+ # @see Ward::Errors.format_inclusive_list
111
+ #
112
+ def format_list(list, conjunction)
113
+ case list.size
114
+ when 0 then ''
115
+ when 1 then list.first.to_s
116
+ when 2 then "#{list.first.to_s} #{conjunction} #{list.last.to_s}"
117
+ else
118
+ as_strings = list.map { |value| value.to_s }
119
+ as_strings[-1] = "#{conjunction} #{as_strings[-1]}"
120
+ as_strings.join("#{message('generic.list_seperator')} ")
121
+ end
122
+ end
123
+
124
+ # Returns the en-US error message hash. TEMPORARY.
125
+ #
126
+ # @return [Hash]
127
+ #
128
+ def messages
129
+ @error_messages ||= normalise_messages(YAML.load(
130
+ File.read(File.expand_path('../../../lang/en.yml', __FILE__)) ))
131
+ end
132
+
133
+ # Transforms a hash of messages to a single hash using dot notation.
134
+ #
135
+ def normalise_messages(messages, transformed = {}, key_prefix = '')
136
+ messages.each do |key, value|
137
+ item_key = key_prefix.empty? ? key : "#{key_prefix}.#{key}"
138
+
139
+ if value.is_a?(Hash)
140
+ normalise_messages(value, transformed, item_key)
141
+ else
142
+ transformed[item_key] = value
143
+ end
144
+ end
145
+
146
+ transformed
147
+ end
148
+
149
+ end # class << self
150
+
151
+ # Creates a new Errors instance.
152
+ #
153
+ def initialize
154
+ @errors = {}
155
+ end
156
+
157
+ # Adds an error message to the instance.
158
+ #
159
+ # @param [Symbol, Ward::Context, Ward::ContextChain] attribute
160
+ # The attribute or context for the error.
161
+ # @param [String] message
162
+ # The error message to add.
163
+ #
164
+ # @return [String]
165
+ # Returns the error message which was set.
166
+ #
167
+ # @todo
168
+ # Support symbols for i18n.
169
+ #
170
+ def add(attribute, message)
171
+ if attribute.kind_of?(Context) or attribute.kind_of?(ContextChain)
172
+ attribute = attribute.attribute
173
+ end
174
+
175
+ @errors[attribute] ||= []
176
+ @errors[attribute] << message
177
+ message
178
+ end
179
+
180
+ # Returns an array of the errors present on an an attribute.
181
+ #
182
+ # @param [Symbol] attribute
183
+ # The attribute whose errors you wish to retrieve.
184
+ #
185
+ # @return [Array]
186
+ # Returns the error messages for an attribute, or nil if there are none.
187
+ #
188
+ def on(attribute)
189
+ @errors[attribute]
190
+ end
191
+
192
+ # Iterates through each attribute and the errors.
193
+ #
194
+ # @yieldparam [Symbol] attribute
195
+ # The attribute name.
196
+ # @yieldparam [Array, nil] messages
197
+ # An array with each error message for the attribute, or nil if the
198
+ # attribute has no errors.
199
+ #
200
+ def each(&block)
201
+ @errors.each(&block)
202
+ end
203
+
204
+ # Returns if there are no errors contained.
205
+ #
206
+ # @return [Boolean]
207
+ #
208
+ def empty?
209
+ @errors.empty?
210
+ end
211
+
212
+ end # Errors
213
+ end # Ward