ups-ruby 0.8.3

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.
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