ups-ruby 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.hound.yml +2 -0
  4. data/.rubocop.yml +1064 -0
  5. data/.travis.yml +10 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +46 -0
  8. data/LICENSE.txt +14 -0
  9. data/README.md +78 -0
  10. data/Rakefile +17 -0
  11. data/lib/ups-ruby.rb +2 -0
  12. data/lib/ups.rb +33 -0
  13. data/lib/ups/builders/address_builder.rb +135 -0
  14. data/lib/ups/builders/builder_base.rb +216 -0
  15. data/lib/ups/builders/organisation_builder.rb +74 -0
  16. data/lib/ups/builders/rate_builder.rb +21 -0
  17. data/lib/ups/builders/ship_accept_builder.rb +30 -0
  18. data/lib/ups/builders/ship_confirm_builder.rb +103 -0
  19. data/lib/ups/builders/shipper_builder.rb +88 -0
  20. data/lib/ups/connection.rb +124 -0
  21. data/lib/ups/data.rb +50 -0
  22. data/lib/ups/data/canadian_states.rb +21 -0
  23. data/lib/ups/data/ie_counties.rb +10 -0
  24. data/lib/ups/data/ie_county_prefixes.rb +15 -0
  25. data/lib/ups/data/us_states.rb +59 -0
  26. data/lib/ups/exceptions.rb +7 -0
  27. data/lib/ups/packaging.rb +27 -0
  28. data/lib/ups/parsers/parser_base.rb +48 -0
  29. data/lib/ups/parsers/rates_parser.rb +60 -0
  30. data/lib/ups/parsers/ship_accept_parser.rb +52 -0
  31. data/lib/ups/parsers/ship_confirm_parser.rb +16 -0
  32. data/lib/ups/services.rb +21 -0
  33. data/lib/ups/version.rb +10 -0
  34. data/spec/spec_helper.rb +18 -0
  35. data/spec/stubs/rates_negotiated_success.xml +227 -0
  36. data/spec/stubs/rates_success.xml +196 -0
  37. data/spec/stubs/ship_accept_failure.xml +12 -0
  38. data/spec/stubs/ship_accept_success.xml +56 -0
  39. data/spec/stubs/ship_confirm_failure.xml +12 -0
  40. data/spec/stubs/ship_confirm_success.xml +50 -0
  41. data/spec/support/RateRequest.xsd +1 -0
  42. data/spec/support/ShipAcceptRequest.xsd +36 -0
  43. data/spec/support/ShipConfirmRequest.xsd +996 -0
  44. data/spec/support/schema_path.rb +5 -0
  45. data/spec/support/shipping_options.rb +48 -0
  46. data/spec/support/xsd_validator.rb +11 -0
  47. data/spec/ups/builders/address_builder_spec.rb +97 -0
  48. data/spec/ups/builders/rate_builder_spec.rb +20 -0
  49. data/spec/ups/builders/ship_accept_builder_spec.rb +16 -0
  50. data/spec/ups/builders/ship_confirm_builder_spec.rb +23 -0
  51. data/spec/ups/connection/rates_negotiated_spec.rb +69 -0
  52. data/spec/ups/connection/rates_standard_spec.rb +71 -0
  53. data/spec/ups/connection/ship_spec.rb +111 -0
  54. data/spec/ups/connection_spec.rb +20 -0
  55. data/ups.gemspec +24 -0
  56. metadata +166 -0
@@ -0,0 +1,10 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.0.0
4
+ - 2.1.6
5
+ - 2.2.2
6
+ - ruby-head
7
+ cache: bundler
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
3
+
4
+ group :development, :test do
5
+ gem 'simplecov'
6
+ gem 'codeclimate-test-reporter', '~> 1.0.0'
7
+ gem 'rake'
8
+ gem 'minitest'
9
+ gem 'nokogiri'
10
+ end
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ups-ruby (0.8.3)
5
+ excon (~> 0.45, >= 0.45.3)
6
+ insensitive_hash (~> 0.3.3)
7
+ levenshtein-ffi (~> 1.1)
8
+ ox (~> 2.2, >= 2.2.0)
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ codeclimate-test-reporter (1.0.5)
14
+ simplecov
15
+ docile (1.1.5)
16
+ excon (0.54.0)
17
+ ffi (1.9.14)
18
+ insensitive_hash (0.3.3)
19
+ json (1.8.3)
20
+ levenshtein-ffi (1.1.0)
21
+ ffi (~> 1.9)
22
+ mini_portile (0.6.2)
23
+ minitest (5.7.0)
24
+ nokogiri (1.6.6.2)
25
+ mini_portile (~> 0.6.0)
26
+ ox (2.4.9)
27
+ rake (12.0.0)
28
+ simplecov (0.10.0)
29
+ docile (~> 1.1.0)
30
+ json (~> 1.8)
31
+ simplecov-html (~> 0.10.0)
32
+ simplecov-html (0.10.0)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ codeclimate-test-reporter (~> 1.0.0)
39
+ minitest
40
+ nokogiri
41
+ rake
42
+ simplecov
43
+ ups-ruby!
44
+
45
+ BUNDLED WITH
46
+ 1.13.6
@@ -0,0 +1,14 @@
1
+ Copyright (c) 2017 Paul Trippett and Veeqo Ltd
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU Affero General Public License as
5
+ published by the Free Software Foundation, either version 3 of the
6
+ License, or (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU Affero General Public License for more details.
12
+
13
+ You should have received a copy of the GNU Affero General Public License
14
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,78 @@
1
+ [![Gem Version](https://img.shields.io/gem/v/ups.svg?style=flat-square)](http://badge.fury.io/rb/ups)
2
+ [![Dependency Status](https://img.shields.io/gemnasium/ptrippett/ups.svg?style=flat-square)](https://gemnasium.com/ptrippett/ups)
3
+ [![Build Status](https://img.shields.io/travis/ptrippett/ups.svg?style=flat-square)](https://travis-ci.org/ptrippett/ups)
4
+ [![Coverage Status](https://img.shields.io/codeclimate/coverage/github/ptrippett/ups.svg?style=flat-square)](https://codeclimate.com/github/ptrippett/ups/coverage)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/ptrippett/ups.svg?style=flat-square)](https://codeclimate.com/github/ptrippett/ups)
6
+
7
+ # UPS
8
+
9
+ UPS Gem for accessing the UPS API from Ruby. Using the gem you can:
10
+ - Return quotes from the UPS API
11
+ - Book shipments
12
+ - Return labels and tracking numbers for a shipment
13
+
14
+ This gem is currently used in production at [Veeqo](http://www.veeqo.com)
15
+
16
+ ## Installation
17
+
18
+ gem install ups-ruby
19
+
20
+ ...or add it to your project's [Gemfile](http://bundler.io/).
21
+
22
+ ## Documentation
23
+
24
+ Yard documentation can be found at [RubyDoc](http://www.rubydoc.info/github/ptrippett/ups).
25
+
26
+ ## Sample Usage
27
+
28
+ ### Return rates
29
+
30
+ ```ruby
31
+ require 'ups'
32
+ server = UPS::Connection.new(test_mode: true)
33
+ response = server.rates do |rate_builder|
34
+ rate_builder.add_access_request 'API_KEY', 'USERNAME', 'PASSWORD'
35
+ rate_builder.add_shipper company_name: 'Veeqo Limited',
36
+ phone_number: '01792 123456',
37
+ address_line_1: '11 Wind Street',
38
+ city: 'Swansea',
39
+ state: 'Wales',
40
+ postal_code: 'SA1 1DA',
41
+ country: 'GB',
42
+ shipper_number: 'ACCOUNT_NUMBER'
43
+ rate_builder.add_ship_from company_name: 'Veeqo Limited',
44
+ phone_number: '01792 123456',
45
+ address_line_1: '11 Wind Street',
46
+ city: 'Swansea',
47
+ state: 'Wales',
48
+ postal_code: 'SA1 1DA',
49
+ country: 'GB',
50
+ shipper_number: ENV['UPS_ACCOUNT_NUMBER']
51
+ rate_builder.add_ship_to company_name: 'Google Inc.',
52
+ phone_number: '0207 031 3000',
53
+ address_line_1: '1 St Giles High Street',
54
+ city: 'London',
55
+ state: 'England',
56
+ postal_code: 'WC2H 8AG',
57
+ country: 'GB'
58
+ rate_builder.add_package weight: '0.5',
59
+ unit: 'KGS'
60
+ end
61
+ ```
62
+
63
+ ```ruby
64
+ # Then use...
65
+ response.success?
66
+ response.graphic_image
67
+ response.tracking_number
68
+ ```
69
+
70
+ ## Running the tests
71
+
72
+ After installing dependencies with `bundle install`, you can run the unit tests using `rake`.
73
+
74
+ ## Contributers
75
+
76
+ Thanks to the following contributers to this project.
77
+
78
+ - [CJ](https://github.com/chirag7jain) - Method to generate labels in available other formats [EPL, ZPL], Constant for packaging type.
@@ -0,0 +1,17 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rake'
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.pattern = 'spec/**/*_spec.rb'
10
+ t.libs.push 'spec'
11
+ end
12
+
13
+ task default: :test
14
+
15
+ task :console do
16
+ exec 'irb -r ups -I ./lib'
17
+ end
@@ -0,0 +1,2 @@
1
+ # This file is automatically loaded by Bundler
2
+ require 'ups'
@@ -0,0 +1,33 @@
1
+ module UPS
2
+ autoload :SERVICES, 'ups/services'
3
+ autoload :PACKAGING, 'ups/packaging'
4
+
5
+ autoload :Version, 'ups/version'
6
+ autoload :Connection, 'ups/connection'
7
+ autoload :Exceptions, 'ups/exceptions'
8
+
9
+ autoload :Data, 'ups/data'
10
+ module Data
11
+ autoload :US_STATES, 'ups/data/us_states'
12
+ autoload :CANADIAN_STATES, 'ups/data/canadian_states'
13
+ autoload :IE_COUNTIES, 'ups/data/ie_counties'
14
+ autoload :IE_COUNTY_PREFIXES, 'ups/data/ie_county_prefixes'
15
+ end
16
+
17
+ module Parsers
18
+ autoload :ParserBase, 'ups/parsers/parser_base'
19
+ autoload :RatesParser, 'ups/parsers/rates_parser'
20
+ autoload :ShipConfirmParser, 'ups/parsers/ship_confirm_parser'
21
+ autoload :ShipAcceptParser, 'ups/parsers/ship_accept_parser'
22
+ end
23
+
24
+ module Builders
25
+ autoload :BuilderBase, 'ups/builders/builder_base'
26
+ autoload :RateBuilder, 'ups/builders/rate_builder'
27
+ autoload :AddressBuilder, 'ups/builders/address_builder'
28
+ autoload :ShipConfirmBuilder, 'ups/builders/ship_confirm_builder'
29
+ autoload :ShipAcceptBuilder, 'ups/builders/ship_accept_builder'
30
+ autoload :OrganisationBuilder, 'ups/builders/organisation_builder'
31
+ autoload :ShipperBuilder, 'ups/builders/shipper_builder'
32
+ end
33
+ end
@@ -0,0 +1,135 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {AddressBuilder} class builds UPS XML Address Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ # @attr [Hash] opts The Address Parts
10
+ class AddressBuilder < BuilderBase
11
+ include Ox
12
+
13
+ attr_accessor :opts
14
+
15
+ # Initializes a new {AddressBuilder} object
16
+ #
17
+ # @param [Hash] opts The Address Parts
18
+ # @option opts [String] :address_line_1 Address Line 1
19
+ # @option opts [String] :city City
20
+ # @option opts [String] :state State
21
+ # @option opts [String] :postal_code Zip or Postal Code
22
+ # @option opts [String] :country Country
23
+ # @raise [InvalidAttributeError] If the passed :state is nil or an
24
+ # empty string and the :country is IE
25
+ def initialize(opts = {})
26
+ self.opts = opts
27
+ validate
28
+ end
29
+
30
+ # Changes :state part of the address based on UPS requirements
31
+ #
32
+ # @raise [InvalidAttributeError] If the passed :state is nil or an
33
+ # empty string and the :country is IE
34
+ # @return [void]
35
+ def validate
36
+ opts[:state] = case opts[:country].downcase
37
+ when 'us'
38
+ normalize_us_state(opts[:state])
39
+ when 'ca'
40
+ normalize_ca_state(opts[:state])
41
+ when 'ie'
42
+ UPS::Data.ie_state_matcher(opts[:state])
43
+ else
44
+ ''
45
+ end
46
+ end
47
+
48
+ # Changes :state based on UPS requirements for US Addresses
49
+ #
50
+ # @param [String] state The US State to normalize
51
+ # @return [String]
52
+ def normalize_us_state(state)
53
+ if state.to_str.length > 2
54
+ UPS::Data::US_STATES[state] || state
55
+ else
56
+ state.upcase
57
+ end
58
+ end
59
+
60
+ # Changes :state based on UPS requirements for CA Addresses
61
+ #
62
+ # @param [String] state The CA State to normalize
63
+ # @return [String]
64
+ def normalize_ca_state(state)
65
+ if state.to_str.length > 2
66
+ UPS::Data::CANADIAN_STATES[state] || state
67
+ else
68
+ state.upcase
69
+ end
70
+ end
71
+
72
+ # Returns an XML representation of address_line_1
73
+ #
74
+ # @return [Ox::Element] XML representation of address_line_1 address part
75
+ def address_line_1
76
+ element_with_value('AddressLine1', opts[:address_line_1][0..34])
77
+ end
78
+
79
+ # Returns an XML representation of address_line_2
80
+ #
81
+ # @return [Ox::Element] XML representation of address_line_2 address part
82
+ def address_line_2
83
+ data = (opts.key? :address_line_2) ? opts[:address_line_2][0..34] : ''
84
+ element_with_value('AddressLine2', data)
85
+ end
86
+
87
+ # Returns an XML representation of city
88
+ #
89
+ # @return [Ox::Element] XML representation of the city address part
90
+ def city
91
+ element_with_value('City', opts[:city][0..29])
92
+ end
93
+
94
+ # Returns an XML representation of state
95
+ #
96
+ # @return [Ox::Element] XML representation of the state address part
97
+ def state
98
+ element_with_value('StateProvinceCode', opts[:state])
99
+ end
100
+
101
+ # Returns an XML representation of postal_code
102
+ #
103
+ # @return [Ox::Element] XML representation of the postal_code address part
104
+ def postal_code
105
+ element_with_value('PostalCode', opts[:postal_code][0..9])
106
+ end
107
+
108
+ # Returns an XML representation of country
109
+ #
110
+ # @return [Ox::Element] XML representation of the country address part
111
+ def country
112
+ element_with_value('CountryCode', opts[:country][0..1])
113
+ end
114
+
115
+ def email_address
116
+ element_with_value('EmailAddress', opts[:email_address][0..49])
117
+ end
118
+
119
+ # Returns an XML representation of a UPS Address
120
+ #
121
+ # @return [Ox::Element] XML representation of the current object
122
+ def to_xml
123
+ Element.new('Address').tap do |address|
124
+ address << address_line_1
125
+ address << address_line_2
126
+ address << email_address if opts[:email_address]
127
+ address << city
128
+ address << state
129
+ address << postal_code
130
+ address << country
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,216 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {BuilderBase} class builds UPS XML Address Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ # @abstract
10
+ # @attr [Ox::Document] document The XML Document being built
11
+ # @attr [Ox::Element] root The XML Root
12
+ # @attr [Ox::Element] shipment_root The XML Shipment Element
13
+ # @attr [Ox::Element] access_request The XML AccessRequest Element
14
+ # @attr [String] license_number The UPS API Key
15
+ # @attr [String] user_id The UPS Username
16
+ # @attr [String] password The UPS Password
17
+ class BuilderBase
18
+ include Ox
19
+ include Exceptions
20
+
21
+ attr_accessor :document,
22
+ :root,
23
+ :shipment_root,
24
+ :access_request,
25
+ :license_number,
26
+ :user_id,
27
+ :password
28
+
29
+ # Initializes a new {BuilderBase} object
30
+ #
31
+ # @param [String] root_name The Name of the XML Root
32
+ # @return [void]
33
+ def initialize(root_name)
34
+ initialize_xml_roots root_name
35
+
36
+ document << access_request
37
+ document << root
38
+
39
+ yield self if block_given?
40
+ end
41
+
42
+ # Initializes a new {BuilderBase} object
43
+ #
44
+ # @param [String] license_number The UPS API Key
45
+ # @param [String] user_id The UPS Username
46
+ # @param [String] password The UPS Password
47
+ # @return [void]
48
+ def add_access_request(license_number, user_id, password)
49
+ self.license_number = license_number
50
+ self.user_id = user_id
51
+ self.password = password
52
+
53
+ access_request << element_with_value('AccessLicenseNumber',
54
+ license_number)
55
+ access_request << element_with_value('UserId', user_id)
56
+ access_request << element_with_value('Password', password)
57
+ end
58
+
59
+ # Adds a Request section to the XML document being built
60
+ #
61
+ # @param [String] action The UPS API Action requested
62
+ # @param [String] option The UPS API Option
63
+ # @return [void]
64
+ def add_request(action, option)
65
+ root << Element.new('Request').tap do |request|
66
+ request << element_with_value('RequestAction', action)
67
+ request << element_with_value('RequestOption', option)
68
+ end
69
+ end
70
+
71
+ # Adds a Shipper section to the XML document being built
72
+ #
73
+ # @param [Hash] opts A Hash of data to build the requested section
74
+ # @option opts [String] :company_name Company Name
75
+ # @option opts [String] :phone_number Phone Number
76
+ # @option opts [String] :address_line_1 Address Line 1
77
+ # @option opts [String] :city City
78
+ # @option opts [String] :state State
79
+ # @option opts [String] :postal_code Zip or Postal Code
80
+ # @option opts [String] :country Country
81
+ # @option opts [String] :shipper_number UPS Account Number
82
+ # @return [void]
83
+ def add_shipper(opts = {})
84
+ shipment_root << ShipperBuilder.new(opts).to_xml
85
+ end
86
+
87
+ # Adds a ShipTo section to the XML document being built
88
+ #
89
+ # @param [Hash] opts A Hash of data to build the requested section
90
+ # @option opts [String] :company_name Company Name
91
+ # @option opts [String] :phone_number Phone Number
92
+ # @option opts [String] :address_line_1 Address Line 1
93
+ # @option opts [String] :city City
94
+ # @option opts [String] :state State
95
+ # @option opts [String] :postal_code Zip or Postal Code
96
+ # @option opts [String] :country Country
97
+ # @return [void]
98
+ def add_ship_to(opts = {})
99
+ shipment_root << OrganisationBuilder.new('ShipTo', opts).to_xml
100
+ end
101
+
102
+ # Adds a ShipFrom section to the XML document being built
103
+ #
104
+ # @param [Hash] opts A Hash of data to build the requested section
105
+ # @option opts [String] :company_name Company Name
106
+ # @option opts [String] :phone_number Phone Number
107
+ # @option opts [String] :address_line_1 Address Line 1
108
+ # @option opts [String] :city City
109
+ # @option opts [String] :state State
110
+ # @option opts [String] :postal_code Zip or Postal Code
111
+ # @option opts [String] :country Country
112
+ # @option opts [String] :shipper_number UPS Account Number
113
+ # @return [void]
114
+ def add_ship_from(opts = {})
115
+ shipment_root << OrganisationBuilder.new('ShipFrom', opts).to_xml
116
+ end
117
+
118
+ # Adds a Package section to the XML document being built
119
+ #
120
+ # @param [Hash] opts A Hash of data to build the requested section
121
+ # @return [void]
122
+ def add_package(opts = {})
123
+ shipment_root << Element.new('Package').tap do |org|
124
+ org << packaging_type
125
+ org << element_with_value('Description', 'Rate')
126
+ org << package_weight(opts[:weight], opts[:unit])
127
+ org << package_dimensions(opts[:dimensions]) if opts[:dimensions]
128
+ end
129
+ end
130
+
131
+ # Adds a PaymentInformation section to the XML document being built
132
+ #
133
+ # @param [String] ship_number The UPS Shipper Number
134
+ # @return [void]
135
+ def add_payment_information(ship_number)
136
+ shipment_root << Element.new('PaymentInformation').tap do |payment|
137
+ payment << Element.new('Prepaid').tap do |prepaid|
138
+ prepaid << Element.new('BillShipper').tap do |bill_shipper|
139
+ bill_shipper << element_with_value('AccountNumber', ship_number)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ # Adds a RateInformation/NegotiatedRatesIndicator section to the XML
146
+ # document being built
147
+ #
148
+ # @return [void]
149
+ def add_rate_information
150
+ shipment_root << Element.new('RateInformation').tap do |rate_info|
151
+ rate_info << element_with_value('NegotiatedRatesIndicator', '1')
152
+ end
153
+ end
154
+
155
+ # Returns a String representation of the XML document being built
156
+ #
157
+ # @return [String]
158
+ def to_xml
159
+ Ox.to_xml document
160
+ end
161
+
162
+ private
163
+
164
+ def initialize_xml_roots(root_name)
165
+ self.document = Document.new
166
+ self.root = Element.new(root_name)
167
+ self.shipment_root = Element.new('Shipment')
168
+ self.access_request = Element.new('AccessRequest')
169
+ root << shipment_root
170
+ end
171
+
172
+ def packaging_type
173
+ code_description 'PackagingType', '02', 'Customer Supplied'
174
+ end
175
+
176
+ def package_weight(weight, unit)
177
+ Element.new('PackageWeight').tap do |org|
178
+ org << unit_of_measurement(unit)
179
+ org << element_with_value('Weight', weight)
180
+ end
181
+ end
182
+
183
+ def package_dimensions(dimensions)
184
+ Element.new('Dimensions').tap do |org|
185
+ org << unit_of_measurement(dimensions[:unit])
186
+ org << element_with_value('Length', dimensions[:length].to_s[0..8])
187
+ org << element_with_value('Width', dimensions[:width].to_s[0..8])
188
+ org << element_with_value('Height', dimensions[:height].to_s[0..8])
189
+ end
190
+ end
191
+
192
+ def unit_of_measurement(unit)
193
+ Element.new('UnitOfMeasurement').tap do |org|
194
+ org << element_with_value('Code', unit.to_s)
195
+ end
196
+ end
197
+
198
+ def element_with_value(name, value)
199
+ fail InvalidAttributeError, name unless value.respond_to?(:to_str)
200
+ Element.new(name).tap do |request_action|
201
+ request_action << value.to_str
202
+ end
203
+ end
204
+
205
+ def code_description(name, code, description)
206
+ multi_valued(name, Code: code, Description: description)
207
+ end
208
+
209
+ def multi_valued(name, params)
210
+ Element.new(name).tap do |e|
211
+ params.each { |key, value| e << element_with_value(key, value) }
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end