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,69 @@
1
+ #
2
+ # The steps in this file are used to test objects
3
+ # which don't include the Validation module.
4
+ #
5
+
6
+ def validator_set
7
+ if @validator_set.nil?
8
+ if @validator_set_definition.nil?
9
+ raise 'No validator set defined'
10
+ else
11
+ @validator_set = Ward::ValidatorSet.build do |object|
12
+ eval(Array(@validator_set_definition).join("\n"))
13
+ end
14
+ end
15
+ end
16
+
17
+ @validator_set
18
+ end
19
+
20
+ Transform %r{^'(\w+)' scenario$} do |scenario|
21
+ scenario.to_sym
22
+ end
23
+
24
+ Given %r{(?:using )?a validation set like} do |definition|
25
+ @validator_set_definition = definition
26
+ end
27
+
28
+ Then %r{^the validation set should pass$} do
29
+ validator_set.valid?(defined_object).should be_true
30
+ validator_set.validate(defined_object).should be_pass
31
+ end
32
+
33
+ Then %r{^the validation set should fail$} do
34
+ validator_set.valid?(defined_object).should be_false
35
+ validator_set.validate(defined_object).should be_fail
36
+ end
37
+
38
+ Then %r{^the validation set should pass when using the ('\w+' scenario)$} do |scenario|
39
+ validator_set.valid?(defined_object, scenario).should be_true
40
+ validator_set.validate(defined_object, scenario).should be_pass
41
+ end
42
+
43
+ Then %r{^the validation set should fail when using the ('\w+' scenario)$} do |scenario|
44
+ validator_set.valid?(defined_object, scenario).should be_false
45
+ validator_set.validate(defined_object, scenario).should be_fail
46
+ end
47
+
48
+ Then %r{^there should be no validation errors$} do
49
+ validator_set.validate(defined_object).errors.should be_empty
50
+ end
51
+
52
+ Then %r{^the error on '([^']+)' should be '([^']+)'$} do |attribute, msg|
53
+ result = validator_set.validate(defined_object)
54
+
55
+ if msg[0].chr == '/' and msg[-1].chr == '/'
56
+ # Regexp.
57
+ result.errors.on(attribute.to_sym).length.should == 1
58
+ result.errors.on(attribute.to_sym).first.should =~ eval(msg)
59
+ else
60
+ # Exact string match.
61
+ result.errors.on(attribute.to_sym).should == [msg]
62
+ end
63
+ end
64
+
65
+ Then %r{^there should be (\S+) validation errors? on '([^']+)'$} do |number, attribute|
66
+ result = validator_set.validate(defined_object)
67
+ number = 0 if number == 'no'
68
+ (result.errors.on(attribute.to_sym) || []).size.should == number.to_i
69
+ end
@@ -0,0 +1,33 @@
1
+ #
2
+ # Generic Validators =========================================================
3
+ #
4
+ # These steps allow us to write less specific scenarios which simply state
5
+ # that an attribute should or should not be valid, without having to worry
6
+ # what a valid or invalid value actually is.
7
+ #
8
+ # They are used in conjunction with the "the instance '<attribute>' attribute
9
+ # is (in)?valid" steps.
10
+ #
11
+ # We accomplish this by testing that an attribute is equal to the string
12
+ # "valid" -- any other value causes a failure.
13
+ #
14
+
15
+ When %r{^validating the ('\w+' attribute)$} do |attribute|
16
+ @validator_set_definition ||= []
17
+ @validator_set_definition << "object.#{attribute}.is.equal_to('valid')"
18
+ end
19
+
20
+ When %r{^validating the ('\w+' attribute) in the ('\w+' scenario)$} do |attribute, scenario|
21
+ @validator_set_definition ||= []
22
+ @validator_set_definition <<
23
+ "object.#{attribute}.is.equal_to('valid').scenario(:#{scenario})"
24
+ end
25
+
26
+ Given %r{^the instance ('\w+' attribute) is valid$} do |attribute|
27
+ When %{the instance '#{attribute}' attribute is '"valid"'}
28
+ end
29
+
30
+ Given %r{^the instance ('\w+' attribute) is invalid$} do |attribute|
31
+ When %{the instance '#{attribute}' attribute is '"invalid"'}
32
+ end
33
+
@@ -0,0 +1,43 @@
1
+ #
2
+ # The steps in this file are used to build classes and
3
+ # objects which are later validated.
4
+ #
5
+
6
+ def object_builder
7
+ # Returns the ObjectBuilder instance for the current feature.
8
+ @object_builder ||= Ward::Spec::ObjectBuilder.new
9
+ end
10
+
11
+ def defined_object
12
+ # Create an oject with the named attributes and values.
13
+ object_builder.to_instance
14
+ end
15
+
16
+ Transform %r{^'(\w+[!\?]?)' attribute$} do |attribute|
17
+ attribute.to_sym
18
+ end
19
+
20
+ Given %r{^a class with an? ('\w+[!\?]?' attribute)$} do |attribute|
21
+ Given "the class also has a '#{attribute}' attribute"
22
+ end
23
+
24
+ Given %r{^the class also has an? ('\w+[!\?]?' attribute)$} do |attribute|
25
+ object_builder.attributes << attribute
26
+ end
27
+
28
+ Given %r{^the instance ('\w+[!\?]?' attribute) is '(.*)'$} do |attribute, value|
29
+ unless object_builder.attributes.include?(attribute)
30
+ raise "The #{attribute.inspect} attribute was not defined"
31
+ end
32
+
33
+ value = "''" if value =~ /^\s*$/ # Empty string.
34
+
35
+ # Attempt to evaluate the value. If the evaluation fails we assume that it
36
+ # is a string which should be used literally.
37
+ object_builder.values[attribute] =
38
+ begin eval(value) ; rescue NameError ; value ; end
39
+ end
40
+
41
+ Given %r{^the class has behaviour like$} do |behaviour|
42
+ object_builder.behaviours << behaviour
43
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '/../../lib'))
2
+
3
+ require 'time' # Used in the CloseTo matcher features.
4
+
5
+ require 'spec'
6
+ require 'spec/expectations'
7
+
8
+ require 'ward'
9
+ require 'ward/spec'
10
+
11
+ require File.join(File.dirname(__FILE__), '/object_builder')
12
+ require File.join(File.dirname(__FILE__), '/struct')
@@ -0,0 +1,33 @@
1
+ module Ward
2
+ module Spec
3
+ # An object which allows new attributes and behaviour to be easily
4
+ # declared, and for validations to be added one at a time.
5
+ class ObjectBuilder
6
+
7
+ attr_reader :attributes, :values, :behaviours
8
+
9
+ def initialize
10
+ @attributes, @values, @behaviours = [], {}, []
11
+ end
12
+
13
+ # Create an oject with the named attributes and values.
14
+ #
15
+ # @return [Ward::Spec::Struct]
16
+ #
17
+ def to_instance
18
+ @attributes = [:__placeholder__] if @attributes.empty?
19
+
20
+ instance = Ward::Spec::Struct.new(*@attributes).new(
21
+ *@attributes.map { |attribute| @values[attribute] })
22
+
23
+ unless @behaviours.empty?
24
+ metaclass = (class << instance; self; end)
25
+ @behaviours.each { |behaviour| metaclass.class_eval(behaviour) }
26
+ end
27
+
28
+ instance
29
+ end
30
+
31
+ end # ObjectBuilder
32
+ end # Spec
33
+ end # Ward
@@ -0,0 +1,38 @@
1
+ module Ward
2
+ module Spec
3
+ # Used in the object definition steps; provides a version of Struct which
4
+ # does not respond to length or size, and allows the use of predicate and
5
+ # bang methods.
6
+ class Struct < ::Struct
7
+
8
+ undef_method :length
9
+ undef_method :size
10
+
11
+ def self.new(*orig_attributes)
12
+ attributes, aliases = [], {}
13
+
14
+ orig_attributes.each do |attribute|
15
+ case attribute.to_s
16
+ when /^(.+)!$/
17
+ attributes << "#{$1}_bang".to_sym
18
+ aliases["#{$1}_bang"] = attribute
19
+ when /^(.+)\?$/
20
+ attributes << "#{$1}_predicate".to_sym
21
+ aliases["#{$1}_predicate"] = attribute
22
+ else
23
+ attributes << attribute
24
+ end
25
+ end
26
+
27
+ struct_class = super(*attributes)
28
+
29
+ struct_class.class_eval(aliases.map do |plain, pretty|
30
+ "alias_method(:#{pretty}, :#{plain}) ; private(:#{plain})"
31
+ end.join("\n"))
32
+
33
+ struct_class
34
+ end
35
+
36
+ end # Struct
37
+ end # Spec
38
+ end # Ward
@@ -0,0 +1,56 @@
1
+ ---
2
+
3
+ generic:
4
+ inclusive_conjunction: 'and'
5
+ exclusive_conjunction: 'or'
6
+ list_seperator: ','
7
+
8
+ acceptance:
9
+ positive: '%{context} should be accepted'
10
+ negative: '%{context} should not be accepted'
11
+
12
+ close_to:
13
+ positive: '%{context} should be within %{delta} of %{expected}'
14
+ negative: '%{context} should not be within %{delta} of %{expected}'
15
+
16
+ equal_to:
17
+ positive: '%{context} should be %{expected}'
18
+ negative: '%{context} should not be %{expected}'
19
+
20
+ exclude:
21
+ positive: '%{context} should not be one of %{expected}'
22
+ negative: '%{context} should be one of %{expected}'
23
+
24
+ has:
25
+ eql:
26
+ positive: '%{context} should have %{expected} %{collection}'
27
+ negative: '%{context} should not have %{expected} %{collection}'
28
+ lte:
29
+ positive: '%{context} should have at most %{expected} %{collection}'
30
+ negative: '%{context} should not have at most %{expected} %{collection}'
31
+ gte:
32
+ positive: '%{context} should have at least %{expected} %{collection}'
33
+ negative: '%{context} should not have at least %{expected} %{collection}'
34
+ between:
35
+ positive: '%{context} should have between %{lower} and %{upper} %{collection}'
36
+ negative: '%{context} should not have between %{lower} and %{upper} %{collection}'
37
+
38
+ include:
39
+ positive: '%{context} should be %{expected}'
40
+ negative: '%{context} should not be %{expected}'
41
+
42
+ match:
43
+ positive: '%{context} format is invalid'
44
+ negative: '%{context} format is invalid'
45
+
46
+ nil:
47
+ positive: '%{context} should be nil'
48
+ negative: '%{context} should not be nil'
49
+
50
+ present:
51
+ positive: '%{context} should be present'
52
+ negative: '%{context} should not be present'
53
+
54
+ satisfy:
55
+ positive: '%{context} is invalid'
56
+ negative: '%{context} is invalid'
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+
3
+ # Add Ruby 1.9-style string interpolation.
4
+ require 'active_support/core_ext/string/interpolation'
5
+
6
+ # Load the ActiveSupport inflector without the String extensions methods.
7
+ require 'active_support/inflector/inflections'
8
+ require 'active_support/inflector/transliterate'
9
+ require 'active_support/inflector/methods'
10
+ require 'active_support/inflections'
11
+
12
+ # On with the library...
13
+ require 'ward/support'
14
+ require 'ward/context'
15
+ require 'ward/context_chain'
16
+ require 'ward/dsl'
17
+ require 'ward/errors'
18
+ require 'ward/matchers'
19
+ require 'ward/validator'
20
+ require 'ward/validator_set'
21
+ require 'ward/version'
22
+
23
+ module Ward
24
+ # Raise when a validator couldn't be built as something was missing.
25
+ class IncompleteValidator < StandardError; end
26
+ end
@@ -0,0 +1,70 @@
1
+ module Ward
2
+ # A class which represents "somewhere" from which a value can be retrieved
3
+ # for validation.
4
+ #
5
+ # A context initialized with a +:length+ attribute assumes that the value
6
+ # for validation can be retrieved by calling +length+ on the target object.
7
+ #
8
+ class Context
9
+
10
+ # Returns the name of the attribute to be validated.
11
+ #
12
+ # @return [Symbol]
13
+ #
14
+ attr_reader :attribute
15
+
16
+ # Returns the 'natural name' of the attribute.
17
+ #
18
+ # This name is used when generating error messages, since you probably
19
+ # don't want you end users to be presented with (occasionally) obscure
20
+ # attribute names.
21
+ #
22
+ # @example
23
+ # :name # => Name
24
+ # :a_field # => A field
25
+ # :post_id # => Post
26
+ #
27
+ # @return [String]
28
+ #
29
+ attr_reader :natural_name
30
+
31
+ # Creates a new validator instance.
32
+ #
33
+ # @param [#to_sym] attribute
34
+ # The name of the attribute to be validated.
35
+ # @param [*] *context_args
36
+ # Arguments to be used when calling the context.
37
+ # @param [Block] context_block
38
+ # A block to be used when calling the context.
39
+ #
40
+ def initialize(attribute, *context_args, &context_block)
41
+ @attribute = attribute.to_sym
42
+ @context_args, @context_block = context_args, context_block
43
+
44
+ @natural_name =
45
+ ActiveSupport::Inflector.humanize(@attribute.to_s).downcase
46
+ end
47
+
48
+ # Returns the value of the context for the given +target+ object.
49
+ #
50
+ # @example
51
+ #
52
+ # Context.new(:length).value('abc')
53
+ # # => 3
54
+ #
55
+ # Context.new(:length_as_string) do |target|
56
+ # target.length.to_s
57
+ # end.value('abc')
58
+ # # => '3'
59
+ #
60
+ # @param [Object] target
61
+ # The object from which the value is to be retrieved.
62
+ #
63
+ # @return [Object]
64
+ #
65
+ def value(target)
66
+ target.__send__(@attribute, *@context_args, &@context_block)
67
+ end
68
+
69
+ end # Context
70
+ end # Ward
@@ -0,0 +1,87 @@
1
+ module Ward
2
+ # ContextChain combines one or more {Context} instances in order to be able
3
+ # to retrieve values from composed objects.
4
+ #
5
+ # For example, if the chain contains two contexts, the first with a +length+
6
+ # attribute, and the second with a +to_s+ attribute, the chain would resolve
7
+ # to calling +target.length.to_s+ in order to retrieve a value for
8
+ # validation.
9
+ #
10
+ class ContextChain
11
+
12
+ # Creates a new ContextChain instance.
13
+ #
14
+ def initialize
15
+ @contexts = []
16
+ end
17
+
18
+ # Returns the name of the attribute to be validated.
19
+ #
20
+ # Returns the attribute for the first context. If the chain is empty,
21
+ # :base is always returned.
22
+ #
23
+ # @return [Symbol]
24
+ #
25
+ # @see Context#attribute
26
+ #
27
+ def attribute
28
+ @contexts.empty? ? :base : @contexts.first.attribute
29
+ end
30
+
31
+ # Returns the 'natural name' of the contained contexts.
32
+ #
33
+ # @return [String]
34
+ #
35
+ # @see Context#natural_name
36
+ #
37
+ def natural_name
38
+ @contexts.map { |context| context.natural_name }.join(' ')
39
+ end
40
+
41
+ # Returns the value of the chain for the given +target+ object.
42
+ #
43
+ # @param [Object] target
44
+ # The object from which the value is to be retrieved.
45
+ #
46
+ # @return [Object]
47
+ #
48
+ def value(target)
49
+ if @contexts.size > 1
50
+ resolved = @contexts[0..-2].inject(target) do |intermediate, context|
51
+ context.value(intermediate) unless intermediate.nil?
52
+ end
53
+
54
+ raise ArgumentError,
55
+ "Couldn't retrieve a value for #{natural_name.downcase}; " \
56
+ "something along the way evaluated to nil" if resolved.nil?
57
+
58
+ @contexts.last.value(resolved)
59
+ elsif @contexts.size == 1
60
+ @contexts.first.value(target)
61
+ else
62
+ target
63
+ end
64
+ end
65
+
66
+ # Returns the contexts contained in the chain as an Array.
67
+ #
68
+ # @return [Array<Ward::Context>]
69
+ # An array containing the contexts.
70
+ #
71
+ def to_a
72
+ @contexts.dup
73
+ end
74
+
75
+ # Adds a new context to the end of the chain.
76
+ #
77
+ # @param [Ward::Context] context
78
+ # The context to be added to the chain.
79
+ #
80
+ def push(context)
81
+ @contexts << context
82
+ end
83
+
84
+ alias_method :<<, :push
85
+
86
+ end # ContextChain
87
+ end # Ward