acquiring-sdk-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +140 -0
  5. data/Rakefile +34 -0
  6. data/acquiring-sdk-ruby.gemspec +30 -0
  7. data/lib/worldline/acquiring/sdk/api_resource.rb +53 -0
  8. data/lib/worldline/acquiring/sdk/authentication/authenticator.rb +21 -0
  9. data/lib/worldline/acquiring/sdk/authentication/authorization_type.rb +17 -0
  10. data/lib/worldline/acquiring/sdk/authentication/oauth2_authenticator.rb +142 -0
  11. data/lib/worldline/acquiring/sdk/authentication/oauth2_exception.rb +15 -0
  12. data/lib/worldline/acquiring/sdk/authentication.rb +1 -0
  13. data/lib/worldline/acquiring/sdk/call_context.rb +9 -0
  14. data/lib/worldline/acquiring/sdk/client.rb +69 -0
  15. data/lib/worldline/acquiring/sdk/communication/communication_exception.rb +21 -0
  16. data/lib/worldline/acquiring/sdk/communication/connection.rb +50 -0
  17. data/lib/worldline/acquiring/sdk/communication/default_connection.rb +429 -0
  18. data/lib/worldline/acquiring/sdk/communication/metadata_provider.rb +162 -0
  19. data/lib/worldline/acquiring/sdk/communication/multipart_form_data_object.rb +54 -0
  20. data/lib/worldline/acquiring/sdk/communication/multipart_form_data_request.rb +15 -0
  21. data/lib/worldline/acquiring/sdk/communication/not_found_exception.rb +21 -0
  22. data/lib/worldline/acquiring/sdk/communication/param_request.rb +16 -0
  23. data/lib/worldline/acquiring/sdk/communication/pooled_connection.rb +28 -0
  24. data/lib/worldline/acquiring/sdk/communication/request_header.rb +64 -0
  25. data/lib/worldline/acquiring/sdk/communication/request_param.rb +30 -0
  26. data/lib/worldline/acquiring/sdk/communication/response_exception.rb +58 -0
  27. data/lib/worldline/acquiring/sdk/communication/response_header.rb +80 -0
  28. data/lib/worldline/acquiring/sdk/communication.rb +1 -0
  29. data/lib/worldline/acquiring/sdk/communicator.rb +506 -0
  30. data/lib/worldline/acquiring/sdk/communicator_configuration.rb +197 -0
  31. data/lib/worldline/acquiring/sdk/domain/data_object.rb +34 -0
  32. data/lib/worldline/acquiring/sdk/domain/shopping_cart_extension.rb +62 -0
  33. data/lib/worldline/acquiring/sdk/domain/uploadable_file.rb +35 -0
  34. data/lib/worldline/acquiring/sdk/domain.rb +1 -0
  35. data/lib/worldline/acquiring/sdk/factory.rb +183 -0
  36. data/lib/worldline/acquiring/sdk/json/default_marshaller.rb +36 -0
  37. data/lib/worldline/acquiring/sdk/json/marshaller.rb +29 -0
  38. data/lib/worldline/acquiring/sdk/json/marshaller_syntax_exception.rb +11 -0
  39. data/lib/worldline/acquiring/sdk/json.rb +1 -0
  40. data/lib/worldline/acquiring/sdk/logging/communicator_logger.rb +26 -0
  41. data/lib/worldline/acquiring/sdk/logging/log_message_builder.rb +91 -0
  42. data/lib/worldline/acquiring/sdk/logging/logging_capable.rb +19 -0
  43. data/lib/worldline/acquiring/sdk/logging/obfuscation/body_obfuscator.rb +101 -0
  44. data/lib/worldline/acquiring/sdk/logging/obfuscation/header_obfuscator.rb +54 -0
  45. data/lib/worldline/acquiring/sdk/logging/obfuscation/obfuscation_capable.rb +23 -0
  46. data/lib/worldline/acquiring/sdk/logging/obfuscation/obfuscation_rule.rb +49 -0
  47. data/lib/worldline/acquiring/sdk/logging/obfuscation.rb +1 -0
  48. data/lib/worldline/acquiring/sdk/logging/request_log_message_builder.rb +52 -0
  49. data/lib/worldline/acquiring/sdk/logging/response_log_message_builder.rb +43 -0
  50. data/lib/worldline/acquiring/sdk/logging/ruby_communicator_logger.rb +63 -0
  51. data/lib/worldline/acquiring/sdk/logging/stdout_communicator_logger.rb +33 -0
  52. data/lib/worldline/acquiring/sdk/logging.rb +1 -0
  53. data/lib/worldline/acquiring/sdk/proxy_configuration.rb +76 -0
  54. data/lib/worldline/acquiring/sdk/v1/acquirer/acquirer_client.rb +35 -0
  55. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/account_verifications_client.rb +60 -0
  56. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications.rb +4 -0
  57. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/dynamic_currency_conversion_client.rb +60 -0
  58. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion.rb +4 -0
  59. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client.rb +66 -0
  60. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/payments/get_payment_status_params.rb +34 -0
  61. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/payments/payments_client.rb +224 -0
  62. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/payments.rb +4 -0
  63. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/get_refund_params.rb +34 -0
  64. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/refunds_client.rb +157 -0
  65. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/refunds.rb +4 -0
  66. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/technical_reversals_client.rb +64 -0
  67. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals.rb +4 -0
  68. data/lib/worldline/acquiring/sdk/v1/acquirer/merchant.rb +4 -0
  69. data/lib/worldline/acquiring/sdk/v1/acquirer.rb +4 -0
  70. data/lib/worldline/acquiring/sdk/v1/api_exception.rb +63 -0
  71. data/lib/worldline/acquiring/sdk/v1/authorization_exception.rb +23 -0
  72. data/lib/worldline/acquiring/sdk/v1/domain/address_verification_data.rb +41 -0
  73. data/lib/worldline/acquiring/sdk/v1/domain/amount_data.rb +48 -0
  74. data/lib/worldline/acquiring/sdk/v1/domain/api_account_verification_request.rb +70 -0
  75. data/lib/worldline/acquiring/sdk/v1/domain/api_account_verification_response.rb +87 -0
  76. data/lib/worldline/acquiring/sdk/v1/domain/api_action_response.rb +71 -0
  77. data/lib/worldline/acquiring/sdk/v1/domain/api_action_response_for_refund.rb +71 -0
  78. data/lib/worldline/acquiring/sdk/v1/domain/api_capture_request.rb +75 -0
  79. data/lib/worldline/acquiring/sdk/v1/domain/api_capture_request_for_refund.rb +43 -0
  80. data/lib/worldline/acquiring/sdk/v1/domain/api_increment_request.rb +61 -0
  81. data/lib/worldline/acquiring/sdk/v1/domain/api_increment_response.rb +43 -0
  82. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_error_response.rb +62 -0
  83. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_refund_request.rb +77 -0
  84. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_request.rb +95 -0
  85. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_resource.rb +103 -0
  86. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_response.rb +126 -0
  87. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_reversal_request.rb +61 -0
  88. data/lib/worldline/acquiring/sdk/v1/domain/api_payment_summary_for_response.rb +66 -0
  89. data/lib/worldline/acquiring/sdk/v1/domain/api_references_for_responses.rb +48 -0
  90. data/lib/worldline/acquiring/sdk/v1/domain/api_refund_request.rb +88 -0
  91. data/lib/worldline/acquiring/sdk/v1/domain/api_refund_resource.rb +110 -0
  92. data/lib/worldline/acquiring/sdk/v1/domain/api_refund_response.rb +133 -0
  93. data/lib/worldline/acquiring/sdk/v1/domain/api_refund_summary_for_response.rb +66 -0
  94. data/lib/worldline/acquiring/sdk/v1/domain/api_reversal_response.rb +36 -0
  95. data/lib/worldline/acquiring/sdk/v1/domain/api_technical_reversal_request.rb +50 -0
  96. data/lib/worldline/acquiring/sdk/v1/domain/api_technical_reversal_response.rb +62 -0
  97. data/lib/worldline/acquiring/sdk/v1/domain/card_data_for_dcc.rb +48 -0
  98. data/lib/worldline/acquiring/sdk/v1/domain/card_on_file_data.rb +52 -0
  99. data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data.rb +114 -0
  100. data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_refund.rb +82 -0
  101. data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_resource.rb +43 -0
  102. data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_response.rb +52 -0
  103. data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_verification.rb +91 -0
  104. data/lib/worldline/acquiring/sdk/v1/domain/dcc_data.rb +55 -0
  105. data/lib/worldline/acquiring/sdk/v1/domain/dcc_proposal.rb +60 -0
  106. data/lib/worldline/acquiring/sdk/v1/domain/e_commerce_data.rb +52 -0
  107. data/lib/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_account_verification.rb +45 -0
  108. data/lib/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_response.rb +41 -0
  109. data/lib/worldline/acquiring/sdk/v1/domain/get_dcc_rate_request.rb +75 -0
  110. data/lib/worldline/acquiring/sdk/v1/domain/get_dcc_rate_response.rb +57 -0
  111. data/lib/worldline/acquiring/sdk/v1/domain/initial_card_on_file_data.rb +41 -0
  112. data/lib/worldline/acquiring/sdk/v1/domain/merchant_data.rb +76 -0
  113. data/lib/worldline/acquiring/sdk/v1/domain/network_token_data.rb +41 -0
  114. data/lib/worldline/acquiring/sdk/v1/domain/payment_references.rb +48 -0
  115. data/lib/worldline/acquiring/sdk/v1/domain/plain_card_data.rb +48 -0
  116. data/lib/worldline/acquiring/sdk/v1/domain/point_of_sale_data.rb +34 -0
  117. data/lib/worldline/acquiring/sdk/v1/domain/point_of_sale_data_for_dcc.rb +41 -0
  118. data/lib/worldline/acquiring/sdk/v1/domain/rate_data.rb +64 -0
  119. data/lib/worldline/acquiring/sdk/v1/domain/sub_operation.rb +94 -0
  120. data/lib/worldline/acquiring/sdk/v1/domain/sub_operation_for_refund.rb +87 -0
  121. data/lib/worldline/acquiring/sdk/v1/domain/subsequent_card_on_file_data.rb +48 -0
  122. data/lib/worldline/acquiring/sdk/v1/domain/three_d_secure.rb +62 -0
  123. data/lib/worldline/acquiring/sdk/v1/domain/transaction_data_for_dcc.rb +52 -0
  124. data/lib/worldline/acquiring/sdk/v1/domain.rb +4 -0
  125. data/lib/worldline/acquiring/sdk/v1/exception_factory.rb +48 -0
  126. data/lib/worldline/acquiring/sdk/v1/ping/ping_client.rb +52 -0
  127. data/lib/worldline/acquiring/sdk/v1/ping.rb +4 -0
  128. data/lib/worldline/acquiring/sdk/v1/platform_exception.rb +23 -0
  129. data/lib/worldline/acquiring/sdk/v1/reference_exception.rb +23 -0
  130. data/lib/worldline/acquiring/sdk/v1/v1_client.rb +43 -0
  131. data/lib/worldline/acquiring/sdk/v1/validation_exception.rb +23 -0
  132. data/lib/worldline/acquiring/sdk/v1.rb +4 -0
  133. data/lib/worldline/acquiring/sdk.rb +1 -0
  134. data/spec/comparable_extension.rb +29 -0
  135. data/spec/fixtures/resources/authentication/oauth2AccessToken.expired.json +4 -0
  136. data/spec/fixtures/resources/authentication/oauth2AccessToken.invalidClient.json +4 -0
  137. data/spec/fixtures/resources/authentication/oauth2AccessToken.json +4 -0
  138. data/spec/fixtures/resources/communication/getWithQueryParams.json +3 -0
  139. data/spec/fixtures/resources/communication/getWithoutQueryParams.json +3 -0
  140. data/spec/fixtures/resources/communication/notFound.html +1 -0
  141. data/spec/fixtures/resources/communication/postWithBadRequestResponse.json +11 -0
  142. data/spec/fixtures/resources/communication/postWithCreatedResponse.json +6 -0
  143. data/spec/fixtures/resources/communication/unknownServerError.json +10 -0
  144. data/spec/fixtures/resources/logging/bodyNoObfuscation.json +7 -0
  145. data/spec/fixtures/resources/logging/bodyWithBinObfuscated.json +3 -0
  146. data/spec/fixtures/resources/logging/bodyWithBinOriginal.json +3 -0
  147. data/spec/fixtures/resources/logging/bodyWithCardCustomObfuscated.json +13 -0
  148. data/spec/fixtures/resources/logging/bodyWithCardObfuscated.json +13 -0
  149. data/spec/fixtures/resources/logging/bodyWithCardOriginal.json +13 -0
  150. data/spec/fixtures/resources/logging/bodyWithObjectObfuscated.json +5 -0
  151. data/spec/fixtures/resources/logging/bodyWithObjectOriginal.json +5 -0
  152. data/spec/fixtures/resources/properties.oauth2.yml +8 -0
  153. data/spec/fixtures/resources/properties.proxy.yml +14 -0
  154. data/spec/integration/connection_pooling_spec.rb +74 -0
  155. data/spec/integration/multipart_form_data_spec.rb +216 -0
  156. data/spec/integration/process_payment_spec.rb +43 -0
  157. data/spec/integration/request_dcc_rate_spec.rb +24 -0
  158. data/spec/integration/sdk_proxy_spec.rb +70 -0
  159. data/spec/integration_setup.rb +111 -0
  160. data/spec/lib/authentication/oauth2_authenticator_spec.rb +68 -0
  161. data/spec/lib/client_spec.rb +47 -0
  162. data/spec/lib/communication/default_connection_logger_spec.rb +484 -0
  163. data/spec/lib/communication/default_connection_spec.rb +352 -0
  164. data/spec/lib/communication/metadata_provider_spec.rb +93 -0
  165. data/spec/lib/communicator_configuration_spec.rb +181 -0
  166. data/spec/lib/communicator_spec.rb +34 -0
  167. data/spec/lib/factory_spec.rb +38 -0
  168. data/spec/lib/json/default_marshaller_spec.rb +39 -0
  169. data/spec/lib/logging/obfuscation/body_obfuscator_spec.rb +86 -0
  170. data/spec/lib/logging/obfuscation/header_obfuscator_spec.rb +100 -0
  171. data/spec/lib/logging/ruby_communicator_logger_spec.rb +92 -0
  172. data/spec/lib/logging/stdout_communicator_logger_spec.rb +64 -0
  173. data/spec/spec_helper.rb +32 -0
  174. metadata +375 -0
@@ -0,0 +1,429 @@
1
+ require 'httpclient'
2
+ require 'securerandom'
3
+ require 'uri'
4
+ require 'worldline/acquiring/sdk/communication/communication_exception'
5
+ require 'worldline/acquiring/sdk/communication/multipart_form_data_object'
6
+ require 'worldline/acquiring/sdk/communication/pooled_connection'
7
+ require 'worldline/acquiring/sdk/communication/response_header'
8
+ require 'worldline/acquiring/sdk/logging/obfuscation/body_obfuscator'
9
+ require 'worldline/acquiring/sdk/logging/obfuscation/header_obfuscator'
10
+ require 'worldline/acquiring/sdk/logging/request_log_message_builder'
11
+ require 'worldline/acquiring/sdk/logging/response_log_message_builder'
12
+
13
+ # @private :nodoc: this is not our class
14
+ module RefineHTTPClient
15
+ refine HTTPClient do
16
+ # (monkey) patch to gain access to the session pool size in HTTPClient
17
+ def session_count
18
+ sess_pool = @session_manager.instance_variable_get(:@sess_pool)
19
+ sess_pool.size
20
+ end
21
+ end
22
+ end
23
+
24
+ module Worldline
25
+ module Acquiring
26
+ module SDK
27
+ module Communication
28
+ class DefaultConnection < PooledConnection
29
+ using RefineHTTPClient
30
+
31
+ private
32
+
33
+ CONTENT_TYPE = 'Content-Type'.freeze
34
+ JSON_CONTENT_TYPE = 'application/json'.freeze
35
+
36
+ public
37
+
38
+ # @param args [Hash] the parameters to initialize the connection with
39
+ # @option args [Integer] :connect_timeout connection timeout in seconds.
40
+ # @option args [Integer] :socket_timeout socket timeout in seconds.
41
+ # @option args [Integer] :max_connections number of connections kept alive in the thread pool.
42
+ # Uses {Worldline::Acquiring::SDK::CommunicatorConfiguration.default_max_connections} if not given.
43
+ # @option args [Worldline::Acquiring::SDK::ProxyConfiguration] :proxy_configuration object that stores the proxy to use.
44
+ # If not given the system default proxy is used;
45
+ # if there is no system default proxy set either, no proxy is used.
46
+ def initialize(args)
47
+ raise ArgumentError unless args.is_a? Hash
48
+
49
+ # Set timeouts to nil if they are negative
50
+ @connect_timeout = args[:connect_timeout]
51
+ @connect_timeout = nil unless @connect_timeout.nil? || @connect_timeout > 0
52
+ @socket_timeout = args[:socket_timeout]
53
+ @socket_timeout = nil unless @socket_timeout.nil? || @socket_timeout > 0
54
+ @max_connections = args[:max_connections] || CommunicatorConfiguration.default_max_connections
55
+ @proxy_configuration = args[:proxy_configuration]
56
+
57
+ # HTTPClient provides the following features:
58
+ # 1) thread safe, an instance can be used by multiple threads without
59
+ # explicit synchronization
60
+ # 2) use persistent connection if HTTP 1.1 is supported. The connection
61
+ # will be left open until explicitly closed or keep_alive_timeout
62
+ # 3) a built-in connection pool with no limit for max connections
63
+ @http_client = create_http_client
64
+ @http_client.connect_timeout = @connect_timeout
65
+ @http_client.send_timeout = @socket_timeout
66
+ @http_client.receive_timeout = @socket_timeout
67
+
68
+ @body_obfuscator = Worldline::Acquiring::SDK::Logging::Obfuscation::BodyObfuscator.default_obfuscator
69
+ @header_obfuscator = Worldline::Acquiring::SDK::Logging::Obfuscation::HeaderObfuscator.default_obfuscator
70
+ end
71
+
72
+ private
73
+
74
+ # Create a HTTPClient instance that uses @proxy_configuration or the system proxy if @proxy_configuration is not set
75
+ def create_http_client
76
+ if @proxy_configuration
77
+ httpclient = HTTPClient.new(@proxy_configuration.proxy_uri)
78
+ httpclient.set_proxy_auth(@proxy_configuration.username, @proxy_configuration.password)
79
+ httpclient.force_basic_auth = true unless @proxy_configuration.username.nil? || @proxy_configuration.password.nil?
80
+ httpclient
81
+ else
82
+ # use system settings
83
+ proxy_string = ENV['https_proxy'] || ENV['http_proxy']
84
+ # proxy string format = 'http://username:password@hostname:port'
85
+ proxy_string =~ %r{https?//(?<username>[^:]):(?<password>[^@])@.*}
86
+ username = Regexp.last_match(1)
87
+ password = Regexp.last_match(2)
88
+ httpclient = HTTPClient.new(proxy_string)
89
+ httpclient.set_proxy_auth(username, password) unless username.nil? || password.nil?
90
+ httpclient.force_basic_auth = true unless username.nil? || password.nil?
91
+ httpclient
92
+ end
93
+ end
94
+
95
+ public
96
+
97
+ # Closes all connections idle for longer than _idle_time_ seconds.
98
+ # In addition, the keep_alive_timeout is set to _idle_time_
99
+ # so any future connections idle for longer than _idle_time_ seconds will also be closed.
100
+ def close_idle_connections(idle_time)
101
+ # in sec
102
+ @http_client.keep_alive_timeout = idle_time # set timeout value
103
+ close_expired_connections
104
+ end
105
+
106
+ # HTTPClient automatically closes expired connections so _close_expired_connections_ is a no-operation.
107
+ def close_expired_connections
108
+ # By default the keep alive timeout is 15 sec, which is the HTTP 1.1
109
+ # standard. To change the value, use keep_alive_timeout= method
110
+ # do nothing, handled by HTTPClient
111
+ end
112
+
113
+ # Frees all networking resources used.
114
+ def close
115
+ @http_client.reset_all
116
+ end
117
+
118
+ # Returns the number of open connections
119
+ def session_count
120
+ @http_client.session_count
121
+ end
122
+
123
+ # Performs a GET request to the Worldline Acquiring platform
124
+ # @see request
125
+ def get(uri, request_headers)
126
+ request('get', uri, request_headers) do |response_status_code, response_headers, response_body|
127
+ yield response_status_code, response_headers, response_body
128
+ end
129
+ end
130
+
131
+ # Performs a DELETE request to the Worldline Acquiring platform
132
+ # @see request
133
+ def delete(uri, request_headers)
134
+ request('delete', uri, request_headers) do |response_status_code, response_headers, response_body|
135
+ yield response_status_code, response_headers, response_body
136
+ end
137
+ end
138
+
139
+ # Performs a POST request to the Worldline Acquiring platform
140
+ # @see request
141
+ def post(uri, request_headers, body)
142
+ request('post', uri, request_headers, body) do |response_status_code, response_headers, response_body|
143
+ yield response_status_code, response_headers, response_body
144
+ end
145
+ end
146
+
147
+ # Performs a PUT request to the Worldline Acquiring platform
148
+ # @see request
149
+ def put(uri, request_headers, body)
150
+ request('put', uri, request_headers, body) do |response_status_code, response_headers, response_body|
151
+ yield response_status_code, response_headers, response_body
152
+ end
153
+ end
154
+
155
+ # Performs a HTTP request and yields the response as the status code, headers and body.
156
+ # Also ensures the request is logged when sent and its response is logged when received.
157
+ #
158
+ # @param method [String] 'GET', 'DELETE', 'POST' or 'PUT' depending on the HTTP method being used.
159
+ # @param uri [URI::HTTP] full URI of the location the request is targeted at, including query parameters.
160
+ # @param request_headers [Array<Worldline::Acquiring::SDK::Communication::RequestHeader>] list of headers that should be used as HTTP headers in the request.
161
+ # @param body [String, Worldline::Acquiring::SDK::Communication::MultipartFormDataObject] request body.
162
+ # @yield (Integer, Array<Worldline::Acquiring::SDK::Communication::ResponseHeader>, IO) The status code, headers and body of the response.
163
+ # @raise [Worldline::Acquiring::SDK::Communication::CommunicationException] when communication with the Worldline Acquiring platform was not successful.
164
+ def request(method, uri, request_headers, body = nil)
165
+ request_headers = convert_from_sdk_headers(request_headers)
166
+ request_id = SecureRandom.uuid
167
+ content_type = request_headers[CONTENT_TYPE]
168
+
169
+ info = { headers: request_headers, content_type: content_type }
170
+ info[:body] = body unless body.nil?
171
+
172
+ log_request(request_id, method.upcase, uri, info)
173
+
174
+ start_time = Time.now
175
+ begin
176
+ response_headers = nil
177
+ response_status_code = nil
178
+ response_content_type = nil
179
+ response_body = ''
180
+
181
+ if body.is_a? MultipartFormDataObject
182
+ multipart_request(method, uri, request_headers, body) do |status_code, headers, r_content_type, r_body|
183
+ response_headers = headers
184
+ response_status_code = status_code
185
+ response_content_type = r_content_type
186
+ unless binary_content_type? response_content_type
187
+ response_body = r_body.read.force_encoding('UTF-8')
188
+ r_body = StringIO.new(response_body)
189
+ end
190
+
191
+ yield status_code, headers, r_body
192
+ end
193
+ else
194
+ raw_request(method, uri, request_headers, body) do |status_code, headers, r_content_type, r_body|
195
+ response_headers = headers
196
+ response_status_code = status_code
197
+ response_content_type = r_content_type
198
+ unless binary_content_type? response_content_type
199
+ response_body = r_body.read.force_encoding('UTF-8')
200
+ r_body = StringIO.new(response_body)
201
+ end
202
+
203
+ yield status_code, headers, r_body
204
+ end
205
+ end
206
+
207
+ log_response(request_id, response_status_code, start_time,
208
+ headers: response_headers, body: response_body,
209
+ content_type: response_content_type)
210
+ rescue HTTPClient::TimeoutError => e
211
+ log_error(request_id, start_time, e)
212
+ raise CommunicationException, e
213
+ rescue HTTPClient::KeepAliveDisconnected, HTTPClient::RetryableResponse => e # retry these?
214
+ log_error(request_id, start_time, e)
215
+ raise CommunicationException, e
216
+ rescue StandardError => e
217
+ log_error(request_id, start_time, e)
218
+ raise CommunicationException, e
219
+ end
220
+ end
221
+
222
+ # logging code
223
+
224
+ # Sets the current body obfuscator to use.
225
+ # @param body_obfuscator [Worldline::Acquiring::SDK::Logging::Obfuscation::BodyObfuscator]
226
+ def set_body_obfuscator(body_obfuscator)
227
+ raise ArgumentError, 'body_obfuscator is required' unless body_obfuscator
228
+
229
+ @body_obfuscator = body_obfuscator
230
+ end
231
+
232
+ # Sets the current header obfuscator to use.
233
+ # @param header_obfuscator [Worldline::Acquiring::SDK::Logging::Obfuscation::HeaderObfuscator]
234
+ def set_header_obfuscator(header_obfuscator)
235
+ raise ArgumentError, 'header_obfuscator is required' unless header_obfuscator
236
+
237
+ @header_obfuscator = header_obfuscator
238
+ end
239
+
240
+ # Enables logging outgoing requests and incoming responses by registering the _communicator_logger_.
241
+ # Note that only one logger can be registered at a time and calling _enable_logging_
242
+ # a second time will override the old logger instance with the new one.
243
+ #
244
+ # @param communicator_logger [Worldline::Acquiring::SDK::Logging::CommunicatorLogger] the communicator logger the requests and responses are logged to
245
+ def enable_logging(communicator_logger)
246
+ raise ArgumentError, 'communicator_logger is required' unless communicator_logger
247
+
248
+ @communicator_logger = communicator_logger
249
+ end
250
+
251
+ # Disables logging by unregistering any previous logger that might be registered.
252
+ def disable_logging
253
+ @communicator_logger = nil
254
+ end
255
+
256
+ private
257
+
258
+ # Converts a {Worldline::Acquiring::SDK::Communication::RequestHeader} list headers to a hash
259
+ def convert_from_sdk_headers(headers)
260
+ headers.each_with_object({}) { |h, hash| hash[h.name] = h.value }
261
+ end
262
+
263
+ # Converts a hash to a {Worldline::Acquiring::SDK::Communication::ResponseHeader} list
264
+ def convert_to_sdk_response_headers(headers)
265
+ arr ||= []
266
+ headers.each { |k, v| arr << ResponseHeader.new(k, v) }
267
+ arr
268
+ end
269
+
270
+ def log_request(request_id, method, uri, args = {})
271
+ logger = @communicator_logger
272
+ return unless logger
273
+
274
+ headers = args[:headers]
275
+ body = args[:body]
276
+ content_type = args[:content_type]
277
+ log_msg_builder = Worldline::Acquiring::SDK::Logging::RequestLogMessageBuilder.new(request_id, method, uri,
278
+ @body_obfuscator,
279
+ @header_obfuscator)
280
+ headers.each { |k, v| log_msg_builder.add_headers(k, v) } if headers
281
+
282
+ if binary?(headers)
283
+ log_msg_builder.set_body('<binary content>', content_type)
284
+ else
285
+ log_msg_builder.set_body(body, content_type)
286
+ end
287
+
288
+ begin
289
+ logger.log(log_msg_builder.get_message)
290
+ rescue StandardError => e
291
+ logger.log("An error occurred trying to log request #{request_id}", e)
292
+ end
293
+ end
294
+
295
+ # Creates the log_response stream
296
+ # both based on whether or not it is binary and on the rest of the response
297
+ def log_response(request_id, status_code, start_time, args = {})
298
+ logger = @communicator_logger
299
+ return unless logger
300
+
301
+ duration = (Time.now - start_time) * 1000 # in milliseconds
302
+ headers = args[:headers]
303
+ body = args[:body] unless args[:body].nil?
304
+ content_type = args[:content_type]
305
+
306
+ log_msg_builder = Worldline::Acquiring::SDK::Logging::ResponseLogMessageBuilder.new(request_id, status_code, duration,
307
+ @body_obfuscator,
308
+ @header_obfuscator)
309
+ unless headers.nil?
310
+ headers = convert_from_sdk_headers(headers)
311
+ headers.each do |key, value|
312
+ log_msg_builder.add_headers(key, value)
313
+ end
314
+ end
315
+
316
+ if binary_content_type?(content_type)
317
+ log_msg_builder.set_body('<binary content>', content_type)
318
+ else
319
+ log_msg_builder.set_body(body, content_type)
320
+ end
321
+
322
+ begin
323
+ logger.log(log_msg_builder.get_message)
324
+ rescue StandardError => e
325
+ logger.log("An error occurred trying to log response #{request_id}", e)
326
+ end
327
+ end
328
+
329
+ def log_error(request_id, start_time, thrown)
330
+ logger = @communicator_logger
331
+ return unless logger
332
+
333
+ duration = (Time.now - start_time) * 1000 # in millisecs
334
+ logger.log("Error occurred for outgoing request (requestId='#{request_id}', #{duration} ms)", thrown)
335
+ end
336
+
337
+ # @param headers [Hash]
338
+ def binary?(headers)
339
+ unless headers.nil?
340
+ content_type = nil
341
+ headers.each { |k, v| content_type = v if k.casecmp(CONTENT_TYPE).zero? }
342
+
343
+ binary_content_type?(content_type)
344
+ end
345
+ end
346
+
347
+ # @param content_type [String, nil]
348
+ def binary_content_type?(content_type)
349
+ unless content_type.nil?
350
+ content_type = content_type.downcase
351
+ return !content_type.start_with?('text/') &&
352
+ !content_type.include?('json') &&
353
+ !content_type.include?('xml')
354
+ end
355
+ false
356
+ end
357
+
358
+ # Makes a request using the specified method
359
+ #
360
+ # Yields a status code, an array of {Worldline::Acquiring::SDK::Communication::ResponseHeader},
361
+ # the content_type and body
362
+ def raw_request(method, uri, headers, body = nil)
363
+ connection = if body
364
+ @http_client.send(method + '_async', uri, body: body, header: headers)
365
+ else
366
+ @http_client.send(method + '_async', uri, header: headers)
367
+ end
368
+
369
+ response = connection.pop
370
+ pipe = response.content
371
+ response_headers = convert_to_sdk_response_headers(response.headers)
372
+
373
+ begin
374
+ yield response.status_code, response_headers, response.content_type, pipe
375
+ ensure
376
+ pipe.close
377
+ end
378
+ end
379
+
380
+ # Makes a request using the specified method
381
+ #
382
+ # Yields a status code, an array of {Worldline::Acquiring::SDK::Communication::ResponseHeader},
383
+ # the content_type and body
384
+ def multipart_request(method, uri, headers, body = nil)
385
+ unless body.is_a? MultipartFormDataObject
386
+ raise ArgumentError, 'body should be a MultipartFormDataObject'
387
+ end
388
+
389
+ if method != 'post' && method != 'put'
390
+ raise ArgumentError, "method #{method} is not supported"
391
+ end
392
+
393
+ connection = @http_client.send method + '_async',
394
+ uri,
395
+ body: multipart_request_body(body),
396
+ header: headers
397
+
398
+ response = connection.pop
399
+ pipe = response.content
400
+ response_headers = convert_to_sdk_response_headers(response.headers)
401
+
402
+ begin
403
+ yield response.status_code, response_headers, response.content_type, pipe
404
+ ensure
405
+ pipe.close
406
+ end
407
+ end
408
+
409
+ # Creates a request body for the multipart request
410
+ def multipart_request_body(body)
411
+ request_body = []
412
+ body.files.each do |k, v|
413
+ request_body.push :content => v.content,
414
+ 'Content-Type' => v.content_type,
415
+ 'Content-Disposition' => "form-data; name=\"#{k}\"; filename=\"#{v.file_name}\"",
416
+ 'Content-Transfer-Encoding' => 'binary'
417
+ end
418
+
419
+ body.values.each do |k, v|
420
+ request_body << { :content => v,
421
+ 'Content-Disposition' => "form-data; name=\"#{k}\"" }
422
+ end
423
+ request_body
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end
@@ -0,0 +1,162 @@
1
+ require 'base64'
2
+ require 'worldline/acquiring/sdk/communication/request_header'
3
+ require 'worldline/acquiring/sdk/domain/data_object'
4
+ require 'worldline/acquiring/sdk/domain/shopping_cart_extension'
5
+ require 'worldline/acquiring/sdk/json/default_marshaller'
6
+
7
+ module Worldline
8
+ module Acquiring
9
+ module SDK
10
+ module Communication
11
+ # Manages metadata about the server using the SDK
12
+ #
13
+ # @attr_reader [Array<Worldline::Acquiring::SDK::Communication::RequestHeader>] metadata_headers List of headers that should be used in all requests.
14
+ class MetadataProvider
15
+ private
16
+
17
+ SDK_VERSION = '0.1.0'.freeze
18
+ SERVER_META_INFO_HEADER = 'X-WL-ServerMetaInfo'.freeze
19
+ PROHIBITED_HEADERS = [SERVER_META_INFO_HEADER, 'Date', 'Content-Type', 'Authorization'].sort!.freeze
20
+ CHARSET = 'utf-8'.freeze
21
+
22
+ public
23
+
24
+ # Stores metadata about the server so it can be sent to the Worldline Acquiring platform
25
+ class ServerMetaInfo < Worldline::Acquiring::SDK::Domain::DataObject
26
+ @platform_identifier = nil
27
+ @sdk_identifier = nil
28
+ @sdk_creator = nil
29
+ @integrator = nil
30
+ @shopping_cart_extension = nil
31
+
32
+ # String containing system information (Operating system and Ruby version).
33
+ attr_accessor :platform_identifier
34
+
35
+ # String containing this SDK version.
36
+ attr_accessor :sdk_identifier
37
+
38
+ attr_accessor :sdk_creator
39
+
40
+ attr_accessor :integrator
41
+
42
+ attr_accessor :shopping_cart_extension
43
+
44
+ # Returns the values of all attributes as a hash.
45
+ def to_h
46
+ hash = super
47
+ hash['platformIdentifier'] = @platform_identifier unless @platform_identifier.nil?
48
+ hash['sdkIdentifier'] = @sdk_identifier unless @sdk_identifier.nil?
49
+ hash['sdkCreator'] = @sdk_creator unless @sdk_creator.nil?
50
+ hash['integrator'] = @integrator unless @integrator.nil?
51
+ hash['shoppingCartExtension'] = @shopping_cart_extension.to_h unless @shopping_cart_extension.nil?
52
+ hash
53
+ end
54
+
55
+ # Initializes the ServerMetaInfo object with properties stored in the parameter hash
56
+ def from_hash(hash)
57
+ super
58
+ @platform_identifier = hash['platformIdentifier'] if hash.has_key? 'platformIdentifier'
59
+ @sdk_identifier = hash['sdkIdentifier'] if hash.has_key? 'sdkIdentifier'
60
+ @sdk_creator = hash['sdkCreator'] if hash.has_key? 'sdkCreator'
61
+ @integrator = hash['integrator'] if hash.has_key? 'integrator'
62
+ @shopping_cart_extension = Worldline::Acquiring::SDK::Domain::ShoppingCartExtension.new_from_hash(hash['shoppingCartExtension']) if hash.has_key? 'shoppingCartExtension'
63
+ end
64
+ end
65
+
66
+ # Create a new MetadataProvider instance that can be used to access platform-related information
67
+ #
68
+ # @param integrator [String] Name of the integrator
69
+ # @param shopping_cart_extension [Worldline::Acquiring::SDK::Domain::ShoppingCartExtension] shopping cart-related metadata.
70
+ # @param additional_request_headers [Array<Worldline::Acquiring::SDK::Communication::RequestHeader>] list of additional headers to include in all requests made.
71
+ # The following headers are not allowed due to conflicts with already added headers:
72
+ # 'Date', 'Content-Type', 'Authorization' and 'X-WL-ServerMetaInfo'
73
+ def initialize(integrator, shopping_cart_extension: nil, additional_request_headers: [].freeze)
74
+ raise ArgumentError.new('integrator is required') if integrator.nil? or integrator.strip.empty?
75
+ MetadataProvider.validate_additional_request_headers(additional_request_headers)
76
+ server_meta_info = ServerMetaInfo.new
77
+ server_meta_info.platform_identifier = get_platform_identifier
78
+ server_meta_info.sdk_identifier = get_sdk_identifier
79
+ server_meta_info.sdk_creator = 'Worldline'
80
+ server_meta_info.integrator = integrator
81
+ server_meta_info.shopping_cart_extension = shopping_cart_extension unless shopping_cart_extension.nil?
82
+ server_meta_info_string = Worldline::Acquiring::SDK::JSON::DefaultMarshaller.instance.marshal(server_meta_info)
83
+ server_meta_info_header = RequestHeader.new(
84
+ SERVER_META_INFO_HEADER, Base64.strict_encode64(
85
+ server_meta_info_string.force_encoding('iso-8859-1').encode(CHARSET)))
86
+ if additional_request_headers.nil? || additional_request_headers.empty?
87
+ @metadata_headers = [server_meta_info_header].freeze
88
+ else
89
+ request_headers = [server_meta_info_header]
90
+ request_headers += additional_request_headers
91
+ @metadata_headers = request_headers.freeze
92
+ end
93
+ end
94
+
95
+ # Checks that none of the {Worldline::Acquiring::SDK::Communication::RequestHeaders} in _additional_request_headers_ is equal to any of the forbidden headers.
96
+ # The forbidden headers are:
97
+ # 'Date', 'Content-Type', 'Authorization' and 'X-WL-ServerMetaInfo'
98
+ # If a header is found that is equal to one of the forbidden headers an ArgumentError is raised.
99
+ def self.validate_additional_request_headers(additional_request_headers)
100
+ unless additional_request_headers.nil?
101
+ additional_request_headers.each { |additional_request_header|
102
+ validate_additional_request_header(additional_request_header)
103
+ }
104
+ end
105
+ end
106
+
107
+ # Checks that the {Worldline::Acquiring::SDK::Communication::RequestHeaders} _additional_request_header_ is equal to any of the forbidden headers.
108
+ # The forbidden headers are:
109
+ # 'Date', 'Content-Type', 'Authorization' and 'X-WL-ServerMetaInfo'
110
+ # If the header is equal to one of the forbidden headers an ArgumentError is raised.
111
+ def self.validate_additional_request_header(additional_request_header)
112
+ if prohibited_headers.include? additional_request_header.name
113
+ raise ArgumentError.new('request header not allowed' + additional_request_header.name)
114
+ end
115
+ end
116
+
117
+ protected
118
+
119
+ # String containing information of the system using the SDK.
120
+ # It contains data like Operating System version and Ruby version
121
+ def get_platform_identifier
122
+ config = RbConfig::CONFIG['host_os']
123
+ if config.include? 'mingw'
124
+ s = 'Windows'
125
+ elsif config.include? 'linux'
126
+ s = 'Linux'
127
+ else
128
+ s = 'Mac OS X'
129
+ end
130
+ s + '/' + RUBY_DESCRIPTION
131
+ end
132
+
133
+ # String describing the version of the SDK being used
134
+ def get_sdk_identifier
135
+ 'RubyServerSDK/v' + SDK_VERSION
136
+ end
137
+
138
+ public
139
+
140
+ # Version of this SDK being used
141
+ def self.sdk_version
142
+ SDK_VERSION
143
+ end
144
+
145
+ # A {Worldline::Acquiring::SDK::Communication::RequestHeader} that contains serialized and encoded
146
+ # {Worldline::Acquiring::SDK::Communication::MetadataProvider::ServerMetaInfo}.
147
+ def self.server_meta_info_header
148
+ SERVER_META_INFO_HEADER
149
+ end
150
+
151
+ # A list of header names that should not be used by any added headers.
152
+ # These headers are reserved for specific purposes.
153
+ def self.prohibited_headers
154
+ PROHIBITED_HEADERS
155
+ end
156
+
157
+ attr_reader :metadata_headers
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,54 @@
1
+ require 'securerandom'
2
+
3
+ module Worldline
4
+ module Acquiring
5
+ module SDK
6
+ module Communication
7
+ # A representation of a multipart/form-data object
8
+ #
9
+ # @attr_reader [String] boundary
10
+ # @attr_reader [String] content_type
11
+ # @attr_reader [Hash] values
12
+ # @attr_reader [Hash] files
13
+ class MultipartFormDataObject
14
+ def initialize
15
+ @boundary = SecureRandom.uuid
16
+ @content_type = 'multipart/form-data; boundary=' + @boundary
17
+ @values = {}
18
+ @files = {}
19
+ end
20
+
21
+ attr_reader :boundary
22
+ attr_reader :content_type
23
+ attr_reader :values
24
+ attr_reader :files
25
+
26
+ def add_value(parameter_name, value)
27
+ if parameter_name.nil? || parameter_name.strip.empty?
28
+ raise ArgumentError, 'parameter_name is required'
29
+ end
30
+ raise ArgumentError, 'value is required' if value.nil?
31
+ if @values.include?(parameter_name) || @files.include?(parameter_name)
32
+ raise ArgumentError, 'duplicate parameterName: ' + parameter_name
33
+ end
34
+
35
+ @values[parameter_name] = value
36
+ end
37
+
38
+ # Adds a file to the multipart Form Data Object
39
+ def add_file(parameter_name, uploadable_file)
40
+ if parameter_name.nil? || parameter_name.strip.empty?
41
+ raise ArgumentError, 'parameter_name is required'
42
+ end
43
+ raise ArgumentError, 'uploadable_file is required' if uploadable_file.nil?
44
+ if @values.include?(parameter_name) || @files.include?(parameter_name)
45
+ raise ArgumentError, 'duplicate parameterName: ' + parameter_name
46
+ end
47
+
48
+ @files[parameter_name] = uploadable_file
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,15 @@
1
+ module Worldline
2
+ module Acquiring
3
+ module SDK
4
+ module Communication
5
+ # A representation of a multipart/form-data request
6
+ class MultipartFormDataRequest
7
+ # @return [Worldline::Acquiring::SDK::Communication::MultipartFormDataObject]
8
+ def to_multipart_form_data_object
9
+ raise NotImplementedError
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Worldline
2
+ module Acquiring
3
+ module SDK
4
+ module Communication
5
+ # Raised when a resource is not found on the Worldline Acquiring platform.
6
+ # This error corresponds to a 404 HTTP response.
7
+ #
8
+ # @attr [Exception] cause The error that is the cause of this error.
9
+ class NotFoundException < RuntimeError
10
+
11
+ def initialize(cause, message = nil)
12
+ super(message)
13
+ @cause = cause
14
+ end
15
+
16
+ attr_accessor :cause
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end