hermod 1.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +3 -0
- data/Gemfile +10 -0
- data/LICENSE +202 -0
- data/README.md +496 -0
- data/Rakefile +10 -0
- data/hermod.gemspec +32 -0
- data/lib/hermod/sanitisation.rb +26 -0
- data/lib/hermod/version.rb +3 -0
- data/lib/hermod/xml_node.rb +55 -0
- data/lib/hermod/xml_section.rb +108 -0
- data/lib/hermod/xml_section_builder.rb +223 -0
- data/lib/hermod.rb +5 -0
- data/spec/hermod/xml_section_builder/date_node_spec.rb +27 -0
- data/spec/hermod/xml_section_builder/monetary_node_spec.rb +50 -0
- data/spec/hermod/xml_section_builder/parent_node_spec.rb +28 -0
- data/spec/hermod/xml_section_builder/string_node_spec.rb +70 -0
- data/spec/hermod/xml_section_builder/yes_no_node_spec.rb +36 -0
- data/spec/hermod/xml_section_builder/yes_node_spec.rb +36 -0
- data/spec/hermod/xml_section_spec.rb +95 -0
- data/spec/hermod_spec.rb +7 -0
- data/spec/minitest_helper.rb +74 -0
- metadata +193 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'xml'
|
2
|
+
require 'hermod/xml_section_builder'
|
3
|
+
require 'hermod/sanitisation'
|
4
|
+
|
5
|
+
module Hermod
|
6
|
+
# A representation of a section of XML sent to HMRC using the Government
|
7
|
+
# Gateway
|
8
|
+
class XmlSection
|
9
|
+
include Sanitisation
|
10
|
+
|
11
|
+
# Public: builds a new class using the XmlSectionBuilder DSL
|
12
|
+
#
|
13
|
+
# Returns the new Class
|
14
|
+
def self.build(options = {}, &block)
|
15
|
+
Class.new(XmlSection).tap do |new_class|
|
16
|
+
options.each do |name, value|
|
17
|
+
new_class.public_send "#{name}=", value
|
18
|
+
end
|
19
|
+
XmlSectionBuilder.new(new_class).build(&block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :attributes
|
24
|
+
|
25
|
+
# Public: turns the XmlSection into an XML::Node instance (from
|
26
|
+
# libxml-ruby). This creates this as a node, adds any attributes (after
|
27
|
+
# sanitising them according to HMRC's rules) and then adds child nodes in
|
28
|
+
# the order they were defined in the DSL. Nodes that have been called multiple
|
29
|
+
# times are added in the order they were called.
|
30
|
+
#
|
31
|
+
# Returns an XML::Node
|
32
|
+
def to_xml
|
33
|
+
XML::Node.new(self.class.xml_name).tap do |root_node|
|
34
|
+
# Add attributes
|
35
|
+
attributes.each do |attribute_name, attribute_value|
|
36
|
+
sane_value = sanitise_attribute(attribute_value)
|
37
|
+
root_node[attribute_name] = sane_value if sane_value.present?
|
38
|
+
end
|
39
|
+
# Add child nodes
|
40
|
+
self.class.node_order.each do |node_name|
|
41
|
+
nodes[node_name].each do |node|
|
42
|
+
root_node << node.to_xml
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Internal: creates an XmlSection. This shouldn't normally be called
|
49
|
+
# directly, instead the subclasses call it as they define a useful
|
50
|
+
# NODE_ORDER.
|
51
|
+
#
|
52
|
+
# name - a Symbol that corresponds to the node name in NODE_ORDER
|
53
|
+
# block - a Block that will be executed in the context of this class for
|
54
|
+
# setting up descendents.
|
55
|
+
def initialize(attributes={}, &block)
|
56
|
+
@attributes = attributes
|
57
|
+
yield self if block_given?
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
attr_writer :xml_name, :formats
|
62
|
+
attr_accessor :node_order
|
63
|
+
end
|
64
|
+
|
65
|
+
# Internal: a class method for getting the name of the XML node used when
|
66
|
+
# converting instances to XML for HMRC. If the `xml_name` has been set then
|
67
|
+
# it will be used, otherwise the class name will be used as a default.
|
68
|
+
#
|
69
|
+
# Returns a String
|
70
|
+
def self.xml_name
|
71
|
+
@xml_name || name.demodulize
|
72
|
+
end
|
73
|
+
|
74
|
+
# Internal: provides access to the formats hash, falling back on an empty
|
75
|
+
# hash by default. These formats are used by the date and monetary nodes
|
76
|
+
# for converting their values to strings HMRC will accept.
|
77
|
+
#
|
78
|
+
# Returns a Hash
|
79
|
+
def self.formats
|
80
|
+
@formats ||= {
|
81
|
+
date: "%Y-%m-%d",
|
82
|
+
money: "%.2f"
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
# Internal: provides access to the hash of nodes where the default for an
|
87
|
+
# unspecified key is an empty array. This stores the nodes as they are
|
88
|
+
# created with the key being the name of the node (which is the name of the
|
89
|
+
# method called to set it) and the value being an array of all the values
|
90
|
+
# set on this node in the order they are set.
|
91
|
+
#
|
92
|
+
# Returns a Hash
|
93
|
+
def nodes
|
94
|
+
@nodes ||= Hash.new { |h, k| h[k] = [] }
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Private: a convenience method for getting the format string for a given
|
100
|
+
# key.
|
101
|
+
#
|
102
|
+
# Returns a format String
|
103
|
+
# Raises a KeyError if the requested format is not found
|
104
|
+
def format_for(type)
|
105
|
+
self.class.formats.fetch(type)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'active_support/core_ext/object/blank'
|
5
|
+
|
6
|
+
require 'hermod/xml_node'
|
7
|
+
|
8
|
+
module Hermod
|
9
|
+
|
10
|
+
# Internal: the base class from which all Hermod errors inherit
|
11
|
+
XmlError = Class.new(StandardError)
|
12
|
+
|
13
|
+
# Public: the error that's raised whenever some validation or constraint
|
14
|
+
# specified on a node fails.
|
15
|
+
InvalidInputError = Class.new(XmlError)
|
16
|
+
|
17
|
+
# Public: the error that's raised when you try to define two nodes with the
|
18
|
+
# same name when building an XmlSection.
|
19
|
+
DuplicateNodeError = Class.new(XmlError)
|
20
|
+
|
21
|
+
# Public: Used to build an anonymous subclass of XmlSection with methods for
|
22
|
+
# defining nodes on that subclass of varying types used by HMRC.
|
23
|
+
class XmlSectionBuilder
|
24
|
+
|
25
|
+
ZERO = BigDecimal.new('0').freeze
|
26
|
+
BOOLEAN_VALUES = [
|
27
|
+
YES = "yes".freeze,
|
28
|
+
NO = "no".freeze,
|
29
|
+
]
|
30
|
+
|
31
|
+
# Internal: Sets up the builder with the anonymous subclass of XmlSection.
|
32
|
+
# Don't use this directly, instead see `Hermod::XmlSection.build`
|
33
|
+
def initialize(new_class)
|
34
|
+
@new_class = new_class
|
35
|
+
@node_order = []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Internal: Takes a block to build the class using the methods on this
|
39
|
+
# builder and then sets the correct node_order (to ensure the nodes are
|
40
|
+
# ordered correctly in the XML).
|
41
|
+
#
|
42
|
+
# Returns the newly built class. This should be assigned to a constant
|
43
|
+
# before use.
|
44
|
+
def build(&block)
|
45
|
+
yield self
|
46
|
+
@new_class.node_order = @node_order
|
47
|
+
@new_class
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: defines a node for sending a string to HMRC
|
51
|
+
#
|
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.
|
55
|
+
#
|
56
|
+
# 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])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Public: defines a node for sending an integer to HMRC
|
87
|
+
#
|
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.
|
91
|
+
#
|
92
|
+
# 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]}"
|
103
|
+
end
|
104
|
+
nodes[symbolic_name] << XmlNode.new(xml_name, value.to_s, attributes).rename_attributes(options[:attributes]) if value.present?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Public: defines a node for sending a date to HMRC
|
109
|
+
#
|
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.
|
113
|
+
#
|
114
|
+
# 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
|
128
|
+
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])
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Public: defines a node for sending a boolean to HMRC. It will only be
|
137
|
+
# sent if the boolean is true.
|
138
|
+
#
|
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.
|
142
|
+
#
|
143
|
+
# 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
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Public: defines a node for sending a boolean to HMRC. A "yes" will be
|
158
|
+
# sent if it's true and a "no" will be sent if it's false
|
159
|
+
#
|
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.
|
163
|
+
#
|
164
|
+
# 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])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Public: defines a node for sending a monetary value to HMRC
|
177
|
+
#
|
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.
|
181
|
+
#
|
182
|
+
# 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?
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Public: defines an XML parent node that wraps other nodes
|
206
|
+
#
|
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.
|
210
|
+
#
|
211
|
+
# 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
|
215
|
+
|
216
|
+
xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
|
217
|
+
|
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?
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
data/lib/hermod.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
describe XmlSection do
|
5
|
+
|
6
|
+
DateXml = XmlSection.build(formats: {date: "%Y-%m-%d"}) do |builder|
|
7
|
+
builder.date_node :date_of_birth
|
8
|
+
builder.date_node :anniversary
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "Date nodes" do
|
12
|
+
subject do
|
13
|
+
DateXml.new do |dummy|
|
14
|
+
dummy.date_of_birth Date.new(1988, 8, 13)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should format the date with the given date string" do
|
19
|
+
value_of_node("DateOfBirth").must_equal "1988-08-13"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should raise an error if given something that isn't a date" do
|
23
|
+
proc { subject.anniversary "yesterday" }.must_raise InvalidInputError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
describe XmlSection do
|
5
|
+
|
6
|
+
MonetaryXml = XmlSection.build(formats: {money: "%.2f"}) do |builder|
|
7
|
+
builder.monetary_node :pay
|
8
|
+
builder.monetary_node :tax, optional: true
|
9
|
+
builder.monetary_node :ni, xml_name: "NI", negative: false
|
10
|
+
builder.monetary_node :pension, whole_units: true
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "Monetary nodes" do
|
14
|
+
subject do
|
15
|
+
MonetaryXml.new do |dummy|
|
16
|
+
dummy.pay 1000
|
17
|
+
dummy.tax 0
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should format values with the provided format string" do
|
22
|
+
value_of_node("Pay").must_equal "1000.00"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not include optional nodes if they're zero" do
|
26
|
+
number_of_nodes("Tax").must_equal 0
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should use xml_name as the node name if provided" do
|
30
|
+
subject.ni 100
|
31
|
+
number_of_nodes("NI").must_equal 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should raise an error if given a negative number for a field that cannot be negative" do
|
35
|
+
ex = proc { subject.ni -100 }.must_raise InvalidInputError
|
36
|
+
ex.message.must_equal "ni cannot be negative"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should allow negative numbers for fields by default" do
|
40
|
+
subject.pension(-100)
|
41
|
+
value_of_node("Pension").must_equal "-100.00"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should not allow pennies for whole unit nodes" do
|
45
|
+
ex = proc { subject.pension BigDecimal.new("12.34") }.must_raise InvalidInputError
|
46
|
+
ex.message.must_equal "pension must be in whole pounds"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
ParentXml = XmlSection.build do |builder|
|
5
|
+
builder.parent_node :inner
|
6
|
+
end
|
7
|
+
|
8
|
+
InnerXml = XmlSection.build do |builder|
|
9
|
+
builder.string_node :inside
|
10
|
+
end
|
11
|
+
|
12
|
+
describe XmlSection do
|
13
|
+
describe "Parent XML nodes" do
|
14
|
+
subject do
|
15
|
+
ParentXml.new do |outer|
|
16
|
+
outer.inner(InnerXml.new do |inner|
|
17
|
+
inner.inside "layered like an onion"
|
18
|
+
end)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should correctly wrap the inner XML" do
|
23
|
+
expected = "<ParentXml>\n <InnerXml>\n <Inside>layered like an onion</Inside>\n </InnerXml>\n</ParentXml>"
|
24
|
+
subject.to_xml.to_s.must_equal expected
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
describe XmlSection do
|
5
|
+
|
6
|
+
StringXml = XmlSection.build do |builder|
|
7
|
+
builder.string_node :greeting
|
8
|
+
builder.string_node :name, optional: true
|
9
|
+
builder.string_node :title, matches: /\ASir|Dame\z/, attributes: {masculine: "Male"}
|
10
|
+
builder.string_node :required
|
11
|
+
builder.string_node :gender, input_mutator: (lambda do |value, attributes|
|
12
|
+
[value == "Male" ? "M" : "F", attributes]
|
13
|
+
end)
|
14
|
+
builder.string_node :mood, allowed_values: %w(Happy Sad Hangry)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "String nodes" do
|
18
|
+
subject do
|
19
|
+
StringXml.new do |string_xml|
|
20
|
+
string_xml.greeting "Hello"
|
21
|
+
string_xml.name "World"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should set node contents correctly" do
|
26
|
+
value_of_node("Greeting").must_equal "Hello"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should allow values that pass the regex validation" do
|
30
|
+
subject.title "Sir"
|
31
|
+
value_of_node("Title").must_equal "Sir"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should raise an error when the regex validation fails" do
|
35
|
+
ex = proc { subject.title "Laird" }.must_raise InvalidInputError
|
36
|
+
ex.message.must_equal %{Value "Laird" for title doesn't match /\\ASir|Dame\\z/}
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should require all non-optional nodes to have content" do
|
40
|
+
ex = proc { subject.required "" }.must_raise InvalidInputError
|
41
|
+
ex.message.must_equal "required isn't optional but no value was provided"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should apply changes to the inputs if a input_mutator is provided" do
|
45
|
+
subject.gender "Male"
|
46
|
+
value_of_node("Gender").must_equal "M"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should restrict values to those in the list of allowed values if such a list is provided" do
|
50
|
+
subject.mood "Hangry"
|
51
|
+
value_of_node("Mood").must_equal "Hangry"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should raise an error if the value is not in the list of allowed values" do
|
55
|
+
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"]}
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should use the given keys for attributes" do
|
60
|
+
subject.title "Sir", masculine: "no"
|
61
|
+
attributes_for_node("Title").keys.first.must_equal "Male"
|
62
|
+
attributes_for_node("Title")["Male"].value.must_equal "no"
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should raise an error if given an attribute that isn't expected" do
|
66
|
+
proc { subject.title "Sir", knight: "yes" }.must_raise KeyError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
describe XmlSection do
|
5
|
+
|
6
|
+
YesNoXml = XmlSection.build do |builder|
|
7
|
+
builder.yes_no_node :awesome
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "Yes/No nodes" do
|
11
|
+
describe "when true" do
|
12
|
+
subject do
|
13
|
+
YesNoXml.new do |yes_no_xml|
|
14
|
+
yes_no_xml.awesome true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should include the node with yes as the contents" do
|
19
|
+
value_of_node("Awesome").must_equal "yes"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "when not true" do
|
24
|
+
subject do
|
25
|
+
YesNoXml.new do |yes_no_xml|
|
26
|
+
yes_no_xml.awesome false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should include the node with no as the contents" do
|
31
|
+
value_of_node("Awesome").must_equal "no"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
describe XmlSection do
|
5
|
+
|
6
|
+
YesXml = XmlSection.build do |builder|
|
7
|
+
builder.yes_node :awesome
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "Yes Only Nodes" do
|
11
|
+
describe "when true" do
|
12
|
+
subject do
|
13
|
+
YesXml.new do |yes_xml|
|
14
|
+
yes_xml.awesome true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should include the node with yes as the contents" do
|
19
|
+
value_of_node("Awesome").must_equal "yes"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "when not true" do
|
24
|
+
subject do
|
25
|
+
YesXml.new do |yes_xml|
|
26
|
+
yes_xml.awesome false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should not include the node" do
|
31
|
+
number_of_nodes("Awesome").must_equal 0
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
module Hermod
|
4
|
+
|
5
|
+
NamedXML = XmlSection.build(xml_name: "Testing") { |builder| }
|
6
|
+
UnnamedXML = XmlSection.build { |builder| }
|
7
|
+
OrderingXML = XmlSection.build do |builder|
|
8
|
+
builder.string_node :first
|
9
|
+
builder.string_node :repeated
|
10
|
+
builder.string_node :last
|
11
|
+
end
|
12
|
+
FormattedXML = XmlSection.build do |builder|
|
13
|
+
builder.date_node :birthday
|
14
|
+
builder.monetary_node :allowance
|
15
|
+
end
|
16
|
+
|
17
|
+
describe XmlSection do
|
18
|
+
describe "building an XML generating class with no arguments" do
|
19
|
+
subject do
|
20
|
+
UnnamedXML.new
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should use the class name as the XML node name" do
|
24
|
+
subject.to_xml.name.must_equal "UnnamedXML"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "building an XML generating class with a custom node name" do
|
29
|
+
subject do
|
30
|
+
NamedXML.new
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should use the class name as the XML node name" do
|
34
|
+
subject.to_xml.name.must_equal "Testing"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "default formats" do
|
39
|
+
subject do
|
40
|
+
FormattedXML.new do |formatted|
|
41
|
+
formatted.birthday Date.new(1988, 8, 13)
|
42
|
+
formatted.allowance BigDecimal.new("20")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it "formats dates in yyyy-mm-dd form" do
|
47
|
+
value_of_node("Birthday").must_equal("1988-08-13")
|
48
|
+
end
|
49
|
+
|
50
|
+
it "formats money to two decimal places" do
|
51
|
+
value_of_node("Allowance").must_equal("20.00")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#to_xml" do
|
56
|
+
subject do
|
57
|
+
OrderingXML.new do |ordering|
|
58
|
+
ordering.repeated "beta"
|
59
|
+
ordering.last "epsilon"
|
60
|
+
ordering.repeated "gamma"
|
61
|
+
ordering.first "alpha"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should order nodes by the order they were defined when the class was built" do
|
66
|
+
node_by_index(0).name.must_equal "First"
|
67
|
+
node_by_index(0).content.must_equal "alpha"
|
68
|
+
|
69
|
+
node_by_index(1).name.must_equal "Repeated"
|
70
|
+
node_by_index(1).content.must_equal "beta"
|
71
|
+
|
72
|
+
node_by_index(-1).name.must_equal "Last"
|
73
|
+
node_by_index(-1).content.must_equal "epsilon"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should order nodes called multiple times in the order they were called" do
|
77
|
+
node_by_index(1).name.must_equal "Repeated"
|
78
|
+
node_by_index(1).content.must_equal "beta"
|
79
|
+
|
80
|
+
node_by_index(2).name.must_equal "Repeated"
|
81
|
+
node_by_index(2).content.must_equal "gamma"
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should order nodes at XML generation time, not at call time" do
|
85
|
+
subject.repeated "delta"
|
86
|
+
|
87
|
+
node_by_index(-2).name.must_equal "Repeated"
|
88
|
+
node_by_index(-2).content.must_equal "delta"
|
89
|
+
|
90
|
+
node_by_index(-1).name.must_equal "Last"
|
91
|
+
node_by_index(-1).content.must_equal "epsilon"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|