hermod 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|