hermod 1.0.2 → 1.2.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 (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