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.
@@ -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,5 @@
1
+ require 'hermod/xml_section'
2
+
3
+ # The namespace for the gem
4
+ module Hermod
5
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Hermod do
4
+ it "should have a version number" do
5
+ Hermod::VERSION.wont_be_nil
6
+ end
7
+ end