braintree 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|