cash_app_pay 0.0.1

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