acquiring-sdk-ruby 0.1.0

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 (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