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,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