Skip to content

Commit 9a67921

Browse files
author
Colin Rood
authored
Merge pull request #45 from robotdana/dont-override-hashes
Remove modifications to core ruby objects
2 parents 231f79a + eb4ccd0 commit 9a67921

File tree

11 files changed

+243
-35
lines changed

11 files changed

+243
-35
lines changed

lib/adyen-ruby-api-library.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative "adyen/services/recurring"
99
require_relative "adyen/services/marketpay"
1010
require_relative "adyen/services/service"
11+
require_relative "adyen/hash_with_accessors"
1112
require_relative "adyen/utils/hmac_validator"
1213

1314
# add snake case to camel case converter to String

lib/adyen/client.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
require "json"
33
require_relative "./errors"
44
require_relative "./result"
5-
require_relative "./util"
65

76
module Adyen
87
class Client

lib/adyen/hash_with_accessors.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This utility method inherits from Hash, but allows keys to be read
2+
# and updated with dot notation. Usage is entirely optional (i.e., hash values
3+
# can still be accessed via symbol and string keys).
4+
#
5+
# Based on: https://gist.github.com/winfred/2185384#file-ruby-dot-hash-access-rb
6+
module Adyen
7+
class HashWithAccessors < Hash
8+
def method_missing(method, *args)
9+
string_key = method.to_s.sub(/=\z/, '')
10+
sym_key = string_key.to_sym
11+
12+
key = if has_key?(string_key)
13+
string_key
14+
elsif has_key?(sym_key)
15+
sym_key
16+
end
17+
18+
return super unless key
19+
20+
assignment = sym_key != method
21+
22+
if assignment
23+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" unless args.size == 1
24+
25+
self[key] = args.first
26+
else
27+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.size == 0
28+
29+
self[key]
30+
end
31+
end
32+
33+
def respond_to_missing?(method, include_private = false)
34+
string_key = method.to_s.sub(/=\z/, '')
35+
has_key?(string_key) || has_key?(string_key.to_sym) || super
36+
end
37+
end
38+
end

lib/adyen/result.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ class AdyenResult
55
attr_reader :response, :header, :status
66

77
def initialize(response, header, status)
8-
@response = JSON.parse(response)
8+
@response = JSON.parse(response, object_class: HashWithAccessors)
99

1010
# `header` in Faraday response is not a JSON string, but rather a
1111
# Faraday `Headers` object. Convert first before parsing
12-
@header = JSON.parse(header.to_json)
12+
@header = JSON.parse(header.to_json, object_class: HashWithAccessors)
1313
@status = status
1414
end
1515
end

lib/adyen/services/service.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ module Adyen
22
class Service
33
attr_accessor :service, :version
44

5+
# add snake case to camel case converter to String
6+
# to convert rubinic method names to Adyen API methods
7+
#
8+
# i.e. snake_case -> snakeCase
9+
# note that the first letter is not capitalized as normal
10+
def self.action_for_method_name(method_name)
11+
method_name.to_s.gsub(/_./) { |x| x[1].upcase }
12+
end
13+
514
def initialize(client, version, service, method_names)
615
@client = client
716
@version = version
@@ -10,7 +19,7 @@ def initialize(client, version, service, method_names)
1019
# dynamically create API methods
1120
method_names.each do |method_name|
1221
define_singleton_method method_name do |request, headers = {}|
13-
action = method_name.to_s.to_camel_case
22+
action = self.class.action_for_method_name(method_name)
1423
@client.call_adyen_api(@service, action, request, headers, @version)
1524
end
1625
end

lib/adyen/util.rb

Lines changed: 0 additions & 21 deletions
This file was deleted.

spec/checkout_spec.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,14 @@
5757
to eq(200)
5858
expect(response_hash).
5959
to eq(JSON.parse(response_body))
60-
expect(response_hash.class).
61-
to be Hash
60+
expect(response_hash).
61+
to be_a Adyen::HashWithAccessors
62+
expect(response_hash).
63+
to be_a_kind_of Hash
6264
expect(response_hash["resultCode"]).
6365
to eq("RedirectShopper")
66+
expect(response_hash.resultCode).
67+
to eq("RedirectShopper")
6468
end
6569

6670
# must be created manually due to payments/result format
@@ -89,10 +93,14 @@
8993
to eq(200)
9094
expect(response_hash).
9195
to eq(JSON.parse(response_body))
92-
expect(response_hash.class).
93-
to be Hash
96+
expect(response_hash).
97+
to be_a Adyen::HashWithAccessors
98+
expect(response_hash).
99+
to be_a_kind_of Hash
94100
expect(response_hash["resultCode"]).
95101
to eq("Authorised")
102+
expect(response_hash.resultCode).
103+
to eq("Authorised")
96104
end
97105

98106
# create client for automated tests

spec/checkout_utility_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
# must be created manually because every field in the response is an array
2020
it "makes an origin_keys call" do
2121
parsed_body = create_test(@shared_values[:client], @shared_values[:service], "origin_keys", @shared_values[:client].checkout_utility)
22-
expect(parsed_body["originKeys"].class).
23-
to be Hash
22+
expect(parsed_body["originKeys"]).
23+
to be_a Adyen::HashWithAccessors
24+
expect(parsed_body["originKeys"]).
25+
to be_a_kind_of Hash
2426
end
2527
end
2628

spec/hash_with_accessors_spec.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe Adyen::HashWithAccessors do
4+
shared_examples :hash_with_accessors do
5+
subject do
6+
h = described_class.new
7+
h[key] = 1
8+
h
9+
end
10+
11+
it 'returns values of a hashes' do
12+
expect(subject.arbitrary_accessor).to be 1
13+
end
14+
15+
it 'can assign existing values' do
16+
subject.arbitrary_accessor = 2
17+
expect(subject.arbitrary_accessor).to be 2
18+
expect(subject[key]).to be 2
19+
end
20+
21+
it 'complains if there are arguments for the accessor' do
22+
expect { subject.arbitrary_accessor(1) }.to raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 0)')
23+
expect { subject.arbitrary_accessor(1, 2) }.to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0)')
24+
end
25+
26+
it 'complains if there are arguments for the accessor =' do
27+
# using send because i'm not sure how to do this wrong with normal ruby setter calling.
28+
# just here for completeness
29+
expect { subject.send(:arbitrary_accessor=) }.to raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1)')
30+
expect { subject.send(:arbitrary_accessor=, 1, 2) }.to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)')
31+
end
32+
33+
it 'responds to the accessor' do
34+
expect(subject).to respond_to(:arbitrary_accessor)
35+
expect(subject).to respond_to(:arbitrary_accessor=)
36+
expect(subject).to_not respond_to(:another_accessor)
37+
expect(subject).to_not respond_to(:another_accessor=)
38+
end
39+
40+
it "raises when the key doesn't exist" do
41+
expect { subject.another_accessor }.to raise_error(NoMethodError)
42+
expect { subject.another_accessor = 1 }.to raise_error(NoMethodError)
43+
end
44+
end
45+
46+
context 'with a string key' do
47+
let(:key) { 'arbitrary_accessor' }
48+
49+
it_behaves_like :hash_with_accessors
50+
end
51+
52+
context 'with a symbol key' do
53+
let(:key) { :arbitrary_accessor }
54+
55+
it_behaves_like :hash_with_accessors
56+
end
57+
58+
context 'with a conflicting key' do
59+
subject do
60+
h = described_class.new
61+
h['keys'] = 'not the keys'
62+
h
63+
end
64+
65+
it "does original thing if there'd be a conflict" do
66+
expect(subject.keys).to eq ['keys'] # the default behaviour
67+
expect(subject['keys']).to eq 'not the keys'
68+
end
69+
70+
it 'still does the writer thing even if the reader is defined' do
71+
subject.keys = 'written keys'
72+
expect(subject['keys']).to eq 'written keys'
73+
expect(subject.keys).to eq ['keys']
74+
end
75+
end
76+
77+
context 'with some other method missing defined' do
78+
# this test setup is kind of janky,
79+
# but we want to confirm super is set up correctly
80+
# We could do a lot more house-keeping if we weren't sure Hash doesn't
81+
# define its own method_missing and respond_to_missing? by default,
82+
# and there was any particular reason to clean up properly and remove our
83+
# called_super method from Hash.
84+
85+
before(:all) do
86+
class Hash
87+
def called_super(*args)
88+
end
89+
90+
def method_missing(*args)
91+
called_super(:method_missing, *args)
92+
super
93+
end
94+
95+
def respond_to_missing?(*args)
96+
called_super(:respond_to_missing?, *args)
97+
super
98+
end
99+
end
100+
end
101+
102+
subject do
103+
h = described_class.new
104+
h[:my_accessor] = 1
105+
h
106+
end
107+
108+
it 'can fall back to another respond_to_missing?' do
109+
expect(subject).to_not receive(:called_super).with(:respond_to_missing?, :my_accessor, false)
110+
expect(subject).to respond_to(:my_accessor)
111+
expect(subject).to receive(:called_super).with(:respond_to_missing?, :literally_anything, false)
112+
expect(subject.respond_to?(:literally_anything)).to be false
113+
end
114+
115+
it 'can fall back to another method_missing' do
116+
expect(subject).to_not receive(:called_super).with(:method_missing, :my_accessor)
117+
expect(subject.my_accessor).to be 1
118+
expect(subject).to receive(:called_super).with(:method_missing, :something_else)
119+
expect { subject.something_else }.to raise_error(NoMethodError)
120+
end
121+
122+
end
123+
124+
it "doesn't modify all hashes" do
125+
expect { {a: 1}.a }.to raise_error(NoMethodError)
126+
end
127+
end

spec/service_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe Adyen::Service do
4+
describe '.action_for_method_name' do
5+
it 'handles all methods that exist currently' do
6+
expect(described_class.action_for_method_name(:adjust_authorisation)).to eq 'adjustAuthorisation'
7+
expect(described_class.action_for_method_name(:authorise)).to eq 'authorise'
8+
expect(described_class.action_for_method_name(:authorise3d)).to eq 'authorise3d'
9+
expect(described_class.action_for_method_name(:authorise3ds2)).to eq 'authorise3ds2'
10+
expect(described_class.action_for_method_name(:cancel)).to eq 'cancel'
11+
expect(described_class.action_for_method_name(:cancel_or_refund)).to eq 'cancelOrRefund'
12+
expect(described_class.action_for_method_name(:capture)).to eq 'capture'
13+
expect(described_class.action_for_method_name(:close_account)).to eq 'closeAccount'
14+
expect(described_class.action_for_method_name(:close_account_holder)).to eq 'closeAccountHolder'
15+
expect(described_class.action_for_method_name(:confirm_third_party)).to eq 'confirmThirdParty'
16+
expect(described_class.action_for_method_name(:create_account)).to eq 'createAccount'
17+
expect(described_class.action_for_method_name(:create_account_holder)).to eq 'createAccountHolder'
18+
expect(described_class.action_for_method_name(:decline_third_party)).to eq 'declineThirdParty'
19+
expect(described_class.action_for_method_name(:delete_bank_accounts)).to eq 'deleteBankAccounts'
20+
expect(described_class.action_for_method_name(:delete_shareholders)).to eq 'deleteShareholders'
21+
expect(described_class.action_for_method_name(:disable)).to eq 'disable'
22+
expect(described_class.action_for_method_name(:get_account_holder)).to eq 'getAccountHolder'
23+
expect(described_class.action_for_method_name(:get_tier_configuration)).to eq 'getTierConfiguration'
24+
expect(described_class.action_for_method_name(:get_uploaded_documents)).to eq 'getUploadedDocuments'
25+
expect(described_class.action_for_method_name(:list_recurring_details)).to eq 'listRecurringDetails'
26+
expect(described_class.action_for_method_name(:origin_keys)).to eq 'originKeys'
27+
expect(described_class.action_for_method_name(:payment_methods)).to eq 'paymentMethods'
28+
expect(described_class.action_for_method_name(:payment_session)).to eq 'paymentSession'
29+
expect(described_class.action_for_method_name(:refund)).to eq 'refund'
30+
expect(described_class.action_for_method_name(:store_detail)).to eq 'storeDetail'
31+
expect(described_class.action_for_method_name(:store_detail_and_submit_third_party)).to eq 'storeDetailAndSubmitThirdParty'
32+
expect(described_class.action_for_method_name(:store_token)).to eq 'storeToken'
33+
expect(described_class.action_for_method_name(:submit_third_party)).to eq 'submitThirdParty'
34+
expect(described_class.action_for_method_name(:suspend_account_holder)).to eq 'suspendAccountHolder'
35+
expect(described_class.action_for_method_name(:un_suspend_account_holder)).to eq 'unSuspendAccountHolder'
36+
expect(described_class.action_for_method_name(:update_account)).to eq 'updateAccount'
37+
expect(described_class.action_for_method_name(:update_account_holder)).to eq 'updateAccountHolder'
38+
expect(described_class.action_for_method_name(:update_account_holder_state)).to eq 'updateAccountHolderState'
39+
expect(described_class.action_for_method_name(:upload_document)).to eq 'uploadDocument'
40+
end
41+
end
42+
end

0 commit comments

Comments
 (0)