hermod 1.0.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -3
  3. data/Guardfile +9 -0
  4. data/hermod.gemspec +4 -1
  5. data/lib/hermod/input_mutator.rb +13 -0
  6. data/lib/hermod/validators/allowed_values.rb +26 -0
  7. data/lib/hermod/validators/attributes.rb +29 -0
  8. data/lib/hermod/validators/base.rb +36 -0
  9. data/lib/hermod/validators/non_negative.rb +19 -0
  10. data/lib/hermod/validators/non_zero.rb +19 -0
  11. data/lib/hermod/validators/range.rb +28 -0
  12. data/lib/hermod/validators/regular_expression.rb +25 -0
  13. data/lib/hermod/validators/type_checker.rb +37 -0
  14. data/lib/hermod/validators/value_presence.rb +19 -0
  15. data/lib/hermod/validators/whole_units.rb +20 -0
  16. data/lib/hermod/version.rb +1 -1
  17. data/lib/hermod/xml_section_builder.rb +103 -117
  18. data/lib/hermod.rb +2 -0
  19. data/spec/hermod/validators/allowed_values_spec.rb +20 -0
  20. data/spec/hermod/validators/attributes_spec.rb +20 -0
  21. data/spec/hermod/validators/base_spec.rb +25 -0
  22. data/spec/hermod/validators/non_negative_spec.rb +24 -0
  23. data/spec/hermod/validators/non_zero_spec.rb +24 -0
  24. data/spec/hermod/validators/range_spec.rb +25 -0
  25. data/spec/hermod/validators/regular_expression_spec.rb +20 -0
  26. data/spec/hermod/validators/type_checker_spec.rb +29 -0
  27. data/spec/hermod/validators/value_presence_spec.rb +20 -0
  28. data/spec/hermod/validators/whole_units_spec.rb +20 -0
  29. data/spec/hermod/xml_section_builder/integer_node_spec.rb +29 -0
  30. data/spec/hermod/xml_section_builder/monetary_node_spec.rb +2 -2
  31. data/spec/hermod/xml_section_builder/string_node_spec.rb +8 -4
  32. data/spec/minitest_helper.rb +6 -2
  33. metadata +79 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e882154a69ab0cd386b9df39efb15a103979d364
4
- data.tar.gz: 10f3968ac6ba1eff02da89697699d7c688aa5ad5
3
+ metadata.gz: c6dae991640bdc325837ebadb8cb95977923ce59
4
+ data.tar.gz: dd973e128dd2e53e411f807fe0257394fb861362
5
5
  SHA512:
6
- metadata.gz: 7fb42ae9eea2b519e5bf048a3d5d8ca52c7d6952a30938b05bb92e1ba128076bba712eecaca4ffe27d56b487fa7168ad5dcd97edcee5a272923ba5fa6bf978df
7
- data.tar.gz: 604fd4890a7868c2f03a7183be4ccf2d93582b2be427d051f653cc3cae104b35232e2f106833b8dbf5846dcde14fb7dd3147947329e0ad924491010e4e503454
6
+ metadata.gz: a83c5e10ad93e618c4b128d3302b86af810e93cfd4368181dcc59006151bf490685d6fa26fb94950863b085bba98334c473ff14854a1e2942b3318d0dd0dc2ab
7
+ data.tar.gz: c7b4f98c51ef71385f2fd202c192745f7fc914dfdcd9254126bf08d675f9f48ab5eae9b97bb57ed959e3b88fb1b5f18b5edfdbcd5f76292d77497cc8c98f2558
data/Gemfile CHANGED
@@ -3,8 +3,7 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in xml-section.gemspec
4
4
  gemspec
5
5
 
6
- gem "pry-byebug", "~> 1.3.3"
7
- gem "pry-rescue", "~> 1.4.1"
8
- gem "pry-stack_explorer", "~> 0.4.9.1"
6
+ gem "pry-byebug", "~> 1.3.3", platform: [:mri_20, :mri_21]
7
+ gem "pry-debugger", "~> 0.2.3", platform: :mri_19
9
8
  gem "pry-doc", "~> 0.6.0"
10
9
  gem "bond", "~> 0.5.1"
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :minitest, all_after_pass: true, include: ["lib"] do
5
+ # with Minitest::Spec
6
+ watch(%r|^spec/(.*)_spec\.rb|)
7
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
8
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
9
+ end
data/hermod.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = ">= 2"
23
+ spec.required_ruby_version = ">= 1.9.3"
24
24
 
25
25
  spec.add_runtime_dependency "libxml-ruby", "~> 2.7", ">= 2.7.0"
26
26
  spec.add_runtime_dependency "activesupport", "> 3.2", "< 5"
@@ -28,5 +28,8 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "bundler", "~> 1.6"
29
29
  spec.add_development_dependency "rake", "~> 10.3"
30
30
  spec.add_development_dependency "minitest", "~> 5.3"
31
+ spec.add_development_dependency "minitest-reporters", "~> 1.0"
32
+ spec.add_development_dependency "guard", "~> 2.6.1"
33
+ spec.add_development_dependency "guard-minitest", "~> 2.3.1"
31
34
  spec.add_development_dependency "nokogiri", "~> 1.5"
32
35
  end
@@ -0,0 +1,13 @@
1
+ module Hermod
2
+ class InputMutator
3
+ attr_reader :mutator_proc
4
+
5
+ def initialize(mutator_proc)
6
+ @mutator_proc = mutator_proc
7
+ end
8
+
9
+ def mutate!(values, attributes)
10
+ mutator_proc.call(values, attributes)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks the given value is in a predefined list of allowed values
6
+ class AllowedValues < Base
7
+ attr_reader :allowed_values
8
+
9
+ # Sets up the validator with the list of allowed values
10
+ def initialize(allowed_values)
11
+ @allowed_values = allowed_values
12
+ end
13
+
14
+ private
15
+
16
+ def test
17
+ allowed_values.include? value
18
+ end
19
+
20
+ def message
21
+ list_of_values = allowed_values.to_sentence(last_word_connector: ", or ", two_words_connector: " or ")
22
+ "must be one of #{list_of_values}, not #{value}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks the attributes are in a list of allowed attributes
6
+ class Attributes < Base
7
+ attr_reader :allowed_attributes, :bad_attributes
8
+
9
+ # Public: Sets up the list of allowed attributes
10
+ def initialize(allowed_attributes)
11
+ @allowed_attributes = allowed_attributes
12
+ end
13
+
14
+ private
15
+
16
+ def test
17
+ @bad_attributes = [] # reset this for each time the validator is used
18
+ attributes.each do |attribute, _|
19
+ bad_attributes << attribute unless allowed_attributes.include? attribute
20
+ end
21
+ bad_attributes == []
22
+ end
23
+
24
+ def message
25
+ "has attributes it doesn't accept: #{bad_attributes.to_sentence}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ require 'active_support/core_ext/array/conversions'
2
+
3
+ module Hermod
4
+ module Validators
5
+ class Base
6
+ attr_reader :value, :attributes
7
+
8
+ # Public: Runs the test for the validator returning true if it passes and
9
+ # raising if it fails
10
+ #
11
+ # Raises a Hermod::InvalidInputError if the test fails
12
+ # Returns true if it succeeds
13
+ def valid?(value, attributes)
14
+ @value, @attributes = value, attributes
15
+ !!test || raise(InvalidInputError, message)
16
+ end
17
+
18
+ private
19
+
20
+ # Private: override in subclasses to implement the logic for that
21
+ # validator
22
+ #
23
+ # Returns a boolean
24
+ def test
25
+ raise NotImplementedError
26
+ end
27
+
28
+ # Private: override in subclasses to provide a more useful error message
29
+ #
30
+ # Returns a string
31
+ def message
32
+ "is invalid"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks a number is not negative
6
+ class NonNegative < Base
7
+
8
+ private
9
+
10
+ def test
11
+ value >= 0
12
+ end
13
+
14
+ def message
15
+ "cannot be negative"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks the value is not zero
6
+ class NonZero < Base
7
+
8
+ private
9
+
10
+ def test
11
+ value.to_i != 0
12
+ end
13
+
14
+ def message
15
+ "cannot be zero"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks a value is in the given range
6
+ class Range < Base
7
+ attr_reader :range
8
+
9
+ def initialize(range_or_min, max = nil)
10
+ if max
11
+ @range = range_or_min..max
12
+ else
13
+ @range = range_or_min
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def test
20
+ range.cover?(value)
21
+ end
22
+
23
+ def message
24
+ "must be between #{range.min} and #{range.max}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks the value matches the given regular expression
6
+ class RegularExpression < Base
7
+ attr_reader :pattern
8
+
9
+ # Sets up the pattern the value is expected to match
10
+ def initialize(pattern)
11
+ @pattern = pattern
12
+ end
13
+
14
+ private
15
+
16
+ def test
17
+ value =~ pattern
18
+ end
19
+
20
+ def message
21
+ "#{value.inspect} does not match #{pattern.inspect}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # This checks if the given value is an instance of a certain class
6
+ class TypeChecker < Base
7
+
8
+ attr_reader :expected_class, :checker
9
+
10
+ # Sets up the validator with the class it is expected to be. You can
11
+ # optionally pass a block to customise the class matching logic
12
+ #
13
+ # Examples
14
+ #
15
+ # TypeChecker.new(Integer)
16
+ #
17
+ # TypeChecker.new(Date) { |value| value.respond_to? :strftime }
18
+ #
19
+ def initialize(expected_class, &block)
20
+ @expected_class = expected_class
21
+ @checker = block || proc { |value| value.is_a? expected_class }
22
+ end
23
+
24
+ private
25
+
26
+ def test
27
+ checker.call(value)
28
+ end
29
+
30
+ def message
31
+ expected_class_name = expected_class.name.downcase
32
+ join_word = (%w(a e i o u).include?(expected_class_name[0]) ? "an" : "a")
33
+ "must be #{join_word} #{expected_class_name}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # checks the value is present
6
+ class ValuePresence < Base
7
+
8
+ private
9
+
10
+ def test
11
+ value.present?
12
+ end
13
+
14
+ def message
15
+ "isn't optional but no value was provided"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require 'hermod/validators/base'
2
+
3
+ module Hermod
4
+ module Validators
5
+ # Checks a decimal value has no decimal componant, i.e. it's just a decimal
6
+ # representation of an integer
7
+ class WholeUnits < Base
8
+
9
+ private
10
+
11
+ def test
12
+ value == value.to_i
13
+ end
14
+
15
+ def message
16
+ "must be in whole units"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Hermod
2
- VERSION = "1.0.2"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -4,6 +4,16 @@ require 'active_support/core_ext/string/inflections'
4
4
  require 'active_support/core_ext/object/blank'
5
5
 
6
6
  require 'hermod/xml_node'
7
+ require 'hermod/input_mutator'
8
+ require 'hermod/validators/allowed_values'
9
+ require 'hermod/validators/attributes'
10
+ require 'hermod/validators/type_checker'
11
+ require 'hermod/validators/non_negative'
12
+ require 'hermod/validators/non_zero'
13
+ require 'hermod/validators/range'
14
+ require 'hermod/validators/regular_expression'
15
+ require 'hermod/validators/value_presence'
16
+ require 'hermod/validators/whole_units'
7
17
 
8
18
  module Hermod
9
19
 
@@ -49,175 +59,151 @@ module Hermod
49
59
 
50
60
  # Public: defines a node for sending a string to HMRC
51
61
  #
52
- # symbolic_name - the name of the node. This will become the name of the
53
- # method on the XmlSection.
54
- # options - a hash of options used to set up validations.
62
+ # name - the name of the node. This will become the name of the method on the XmlSection.
63
+ # options - a hash of options used to set up validations.
55
64
  #
56
65
  # Returns nothing you should rely on
57
- def string_node(symbolic_name, options={})
58
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
59
- @node_order << symbolic_name
60
-
61
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
62
-
63
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
64
- if options.has_key?(:input_mutator)
65
- value, attributes = options[:input_mutator].call(value, attributes)
66
- end
67
- if value.blank?
68
- if options[:optional]
69
- return # Don't need to add an empty node
70
- else
71
- raise InvalidInputError, "#{symbolic_name} isn't optional but no value was provided"
72
- end
73
- end
74
- if options.has_key?(:allowed_values) && !options[:allowed_values].include?(value)
75
- raise InvalidInputError,
76
- "#{value.inspect} is not in the list of allowed values for #{symbolic_name}: #{options[:allowed_values].inspect}"
77
- end
78
- if options.has_key?(:matches) && value !~ options[:matches]
79
- raise InvalidInputError,
80
- "Value #{value.inspect} for #{symbolic_name} doesn't match #{options[:matches].inspect}"
81
- end
82
- nodes[symbolic_name] << XmlNode.new(xml_name, value.to_s, attributes).rename_attributes(options[:attributes])
66
+ def string_node(name, options={})
67
+ mutators = [].tap do |mutators|
68
+ mutators << InputMutator.new(options.delete(:input_mutator)) if options.has_key? :input_mutator
69
+ end
70
+ validators = [].tap do |validators|
71
+ validators << Validators::AllowedValues.new(options.delete(:allowed_values)) if options.has_key? :allowed_values
72
+ validators << Validators::RegularExpression.new(options.delete(:matches)) if options.has_key? :matches
73
+ validators << Validators::ValuePresence.new unless options.delete(:optional)
74
+ validators << Validators::Attributes.new(options.fetch(:attributes, {}).keys)
75
+ end
76
+ create_method(name, mutators, validators, options) do |value, attributes|
77
+ [value, attributes]
83
78
  end
84
79
  end
85
80
 
86
81
  # Public: defines a node for sending an integer to HMRC
87
82
  #
88
- # symbolic_name - the name of the node. This will become the name of the
89
- # method on the XmlSection.
90
- # options - a hash of options used to set up validations.
83
+ # name - the name of the node. This will become the name of the method on the XmlSection.
84
+ # options - a hash of options used to set up validations.
91
85
  #
92
86
  # Returns nothing you should rely on
93
- def integer_node(symbolic_name, options={})
94
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
95
- @node_order << symbolic_name
96
-
97
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
98
-
99
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
100
- if options.has_key?(:range) && (options[:range][:min] > value || options[:range][:max] < value)
101
- raise InvalidInputError,
102
- "#{value} is outwith the allowable range for #{symbolic_name}: #{options[:range][:min]} - #{options[:range][:max]}"
87
+ def integer_node(name, options={})
88
+ validators = [].tap do |validators|
89
+ if options.has_key? :range
90
+ validators << Validators::Range.new(options[:range][:min], options[:range][:max])
91
+ end
103
92
  end
104
- nodes[symbolic_name] << XmlNode.new(xml_name, value.to_s, attributes).rename_attributes(options[:attributes]) if value.present?
93
+ create_method(name, [], validators, options) do |value, attributes|
94
+ [value.to_s, attributes]
105
95
  end
106
96
  end
107
97
 
108
98
  # Public: defines a node for sending a date to HMRC
109
99
  #
110
- # symbolic_name - the name of the node. This will become the name of the
111
- # method on the XmlSection.
112
- # options - a hash of options used to set up validations.
100
+ # name - the name of the node. This will become the name of the method on the XmlSection.
101
+ # options - a hash of options used to set up validations.
113
102
  #
114
103
  # Returns nothing you should rely on
115
- def date_node(symbolic_name, options={})
116
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
117
- @node_order << symbolic_name
118
-
119
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
120
-
121
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
122
- if value.blank?
123
- if options[:optional]
124
- return # Don't need to add an empty node
125
- else
126
- raise InvalidInputError, "#{symbolic_name} isn't optional but no value was provided"
127
- end
104
+ def date_node(name, options={})
105
+ validators = [].tap do |validators|
106
+ validators << Validators::ValuePresence.new unless options.delete(:optional)
107
+ validators << Validators::TypeChecker.new(Date) { |value| value.respond_to? :strftime }
128
108
  end
129
- unless value.respond_to?(:strftime)
130
- raise InvalidInputError, "#{symbolic_name} must be set to a date"
131
- end
132
- nodes[symbolic_name] << XmlNode.new(xml_name, value.strftime(format_for(:date)), attributes).rename_attributes(options[:attributes])
109
+
110
+ create_method(name, [], validators, options) do |value, attributes|
111
+ [value.strftime(format_for(:date)), attributes]
133
112
  end
134
113
  end
135
114
 
136
115
  # Public: defines a node for sending a boolean to HMRC. It will only be
137
116
  # sent if the boolean is true.
138
117
  #
139
- # symbolic_name - the name of the node. This will become the name of the
140
- # method on the XmlSection.
141
- # options - a hash of options used to set up validations.
118
+ # name - the name of the node. This will become the name of the method on the XmlSection.
119
+ # options - a hash of options used to set up validations.
142
120
  #
143
121
  # Returns nothing you should rely on
144
- def yes_node(symbolic_name, options={})
145
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
146
- @node_order << symbolic_name
147
-
148
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
149
-
150
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
151
- if value
152
- nodes[symbolic_name] << XmlNode.new(xml_name, YES, attributes).rename_attributes(options[:attributes]) if value.present?
153
- end
122
+ def yes_node(name, options={})
123
+ create_method(name, [], [], options) do |value, attributes|
124
+ [(value ? YES : nil), attributes]
154
125
  end
155
126
  end
156
127
 
157
128
  # Public: defines a node for sending a boolean to HMRC. A "yes" will be
158
129
  # sent if it's true and a "no" will be sent if it's false
159
130
  #
160
- # symbolic_name - the name of the node. This will become the name of the
161
- # method on the XmlSection.
162
- # options - a hash of options used to set up validations.
131
+ # name - the name of the node. This will become the name of the method on the XmlSection.
132
+ # options - a hash of options used to set up validations.
163
133
  #
164
134
  # Returns nothing you should rely on
165
- def yes_no_node(symbolic_name, options={})
166
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
167
- @node_order << symbolic_name
168
-
169
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
170
-
171
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
172
- nodes[symbolic_name] << XmlNode.new(xml_name, value ? YES : NO, attributes).rename_attributes(options[:attributes])
135
+ def yes_no_node(name, options={})
136
+ create_method(name, [], [], options) do |value, attributes|
137
+ [(value ? YES : NO), attributes]
173
138
  end
174
139
  end
175
140
 
176
141
  # Public: defines a node for sending a monetary value to HMRC
177
142
  #
178
- # symbolic_name - the name of the node. This will become the name of the
179
- # method on the XmlSection.
180
- # options - a hash of options used to set up validations.
143
+ # name - the name of the node. This will become the name of the method on the XmlSection.
144
+ # options - a hash of options used to set up validations.
181
145
  #
182
146
  # Returns nothing you should rely on
183
- def monetary_node(symbolic_name, options={})
184
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
185
- @node_order << symbolic_name
186
-
187
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
188
-
189
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
190
- value ||= 0 # nils are zero
191
- if !options.fetch(:negative, true) && value < ZERO
192
- raise InvalidInputError, "#{symbolic_name} cannot be negative"
193
- end
194
- # Don't allow fractional values for whole number nodes
195
- if options[:whole_units] && value != value.to_i
196
- raise InvalidInputError, "#{symbolic_name} must be in whole pounds"
197
- end
198
- # Don't include optional nodes if they're zero
199
- if !(options[:optional] && value.zero?)
200
- nodes[symbolic_name] << XmlNode.new(xml_name, sprintf(format_for(:money), value), attributes).rename_attributes(options[:attributes]) if value.present?
147
+ def monetary_node(name, options={})
148
+ validators = [].tap do |validators|
149
+ validators << Validators::NonNegative.new unless options.fetch(:negative, true)
150
+ validators << Validators::WholeUnits.new if options[:whole_units]
151
+ validators << Validators::NonZero.new unless options[:optional]
201
152
  end
153
+
154
+ create_method(name, [], validators, options) do |value, attributes|
155
+ if options[:optional] && value == 0
156
+ [nil, attributes]
157
+ else
158
+ [sprintf(format_for(:money), value), attributes]
159
+ end
202
160
  end
203
161
  end
204
162
 
205
163
  # Public: defines an XML parent node that wraps other nodes
206
164
  #
207
- # symbolic_name - the name of the node. This will become the name of the
208
- # method on the XmlSection.
209
- # options - a hash of options used to set up validations.
165
+ # name - the name of the node. This will become the name of the method on the XmlSection.
166
+ # options - a hash of options used to set up validations.
210
167
  #
211
168
  # Returns nothing you should rely on
212
- def parent_node(symbolic_name, options={})
213
- raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
214
- @node_order << symbolic_name
169
+ def parent_node(name, options={})
170
+ create_method(name, [], [], options) do |value, attributes|
171
+ [value, attributes]
172
+ end
173
+ end
174
+
175
+ private
215
176
 
216
- xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
177
+ # Private: creates a method with a given name that uses a set of mutators
178
+ # and a set of validators to change and validate the input according to
179
+ # certain options. This is used to implement all the node type methods.
180
+ #
181
+ # name - the name of the new method
182
+ # mutators - an array of InputMutator objects (normally one) that change the value and attributes in some way
183
+ # validators - an array of Validator::Base subclasses that are applied to the value and attributes and raise
184
+ # errors under given conditions
185
+ # block - a block that takes the value and attributes and does any post-validation mutation on them
186
+ #
187
+ # Returns nothing you should rely on
188
+ def create_method(name, mutators, validators, options = {}, &block)
189
+ raise DuplicateNodeError, "#{name} is already defined" if @node_order.include? name
190
+ @node_order << name
191
+ xml_name = options.fetch(:xml_name, name.to_s.camelize)
192
+
193
+ @new_class.send :define_method, name do |value, attributes = {}|
194
+ mutators.each { |mutator| value, attributes = mutator.mutate!(value, attributes) }
195
+ begin
196
+ validators.each { |validator| validator.valid?(value, attributes) }
197
+ rescue InvalidInputError => ex
198
+ raise InvalidInputError, "#{name} #{ex.message}"
199
+ end
217
200
 
218
- @new_class.send :define_method, symbolic_name do |value, attributes={}|
219
- nodes[symbolic_name] << XmlNode.new(xml_name, value, attributes).rename_attributes(options[:attributes]) if value.present?
201
+ value, attributes = instance_exec(value, attributes, &block)
202
+ if value.present?
203
+ nodes[name] << XmlNode.new(xml_name, value, attributes).rename_attributes(options[:attributes])
204
+ end
220
205
  end
221
206
  end
207
+
222
208
  end
223
209
  end
data/lib/hermod.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'hermod/xml_section'
2
2
 
3
+ I18n.enforce_available_locales = false
4
+
3
5
  # The namespace for the gem
4
6
  module Hermod
5
7
  end
@@ -0,0 +1,20 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe AllowedValues do
6
+ subject do
7
+ AllowedValues.new(%w(Antelope Bear Cat Dog Elephant))
8
+ end
9
+
10
+ it "permits values in the list" do
11
+ subject.valid?("Cat", {}).must_equal true
12
+ end
13
+
14
+ it "raises an error for values not in the list" do
15
+ ex = proc { subject.valid?("Albatross", {}) }.must_raise InvalidInputError
16
+ ex.message.must_equal "must be one of Antelope, Bear, Cat, Dog, or Elephant, not Albatross"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe Attributes do
6
+ subject do
7
+ Attributes.new([:species, :genus])
8
+ end
9
+
10
+ it "permits attributes in the list" do
11
+ subject.valid?(nil, {species: "Felis catus", genus: "Felis"}).must_equal true
12
+ end
13
+
14
+ it "raises an error for attributes not in the list" do
15
+ ex = proc { subject.valid?(nil, {phylum: "Chordata"}) }.must_raise InvalidInputError
16
+ ex.message.must_equal "has attributes it doesn't accept: phylum"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe Base do
6
+ subject do
7
+ Base.new
8
+ end
9
+
10
+ it "doesn't implement a test" do
11
+ proc { subject.valid?(nil, {}) }.must_raise NotImplementedError
12
+ end
13
+
14
+ it "has a default error message" do
15
+ class TestValidator < Base
16
+ def test
17
+ false
18
+ end
19
+ end
20
+ ex = proc { TestValidator.new.valid?(nil, {}) }.must_raise InvalidInputError
21
+ ex.message.must_equal "is invalid"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe NonNegative do
6
+ subject do
7
+ NonNegative.new
8
+ end
9
+
10
+ it "allows positive values" do
11
+ subject.valid?(1, {}).must_equal true
12
+ end
13
+
14
+ it "allows zero values" do
15
+ subject.valid?(0, {}).must_equal true
16
+ end
17
+
18
+ it "raises an error for negative values" do
19
+ ex = proc { subject.valid?(-1, {}) }.must_raise InvalidInputError
20
+ ex.message.must_equal "cannot be negative"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe NonZero do
6
+ subject do
7
+ NonZero.new
8
+ end
9
+
10
+ it "allows positive values" do
11
+ subject.valid?(1, {}).must_equal true
12
+ end
13
+
14
+ it "allows negative values" do
15
+ subject.valid?(-1, {}).must_equal true
16
+ end
17
+
18
+ it "raises an error for zero values" do
19
+ ex = proc { subject.valid?(0, {}) }.must_raise InvalidInputError
20
+ ex.message.must_equal "cannot be zero"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe Range do
6
+ subject do
7
+ Range.new(1..7)
8
+ end
9
+
10
+ it "allows values in the range" do
11
+ subject.valid?(1, {}).must_equal true
12
+ subject.valid?(7, {}).must_equal true
13
+ end
14
+
15
+ it "raises an error for values outwith the range" do
16
+ ex = proc { subject.valid?(0, {}) }.must_raise InvalidInputError
17
+ ex.message.must_equal "must be between 1 and 7"
18
+ end
19
+
20
+ it "can also take a min and max as arguments" do
21
+ Range.new(1, 7).valid?(3, {}).must_equal true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe RegularExpression do
6
+ subject do
7
+ RegularExpression.new(/\A[A-Z]{2} [0-9]{6} [A-D]\z/x)
8
+ end
9
+
10
+ it "allows values that match the pattern" do
11
+ subject.valid?("AB123456C", {}).must_equal true
12
+ end
13
+
14
+ it "raises an error for values that don't match the pattern" do
15
+ ex = proc { subject.valid?("fish", {}) }.must_raise InvalidInputError
16
+ ex.message.must_equal "\"fish\" does not match /\\A[A-Z]{2} [0-9]{6} [A-D]\\z/x"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe TypeChecker do
6
+ it "uses a default block that checks the value is an instance of the given class" do
7
+ checker = TypeChecker.new(Integer)
8
+ checker.valid?(1, {}).must_equal true
9
+ proc { checker.valid?(1.0, {}) }.must_raise InvalidInputError
10
+ end
11
+
12
+ it "allows you to give a block to be more discerning" do
13
+ checker = TypeChecker.new(Date) {|val| val.respond_to? :strftime }
14
+ dateish = Minitest::Mock.new
15
+ dateish.expect :strftime, "today"
16
+ checker.valid?(dateish, {}).must_equal true
17
+ proc { checker.valid?(Minitest::Mock.new, {}) }.must_raise InvalidInputError
18
+ end
19
+
20
+ it "gives the correct message" do
21
+ ex = proc { TypeChecker.new(Integer).valid?(1.0, {}) }.must_raise InvalidInputError
22
+ ex.message.must_equal "must be an integer"
23
+
24
+ ex = proc { TypeChecker.new(Float).valid?(1, {}) }.must_raise InvalidInputError
25
+ ex.message.must_equal "must be a float"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe ValuePresence do
6
+ subject do
7
+ ValuePresence.new
8
+ end
9
+
10
+ it "allows values that are present" do
11
+ subject.valid?(1, {}).must_equal true
12
+ end
13
+
14
+ it "raises an error for missing values" do
15
+ ex = proc { subject.valid?(nil, {}) }.must_raise InvalidInputError
16
+ ex.message.must_equal "isn't optional but no value was provided"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ module Validators
5
+ describe WholeUnits do
6
+ subject do
7
+ WholeUnits.new
8
+ end
9
+
10
+ it "allows values that are in whole units" do
11
+ subject.valid?(1.0, {}).must_equal true
12
+ end
13
+
14
+ it "raises an error for values with a fractional componant" do
15
+ ex = proc { subject.valid?(3.1415, {}) }.must_raise InvalidInputError
16
+ ex.message.must_equal "must be in whole units"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ describe XmlSection do
5
+
6
+ IntegerXml = XmlSection.build do |builder|
7
+ builder.integer_node :day_of_the_week, range: {min: 1, max: 7}
8
+ end
9
+
10
+ describe "Integer nodes" do
11
+ subject do
12
+ IntegerXml.new
13
+ end
14
+
15
+ it "should accept a valid number" do
16
+ subject.day_of_the_week 7
17
+ value_of_node("DayOfTheWeek").must_equal "7"
18
+ end
19
+
20
+ it "should raise an error if the number is above the maximum" do
21
+ proc { subject.day_of_the_week 8 }.must_raise InvalidInputError
22
+ end
23
+
24
+ it "should raise an error if the number is below the minimum" do
25
+ proc { subject.day_of_the_week 0 }.must_raise InvalidInputError
26
+ end
27
+ end
28
+ end
29
+ end
@@ -41,9 +41,9 @@ module Hermod
41
41
  value_of_node("Pension").must_equal "-100.00"
42
42
  end
43
43
 
44
- it "should not allow pennies for whole unit nodes" do
44
+ it "should not allow decimal values for whole unit nodes" do
45
45
  ex = proc { subject.pension BigDecimal.new("12.34") }.must_raise InvalidInputError
46
- ex.message.must_equal "pension must be in whole pounds"
46
+ ex.message.must_equal "pension must be in whole units"
47
47
  end
48
48
  end
49
49
  end
@@ -18,7 +18,6 @@ module Hermod
18
18
  subject do
19
19
  StringXml.new do |string_xml|
20
20
  string_xml.greeting "Hello"
21
- string_xml.name "World"
22
21
  end
23
22
  end
24
23
 
@@ -33,7 +32,7 @@ module Hermod
33
32
 
34
33
  it "should raise an error when the regex validation fails" do
35
34
  ex = proc { subject.title "Laird" }.must_raise InvalidInputError
36
- ex.message.must_equal %{Value "Laird" for title doesn't match /\\ASir|Dame\\z/}
35
+ ex.message.must_equal %(title "Laird" does not match /\\ASir|Dame\\z/)
37
36
  end
38
37
 
39
38
  it "should require all non-optional nodes to have content" do
@@ -53,7 +52,7 @@ module Hermod
53
52
 
54
53
  it "should raise an error if the value is not in the list of allowed values" do
55
54
  ex = proc { subject.mood "Jubilant" }.must_raise InvalidInputError
56
- ex.message.must_equal %{"Jubilant" is not in the list of allowed values for mood: ["Happy", "Sad", "Hangry"]}
55
+ ex.message.must_equal "mood must be one of Happy, Sad, or Hangry, not Jubilant"
57
56
  end
58
57
 
59
58
  it "should use the given keys for attributes" do
@@ -63,7 +62,12 @@ module Hermod
63
62
  end
64
63
 
65
64
  it "should raise an error if given an attribute that isn't expected" do
66
- proc { subject.title "Sir", knight: "yes" }.must_raise KeyError
65
+ proc { subject.title "Sir", knight: "yes" }.must_raise InvalidInputError
66
+ end
67
+
68
+ it "should not include empty, optional nodes" do
69
+ subject.name ""
70
+ nodes("Name").must_be_empty
67
71
  end
68
72
  end
69
73
  end
@@ -1,8 +1,12 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'hermod'
3
3
 
4
- require 'minitest/autorun'
5
- require 'pry-rescue/minitest'
4
+ require "minitest/autorun"
5
+ require "minitest/hell"
6
+ require "minitest/reporters"
7
+ require "nokogiri"
8
+
9
+ Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
6
10
 
7
11
  require 'nokogiri'
8
12
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hermod
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Mills
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-01 00:00:00.000000000 Z
11
+ date: 2014-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: libxml-ruby
@@ -92,6 +92,48 @@ dependencies:
92
92
  - - "~>"
93
93
  - !ruby/object:Gem::Version
94
94
  version: '5.3'
95
+ - !ruby/object:Gem::Dependency
96
+ name: minitest-reporters
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '1.0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '1.0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: guard
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: 2.6.1
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: 2.6.1
123
+ - !ruby/object:Gem::Dependency
124
+ name: guard-minitest
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: 2.3.1
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: 2.3.1
95
137
  - !ruby/object:Gem::Dependency
96
138
  name: nokogiri
97
139
  requirement: !ruby/object:Gem::Requirement
@@ -119,17 +161,40 @@ files:
119
161
  - ".gitignore"
120
162
  - ".travis.yml"
121
163
  - Gemfile
164
+ - Guardfile
122
165
  - LICENSE
123
166
  - README.md
124
167
  - Rakefile
125
168
  - hermod.gemspec
126
169
  - lib/hermod.rb
170
+ - lib/hermod/input_mutator.rb
127
171
  - lib/hermod/sanitisation.rb
172
+ - lib/hermod/validators/allowed_values.rb
173
+ - lib/hermod/validators/attributes.rb
174
+ - lib/hermod/validators/base.rb
175
+ - lib/hermod/validators/non_negative.rb
176
+ - lib/hermod/validators/non_zero.rb
177
+ - lib/hermod/validators/range.rb
178
+ - lib/hermod/validators/regular_expression.rb
179
+ - lib/hermod/validators/type_checker.rb
180
+ - lib/hermod/validators/value_presence.rb
181
+ - lib/hermod/validators/whole_units.rb
128
182
  - lib/hermod/version.rb
129
183
  - lib/hermod/xml_node.rb
130
184
  - lib/hermod/xml_section.rb
131
185
  - lib/hermod/xml_section_builder.rb
186
+ - spec/hermod/validators/allowed_values_spec.rb
187
+ - spec/hermod/validators/attributes_spec.rb
188
+ - spec/hermod/validators/base_spec.rb
189
+ - spec/hermod/validators/non_negative_spec.rb
190
+ - spec/hermod/validators/non_zero_spec.rb
191
+ - spec/hermod/validators/range_spec.rb
192
+ - spec/hermod/validators/regular_expression_spec.rb
193
+ - spec/hermod/validators/type_checker_spec.rb
194
+ - spec/hermod/validators/value_presence_spec.rb
195
+ - spec/hermod/validators/whole_units_spec.rb
132
196
  - spec/hermod/xml_section_builder/date_node_spec.rb
197
+ - spec/hermod/xml_section_builder/integer_node_spec.rb
133
198
  - spec/hermod/xml_section_builder/monetary_node_spec.rb
134
199
  - spec/hermod/xml_section_builder/parent_node_spec.rb
135
200
  - spec/hermod/xml_section_builder/string_node_spec.rb
@@ -150,7 +215,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
150
215
  requirements:
151
216
  - - ">="
152
217
  - !ruby/object:Gem::Version
153
- version: '2'
218
+ version: 1.9.3
154
219
  required_rubygems_version: !ruby/object:Gem::Requirement
155
220
  requirements:
156
221
  - - ">="
@@ -163,7 +228,18 @@ signing_key:
163
228
  specification_version: 4
164
229
  summary: A Ruby library for talking to the HMRC Government Gateway.
165
230
  test_files:
231
+ - spec/hermod/validators/allowed_values_spec.rb
232
+ - spec/hermod/validators/attributes_spec.rb
233
+ - spec/hermod/validators/base_spec.rb
234
+ - spec/hermod/validators/non_negative_spec.rb
235
+ - spec/hermod/validators/non_zero_spec.rb
236
+ - spec/hermod/validators/range_spec.rb
237
+ - spec/hermod/validators/regular_expression_spec.rb
238
+ - spec/hermod/validators/type_checker_spec.rb
239
+ - spec/hermod/validators/value_presence_spec.rb
240
+ - spec/hermod/validators/whole_units_spec.rb
166
241
  - spec/hermod/xml_section_builder/date_node_spec.rb
242
+ - spec/hermod/xml_section_builder/integer_node_spec.rb
167
243
  - spec/hermod/xml_section_builder/monetary_node_spec.rb
168
244
  - spec/hermod/xml_section_builder/parent_node_spec.rb
169
245
  - spec/hermod/xml_section_builder/string_node_spec.rb