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/support/basic_object'
2
+ require 'ward/support/result'
3
+
4
+ module Ward
5
+ # Support classes.
6
+ module Support; end
7
+ end # Ward
@@ -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
@@ -0,0 +1,3 @@
1
+ module Ward
2
+ VERSION = File.read(File.expand_path('../../../VERSION', __FILE__)).strip
3
+ end
@@ -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
@@ -0,0 +1,8 @@
1
+ --exclude "spec"
2
+ --exclude "activesupport"
3
+ --exclude "rcov"
4
+ --sort coverage
5
+ --callsites
6
+ --xrefs
7
+ --profile
8
+ --text-summary
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --loadby random
3
+ --format progress
4
+ --backtrace
@@ -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