adyen-ruby-api-library 4.0.0 → 4.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 57203cec793176b8f389062f66cf734dcc43e183
4
- data.tar.gz: 6680ea0897e40cad89569ed46c841dcf45b900a4
3
+ metadata.gz: 50ac4158ef06b200711ce1b69d7b6c12cad0df4b
4
+ data.tar.gz: 338fbfd9fed83cd6a3569dd1f706fdf439e259c1
5
5
  SHA512:
6
- metadata.gz: 16075771ada5ca4afb71c798e7823fcd1c2b162a6d0d21919b0a3ea8db301ec5379525c726ebc3d71b0b39d87ada719b539b7457fe927a5aa6ef4754b7dca3be
7
- data.tar.gz: 968d9f0cb9a426ce89d8bc17a00cdf50766f1f48dacbb58a1966e3d29ae5fe99a3e784e6d0f40fa86f4f0ef5bfad27576124ad1504746e121af9998abd06f8a1
6
+ metadata.gz: c71dfcff2e34dd0244abdd56da08a0ab430b608de4fc095a472711a69269c416ffcee0b6021ed0db56bff3b8a5d1fc25e1fe13bac4f0c844367a61498ad6de1b
7
+ data.tar.gz: 9ea5ee2a69f6de0523ba4fff25975202583bdf3d1218a8cda8d0abe978b57e30158cccd4bb94921c928531ce40442df067089cdb74a066567a4f2cd1f526bcf5
@@ -8,6 +8,8 @@ require_relative "adyen/services/payouts"
8
8
  require_relative "adyen/services/recurring"
9
9
  require_relative "adyen/services/marketpay"
10
10
  require_relative "adyen/services/service"
11
+ require_relative "adyen/hash_with_accessors"
12
+ require_relative "adyen/utils/hmac_validator"
11
13
 
12
14
  # add snake case to camel case converter to String
13
15
  # to convert rubinic method names to Adyen API methods
data/lib/adyen/client.rb CHANGED
@@ -2,7 +2,6 @@ require "faraday"
2
2
  require "json"
3
3
  require_relative "./errors"
4
4
  require_relative "./result"
5
- require_relative "./util"
6
5
 
7
6
  module Adyen
8
7
  class Client
@@ -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
data/lib/adyen/result.rb CHANGED
@@ -5,11 +5,11 @@ module Adyen
5
5
  attr_reader :response, :header, :status
6
6
 
7
7
  def initialize(response, header, status)
8
- @response = JSON.parse(response)
8
+ @response = JSON.parse(response, object_class: HashWithAccessors)
9
9
 
10
10
  # `header` in Faraday response is not a JSON string, but rather a
11
11
  # Faraday `Headers` object. Convert first before parsing
12
- @header = JSON.parse(header.to_json)
12
+ @header = JSON.parse(header.to_json, object_class: HashWithAccessors)
13
13
  @status = status
14
14
  end
15
15
  end
@@ -2,6 +2,15 @@ module Adyen
2
2
  class Service
3
3
  attr_accessor :service, :version
4
4
 
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
+
5
14
  def initialize(client, version, service, method_names)
6
15
  @client = client
7
16
  @version = version
@@ -10,7 +19,7 @@ module Adyen
10
19
  # dynamically create API methods
11
20
  method_names.each do |method_name|
12
21
  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)
14
23
  @client.call_adyen_api(@service, action, request, headers, @version)
15
24
  end
16
25
  end
@@ -0,0 +1,48 @@
1
+ module Adyen
2
+ module Utils
3
+ class HmacValidator
4
+ HMAC_ALGORITHM = 'sha256'.freeze
5
+ DATA_SEPARATOR = ':'.freeze
6
+ NOTIFICATION_VALIDATION_KEYS = %w[
7
+ pspReference originalReference merchantAccountCode merchantReference
8
+ amount.value amount.currency eventCode success
9
+ ].freeze
10
+
11
+ def valid_notification_hmac?(notification_request_item, hmac_key)
12
+ expected_sign = calculate_notification_hmac(notification_request_item, hmac_key)
13
+ merchant_sign = fetch(notification_request_item, 'additionalData.hmacSignature')
14
+
15
+ expected_sign == merchant_sign
16
+ end
17
+
18
+ def calculate_notification_hmac(notification_request_item, hmac_key)
19
+ data = data_to_sign(notification_request_item)
20
+
21
+ Base64.strict_encode64(OpenSSL::HMAC.digest(HMAC_ALGORITHM, [hmac_key].pack('H*'), data))
22
+ end
23
+
24
+ def data_to_sign(notification_request_item)
25
+ NOTIFICATION_VALIDATION_KEYS.map { |key| fetch(notification_request_item, key).to_s }
26
+ .map { |value| value.gsub('\\', '\\\\').gsub(':', '\\:') }
27
+ .join(DATA_SEPARATOR)
28
+ end
29
+
30
+ private
31
+
32
+ def fetch(hash, keys)
33
+ value = hash
34
+
35
+ keys.to_s.split('.').each do |key|
36
+ value = if key.to_i.to_s == key
37
+ value[key.to_i]
38
+ else
39
+ value[key].nil? ? value[key.to_sym] : value[key]
40
+ end
41
+ break if value.nil?
42
+ end
43
+
44
+ value
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/adyen/version.rb CHANGED
@@ -1,4 +1,4 @@
1
- module Adyen
1
+ module Adyen
2
2
  NAME = "adyen-ruby-api-library"
3
- VERSION = "4.0.0".freeze
3
+ VERSION = "4.0.1".freeze
4
4
  end
@@ -57,10 +57,14 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
57
57
  to eq(200)
58
58
  expect(response_hash).
59
59
  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
62
64
  expect(response_hash["resultCode"]).
63
65
  to eq("RedirectShopper")
66
+ expect(response_hash.resultCode).
67
+ to eq("RedirectShopper")
64
68
  end
65
69
 
66
70
  # must be created manually due to payments/result format
@@ -89,10 +93,14 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
89
93
  to eq(200)
90
94
  expect(response_hash).
91
95
  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
94
100
  expect(response_hash["resultCode"]).
95
101
  to eq("Authorised")
102
+ expect(response_hash.resultCode).
103
+ to eq("Authorised")
96
104
  end
97
105
 
98
106
  # create client for automated tests
@@ -19,8 +19,10 @@ RSpec.describe Adyen::CheckoutUtility, service: "checkout utility" do
19
19
  # must be created manually because every field in the response is an array
20
20
  it "makes an origin_keys call" do
21
21
  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
24
26
  end
25
27
  end
26
28
 
@@ -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
@@ -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
data/spec/spec_helper.rb CHANGED
@@ -39,7 +39,8 @@ def create_test(client, service, method_name, parent_object)
39
39
  end
40
40
 
41
41
  # stub request
42
- url = client.service_url(service, method_name.to_camel_case, parent_object.version)
42
+ action = Adyen::Service.action_for_method_name(method_name)
43
+ url = client.service_url(service, action, parent_object.version)
43
44
  WebMock.stub_request(:post, url).
44
45
  with(
45
46
  body: request_body,
@@ -50,7 +51,7 @@ def create_test(client, service, method_name, parent_object)
50
51
  )
51
52
  result = parent_object.public_send(method_name, request_body)
52
53
 
53
- # result.response is already a Ruby hash (rather than an unparsed JSON string)
54
+ # result.response is already a Ruby object (Adyen::HashWithAccessors) (rather than an unparsed JSON string)
54
55
  response_hash = result.response
55
56
 
56
57
  # boilerplate error checks
@@ -58,8 +59,10 @@ def create_test(client, service, method_name, parent_object)
58
59
  to eq(200)
59
60
  expect(response_hash).
60
61
  to eq(JSON.parse(response_body))
61
- expect(response_hash.class).
62
- to be Hash
62
+ expect(response_hash).
63
+ to be_a Adyen::HashWithAccessors
64
+ expect(response_hash).
65
+ to be_a_kind_of Hash
63
66
 
64
67
  response_hash
65
68
  end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Adyen::Utils::HmacValidator do
4
+ let(:validator) { described_class.new }
5
+ let(:key) { '44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056' }
6
+ let(:expected_sign) { 'coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=' }
7
+ let(:notification_request_item) do
8
+ {
9
+ additionalData: {
10
+ hmacSignature: expected_sign
11
+ },
12
+ amount: {
13
+ value: 1130,
14
+ currency: 'EUR'
15
+ },
16
+ pspReference: '7914073381342284',
17
+ eventCode: 'AUTHORISATION',
18
+ merchantAccountCode: 'TestMerchant',
19
+ merchantReference: 'TestPayment-1407325143704',
20
+ paymentMethod: 'visa',
21
+ success: 'true'
22
+ }
23
+ end
24
+
25
+ describe 'HMAC Validator' do
26
+ it 'should get correct data' do
27
+ data_to_sign = validator.data_to_sign(notification_request_item)
28
+ expect(data_to_sign).to eq '7914073381342284::TestMerchant:TestPayment-1407325143704:1130:EUR:AUTHORISATION:true'
29
+ end
30
+
31
+ it 'should get correct data with escaped characters' do
32
+ notification_request_item['merchantAccountCode'] = 'Test:\\Merchant'
33
+ data_to_sign = validator.data_to_sign(notification_request_item)
34
+ expect(data_to_sign).to eq '7914073381342284::Test\\:\\Merchant:TestPayment-1407325143704:1130:EUR:AUTHORISATION:true'
35
+ end
36
+
37
+ it 'should encrypt properly' do
38
+ encrypted = validator.calculate_notification_hmac(notification_request_item, key)
39
+ expect(encrypted).to eq expected_sign
40
+ end
41
+
42
+ it 'should have a valid hmac' do
43
+ expect(validator.valid_notification_hmac?(notification_request_item, key)).to be true
44
+ end
45
+
46
+ it 'should have an invalid hmac' do
47
+ notification_request_item['additionalData'] = { 'hmacSignature' => 'invalidHMACsign' }
48
+
49
+ expect(validator.valid_notification_hmac?(notification_request_item, key)).to be false
50
+ end
51
+ end
52
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adyen-ruby-api-library
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adyen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-12 00:00:00.000000000 Z
11
+ date: 2019-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -103,6 +103,7 @@ files:
103
103
  - lib/adyen-ruby-api-library.rb
104
104
  - lib/adyen/client.rb
105
105
  - lib/adyen/errors.rb
106
+ - lib/adyen/hash_with_accessors.rb
106
107
  - lib/adyen/result.rb
107
108
  - lib/adyen/services/checkout.rb
108
109
  - lib/adyen/services/checkout_utility.rb
@@ -111,7 +112,7 @@ files:
111
112
  - lib/adyen/services/payouts.rb
112
113
  - lib/adyen/services/recurring.rb
113
114
  - lib/adyen/services/service.rb
114
- - lib/adyen/util.rb
115
+ - lib/adyen/utils/hmac_validator.rb
115
116
  - lib/adyen/version.rb
116
117
  - spec/account_spec.rb
117
118
  - spec/checkout_spec.rb
@@ -119,6 +120,7 @@ files:
119
120
  - spec/client_spec.rb
120
121
  - spec/errors_spec.rb
121
122
  - spec/fund_spec.rb
123
+ - spec/hash_with_accessors_spec.rb
122
124
  - spec/mocks/requests/Account/close_account.json
123
125
  - spec/mocks/requests/Account/close_account_holder.json
124
126
  - spec/mocks/requests/Account/create_account.json
@@ -221,7 +223,9 @@ files:
221
223
  - spec/payments_spec.rb
222
224
  - spec/payouts_spec.rb
223
225
  - spec/recurring_spec.rb
226
+ - spec/service_spec.rb
224
227
  - spec/spec_helper.rb
228
+ - spec/utils/hmac_validator_spec.rb
225
229
  homepage: https://www.adyen.com
226
230
  licenses:
227
231
  - MIT
data/lib/adyen/util.rb DELETED
@@ -1,21 +0,0 @@
1
- # This utility method monkey-patches Ruby's Hash class to allow 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
- # Credit: https://gist.github.com/winfred/2185384#file-ruby-dot-hash-access-rb
6
-
7
- class Hash
8
- class NoKeyOrMethodError < NoMethodError; end
9
- def method_missing(method,*args)
10
- m = method.to_s
11
- string_key = m.gsub(/=$/,'')
12
- sym_key = string_key.to_sym
13
- if self.has_key? string_key
14
- m.match(/=$/) ? self.send("[#{string_key}]=", *args) : self[string_key]
15
- elsif self.has_key? sym_key
16
- m.match(/=$/) ? self.send("[#{sym_key}]=", *args) : self[sym_key]
17
- else
18
- raise NoKeyOrMethodError
19
- end
20
- end
21
- end