Skip to content

Commit c4e0b0a

Browse files
authored
Fix failing deserialization (#236)
- Fix bug not pulling key-value pair from hash during JSON deserialization - Add unit tests to confirm deserialization logic branches
1 parent 6655c04 commit c4e0b0a

File tree

3 files changed

+99
-69
lines changed

3 files changed

+99
-69
lines changed

lib/easypost/util.rb

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,26 +150,28 @@ def self.convert_to_easypost_object(response, api_key, parent = nil, name = nil)
150150
when Array
151151
response.map { |i| convert_to_easypost_object(i, api_key, parent) }
152152
when Hash
153-
if (cls_name = response[:object])
154-
# TODO: This line was never hit when debugging all unit tests, suggesting it's broken
153+
# Determine class based on the "object" key in the JSON response
154+
cls_name = response[:object] || response['object']
155+
if cls_name
156+
# Use the "object" key value to look up the class
155157
cls = BY_TYPE[cls_name]
156-
elsif response[:id]
157-
if response[:id].index('_').nil?
158-
cls = EasyPost::EasyPostObject
159-
elsif (cls_prefix = response[:id][0..response[:id].index('_')])
160-
cls = BY_PREFIX[cls_prefix[0..-2]]
161-
end
162-
elsif response['id']
163-
if response['id'].index('_').nil?
158+
else
159+
# Fallback to determining class based on the "id" prefix in the JSON response
160+
id = response[:id] || response['id']
161+
if id.nil? || id.index('_').nil?
162+
# ID not present or prefix not present (ID malformed)
164163
cls = EasyPost::EasyPostObject
165-
elsif (cls_prefix = response['id'][0..response['id'].index('_')])
166-
cls = BY_PREFIX[cls_prefix[0..-2]]
164+
else
165+
# Parse the prefix from the ID and use it to look up the class
166+
cls_prefix = id[0..id.index('_')][0..-2]
167+
cls = BY_PREFIX[cls_prefix]
167168
end
168169
end
169-
170+
# Fallback to using the generic class if other determination methods fail (or class lookup produced no results)
170171
cls ||= EasyPost::EasyPostObject
171172
cls.construct_from(response, api_key, parent, name)
172173
else
174+
# response is neither a Hash nor Array (used mostly when dealing with final values like strings, booleans, etc.)
173175
response
174176
end
175177
end

spec/billing_spec.rb

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -52,62 +52,7 @@
5252

5353
response = described_class.retrieve_payment_methods
5454

55-
expect(response).to be_an_instance_of(EasyPost::EasyPostObject)
56-
end
57-
58-
it 'deserializes the payment methods by ID prefix' do
59-
allow(EasyPost).to receive(:make_request).with(
60-
:get, '/v2/payment_methods', nil,
61-
).and_return(
62-
{
63-
'id' => '123',
64-
'primary_payment_method' => { 'id' => 'bank_123' },
65-
'secondary_payment_method' => { 'id' => 'card_123' },
66-
},
67-
)
68-
69-
response = described_class.retrieve_payment_methods
70-
71-
expect(response).to be_an_instance_of(EasyPost::EasyPostObject)
72-
expect(response[:primary_payment_method]).to be_an_instance_of(EasyPost::PaymentMethod)
73-
expect(response[:secondary_payment_method]).to be_an_instance_of(EasyPost::PaymentMethod)
74-
end
75-
76-
# TODO: Reactivate when deserialization by "object" is fixed
77-
skip it 'deserializes the payment methods by object type' do
78-
allow(EasyPost).to receive(:make_request).with(
79-
:get, '/v2/payment_methods', nil,
80-
).and_return(
81-
{
82-
'id' => '123',
83-
'primary_payment_method' => { 'object' => 'BankAccount' },
84-
'secondary_payment_method' => { 'object' => 'CreditCard' },
85-
},
86-
)
87-
88-
response = described_class.retrieve_payment_methods
89-
90-
expect(response).to be_an_instance_of(EasyPost::EasyPostObject)
91-
expect(response[:primary_payment_method]).to be_an_instance_of(EasyPost::PaymentMethod)
92-
expect(response[:secondary_payment_method]).to be_an_instance_of(EasyPost::PaymentMethod)
93-
end
94-
95-
it 'does not deserialize the payment methods if prefix and object type missing' do
96-
allow(EasyPost).to receive(:make_request).with(
97-
:get, '/v2/payment_methods', nil,
98-
).and_return(
99-
{
100-
'id' => '123',
101-
'primary_payment_method' => { 'random_key' => 'random_value' },
102-
'secondary_payment_method' => { 'random_key' => 'random_value' },
103-
},
104-
)
105-
106-
response = described_class.retrieve_payment_methods
107-
108-
expect(response).to be_an_instance_of(EasyPost::EasyPostObject)
109-
expect(response[:primary_payment_method]).to be_an_instance_of(EasyPost::EasyPostObject)
110-
expect(response[:secondary_payment_method]).to be_an_instance_of(EasyPost::EasyPostObject)
55+
expect(response).to be_an_instance_of(EasyPost::EasyPostObject) # TODO: There's not "PaymentMethodSummary"-equivalent class yet
11156
end
11257
end
11358

spec/util_spec.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,87 @@
4545
.to eq({ 'parent_key[nested_key]' => '123' })
4646
end
4747
end
48+
49+
describe '.convert_to_easypost_object' do
50+
it 'converts a hash to a specific class by matching ID prefix' do
51+
data = {
52+
id: 'adr_123',
53+
}.to_hash
54+
object = described_class.convert_to_easypost_object(data, nil)
55+
56+
expect(object).to be_a(EasyPost::Address)
57+
end
58+
59+
it 'converts a hash to a specific class by matching object type' do
60+
data = {
61+
object: 'Address',
62+
}
63+
object = described_class.convert_to_easypost_object(data, nil)
64+
65+
expect(object).to be_a(EasyPost::Address)
66+
end
67+
68+
it 'converts a hash to a generic EasyPostObject if no matching ID or object type' do
69+
data = {
70+
id: 'foo_123',
71+
object: 'Nothing',
72+
}
73+
object = described_class.convert_to_easypost_object(data, nil)
74+
75+
expect(object).to be_a(EasyPost::EasyPostObject)
76+
end
77+
78+
it 'converts a hash to a generic EasyPostObject if missing ID and object type' do
79+
data = {
80+
random_key: 'random_value',
81+
}
82+
object = described_class.convert_to_easypost_object(data, nil)
83+
84+
expect(object).to be_a(EasyPost::EasyPostObject)
85+
end
86+
87+
it 'converts sub-hashes to EasyPost object' do
88+
data = {
89+
'id' => '123',
90+
'primary_payment_method' => {
91+
'id' => 'bank_123',
92+
},
93+
'secondary_payment_method' => {
94+
'id' => 'card_123',
95+
},
96+
}
97+
object = described_class.convert_to_easypost_object(data, nil)
98+
99+
# No matching ID prefix or "object" key means the outer object will just be deserialized to an EasyPostObject
100+
expect(object).to be_a(EasyPost::EasyPostObject)
101+
102+
# The sub-hashes have proper prefixes, so they will be converted to their respective objects
103+
expect(object[:primary_payment_method]).to be_an_instance_of(EasyPost::PaymentMethod)
104+
expect(object[:secondary_payment_method]).to be_an_instance_of(EasyPost::PaymentMethod)
105+
end
106+
107+
it 'converts an array of hashes to an array of EasyPostObjects' do
108+
data = [
109+
{
110+
'id' => 'foo_123',
111+
},
112+
]
113+
object = described_class.convert_to_easypost_object(data, nil)
114+
115+
expect(object).to be_an(Array)
116+
expect(object.first).to be_a(EasyPost::EasyPostObject)
117+
end
118+
119+
it 'converts an array of hashes to an array of specific classes if matching ID prefix or object type' do
120+
data = [
121+
{
122+
'id' => 'adr_123',
123+
},
124+
]
125+
object = described_class.convert_to_easypost_object(data, nil)
126+
127+
expect(object).to be_an(Array)
128+
expect(object.first).to be_a(EasyPost::Address)
129+
end
130+
end
48131
end

0 commit comments

Comments
 (0)