ward 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +28 -0
- data/LICENSE +19 -0
- data/README.markdown +99 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/features/acceptance_matcher.feature +78 -0
- data/features/attribute_keyword.feature +13 -0
- data/features/close_to_matcher.feature +130 -0
- data/features/context_arguments.feature +47 -0
- data/features/equal_to_matcher.feature +25 -0
- data/features/error_messages.feature +69 -0
- data/features/external_validation.feature +15 -0
- data/features/has_matcher.feature +72 -0
- data/features/has_matcher_initialized_with_expectation.feature +94 -0
- data/features/has_matcher_relativities.feature +171 -0
- data/features/include_matcher.feature +28 -0
- data/features/is_keyword.feature +42 -0
- data/features/is_not_keyword.feature +62 -0
- data/features/match_matcher.feature +49 -0
- data/features/multiple_validators.feature +29 -0
- data/features/nil_matcher.feature +25 -0
- data/features/predicate_matcher.feature +23 -0
- data/features/present_matcher.feature +59 -0
- data/features/satisfy_matcher.feature +80 -0
- data/features/scenario_validation.feature +81 -0
- data/features/step_definitions/external_validation_steps.rb +69 -0
- data/features/step_definitions/generic_validation_steps.rb +33 -0
- data/features/step_definitions/object_definition_steps.rb +43 -0
- data/features/support/env.rb +12 -0
- data/features/support/object_builder.rb +33 -0
- data/features/support/struct.rb +38 -0
- data/lang/en.yml +56 -0
- data/lib/ward.rb +26 -0
- data/lib/ward/context.rb +70 -0
- data/lib/ward/context_chain.rb +87 -0
- data/lib/ward/dsl.rb +7 -0
- data/lib/ward/dsl/validation_block.rb +73 -0
- data/lib/ward/dsl/validation_builder.rb +190 -0
- data/lib/ward/errors.rb +213 -0
- data/lib/ward/matchers.rb +97 -0
- data/lib/ward/matchers/acceptance.rb +43 -0
- data/lib/ward/matchers/close_to.rb +60 -0
- data/lib/ward/matchers/equal_to.rb +33 -0
- data/lib/ward/matchers/has.rb +283 -0
- data/lib/ward/matchers/include.rb +54 -0
- data/lib/ward/matchers/match.rb +29 -0
- data/lib/ward/matchers/matcher.rb +68 -0
- data/lib/ward/matchers/nil.rb +30 -0
- data/lib/ward/matchers/predicate.rb +31 -0
- data/lib/ward/matchers/present.rb +56 -0
- data/lib/ward/matchers/satisfy.rb +65 -0
- data/lib/ward/spec.rb +17 -0
- data/lib/ward/spec/matcher_matcher.rb +114 -0
- data/lib/ward/support.rb +7 -0
- data/lib/ward/support/basic_object.rb +55 -0
- data/lib/ward/support/result.rb +49 -0
- data/lib/ward/validator.rb +147 -0
- data/lib/ward/validator_set.rb +115 -0
- data/lib/ward/version.rb +3 -0
- data/spec/lib/has_matcher_relativity_examples.rb +15 -0
- data/spec/lib/have_public_method_defined.rb +22 -0
- data/spec/rcov.opts +8 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/ward/context_chain_spec.rb +178 -0
- data/spec/ward/context_spec.rb +57 -0
- data/spec/ward/dsl/validation_block_spec.rb +27 -0
- data/spec/ward/dsl/validation_builder_spec.rb +212 -0
- data/spec/ward/errors_spec.rb +149 -0
- data/spec/ward/matchers/acceptance_spec.rb +16 -0
- data/spec/ward/matchers/close_to_spec.rb +57 -0
- data/spec/ward/matchers/equal_to_spec.rb +16 -0
- data/spec/ward/matchers/has_spec.rb +175 -0
- data/spec/ward/matchers/include_spec.rb +41 -0
- data/spec/ward/matchers/match_spec.rb +21 -0
- data/spec/ward/matchers/matcher_spec.rb +54 -0
- data/spec/ward/matchers/nil_spec.rb +16 -0
- data/spec/ward/matchers/predicate_spec.rb +19 -0
- data/spec/ward/matchers/present_spec.rb +16 -0
- data/spec/ward/matchers/satisfy_spec.rb +68 -0
- data/spec/ward/matchers_spec.rb +51 -0
- data/spec/ward/spec/have_public_method_defined_spec.rb +31 -0
- data/spec/ward/spec/matcher_matcher_spec.rb +217 -0
- data/spec/ward/validator_set_spec.rb +178 -0
- data/spec/ward/validator_spec.rb +264 -0
- data/tasks/features.rake +15 -0
- data/tasks/rcov.rake +24 -0
- data/tasks/spec.rake +18 -0
- data/tasks/yard.rake +9 -0
- data/ward.gemspec +176 -0
- metadata +239 -0
data/lib/ward/support.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Ward
|
2
|
+
module Support
|
3
|
+
# Used by the DSL classes. Provides a BasicObject implementation for
|
4
|
+
# Ruby versions older than 1.9.
|
5
|
+
#
|
6
|
+
# From Sequel, with modifications to support RSpec.
|
7
|
+
#
|
8
|
+
class BasicObject
|
9
|
+
|
10
|
+
# Lookup missing constants in ::Object
|
11
|
+
def self.const_missing(name)
|
12
|
+
::Object.const_get(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
if RUBY_VERSION < '1.9.0'
|
16
|
+
# If on Ruby 1.8, create a Ward::BasicObject class that is similar to
|
17
|
+
# the Ruby 1.9 BasicObject class. This is used as the basis for the
|
18
|
+
# DSL classes.
|
19
|
+
(instance_methods - %w( __id__ __send__ instance_eval == equal?
|
20
|
+
should should_not )).each do |method|
|
21
|
+
undef_method(method.to_sym)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end # BasicObject
|
26
|
+
end # Support
|
27
|
+
end # Ward
|
28
|
+
|
29
|
+
#
|
30
|
+
# Sequel License:
|
31
|
+
#
|
32
|
+
# Copyright (c) 2007-2008 Sharon Rosner
|
33
|
+
# Copyright (c) 2008-2010 Jeremy Evans
|
34
|
+
#
|
35
|
+
# Permission is hereby granted, free of charge, to any person
|
36
|
+
# obtaining a copy of this software and associated documentation
|
37
|
+
# files (the "Software"), to deal in the Software without
|
38
|
+
# restriction, including without limitation the rights to use,
|
39
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or
|
40
|
+
# sell copies of the Software, and to permit persons to whom the
|
41
|
+
# Software is furnished to do so, subject to the following
|
42
|
+
# conditions:
|
43
|
+
#
|
44
|
+
# The above copyright notice and this permission notice shall be
|
45
|
+
# included in all copies or substantial portions of the Software.
|
46
|
+
#
|
47
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
48
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
49
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
50
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
51
|
+
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
52
|
+
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
53
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
54
|
+
# THE SOFTWARE.
|
55
|
+
#
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Ward
|
2
|
+
module Support
|
3
|
+
# Used when validating objects which don't include the Validation
|
4
|
+
# module. Contains the results of running a ValidatorSet against a record,
|
5
|
+
# and any errors which occurred.
|
6
|
+
#
|
7
|
+
#
|
8
|
+
class Result
|
9
|
+
|
10
|
+
# Returns the errors which were found when validating.
|
11
|
+
#
|
12
|
+
# An Errors instance will always be returned, even if there were no
|
13
|
+
# errors with the record.
|
14
|
+
#
|
15
|
+
# @return [Ward:Errors]
|
16
|
+
#
|
17
|
+
attr_reader :errors
|
18
|
+
|
19
|
+
# Creates a new Result instance.
|
20
|
+
#
|
21
|
+
# @param [Ward::Errors] errors
|
22
|
+
# The errors which were found by the validations.
|
23
|
+
#
|
24
|
+
def initialize(errors)
|
25
|
+
@errors = errors
|
26
|
+
@result = errors.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns true if the validations passed.
|
30
|
+
#
|
31
|
+
# @return [Boolean]
|
32
|
+
#
|
33
|
+
def pass?
|
34
|
+
@result
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :valid?, :pass?
|
38
|
+
|
39
|
+
# Returns true if the validations failed.
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
#
|
43
|
+
def fail?
|
44
|
+
not pass?
|
45
|
+
end
|
46
|
+
|
47
|
+
end # Result
|
48
|
+
end # Support
|
49
|
+
end # Ward
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Ward
|
2
|
+
# Pairs a context (or chain) with a matcher.
|
3
|
+
#
|
4
|
+
class Validator
|
5
|
+
|
6
|
+
# Returns the context.
|
7
|
+
#
|
8
|
+
# @return [Ward::ContextChain, Ward::Context]
|
9
|
+
#
|
10
|
+
attr_reader :context
|
11
|
+
|
12
|
+
# Returns the matcher.
|
13
|
+
#
|
14
|
+
# @return [Ward::Matchers::Matcher]
|
15
|
+
#
|
16
|
+
attr_reader :matcher
|
17
|
+
|
18
|
+
# The scenarios under which the validator will be run.
|
19
|
+
#
|
20
|
+
# @return [Enumerable<Symbol>]
|
21
|
+
#
|
22
|
+
attr_reader :scenarios
|
23
|
+
|
24
|
+
# An override to the default message.
|
25
|
+
#
|
26
|
+
# If set, this message will always be used if the validation fails,
|
27
|
+
# regardless of whether it is used as a positive or negative assertion.
|
28
|
+
#
|
29
|
+
# @return [Hash, String, nil]
|
30
|
+
# Returns a Hash if the user wishes to use a different message depending
|
31
|
+
# on the error state of the matcher (certain matchers, like Has, return
|
32
|
+
# a different error depending on what went wrong), a String if an
|
33
|
+
# explicit error message is set, or nil if no error message is set (in
|
34
|
+
# which case the default error for the matcher will be used).
|
35
|
+
#
|
36
|
+
attr_reader :message
|
37
|
+
|
38
|
+
# Creates a new Validator.
|
39
|
+
#
|
40
|
+
# @param [Ward::ContextChain, Ward::Context] context
|
41
|
+
# The context to be used to fetch the value for the matcher.
|
42
|
+
# @param [Ward::Matchers::Matcher] matcher
|
43
|
+
# A matcher to be used to validate the context value.
|
44
|
+
# @param [Hash] options
|
45
|
+
# Extra options used to customise the Validator.
|
46
|
+
#
|
47
|
+
# @option options [Symbol, Array<Symbol>] :scenarios
|
48
|
+
# An array of scenarios in which this validator should be run
|
49
|
+
# @option options [Hash{Symbol => String}, String] :message
|
50
|
+
# The error message to be used if the validator fails (see
|
51
|
+
# DSL::ValidationBuilder#message).
|
52
|
+
# @option options [String] :context_name
|
53
|
+
# The name to be used for the context when generating error messages.
|
54
|
+
#
|
55
|
+
def initialize(context, matcher, options = {})
|
56
|
+
@context, @matcher = context, matcher
|
57
|
+
@scenarios = Array(options[:scenarios] || :default).freeze
|
58
|
+
@negative = options[:negative] || false
|
59
|
+
@message = options[:message].freeze
|
60
|
+
@context_name = options[:context_name].freeze
|
61
|
+
end
|
62
|
+
|
63
|
+
# Determines if the validator is valid for the given record.
|
64
|
+
#
|
65
|
+
# The return value deviates from the ValidatorSet API -- where #valid?
|
66
|
+
# returns only a boolean value -- but that's fine since you shouldn't be
|
67
|
+
# calling Validator#valid? in your applications anyway. :)
|
68
|
+
#
|
69
|
+
# @param [Object] record
|
70
|
+
# The object whose attribute is to be validated.
|
71
|
+
#
|
72
|
+
# @return [Array<Boolean, {nil, String, Symbol}>]
|
73
|
+
# Returns an array with two elements. The first is always either true or
|
74
|
+
# false, indicating whether the validator passed, and the second is the
|
75
|
+
# raw error message returned by the matcher.
|
76
|
+
#
|
77
|
+
def validate(record)
|
78
|
+
# If the matches? method on the matcher takes two arguments, send in the
|
79
|
+
# record as well as the value.
|
80
|
+
result = if @matcher.method(:matches?).arity != 1
|
81
|
+
@matcher.matches?(@context.value(record), record)
|
82
|
+
else
|
83
|
+
@matcher.matches?(@context.value(record))
|
84
|
+
end
|
85
|
+
|
86
|
+
result, error = if defined?(Rubinius)
|
87
|
+
# Rubinius treats any value which responds to #to_a as being
|
88
|
+
# array-like, thus multiple assignment breaks if the matcher returns
|
89
|
+
# such an object.
|
90
|
+
result.is_a?(Array) ? result : [result].flatten
|
91
|
+
else
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
(!! result) ^ negative? ? [ true, nil ] : [ false, error_for(error) ]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns if the validator should be run as part of the given scenario.
|
99
|
+
#
|
100
|
+
# @param [Symbol] scenario
|
101
|
+
# A name identifying the scenario.
|
102
|
+
#
|
103
|
+
# @return [Boolean]
|
104
|
+
#
|
105
|
+
def scenario?(scenario)
|
106
|
+
@scenarios.include?(scenario)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns if the validator expects that the matcher _not_ match.
|
110
|
+
#
|
111
|
+
# Use with the +is_not+ and +does_not+ DSL keyword.
|
112
|
+
#
|
113
|
+
# @return [Boolean]
|
114
|
+
#
|
115
|
+
def negative?
|
116
|
+
@negative
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Returns the error message when the matcher fails.
|
122
|
+
#
|
123
|
+
# If the validator has a message defined, it will always be used in
|
124
|
+
# preference over any error returned by the matcher, or defiend in the
|
125
|
+
# language files.
|
126
|
+
#
|
127
|
+
# @param [nil, Symbol, String] key
|
128
|
+
# @return [String]
|
129
|
+
#
|
130
|
+
# @see Ward::Errors.error_for
|
131
|
+
# @see Ward::Matchers::Matcher#format_error
|
132
|
+
#
|
133
|
+
def error_for(key)
|
134
|
+
initial = @message || Ward::Errors.error_for(matcher, negative?, key)
|
135
|
+
|
136
|
+
error = initial % matcher.customise_error_values(
|
137
|
+
:expected => matcher.expected,
|
138
|
+
:context => @context_name || context.natural_name)
|
139
|
+
|
140
|
+
error.strip!
|
141
|
+
error[0] = error[0].chr.upcase
|
142
|
+
|
143
|
+
error
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end # Ward
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Ward
|
2
|
+
# Holds one or more Validators which are associated with a particular class.
|
3
|
+
#
|
4
|
+
# This class is considered part of Ward's semi-public API; which is to say
|
5
|
+
# that it's methods should not be called in end-user applications, but are
|
6
|
+
# available for library and plugin authors who wish to extend it's
|
7
|
+
# functionality.
|
8
|
+
#
|
9
|
+
# @example Retrieving the validators for a class.
|
10
|
+
# MyClass.validators # => #<ValidatorSet [#<Validator>, ...]>
|
11
|
+
#
|
12
|
+
class ValidatorSet
|
13
|
+
|
14
|
+
# Builds a ValidatorSet using the given block.
|
15
|
+
#
|
16
|
+
# NOTE: Providing an existing ValidatorSet will result in a copy of that
|
17
|
+
# set being mutated; the original will not be changed.
|
18
|
+
#
|
19
|
+
# @param [Ward::ValidatorSet] set
|
20
|
+
# A ValidatorSet to which the built validators should be added.
|
21
|
+
#
|
22
|
+
# @return [Ward::ValidatorSet]
|
23
|
+
#
|
24
|
+
def self.build(set = nil, &block)
|
25
|
+
Ward::DSL::ValidationBlock.new(set, &block).to_validator_set
|
26
|
+
end
|
27
|
+
|
28
|
+
include Enumerable
|
29
|
+
|
30
|
+
# Creates a new ValidatorSet.
|
31
|
+
#
|
32
|
+
# @param [Enumerable<Ward::Validator>] validators
|
33
|
+
# An optional collection of validators.
|
34
|
+
#
|
35
|
+
def initialize(validators = [])
|
36
|
+
@validators = validators
|
37
|
+
end
|
38
|
+
|
39
|
+
# Determines if all of the contained validators are valid for the given
|
40
|
+
# record in the given scenario.
|
41
|
+
#
|
42
|
+
# @param [Object] record
|
43
|
+
# The object whose validations are to be run.
|
44
|
+
# @param [Symbol] scenario
|
45
|
+
# A name identifying the scenario.
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
# Returns true if the record validated, otherwise returns false.
|
49
|
+
#
|
50
|
+
def valid?(record, scenario = :default)
|
51
|
+
inject(true) do |result, validator|
|
52
|
+
(! validator.scenario?(scenario) or
|
53
|
+
validator.validate(record).first) and result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# A more useful version of #valid?
|
58
|
+
#
|
59
|
+
# Whereas #valid? simply returns true or false indicating whether the
|
60
|
+
# validations passed, #validate returns a Ward::Support::Result instance
|
61
|
+
# which describes the outcome of running the validators, and encapsulates
|
62
|
+
# any errors messages which resulted.
|
63
|
+
#
|
64
|
+
# @param [Object] record
|
65
|
+
# The object whose validations are to be run.
|
66
|
+
# @param [Symbol] scenario
|
67
|
+
# A name identifying the scenario.
|
68
|
+
#
|
69
|
+
# @return [Ward::Support::Result]
|
70
|
+
#
|
71
|
+
def validate(record, scenario = :default)
|
72
|
+
errors = Ward::Errors.new
|
73
|
+
|
74
|
+
@validators.each do |validator|
|
75
|
+
next unless validator.scenario?(scenario)
|
76
|
+
result, error = validator.validate(record)
|
77
|
+
errors.add(validator.context, error) unless result == true
|
78
|
+
end
|
79
|
+
|
80
|
+
Support::Result.new(errors)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Iterates through each validator in the set.
|
84
|
+
#
|
85
|
+
# @yield [validator] Yields each validator in turn.
|
86
|
+
# @yieldparam [Ward::Validator]
|
87
|
+
#
|
88
|
+
def each(&block)
|
89
|
+
@validators.each(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Adds a new validator to set.
|
93
|
+
#
|
94
|
+
# @param [Ward::Validator] validator
|
95
|
+
# The validator to be added to the set.
|
96
|
+
#
|
97
|
+
def push(validator)
|
98
|
+
@validators << validator
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :<<, :push
|
102
|
+
|
103
|
+
# Adds the validators contained in +other+ to the receiving set.
|
104
|
+
#
|
105
|
+
# @param [Ward::ValidatorSet] set
|
106
|
+
# The validator set whose validators are to be added to the receiver.
|
107
|
+
#
|
108
|
+
#
|
109
|
+
def merge!(other)
|
110
|
+
@validators |= other.to_a
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
end # ValidatorSet
|
115
|
+
end # Ward
|
data/lib/ward/version.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
share_examples_for 'Has matcher relativity method' do
|
2
|
+
before(:all) do
|
3
|
+
@expectation ||= [5]
|
4
|
+
@matcher = Ward::Matchers::Has.new
|
5
|
+
@return = @matcher.__send__(@method, *@expectation)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should return the matcher' do
|
9
|
+
@return.should == @matcher
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should set the matcher relativity" do
|
13
|
+
@matcher.relativity.should == @relativity
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Ensures that a given class has a particular public instance method defined
|
2
|
+
# without instantiation. Works around Ruby 1.8 and 1.9 differences.
|
3
|
+
#
|
4
|
+
::Spec::Matchers.define :have_public_method_defined do |value|
|
5
|
+
match do |klass|
|
6
|
+
klass.public_method_defined?(value.to_sym)
|
7
|
+
end
|
8
|
+
|
9
|
+
description do
|
10
|
+
"should have public instance method defined ##{value.to_s}"
|
11
|
+
end
|
12
|
+
|
13
|
+
failure_message_for_should do |klass|
|
14
|
+
"expected #{klass.inspect} to define public instance method " \
|
15
|
+
"#{value.inspect}, but it didn't"
|
16
|
+
end
|
17
|
+
|
18
|
+
failure_message_for_should_not do |klass|
|
19
|
+
"expected #{klass.inspect} to not define public instance method " \
|
20
|
+
"#{value.inspect}, but it did"
|
21
|
+
end
|
22
|
+
end
|
data/spec/rcov.opts
ADDED
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'date' # Used in the CloseTo matcher spec.
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'spec'
|
8
|
+
require 'spec/autorun'
|
9
|
+
|
10
|
+
require 'ward'
|
11
|
+
require 'ward/spec'
|
12
|
+
|
13
|
+
# Spec libraries.
|
14
|
+
spec_libs = Dir.glob(File.expand_path(File.dirname(__FILE__)) + '/lib/**/*.rb')
|
15
|
+
spec_libs.each { |file| require file }
|
16
|
+
|
17
|
+
Spec::Runner.configure do |config|
|
18
|
+
|
19
|
+
end
|