braintree 1.0.0
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/LICENSE +22 -0
- data/README.rdoc +62 -0
- data/lib/braintree.rb +66 -0
- data/lib/braintree/address.rb +122 -0
- data/lib/braintree/base_module.rb +29 -0
- data/lib/braintree/configuration.rb +99 -0
- data/lib/braintree/credit_card.rb +231 -0
- data/lib/braintree/credit_card_verification.rb +31 -0
- data/lib/braintree/customer.rb +231 -0
- data/lib/braintree/digest.rb +20 -0
- data/lib/braintree/error_codes.rb +95 -0
- data/lib/braintree/error_result.rb +39 -0
- data/lib/braintree/errors.rb +29 -0
- data/lib/braintree/http.rb +105 -0
- data/lib/braintree/paged_collection.rb +55 -0
- data/lib/braintree/ssl_expiration_check.rb +28 -0
- data/lib/braintree/successful_result.rb +38 -0
- data/lib/braintree/test/credit_card_numbers.rb +50 -0
- data/lib/braintree/test/transaction_amounts.rb +10 -0
- data/lib/braintree/transaction.rb +360 -0
- data/lib/braintree/transaction/address_details.rb +15 -0
- data/lib/braintree/transaction/credit_card_details.rb +22 -0
- data/lib/braintree/transaction/customer_details.rb +13 -0
- data/lib/braintree/transparent_redirect.rb +110 -0
- data/lib/braintree/util.rb +94 -0
- data/lib/braintree/validation_error.rb +15 -0
- data/lib/braintree/validation_error_collection.rb +80 -0
- data/lib/braintree/version.rb +9 -0
- data/lib/braintree/xml.rb +12 -0
- data/lib/braintree/xml/generator.rb +80 -0
- data/lib/braintree/xml/libxml.rb +69 -0
- data/lib/braintree/xml/parser.rb +93 -0
- data/lib/ssl/securetrust_ca.crt +44 -0
- data/lib/ssl/valicert_ca.crt +18 -0
- data/spec/integration/braintree/address_spec.rb +352 -0
- data/spec/integration/braintree/credit_card_spec.rb +676 -0
- data/spec/integration/braintree/customer_spec.rb +664 -0
- data/spec/integration/braintree/http_spec.rb +201 -0
- data/spec/integration/braintree/test/transaction_amounts_spec.rb +29 -0
- data/spec/integration/braintree/transaction_spec.rb +900 -0
- data/spec/integration/spec_helper.rb +38 -0
- data/spec/script/httpsd.rb +27 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/unit/braintree/address_spec.rb +86 -0
- data/spec/unit/braintree/configuration_spec.rb +190 -0
- data/spec/unit/braintree/credit_card_spec.rb +137 -0
- data/spec/unit/braintree/credit_card_verification_spec.rb +17 -0
- data/spec/unit/braintree/customer_spec.rb +103 -0
- data/spec/unit/braintree/digest_spec.rb +28 -0
- data/spec/unit/braintree/error_result_spec.rb +42 -0
- data/spec/unit/braintree/errors_spec.rb +81 -0
- data/spec/unit/braintree/http_spec.rb +42 -0
- data/spec/unit/braintree/paged_collection_spec.rb +128 -0
- data/spec/unit/braintree/ssl_expiration_check_spec.rb +92 -0
- data/spec/unit/braintree/successful_result_spec.rb +27 -0
- data/spec/unit/braintree/transaction/credit_card_details_spec.rb +22 -0
- data/spec/unit/braintree/transaction_spec.rb +136 -0
- data/spec/unit/braintree/transparent_redirect_spec.rb +154 -0
- data/spec/unit/braintree/util_spec.rb +142 -0
- data/spec/unit/braintree/validation_error_collection_spec.rb +128 -0
- data/spec/unit/braintree/validation_error_spec.rb +19 -0
- data/spec/unit/braintree/xml/libxml_spec.rb +51 -0
- data/spec/unit/braintree/xml_spec.rb +122 -0
- data/spec/unit/spec_helper.rb +1 -0
- metadata +118 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module Braintree
|
2
|
+
class Transaction
|
3
|
+
class AddressDetails # :nodoc:
|
4
|
+
include BaseModule
|
5
|
+
|
6
|
+
attr_reader :first_name, :last_name, :company,
|
7
|
+
:street_address, :extended_address, :locality, :region,
|
8
|
+
:postal_code, :country_name
|
9
|
+
|
10
|
+
def initialize(attributes)
|
11
|
+
set_instance_variables_from_hash attributes unless attributes.nil?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Braintree
|
2
|
+
class Transaction
|
3
|
+
class CreditCardDetails # :nodoc:
|
4
|
+
include BaseModule
|
5
|
+
|
6
|
+
attr_reader :bin, :card_type, :expiration_month,
|
7
|
+
:expiration_year, :issuer_location, :last_4, :token
|
8
|
+
|
9
|
+
def initialize(attributes)
|
10
|
+
set_instance_variables_from_hash attributes unless attributes.nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
def expiration_date
|
14
|
+
"#{expiration_month}/#{expiration_year}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def masked_number
|
18
|
+
"#{bin}******#{last_4}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Braintree
|
2
|
+
class Transaction
|
3
|
+
class CustomerDetails # :nodoc:
|
4
|
+
include BaseModule
|
5
|
+
|
6
|
+
attr_reader :company, :email, :fax, :first_name, :id, :last_name, :phone, :website
|
7
|
+
|
8
|
+
def initialize(attributes)
|
9
|
+
set_instance_variables_from_hash attributes unless attributes.nil?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Braintree
|
2
|
+
# The TransparentRedirect module provides methods to build the tr_data param
|
3
|
+
# that must be submitted when using the transparent redirect API. For more information
|
4
|
+
# about transparent redirect, see (TODO).
|
5
|
+
#
|
6
|
+
# You must provide a redirect_url that the gateway will redirect the user to when the
|
7
|
+
# action is complete.
|
8
|
+
#
|
9
|
+
# tr_data = Braintree::TransparentRedirect.create_customer_data(
|
10
|
+
# :redirect_url => "http://example.com/redirect_back_to_merchant_site
|
11
|
+
# )
|
12
|
+
#
|
13
|
+
# In addition to the redirect_url, any data that needs to be protected from user tampering
|
14
|
+
# should be included in the tr_data. For example, to prevent the user from tampering with the transaction
|
15
|
+
# amount, include the amount in the tr_data.
|
16
|
+
#
|
17
|
+
# tr_data = Braintree::TransparentRedirect.transaction_data(
|
18
|
+
# :redirect_url => "http://example.com/complete_transaction",
|
19
|
+
# :transaction => {:amount => "100.00"}
|
20
|
+
# )
|
21
|
+
module TransparentRedirect
|
22
|
+
TransparentRedirectKeys = [:redirect_url] # :nodoc:
|
23
|
+
CreateCustomerSignature = TransparentRedirectKeys + [{:customer => Customer._create_signature}] # :nodoc:
|
24
|
+
UpdateCustomerSignature = TransparentRedirectKeys + [:customer_id, {:customer => Customer._update_signature}] # :nodoc:
|
25
|
+
TransactionSignature = TransparentRedirectKeys + [{:transaction => Transaction._create_signature}] # :nodoc:
|
26
|
+
CreateCreditCardSignature = TransparentRedirectKeys + [{:credit_card => CreditCard._create_signature}] # :nodoc:
|
27
|
+
UpdateCreditCardSignature = TransparentRedirectKeys + [:payment_method_token, {:credit_card => CreditCard._update_signature}] # :nodoc:
|
28
|
+
|
29
|
+
# Returns the tr_data string for creating a credit card.
|
30
|
+
def self.create_credit_card_data(params)
|
31
|
+
Util.verify_keys(CreateCreditCardSignature, params)
|
32
|
+
_data(params)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the tr_data string for creating a customer.
|
36
|
+
def self.create_customer_data(params)
|
37
|
+
Util.verify_keys(CreateCustomerSignature, params)
|
38
|
+
_data(params)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the tr_data string for creating a transaction.
|
42
|
+
def self.transaction_data(params)
|
43
|
+
Util.verify_keys(TransactionSignature, params)
|
44
|
+
transaction_type = params[:transaction] && params[:transaction][:type]
|
45
|
+
unless %w[sale credit].include?(transaction_type)
|
46
|
+
raise ArgumentError, "expected transaction[type] of sale or credit, was: #{transaction_type.inspect}"
|
47
|
+
end
|
48
|
+
_data(params)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the tr_data string for updating a credit card.
|
52
|
+
# The payment_method_token of the credit card to update is required.
|
53
|
+
#
|
54
|
+
# tr_data = Braintree::TransparentRedirect.update_credit_card_data(
|
55
|
+
# :redirect_url => "http://example.com/redirect_here",
|
56
|
+
# :payment_method_token => "token123"
|
57
|
+
# )
|
58
|
+
def self.update_credit_card_data(params)
|
59
|
+
Util.verify_keys(UpdateCreditCardSignature, params)
|
60
|
+
unless params[:payment_method_token]
|
61
|
+
raise ArgumentError, "expected params to contain :payment_method_token of payment method to update"
|
62
|
+
end
|
63
|
+
_data(params)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the tr_data string for updating a customer.
|
67
|
+
# The customer_id of the customer to update is required.
|
68
|
+
#
|
69
|
+
# tr_data = Braintree::TransparentRedirect.update_customer_data(
|
70
|
+
# :redirect_url => "http://example.com/redirect_here",
|
71
|
+
# :customer_id => "customer123"
|
72
|
+
# )
|
73
|
+
def self.update_customer_data(params)
|
74
|
+
Util.verify_keys(UpdateCustomerSignature, params)
|
75
|
+
unless params[:customer_id]
|
76
|
+
raise ArgumentError, "expected params to contain :customer_id of customer to update"
|
77
|
+
end
|
78
|
+
_data(params)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.parse_and_validate_query_string(query_string) # :nodoc:
|
82
|
+
params = Util.symbolize_keys(Util.parse_query_string(query_string))
|
83
|
+
query_string_without_hash = query_string[/(.*)&hash=.*/, 1]
|
84
|
+
if _hash(query_string_without_hash) == params[:hash]
|
85
|
+
if params[:http_status] == '200'
|
86
|
+
params
|
87
|
+
else
|
88
|
+
Util.raise_exception_for_status_code(params[:http_status])
|
89
|
+
end
|
90
|
+
else
|
91
|
+
raise ForgedQueryString
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self._data(params) # :nodoc:
|
96
|
+
raise ArgumentError, "expected params to contain :redirect_url" unless params[:redirect_url]
|
97
|
+
tr_data_segment = Util.hash_to_query_string(params.merge(
|
98
|
+
:api_version => Configuration::API_VERSION,
|
99
|
+
:time => Time.now.utc.strftime("%Y%m%d%H%M%S"),
|
100
|
+
:public_key => Configuration.public_key
|
101
|
+
))
|
102
|
+
tr_data_hash = _hash(tr_data_segment)
|
103
|
+
"#{tr_data_hash}|#{tr_data_segment}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def self._hash(string) # :nodoc:
|
107
|
+
::Braintree::Digest.hexdigest(string)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Braintree
|
2
|
+
module Util # :nodoc:
|
3
|
+
def self.extract_attribute_as_array(hash, attribute)
|
4
|
+
value = hash.delete(attribute)
|
5
|
+
value.is_a?(Array) ? value : [value]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.hash_to_query_string(hash, namespace = nil)
|
9
|
+
hash.collect do |key, value|
|
10
|
+
full_key = namespace ? "#{namespace}[#{key}]" : key
|
11
|
+
if value.is_a?(Hash)
|
12
|
+
hash_to_query_string(value, full_key)
|
13
|
+
else
|
14
|
+
url_encode(full_key) + "=" + url_encode(value)
|
15
|
+
end
|
16
|
+
end.sort * '&'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse_query_string(qs)
|
20
|
+
qs.split('&').inject({}) do |result, couplet|
|
21
|
+
pair = couplet.split('=')
|
22
|
+
result[CGI.unescape(pair[0]).to_sym] = CGI.unescape(pair[1])
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.url_encode(text)
|
28
|
+
CGI.escape text.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.symbolize_keys(hash)
|
32
|
+
hash.each do |key, value|
|
33
|
+
hash.delete(key)
|
34
|
+
hash[key.to_sym] = value
|
35
|
+
if value.is_a?(Hash)
|
36
|
+
symbolize_keys(value)
|
37
|
+
elsif value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) }
|
38
|
+
value.each { |v| symbolize_keys(v) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.raise_exception_for_status_code(status_code)
|
45
|
+
case status_code.to_i
|
46
|
+
when 401
|
47
|
+
raise AuthenticationError
|
48
|
+
when 403
|
49
|
+
raise AuthorizationError
|
50
|
+
when 404
|
51
|
+
raise NotFoundError
|
52
|
+
when 500
|
53
|
+
raise ServerError
|
54
|
+
when 503
|
55
|
+
raise DownForMaintenanceError
|
56
|
+
else
|
57
|
+
raise UnexpectedError, "Unexpected HTTP_RESPONSE #{status_code.to_i}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.verify_keys(valid_keys, hash)
|
62
|
+
invalid_keys = _flatten_hash_keys(hash) - _flatten_valid_keys(valid_keys)
|
63
|
+
if invalid_keys.any?
|
64
|
+
sorted = invalid_keys.sort_by { |k| k.to_s }.join(", ")
|
65
|
+
raise ArgumentError, "invalid keys: #{sorted}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self._flatten_valid_keys(valid_keys, namespace = nil)
|
70
|
+
valid_keys.inject([]) do |result, key|
|
71
|
+
if key.is_a?(Hash)
|
72
|
+
full_key = key.keys[0]
|
73
|
+
full_key = (namespace ? "#{namespace}[#{full_key}]" : full_key)
|
74
|
+
result += _flatten_valid_keys(key.values[0], full_key)
|
75
|
+
else
|
76
|
+
result << (namespace ? "#{namespace}[#{key}]" : key.to_s)
|
77
|
+
end
|
78
|
+
result
|
79
|
+
end.sort
|
80
|
+
end
|
81
|
+
|
82
|
+
def self._flatten_hash_keys(hash, namespace = nil)
|
83
|
+
hash.inject([]) do |result, (key, value)|
|
84
|
+
full_key = (namespace ? "#{namespace}[#{key}]" : key.to_s)
|
85
|
+
if value.is_a?(Hash)
|
86
|
+
result += _flatten_hash_keys(value, full_key)
|
87
|
+
else
|
88
|
+
result << full_key
|
89
|
+
end
|
90
|
+
result
|
91
|
+
end.sort
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Braintree
|
2
|
+
class ValidationError
|
3
|
+
include BaseModule
|
4
|
+
|
5
|
+
attr_reader :attribute, :code, :message
|
6
|
+
|
7
|
+
def initialize(attributes)
|
8
|
+
set_instance_variables_from_hash attributes
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect # :nodoc:
|
12
|
+
"#<#{self.class} (#{code}) #{message}>"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Braintree
|
2
|
+
# A collection of validation errors.
|
3
|
+
#
|
4
|
+
# result = Braintree::Customer.create(
|
5
|
+
# :email => "invalid",
|
6
|
+
# :credit_card => {
|
7
|
+
# :number => "invalidnumber",
|
8
|
+
# :billing_address => {
|
9
|
+
# :country_name => "invalid"
|
10
|
+
# }
|
11
|
+
# }
|
12
|
+
# )
|
13
|
+
# result.success?
|
14
|
+
# #=> false
|
15
|
+
# result.errors.for(:customer).on(:email)
|
16
|
+
# #=> [#<Braintree::ValidationError (81604) Email is an invalid format.>]
|
17
|
+
# result.errors.for(:customer).for(:credit_card).on(:number)
|
18
|
+
# #=> [#<Braintree::ValidationError (81715) Credit card number is invalid.>]
|
19
|
+
# result.errors.for(:customer).for(:credit_card).for(:billing_address).on(:country_name)
|
20
|
+
# #=> [#<Braintree::ValidationError (91803) Country name is not an accepted country.>]
|
21
|
+
class ValidationErrorCollection
|
22
|
+
include Enumerable
|
23
|
+
|
24
|
+
def initialize(data) # :nodoc:
|
25
|
+
@errors = data[:errors].map { |hash| Braintree::ValidationError.new(hash) }
|
26
|
+
@nested = {}
|
27
|
+
data.keys.each do |key|
|
28
|
+
next if key == :errors
|
29
|
+
@nested[key] = ValidationErrorCollection.new(data[key])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Accesses the error at the given index.
|
34
|
+
def [](index)
|
35
|
+
@errors[index]
|
36
|
+
end
|
37
|
+
|
38
|
+
def deep_size # :nodoc:
|
39
|
+
size + @nested.values.inject(0) { |count, error_collection| count + error_collection.deep_size }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Iterates over errors at the current level. Nested errors will not be yielded.
|
43
|
+
def each(&block)
|
44
|
+
@errors.each(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a ValidationErrorCollection of errors nested under the given nested_key.
|
48
|
+
# Returns nil if there are not any errors nested under the given key.
|
49
|
+
def for(nested_key)
|
50
|
+
@nested[nested_key]
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect # :nodoc:
|
54
|
+
"#<#{self.class} errors#{_inner_inspect}>"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns an array of ValidationError objects on the given attribute.
|
58
|
+
def on(attribute)
|
59
|
+
@errors.select { |error| error.attribute == attribute.to_s }
|
60
|
+
end
|
61
|
+
|
62
|
+
# The number of errors at this level. This does not include nested errors.
|
63
|
+
def size
|
64
|
+
@errors.size
|
65
|
+
end
|
66
|
+
|
67
|
+
def _inner_inspect(scope = []) # :nodoc:
|
68
|
+
all = []
|
69
|
+
scope_string = scope.join("/")
|
70
|
+
if @errors.any?
|
71
|
+
all << "#{scope_string}:[" + @errors.map { |e| "(#{e.code}) #{e.message}" }.join(", ") + "]"
|
72
|
+
end
|
73
|
+
@nested.each do |key, values|
|
74
|
+
all << values._inner_inspect(scope + [key])
|
75
|
+
end
|
76
|
+
all.join(", ")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Portions of this code were copied and modified from Ruby on Rails, released
|
2
|
+
# under the MIT license, copyright (c) 2005-2009 David Heinemeier Hansson
|
3
|
+
module Braintree
|
4
|
+
module Xml
|
5
|
+
module Generator # :nodoc:
|
6
|
+
XML_TYPE_NAMES = {
|
7
|
+
"Fixnum" => "integer",
|
8
|
+
"Bignum" => "integer",
|
9
|
+
"TrueClass" => "boolean",
|
10
|
+
"FalseClass" => "boolean",
|
11
|
+
"DateTime" => "datetime",
|
12
|
+
"Time" => "datetime",
|
13
|
+
}
|
14
|
+
XML_FORMATTING = {
|
15
|
+
"symbol" => Proc.new { |symbol| symbol.to_s },
|
16
|
+
"datetime" => Proc.new { |time| time.xmlschema },
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.hash_to_xml(hash)
|
20
|
+
root, contents = hash.keys[0], hash.values[0]
|
21
|
+
|
22
|
+
if contents.is_a?(String)
|
23
|
+
builder = Builder::XmlMarkup.new
|
24
|
+
builder.__send__(root) { |b| b.text! contents }
|
25
|
+
else
|
26
|
+
_convert_to_xml contents, :root => root
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self._convert_to_xml(hash_to_convert, options = {})
|
31
|
+
raise ArgumentError, "need root" unless options[:root]
|
32
|
+
options[:indent] ||= 2
|
33
|
+
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
34
|
+
options[:builder].instruct! unless options.delete(:skip_instruct)
|
35
|
+
root = options[:root].to_s.tr("_", "-")
|
36
|
+
|
37
|
+
options[:builder].__send__(:method_missing, root) do
|
38
|
+
hash_to_convert.each do |key, value|
|
39
|
+
case value
|
40
|
+
when ::Hash
|
41
|
+
_convert_to_xml(value, options.merge(:root => key, :skip_instruct => true))
|
42
|
+
when ::Array
|
43
|
+
_array_to_xml(value, options.merge(:root => key, :skip_instruct => true))
|
44
|
+
else
|
45
|
+
type_name = XML_TYPE_NAMES[value.class.name]
|
46
|
+
|
47
|
+
key = key.to_s.tr("_", "-")
|
48
|
+
|
49
|
+
attributes = ((value.nil? || type_name.nil?) ? {} : { :type => type_name })
|
50
|
+
if value.nil?
|
51
|
+
attributes[:nil] = true
|
52
|
+
end
|
53
|
+
|
54
|
+
options[:builder].tag!(key,
|
55
|
+
XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
|
56
|
+
attributes
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def self._array_to_xml(array, options = {})
|
65
|
+
raise "expected all elements to be hashes" unless array.all? { |e| e.is_a?(Hash) }
|
66
|
+
raise "expected options[:root]" unless options[:root]
|
67
|
+
raise "expected options[:builder]" unless options[:builder]
|
68
|
+
options[:indent] ||= 2
|
69
|
+
root = options.delete(:root).to_s.tr("_", "-")
|
70
|
+
if array.empty?
|
71
|
+
options[:builder].tag!(root, :type => "array")
|
72
|
+
else
|
73
|
+
options[:builder].tag!(root, :type => "array") do
|
74
|
+
array.each { |e| _convert_to_xml(e, options.merge(:root => "item", :skip_instruct => true)) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|