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.
data/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # Hermod
2
+
3
+ This gem makes it easier to talk to HMRC through the [Government Gateway][1] by
4
+ providing a DSL you can use to create Ruby classes to build the XML required in
5
+ a form that meets HMRC's specification.
6
+
7
+ It ensures that nodes appear in the correct order with the correct formatting
8
+ and allows you to preprocess values and apply validations at submission time.
9
+
10
+ [1]: http://www.hmrc.gov.uk/schemas/GatewayDocumentSubmissionProtocol_V3.1.pdf "HMRC's specification"
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'hermod'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install hermod
25
+
26
+ ## Usage
27
+
28
+ This gem allows you to describe classes that represent a section of XML that
29
+ will be sent to HMRC. This description includes type, validation, and format
30
+ information as well as any runtime mutations that should be applied to inputs
31
+ you provide.
32
+
33
+ ### Supported Types
34
+
35
+ The following types of XML node are supported:
36
+
37
+ * Strings
38
+ * Integers
39
+ * Dates
40
+ * Yes/No
41
+ * Yes only
42
+ * Monetary values
43
+ * Parent XML
44
+
45
+ #### Global Options
46
+
47
+ There are some options that can be passed to all or some of the different node
48
+ types.
49
+
50
+ **XML Name**
51
+
52
+ By default the name used for the XML node is generated by converting the node
53
+ name from `snake_case` to `TitleCase`. For example, the `date_of_birth` node in
54
+ the example above would become `DateOfBirth`. By providing an `xml_name` you
55
+ can override this, thus changing it to `BirthDate`
56
+
57
+ *Building an XmlSection*
58
+ ```ruby
59
+ Example = Hermod::XmlSection.build do |builder|
60
+ builder.string_node :ni_number, xml_name: "NINumber"
61
+ end
62
+ ```
63
+
64
+ *Using that XmlSection*
65
+ ```ruby
66
+ Example.new do |example|
67
+ example.ni_number "AB123456C"
68
+ end
69
+ ```
70
+
71
+ *The Resulting XML*
72
+ ```xml
73
+ <Example>
74
+ <NINumber>AB123456C</NINumber>
75
+ </Example>
76
+ ```
77
+
78
+ **Attributes**
79
+
80
+ Any node can have attributes which are defined by passing a `Hash` of symbol,
81
+ string pairs. The symbol is used to refer to the attribute when setting the
82
+ value of the node and the string is the form that will be sent to HMRC.
83
+
84
+ *Building an XmlSection*
85
+ ```ruby
86
+ Example = Hermod::XmlSection.build do |builder|
87
+ builder.string_node :tax_code, attributes: {week_1_month_1: "WeekOneMonthOne"}
88
+ end
89
+ ```
90
+
91
+ *Using that XmlSection*
92
+ ```ruby
93
+ Example.new do |example|
94
+ example.tax_code "1000L", week_1_month_1: true
95
+ end
96
+ ```
97
+
98
+ *The Resulting XML*
99
+ ```xml
100
+ <Example>
101
+ <TaxCode WeekOneMonthOne="yes">1000L</TaxCode>
102
+ </Example>
103
+ ```
104
+
105
+ **Optional**
106
+
107
+ Not all nodes allow this but for those that do (String, Date and Monetary nodes)
108
+ if a node is marked as optional then any blank values (like nil or an empty
109
+ string) will be ignored.
110
+
111
+ *Building an XmlSection*
112
+ ```ruby
113
+ Example = Hermod::XmlSection.build do |builder|
114
+ builder.string_node :middle_name, optional: true
115
+ end
116
+ ```
117
+
118
+ *Using that XmlSection*
119
+ ```ruby
120
+ Example.new do |example|
121
+ example.middle_name nil
122
+ end
123
+ ```
124
+
125
+ *No XML will be produced*
126
+
127
+ #### String Nodes
128
+
129
+ String nodes handle a wide variety of cases and can take regular expressions
130
+ and lists of values to restrict the provided values. If they are marked as
131
+ optional then the node will be excluded if the value given is blank
132
+ (`nil` or the empty string).
133
+
134
+ **Regular Expressions**
135
+
136
+ The `matches` option allows you to provide a regular expression that is used to
137
+ validate the input. If you try to pass a value that doesn't match the expression
138
+ a `Hermod::InvalidInputError` will be raised.
139
+
140
+ *Building an XmlSection*
141
+ ```ruby
142
+ Example = Hermod::XmlSection.build do |builder|
143
+ builder.string_node :ni_number, matches: /\A[A-Z]{2}[0-9]{6}[A-D ]\z/
144
+ end
145
+ ```
146
+
147
+ *Using that XmlSection*
148
+ ```ruby
149
+ Example.new do |example|
150
+ example.ni_number "I can't remember it"
151
+ end
152
+ ```
153
+
154
+ *A `Hermod::InvalidInputError` will be raised*
155
+
156
+
157
+ **Allowable Values**
158
+
159
+ The `allowable_values` lets you specify a list of string that are allowed for
160
+ this node. Passing a value not in this list will raise
161
+ a `Hermod::InvalidInputError`.
162
+
163
+ *Building an XmlSection*
164
+ ```ruby
165
+ Example = Hermod::XmlSection.build do |builder|
166
+ builder.string_node :mood, allowable_values: %w(Happy Sad Hangry)
167
+ end
168
+ ```
169
+
170
+ *Using that XmlSection*
171
+ ```ruby
172
+ Example.new do |example|
173
+ example.gender "Wrathful"
174
+ end
175
+ ```
176
+
177
+ *A `Hermod::InvalidInputError` will be raised*
178
+
179
+ **Input Mutator**
180
+
181
+ The `input_mutator` option allows you to provide a lambda that is provided with
182
+ two arguments, the value assigned to the node and the `Hash` of attributes (if
183
+ any). This can be used to change either or both of these and the lambda must
184
+ return both the value and the attributes as an array (`[value, attributes]`)
185
+ after they've been modified.
186
+
187
+ *Building an XmlSection*
188
+ ```ruby
189
+ Example = Hermod::XmlSection.build do |builder|
190
+ builder.string_node :ni_number, xml_name: "NINO", optional: true, matches: /\A[A-Z]{2}[0-9]{6}[A-D ]\z/,
191
+ input_mutator: (lambda { |value, attrs| [value.delete(' ').upcase, attrs] })
192
+ end
193
+ ```
194
+
195
+ *Using that XmlSection*
196
+ ```ruby
197
+ Example.new do |example|
198
+ example.ni_number "AB 12 34 56 C"
199
+ end
200
+ ```
201
+
202
+ *The Resulting XML*
203
+ ```xml
204
+ <Example>
205
+ <NiNumber>AB123456C</NiNumber>
206
+ </Example>
207
+ ```
208
+
209
+ #### Integer Nodes
210
+
211
+ Integer nodes let you provide a whole number that won't be formatted as
212
+ a monetary value.
213
+
214
+ **Range**
215
+
216
+ You can specify a `range` option as a hash with a `min` and `max` value. If you
217
+ provide a value outwith the range (inclusive) then
218
+ a `Hermod::InvalidInputError` exception will be raised.
219
+
220
+ *Building an XmlSection*
221
+ ```ruby
222
+ Example = Hermod::XmlSection.build do |builder|
223
+ builder.integer_node :day_of_the_week, range: {min: 1, max: 7}
224
+ end
225
+ ```
226
+
227
+ *Using that XmlSection*
228
+ ```ruby
229
+ Example.new do |example|
230
+ example.day_of_the_week 8
231
+ end
232
+ ```
233
+
234
+ *A `Hermod::InvalidInputError` will be raised*
235
+
236
+ #### Date Nodes
237
+
238
+ Date nodes let you send through a date to HMRC. It will be converted to the
239
+ given date format which you can specify as a format string in the `formats`
240
+ option passed to the `Hermod::XmlSection.build` call. Anything that responds to
241
+ `strftime` can be passed to the node. Anything else will cause an
242
+ `Hermod::InvalidInputError` exception to be raised.
243
+
244
+ *Building an XmlSection*
245
+ ```ruby
246
+ Example = Hermod::XmlSection.build(formats: {date: "%Y-%m-%d"}) do |builder|
247
+ builder.date_node :date_of_birth
248
+ end
249
+ ```
250
+
251
+ *Using that XmlSection*
252
+ ```ruby
253
+ Example.new do |example|
254
+ example.date_of_birth Date.new(1988, 8, 13)
255
+ end
256
+ ```
257
+
258
+ *The Resulting XML*
259
+ ```xml
260
+ <Example>
261
+ <DateOfBirth>1988-08-13</DateOfBirth>
262
+ </Example>
263
+ ```
264
+
265
+ #### Yes Nodes
266
+
267
+ Yes nodes allow you to send a boolean value to HMRC provided that value is
268
+ true. Nothing will be sent if the value is false. This pattern is commonly used
269
+ by HMRC for optional boolean nodes. They're known as "yes nodes" because HMRC
270
+ use "yes" and "no" in place of true and false in their XML.
271
+
272
+ *Building an XmlSection*
273
+ ```ruby
274
+ Example = Hermod::XmlSection.build do |builder|
275
+ builder.yes_node :verily
276
+ builder.yes_node :nae
277
+ end
278
+ ```
279
+
280
+ *Using that XmlSection*
281
+ ```ruby
282
+ Example.new do |example|
283
+ example.verily true
284
+ example.nae false
285
+ end
286
+ ```
287
+
288
+ *The Resulting XML*
289
+ ```xml
290
+ <Example>
291
+ <Verily>yes</Verily>
292
+ </Example>
293
+ ```
294
+
295
+ #### Yes/No Nodes
296
+
297
+ This works in a similar fashion to the yes nodes described above but if a false
298
+ value is provided a "no" will be sent instead of the node being excluded.
299
+
300
+ *Building an XmlSection*
301
+ ```ruby
302
+ Example = Hermod::XmlSection.build do |builder|
303
+ builder.yes_no_node :verily
304
+ builder.yes_no_node :nae
305
+ end
306
+ ```
307
+
308
+ *Using that XmlSection*
309
+ ```ruby
310
+ Example.new do |example|
311
+ example.verily true
312
+ example.nae false
313
+ end
314
+ ```
315
+
316
+ *The Resulting XML*
317
+ ```xml
318
+ <Example>
319
+ <Verily>yes</Verily>
320
+ <Nae>no</Nae>
321
+ </Example>
322
+ ```
323
+
324
+ #### Monetary Nodes
325
+
326
+ Monetary nodes let you send through monetary values to HMRC. They will be
327
+ converted to the given monetary format which you can specify as a format string
328
+ in the `formats` option passed to the `Hermod::XmlSection.build` call. Values
329
+ passed to monetary nodes should be BigDecimal objects.
330
+
331
+ **Negative**
332
+
333
+ By default negative numbers are allowed. If you need to prevent them you can
334
+ set the `negative` option to false.
335
+
336
+ *Building an XmlSection*
337
+ ```ruby
338
+ Example = Hermod::XmlSection.build(formats: {money: "%.2f"}) do |builder|
339
+ builder.monetary_node :taxable_pay, negative: false
340
+ end
341
+ ```
342
+
343
+ *Using that XmlSection*
344
+ ```ruby
345
+ Example.new do |example|
346
+ example.taxable_pay BigDecimal.new("-300")
347
+ end
348
+ ```
349
+
350
+ *A `Hermod::InvalidInputError` will be raised*
351
+
352
+ **Whole Units**
353
+
354
+ Sometimes HMRC require that you send through a value as a whole unit. If this
355
+ is the case you can set the `whole_units` option to true and if an invalid
356
+ value is passed a `Hermod::InvalidInputError` exception will be raised.
357
+
358
+ *Building an XmlSection*
359
+ ```ruby
360
+ Example = Hermod::XmlSection.build(formats: {money: "%.2f"}) do |builder|
361
+ builder.monetary_node :lower_earnings_limit, whole_units: true
362
+ end
363
+ ```
364
+
365
+ *Using that XmlSection*
366
+ ```ruby
367
+ Example.new do |example|
368
+ example.lower_earnings_limit BigDecimal.new("153.49")
369
+ end
370
+ ```
371
+
372
+ *A `Hermod::InvalidInputError` will be raised*
373
+
374
+ **Optional**
375
+
376
+ For monetary nodes the `optional` option will also prevent zero values from
377
+ being submitted.
378
+
379
+ *Building an XmlSection*
380
+ ```ruby
381
+ Example = Hermod::XmlSection.build(formats: {money: "%.2f"}) do |builder|
382
+ builder.monetary_node :taxable_pay
383
+ builder.monetary_node :tax, optional: true
384
+ end
385
+ ```
386
+
387
+ *Using that XmlSection*
388
+ ```ruby
389
+ Example.new do |example|
390
+ example.taxable_pay BigDecimal.new("1000")
391
+ example.tax BigDecimal.new("0")
392
+ end
393
+ ```
394
+
395
+ *The Resulting XML*
396
+ ```xml
397
+ <Example>
398
+ <TaxablePay>1000.00</TaxablePay>
399
+ </Example>
400
+ ```
401
+
402
+ #### Parent Nodes
403
+
404
+ Parent nodes are the way you specify that the contents of this node is another
405
+ `XmlSection`. The `xml_name` is ignored (whether you supply it or rely on the
406
+ default) so the given `symbolic_name` is just the name of the method you call
407
+ to add content. Instead the node name is picked up from the class name of the
408
+ XmlSection you add as a child.
409
+
410
+ *Building an XmlSection*
411
+ ```ruby
412
+ Example = Hermod::XmlSection.build do |builder|
413
+ builder.parent_node :inner
414
+ end
415
+
416
+ Inside = Hermod::XmlSection.build do |builder|
417
+ builder.string_node :text"
418
+ end
419
+ ```
420
+
421
+ *Using that XmlSection*
422
+ ```ruby
423
+ Example.new do |example|
424
+ example.inner(Inside.new do |inside|
425
+ inside.text "Hello, World"
426
+ end)
427
+ end
428
+ ```
429
+
430
+ *The Resulting XML*
431
+ ```xml
432
+ <Example>
433
+ <Inside>
434
+ <Text>Hello, World</Text>
435
+ </Inside>
436
+ </Example>
437
+ ```
438
+
439
+ ### Full Example
440
+
441
+ This is all explained in more detail below but a reasonably complex XML section
442
+ may be described as follows.
443
+
444
+ ```ruby
445
+ Details = Hermod::XmlSection.build(xml_name: "EmployeeDetails", formats: Payroll::RTI::FORMATS) do |builder|
446
+ builder.string_node :ni_number, xml_name: "NINO", optional: true, matches: /\A[A-Z]{2}[0-9]{6}[A-D ]\z/,
447
+ input_mutator: (lambda do |value, attrs|
448
+ [value.delete(' ').upcase, attrs]
449
+ end)
450
+ builder.parent_node :name
451
+ builder.parent_node :address
452
+ builder.date_node :date_of_birth, xml_name: "BirthDate", optional: true
453
+ builder.string_node :gender, allowable_values: %w(Male Female),
454
+ input_mutator: (lambda do |input, attrs|
455
+ [input == AppConstants::MALE ? Payroll::RTI::MALE : Payroll::RTI::FEMALE, attrs]
456
+ end)
457
+ end
458
+ ```
459
+
460
+ This creates a class that can be used like so.
461
+
462
+ ```ruby
463
+ xml = Payroll::RTI::Employee::Details.new do |details|
464
+ details.name(Payroll::RTI::Name.new do |name|
465
+ name.title employee.title
466
+ employee.forenames.each do |forename|
467
+ name.forename forename
468
+ end
469
+ name.surname employee.last_name
470
+ end)
471
+ details.gender employee.gender
472
+
473
+ details.address(Address.new do |address|
474
+ employee.address_lines.each do |line|
475
+ address.line line
476
+ end
477
+ address.postcode profile.postcode
478
+ end)
479
+
480
+ details.ni_number employee.ni_number
481
+ details.date_of_birth employee.date_of_birth
482
+ end)
483
+ ```
484
+
485
+ Nodes are defined in the builder in the order they will be sent to HMRC. They
486
+ can then be called in any order when using the class. Calling the same method
487
+ multiple times will add multiple instances of that node and they will be output
488
+ in the order the calls were made in.
489
+
490
+ ## Contributing
491
+
492
+ 1. Fork it ( https://github.com/fac/hermod/fork )
493
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
494
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
495
+ 4. Push to the branch (`git push origin my-new-feature`)
496
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "spec"
6
+ t.test_files = FileList["spec/**/*_spec.rb"]
7
+ end
8
+
9
+ task :default => :test
10
+
data/hermod.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hermod/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hermod"
8
+ spec.version = Hermod::VERSION
9
+ spec.authors = ["Harry Mills"]
10
+ spec.email = ["harry@freeagent.com"]
11
+ spec.summary = %q{A Ruby library for talking to the HMRC Government Gateway.}
12
+ spec.description = %q{A Ruby library for talking to the HMRC Government Gateway.
13
+ This provides a builder for creating classes that can generate the XML needed complete with type information and
14
+ runtime validation.}
15
+ spec.homepage = ""
16
+ spec.license = "Apache License, Version 2.0"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.required_ruby_version = ">= 2"
24
+
25
+ spec.add_runtime_dependency "libxml-ruby", "~> 2.7", ">= 2.7.0"
26
+ spec.add_runtime_dependency "activesupport", "~> 4.1", ">= 4.1.4"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.6"
29
+ spec.add_development_dependency "rake", "~> 10.3", ">= 10.3.2"
30
+ spec.add_development_dependency "minitest", "~> 5.3", ">= 5.3.5"
31
+ spec.add_development_dependency "nokogiri", "~> 1.6", ">= 1.6.2.1"
32
+ end
@@ -0,0 +1,26 @@
1
+ module Hermod
2
+ module Sanitisation
3
+ # TODO: replace this module with something better
4
+ # Any replacement should make it possible for both yes only attributes and
5
+ # yes/no attributes to work correctly.
6
+
7
+ private
8
+
9
+ # Private: alters attributes so a true becomes "yes", a no isn't sent and
10
+ # anything else gets turned into a String.
11
+ #
12
+ # value - the non-sanitised value
13
+ #
14
+ # Returns the sanitised value of the attribute ready for sending to HMRC.
15
+ def sanitise_attribute(value)
16
+ case value
17
+ when true
18
+ XmlSectionBuilder::YES
19
+ when false
20
+ nil # Attributes aren't included if they're false
21
+ else
22
+ value.to_s
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Hermod
2
+ VERSION = "1.0.1"
3
+ end
@@ -0,0 +1,55 @@
1
+ require 'hermod/sanitisation'
2
+
3
+ module Hermod
4
+ # A representation of an XML node with content and attributes.
5
+ class XmlNode
6
+ include Sanitisation
7
+
8
+ attr_reader :name, :value, :attributes
9
+
10
+ # Internal: creates a XmlNode. This is used by the XmlSectionBuilder's node
11
+ # building methods and should not be called manually.
12
+ #
13
+ # name - the name of the node as it appears in the XML
14
+ # value - the node contents as a string.
15
+ # attributes - a Hash of attributes as Symbol -> value pairs. The symbol
16
+ # must be in the list of attributes allowed for the node as
17
+ # set in the builder.
18
+ def initialize(name, value, attributes={})
19
+ @name = name
20
+ @value = value
21
+ @attributes = attributes
22
+ end
23
+
24
+ # Internal: turns the XmlNode into an XML::Node including any attributes
25
+ # without any sanitisation (currently - this may change in a future
26
+ # version).
27
+ #
28
+ # Returns an XML::Node built from the XmlNode object.
29
+ def to_xml
30
+ if value.respond_to? :to_xml
31
+ value.to_xml
32
+ else
33
+ XML::Node.new(@name, @value).tap do |node|
34
+ @attributes.each do |attribute_name, attribute_value|
35
+ node[attribute_name] = attribute_value if attribute_value.present?
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Internal: replaces symbol attributes with strings looked up in the provided
42
+ # hash
43
+ #
44
+ # lookup_hash - the hash to use to convert symbols to strings HMRC recognise
45
+ #
46
+ # Returns self so it can be used in a call chain (This may change in
47
+ # future)
48
+ def rename_attributes(lookup_hash)
49
+ attributes.keys.each do |attribute|
50
+ attributes[lookup_hash.fetch(attribute)] = sanitise_attribute(attributes.delete(attribute))
51
+ end
52
+ self
53
+ end
54
+ end
55
+ end