cash_app_pay 0.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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/lib/cash_app_pay/api_operations/create.rb +16 -0
  3. data/lib/cash_app_pay/api_operations/delete.rb +27 -0
  4. data/lib/cash_app_pay/api_operations/list.rb +17 -0
  5. data/lib/cash_app_pay/api_operations/request.rb +45 -0
  6. data/lib/cash_app_pay/api_operations/retrieve.rb +24 -0
  7. data/lib/cash_app_pay/api_operations/save.rb +16 -0
  8. data/lib/cash_app_pay/api_operations/update.rb +31 -0
  9. data/lib/cash_app_pay/api_operations/upsert.rb +22 -0
  10. data/lib/cash_app_pay/api_resource.rb +58 -0
  11. data/lib/cash_app_pay/cash_app_pay_client.rb +117 -0
  12. data/lib/cash_app_pay/cash_app_pay_configuration.rb +19 -0
  13. data/lib/cash_app_pay/cash_app_pay_object.rb +77 -0
  14. data/lib/cash_app_pay/cash_app_pay_response.rb +17 -0
  15. data/lib/cash_app_pay/connection_manager.rb +79 -0
  16. data/lib/cash_app_pay/endpoint.rb +8 -0
  17. data/lib/cash_app_pay/error_object.rb +6 -0
  18. data/lib/cash_app_pay/errors.rb +45 -0
  19. data/lib/cash_app_pay/helpers/symbolize.rb +31 -0
  20. data/lib/cash_app_pay/list_object.rb +64 -0
  21. data/lib/cash_app_pay/persistent_http_client.rb +48 -0
  22. data/lib/cash_app_pay/resources/api_key.rb +19 -0
  23. data/lib/cash_app_pay/resources/brand.rb +20 -0
  24. data/lib/cash_app_pay/resources/customer.rb +56 -0
  25. data/lib/cash_app_pay/resources/customer_request.rb +18 -0
  26. data/lib/cash_app_pay/resources/dispute.rb +142 -0
  27. data/lib/cash_app_pay/resources/dispute_evidence.rb +9 -0
  28. data/lib/cash_app_pay/resources/fee_plan.rb +16 -0
  29. data/lib/cash_app_pay/resources/grant.rb +13 -0
  30. data/lib/cash_app_pay/resources/merchant.rb +20 -0
  31. data/lib/cash_app_pay/resources/payment.rb +63 -0
  32. data/lib/cash_app_pay/resources/refund.rb +62 -0
  33. data/lib/cash_app_pay/resources/webhook.rb +20 -0
  34. data/lib/cash_app_pay/resources/webhook_event.rb +15 -0
  35. data/lib/cash_app_pay/version.rb +5 -0
  36. data/lib/cash_app_pay.rb +69 -0
  37. metadata +85 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 56ca23a3952f68fcf4c16941a887ee4a66016774074405b6cadcc5ce63cc8168
4
+ data.tar.gz: 57edbf0f525f96d03da5e838b31192472bd9dc643b49cc6514dd5e227e93329e
5
+ SHA512:
6
+ metadata.gz: 8cb314461eea62e42789ffc4a0de5ddd88d1c6b45c64ec30575e0f9ff36e996852cb2d3717fa46de7de492928cfc980ce26ee6edbc60aded6d7c4101816d1b16
7
+ data.tar.gz: 0755fbff98ba362c1756e23420701f7c2a186dec0fb210bb259db713267b86303ed7e5ca578535236b1ed6e95a07af713f511a5f20dc06ed1ad649a36ac5e2bf
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Create
6
+ def create(params = {}, opts = {})
7
+ request_cash_app_pay_object(
8
+ method: :post,
9
+ path: resource_url,
10
+ params: params,
11
+ opts: opts
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Delete
6
+ def delete(opts = {})
7
+ delete_cash_app_pay_object(
8
+ path: resource_url,
9
+ opts: opts
10
+ )
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ module ClassMethods
18
+ def delete(resource, _params = {}, opts = {})
19
+ delete_cash_app_pay_object(
20
+ path: "#{resource_url}/#{CGI.escape(resource)}",
21
+ opts: opts
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module List
6
+ def list(filters = {}, opts = {})
7
+ response, opts = execute_resource_request(
8
+ method: :get,
9
+ url: resource_url,
10
+ url_params: filters,
11
+ opts: opts
12
+ )
13
+ ListObject.initialize_from_response(self, response, opts, filters)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Request
6
+ def request_cash_app_pay_object(method:, path:, params:, opts: {})
7
+ body = self.class.encode_body(params) unless params.nil?
8
+ response, opts = self.class.execute_resource_request(method: method, url: path, body_params: body, opts: opts)
9
+ initialize_from(response.data, opts)
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ private
17
+
18
+ def delete_cash_app_pay_object(path:, opts: {})
19
+ response, = execute_resource_request(method: :delete, url: path, opts: opts)
20
+ response.http_status == 200
21
+ end
22
+
23
+ module ClassMethods
24
+ def execute_resource_request(method:, url:, url_params: nil, body_params: nil, opts: {})
25
+ response = CashAppPay::CashAppPayClient.execute_request(method_name: method, path: url,
26
+ url_params: url_params, body_params: body_params, opts: opts)
27
+ [response, opts]
28
+ end
29
+
30
+ def request_cash_app_pay_object(method:, path:, params:, opts: {})
31
+ body = encode_body(params) unless params.nil?
32
+ response, opts = execute_resource_request(method: method, url: path, body_params: body, opts: opts)
33
+ initialize_from_net_response(response, opts)
34
+ end
35
+
36
+ private
37
+
38
+ def delete_cash_app_pay_object(path:, opts: {})
39
+ response, = execute_resource_request(method: :delete, url: path, opts: opts)
40
+ response.http_status == 200
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Retrieve
6
+ def refresh
7
+ response, opts = self.class.execute_resource_request(method: :get, url: resource_url, opts: @opts)
8
+ initialize_from(response.data, opts)
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ def retrieve(id, opts = {})
17
+ instance = new({ id: id }, opts)
18
+ instance.refresh
19
+ instance
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Save
6
+ def save(opts = {})
7
+ request_cash_app_pay_object(
8
+ method: :post,
9
+ path: self.class.resource_url,
10
+ params: @values,
11
+ opts: opts
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Update
6
+ def update(params = {}, opts = {})
7
+ request_cash_app_pay_object(
8
+ method: :patch,
9
+ path: resource_url,
10
+ params: params,
11
+ opts: opts
12
+ )
13
+ end
14
+
15
+ def self.included(base)
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ def update(resource, params = {}, opts = {})
21
+ request_cash_app_pay_object(
22
+ method: :patch,
23
+ path: "#{resource_url}/#{CGI.escape(resource)}",
24
+ params: params,
25
+ opts: opts
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module APIOperations
5
+ module Upsert
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def upsert(resource, params = {}, opts = {})
12
+ request_cash_app_pay_object(
13
+ method: :put,
14
+ path: "#{resource_url}/#{CGI.escape(resource)}",
15
+ params: params,
16
+ opts: opts
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ class APIResource < CashAppPayObject
5
+ include CashAppPay::APIOperations::Request
6
+
7
+ attr_reader :opts
8
+
9
+ def initialize(values = {}, opts = {})
10
+ super(values)
11
+ @opts = opts
12
+ end
13
+
14
+ def self.resource_url
15
+ raise NotImplementedError, 'API Resource is an abstract class'
16
+ end
17
+
18
+ private
19
+
20
+ def self.initialize_from_net_response(response, opts)
21
+ instance = new
22
+ instance.send(:initialize_from, response.data, opts)
23
+ instance
24
+ end
25
+
26
+ def initialize_from(values, opts)
27
+ @values = values.fetch(self.class.object_name, {})
28
+ @opts = opts
29
+ self
30
+ end
31
+
32
+ def resource_url
33
+ unless (id = self.id)
34
+ raise InvalidRequestError.new(
35
+ "Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}",
36
+ 'id'
37
+ )
38
+ end
39
+ "#{self.class.resource_url}/#{CGI.escape(id)}"
40
+ end
41
+
42
+ # Encode the params for the body of the request.
43
+ # If the params contains `idempotency_key` then put this at the root
44
+ # e.g. { request: { id: 1 }, idempotency_key: 'key' }
45
+ # params: Hash of params
46
+ def self.encode_body(params)
47
+ idempotency_key = params.delete(:idempotency_key) || params.delete('idempotency_key')
48
+ body = if params.empty? && !idempotency_key.nil?
49
+ { idempotency_key: idempotency_key }
50
+ else
51
+ named_body_params = Hash[object_name, params]
52
+ named_body_params[:idempotency_key] = idempotency_key unless idempotency_key.nil?
53
+ named_body_params
54
+ end
55
+ body.to_json
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cash_app_pay/version'
4
+
5
+ module CashAppPay
6
+ class CashAppPayClient
7
+ def self.execute_request(method_name:, path:, url_params:, body_params: nil, opts: {})
8
+ base_uri = opts[:api_base] || CashAppPay.api_base
9
+ client_id = opts[:client_id] || CashAppPay.client_id
10
+
11
+ check_client_id!(client_id)
12
+
13
+ method = method_name.to_s.upcase
14
+ url = URI::HTTPS.build(host: base_uri, path: path)
15
+ url.query = URI.encode_www_form(url_params) if !url_params.nil? && !url_params.empty?
16
+ http = CashAppPay::PersistentHttpClient.get(url)
17
+
18
+ headers = if path.start_with?(NETWORK_API_PATH_PREFIX) || path.start_with?(MANAGE_API_PATH_PREFIX)
19
+ network_api_headers(client_id, opts)
20
+ elsif path.start_with?(CUSTOMER_REQUEST_API_PATH_PREFIX)
21
+ customer_request_api_headers(client_id)
22
+ else
23
+ raise InvalidRequestError.new('path', '')
24
+ end
25
+
26
+ request = Net::HTTPGenericRequest.new(method, !body_params.nil?, true, url, headers)
27
+ request.body = body_params unless body_params.nil?
28
+
29
+ response = http.request(request)
30
+
31
+ begin
32
+ resp = CashAppPayResponse.from_net_http(response)
33
+ handle_error_response(resp.error_data) if resp.error_data
34
+ rescue JSON::ParserError
35
+ raise general_api_error(response.code.to_i, response.body)
36
+ end
37
+
38
+ resp
39
+ end
40
+
41
+ def self.handle_error_response(response_errors)
42
+ return unless (error_response = response_errors.first)
43
+
44
+ raise APIResponseError, error_response
45
+ end
46
+
47
+ def self.check_client_id!(client_id)
48
+ return if client_id
49
+
50
+ raise AuthenticationError, 'No Client ID provided. Set your API key using CashAppPay.client_id = <CLIENT-ID>'
51
+ end
52
+
53
+ def self.check_api_base!(api_base)
54
+ return if api_base
55
+
56
+ raise AuthenticationError, 'No API base provided. Set your API key using CashAppPay.api_base = <API-BASE>'
57
+ end
58
+
59
+ def self.check_region!(region)
60
+ raise AuthenticationError, 'No Region provided. Set your API key using CashAppPay.region = <REGION>' unless region
61
+ end
62
+
63
+ def self.check_signature!(signature)
64
+ return if signature
65
+
66
+ raise AuthenticationError, 'No Signature provided. Set your API key using CashAppPay.signature = <SIGNATURE>'
67
+ end
68
+
69
+ def self.check_api_key!(api_key)
70
+ return if api_key
71
+
72
+ raise AuthenticationError, 'No Region provided. Set your API key using CashAppPay.api_key = <API-KEY>'
73
+ end
74
+
75
+ def self.general_api_error(status, body)
76
+ APIError.new("Invalid response object from API: #{body.inspect} (HTTP response code was #{status})",
77
+ http_status: status, http_body: body)
78
+ end
79
+
80
+ def self.user_agent
81
+ "cash-app-pay-ruby/v#{CashAppPay::VERSION} RubyBindings (#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})) RUBY_PLATFORM #{defined?(RUBY_ENGINE) ? "(#{RUBY_ENGINE})" : ''}"
82
+ end
83
+
84
+ CUSTOMER_REQUEST_API_PATH_PREFIX = '/customer-request/'
85
+ NETWORK_API_PATH_PREFIX = '/network/'
86
+ MANAGE_API_PATH_PREFIX = '/management/'
87
+
88
+ def self.customer_request_api_headers(client_id)
89
+ {
90
+ "Authorization": ['Client', client_id].join(' '),
91
+ "Accept": 'application/json',
92
+ "Content-Type": 'application/json',
93
+ "User-Agent": user_agent
94
+ }
95
+ end
96
+
97
+ def self.network_api_headers(client_id, opts)
98
+ api_key = opts[:api_key] || CashAppPay.api_key
99
+ signature = opts[:signature] || CashAppPay.signature
100
+ region = opts[:region] || CashAppPay.region
101
+
102
+ check_api_key!(api_key)
103
+ check_signature!(signature)
104
+ check_region!(region)
105
+
106
+ authorization = ['Client', client_id, api_key].join(' ')
107
+ {
108
+ "Authorization": authorization,
109
+ "X-Region": region,
110
+ "X-Signature": signature,
111
+ "Accept": 'application/json',
112
+ "Content-Type": 'application/json',
113
+ "User-Agent": user_agent
114
+ }
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cash_app_pay/endpoint'
4
+
5
+ module CashAppPay
6
+ class CashAppPayConfiguration
7
+ attr_accessor :client_id, :api_base, :region, :signature, :api_key
8
+
9
+ def self.setup
10
+ new.tap do |instance|
11
+ yield(instance) if block_given?
12
+ end
13
+ end
14
+
15
+ def initialize
16
+ @api_base = Endpoint::PRODUCTION
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ class CashAppPayObject
5
+ using CashAppPay::Helpers::Symbolize
6
+
7
+ attr_accessor :values
8
+
9
+ def initialize(values)
10
+ @values = values.deep_symbolize_keys
11
+ end
12
+
13
+ def to_h(*_args)
14
+ @values
15
+ end
16
+
17
+ alias to_hash to_h
18
+ alias as_json to_h
19
+
20
+ def to_json(*_args)
21
+ @values.to_json
22
+ end
23
+
24
+ def to_s(*_args)
25
+ JSON.pretty_generate(to_h)
26
+ end
27
+
28
+ def to_str
29
+ self['id'].to_s
30
+ end
31
+
32
+ def inspect
33
+ id_string = !id.nil? ? " id=#{id}" : ''
34
+ "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " +
35
+ JSON.pretty_generate(@values)
36
+ end
37
+
38
+ def [](name)
39
+ data = @values[typed_key(name)]
40
+ if data.is_a?(Hash)
41
+ CashAppPay::CashAppPayObject.new(data)
42
+ elsif data.is_a?(Array)
43
+ data.map do |item|
44
+ item.is_a?(Hash) ? CashAppPay::CashAppPayObject.new(item) : item
45
+ end
46
+ else
47
+ data
48
+ end
49
+ end
50
+
51
+ def ==(other)
52
+ if other.is_a?(CashAppPay::CashAppPayObject)
53
+ @values == other.values
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def []=(name, value)
60
+ @values[typed_key(name)] = value
61
+ end
62
+
63
+ def method_missing(name, *_args)
64
+ if name.to_s.end_with?('=')
65
+ attr = name.to_s[0...-1].to_sym
66
+ val = _args.first
67
+ self[attr] = val
68
+ else
69
+ self[name]
70
+ end
71
+ end
72
+
73
+ def typed_key(key)
74
+ key.to_sym
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ class CashAppPayResponse
5
+ attr_accessor :http_headers, :http_status, :data, :error_data, :http_body
6
+
7
+ def self.from_net_http(http_resp)
8
+ resp = CashAppPayResponse.new
9
+ resp.http_headers = http_resp.each_header.to_h
10
+ resp.http_status = http_resp.code.to_i
11
+ resp.http_body = http_resp.body
12
+ resp.data = JSON.parse(http_resp.read_body, symbolize_names: true) unless resp.http_body.empty?
13
+ resp.error_data = resp&.data&.fetch(:errors, nil)
14
+ resp
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # connection manager represents
4
+ # a cache of all keep-alive connections
5
+ # in a current thread
6
+ module CashAppPay
7
+ class ConnectionManager
8
+ DEFAULT_OPTIONS = { read_timeout: 80, open_timeout: 30 }.freeze
9
+ # if a client wasn't used within this time range
10
+ # it gets removed from the cache and the connection closed.
11
+ # This helps to make sure there are no memory leaks.
12
+ STALE_AFTER = 60 * 5 # 5.minutes
13
+
14
+ # Seconds to reuse the connection of the previous request. If the idle time is less than this Keep-Alive Timeout, Net::HTTP reuses the TCP/IP socket used by the previous communication. Source: Ruby docs
15
+ KEEP_ALIVE_TIMEOUT = 30 # seconds
16
+
17
+ # KEEP_ALIVE_TIMEOUT vs STALE_AFTER
18
+ # STALE_AFTER - how long an Net::HTTP client object is cached in ruby
19
+ # KEEP_ALIVE_TIMEOUT - how long that client keeps TCP/IP socket open.
20
+
21
+ attr_accessor :clients_store, :last_used
22
+
23
+ def initialize
24
+ self.clients_store = {}
25
+ self.last_used = Time.now
26
+ end
27
+
28
+ def get_client(uri, options)
29
+ mutex.synchronize do
30
+ # refresh the last time a client was used,
31
+ # this prevents the client from becoming stale
32
+ self.last_used = Time.now
33
+
34
+ # we use params as a cache key for clients.
35
+ # 2 connections to the same host but with different
36
+ # options are going to use different HTTP clients
37
+ params = [uri.host, uri.port, options]
38
+ client = clients_store[params]
39
+
40
+ return client if client
41
+
42
+ client = Net::HTTP.new(uri.host, uri.port)
43
+ client.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
44
+
45
+ # set SSL to true if a scheme is https
46
+ client.use_ssl = uri.scheme == 'https'
47
+
48
+ # dynamically set Net::HTTP options
49
+ DEFAULT_OPTIONS.merge(options).each_pair do |key, value|
50
+ client.public_send("#{key}=", value)
51
+ end
52
+
53
+ # open connection
54
+ client.start
55
+
56
+ # cache the client
57
+ clients_store[params] = client
58
+
59
+ client
60
+ end
61
+ end
62
+
63
+ # close connections for each client
64
+ def close_connections!
65
+ mutex.synchronize do
66
+ clients_store.each_value(&:finish)
67
+ self.clients_store = {}
68
+ end
69
+ end
70
+
71
+ def stale?
72
+ Time.now - last_used > STALE_AFTER
73
+ end
74
+
75
+ def mutex
76
+ @mutex ||= Mutex.new
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module Endpoint
5
+ SANDBOX = 'sandbox.api.cash.app'
6
+ PRODUCTION = 'api.cash.app'
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ class ErrorObject < CashAppPayObject
5
+ end
6
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ # CashAppPayError is the base error from which all other more specific errors derive.
5
+ class CashAppPayError < StandardError
6
+ attr_reader :message
7
+
8
+ def initialize(message)
9
+ @message = message
10
+ end
11
+ end
12
+
13
+ # InvalidRequestError is raised when a request is initiated with invalid
14
+ # parameters.
15
+ class InvalidRequestError < CashAppPayError
16
+ attr_accessor :param
17
+
18
+ def initialize(message, param)
19
+ super(message)
20
+ @param = param
21
+ end
22
+ end
23
+
24
+ # AuthenticationError is raised when invalid credentials are used to connect
25
+ # to Cash App servers.
26
+ class AuthenticationError < CashAppPayError
27
+ end
28
+
29
+ class APIError < CashAppPayError
30
+ attr_reader :http_status, :http_body
31
+
32
+ def initialize(message, http_status:, http_body:)
33
+ super(message)
34
+ @http_status = http_status
35
+ @http_body = http_body
36
+ end
37
+ end
38
+
39
+ class APIResponseError < CashAppPayError
40
+ def initialize(response)
41
+ error_object = ErrorObject.new(response)
42
+ super(error_object.to_s)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashAppPay
4
+ module Helpers
5
+ module Symbolize
6
+ extend self
7
+
8
+ def symbolize_recursive(hash)
9
+ {}.tap do |h|
10
+ hash.each { |key, value| h[key.to_sym] = transform(value) }
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def transform(thing)
17
+ case thing
18
+ when Hash then symbolize_recursive(thing)
19
+ when Array then thing.map { |v| transform(v) }
20
+ else; thing
21
+ end
22
+ end
23
+
24
+ refine Hash do
25
+ def deep_symbolize_keys
26
+ Symbolize.symbolize_recursive(self)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end