adyen-ruby-api-library 4.0.0 → 4.0.1

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