simplyq 0.8.0rc

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+
6
+ module Simplyq
7
+ module API
8
+ class EventAPI
9
+ attr_reader :client
10
+
11
+ API_PATH = "/v1/application/{app_id}/event"
12
+ API_RETRIEVE_PATH = "/v1/application/{app_id}/event/{event_id}"
13
+ API_DELIVERY_ATTEMPTS_PATH = "/v1/application/{app_id}/event/{event_id}/delivery_attempt"
14
+ API_DELIVERY_ATTEMPT_PATH = "/v1/application/{app_id}/event/{event_id}/delivery_attempt/{delivery_attempt_id}/"
15
+ API_ENDPOINTS_PATH = "/v1/application/{app_id}/event/{event_id}/endpoint"
16
+ API_RETRY_PATH = "/v1/application/{app_id}/endpoint/{endpoint_id}/event/{event_id}"
17
+
18
+ # Initializes a new API object.
19
+ #
20
+ # @param client [Simplyq::Client] the client object that will be used to
21
+ # make HTTP requests.
22
+ def initialize(client)
23
+ @client = client
24
+ end
25
+
26
+ def retrieve(application_id, event_id)
27
+ path = API_RETRIEVE_PATH.gsub("{app_id}", application_id.to_s).gsub("{event_id}", event_id.to_s)
28
+
29
+ data, status, headers = client.call_api(:get, path)
30
+ decerialize(data)
31
+ end
32
+
33
+ def list(application_id, params = {})
34
+ path = API_PATH.gsub("{app_id}", application_id.to_s)
35
+
36
+ data, status, headers = client.call_api(:get, path, { query_params: params })
37
+ decerialize_list(data, params: params, list_args: [application_id])
38
+ end
39
+
40
+ def create(application_id, event)
41
+ path = API_PATH.gsub("{app_id}", application_id.to_s)
42
+
43
+ data, status, headers = client.call_api(:post, path, { body: build_model(event).to_h })
44
+ decerialize(data)
45
+ end
46
+
47
+ def retrieve_delivery_attempts(application_id, event_id, params = {})
48
+ path = API_DELIVERY_ATTEMPTS_PATH.gsub("{app_id}", application_id.to_s).gsub("{event_id}", event_id.to_s)
49
+
50
+ data, status, headers = client.call_api(:get, path, { query_params: params })
51
+ decerialize_delivery_attempts_list(data, params: params, list_args: [application_id, event_id])
52
+ end
53
+
54
+ def retrieve_endpoints(application_id, event_id, params = {})
55
+ path = API_ENDPOINTS_PATH.gsub("{app_id}", application_id.to_s).gsub("{event_id}", event_id.to_s)
56
+
57
+ data, status, headers = client.call_api(:get, path, { query_params: params })
58
+ decerialize_endpoints_list(data, params: params, list_args: [application_id, event_id])
59
+ end
60
+
61
+ def retry(application_id, endpoint_id, event_id)
62
+ path = API_RETRY_PATH.gsub("{app_id}", application_id.to_s)
63
+ .gsub("{endpoint_id}", endpoint_id.to_s)
64
+ .gsub("{event_id}", event_id.to_s)
65
+
66
+ data, status, headers = client.call_api(:post, path)
67
+ status == 202
68
+ end
69
+
70
+ def retrieve_delivery_attempt(application_id, event_id, delivery_attempt_id)
71
+ path = API_DELIVERY_ATTEMPT_PATH.gsub("{app_id}", application_id.to_s)
72
+ .gsub("{event_id}", event_id.to_s)
73
+ .gsub("{delivery_attempt_id}", delivery_attempt_id.to_s)
74
+
75
+ data, status, headers = client.call_api(:get, path)
76
+ decerialize_delivery_attempt(data)
77
+ end
78
+
79
+ private def build_model(data)
80
+ return data if data.is_a?(Simplyq::Model::Event)
81
+ raise ArgumentError, "Invalid data must be a Simplyq::Model::Event or Hash" unless data.is_a?(Hash)
82
+
83
+ Simplyq::Model::Event.from_hash(data)
84
+ end
85
+
86
+ private def decerialize(json_data)
87
+ data = body_to_json(json_data)
88
+
89
+ Simplyq::Model::Event.from_hash(data)
90
+ end
91
+
92
+ private def decerialize_delivery_attempt(json_data)
93
+ data = body_to_json(json_data)
94
+
95
+ Simplyq::Model::DeliveryAttempt.from_hash(data)
96
+ end
97
+
98
+ private def decerialize_list(json_data, params: {}, list_args: [])
99
+ data = body_to_json(json_data)
100
+
101
+ Simplyq::Model::List.new(
102
+ Simplyq::Model::Event, data,
103
+ api_method: :list,
104
+ list_args: list_args,
105
+ filters: params, api: self
106
+ )
107
+ end
108
+
109
+ private def decerialize_delivery_attempts_list(json_data, params: {}, list_args: [])
110
+ data = body_to_json(json_data)
111
+
112
+ Simplyq::Model::List.new(
113
+ Simplyq::Model::DeliveryAttempt, data,
114
+ api_method: :retrieve_delivery_attempts,
115
+ list_args: list_args,
116
+ filters: params, api: self
117
+ )
118
+ end
119
+
120
+ private def decerialize_endpoints_list(json_data, params: {}, list_args: [])
121
+ data = body_to_json(json_data)
122
+
123
+ Simplyq::Model::List.new(
124
+ Simplyq::Model::Endpoint, data,
125
+ api_method: :retrieve_endpoints,
126
+ list_args: list_args,
127
+ filters: params, api: self
128
+ )
129
+ end
130
+
131
+ private def body_to_json(body)
132
+ return if body.nil?
133
+
134
+ JSON.parse(body, symbolize_names: true)
135
+ rescue JSON::ParserError
136
+ raise Simplyq::APIError.new("Invalid JSON in response body.", http_body: body)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Simplyq
7
+ class Client
8
+ HEADER_AUTHORIZATION = "Authorization"
9
+
10
+ USER_AGENT = "simplyq-ruby/#{Simplyq::VERSION}"
11
+
12
+ attr_reader :config
13
+
14
+ # Initialize client to connect to SimplyQ API
15
+ #
16
+ # @param config_options [Simplyq::Configuration|Hash] a configuration object or a hash of configuration options
17
+ def initialize(config_options = {})
18
+ @config = case config_options
19
+ when Hash
20
+ Simplyq::Configuration.setup do |config|
21
+ config_options.each do |key, value|
22
+ config.send("#{key}=", value)
23
+ end
24
+ end
25
+ when Simplyq::Configuration
26
+ config_options
27
+ else
28
+ raise ArgumentError, "Invalid configuration options #{config_options}"
29
+ end
30
+ @default_headers = {
31
+ "Content-Type" => "application/json",
32
+ "User-Agent" => USER_AGENT
33
+ }
34
+ end
35
+
36
+ def applications
37
+ @applications ||= Simplyq::API::ApplicationAPI.new(self)
38
+ end
39
+
40
+ def endpoints
41
+ @endpoints ||= Simplyq::API::EndpointAPI.new(self)
42
+ end
43
+
44
+ def events
45
+ @events ||= Simplyq::API::EventAPI.new(self)
46
+ end
47
+
48
+ def check_api_key!
49
+ raise AuthenticationError, "No API key provided." unless config.api_key
50
+
51
+ raise AuthenticationError, "Invalid API key as it includes spaces" if config.api_key =~ /\s/
52
+ end
53
+
54
+ ERROR_MESSAGE_CONNECTION =
55
+ "Unexpected error communicating when trying to connect to " \
56
+ "SimplyQ (%s). You may be seeing this message because your DNS is not " \
57
+ "working or you don't have an internet connection. To check, try " \
58
+ "running `host api.simplyq.com` from the command line."
59
+ ERROR_MESSAGE_SSL =
60
+ "Could not establish a secure connection to SimplyQ (%s), you " \
61
+ "may need to upgrade your OpenSSL version. To check, try running " \
62
+ "`openssl s_client -connect api.simplyq.com:443` from the command " \
63
+ "line."
64
+
65
+ ERROR_MESSAGE_TIMEOUT_SUFFIX =
66
+ "Please check your internet connection and try again. " \
67
+ "If this problem persists, you should check SimplyQ's service " \
68
+ "status at https://simplyq.statuspage.io, or let us know at " \
69
+ "support@simplyq.io."
70
+
71
+ ERROR_MESSAGE_TIMEOUT_CONNECT =
72
+ "Timed out connecting to SimplyQ (%s). #{ERROR_MESSAGE_TIMEOUT_SUFFIX}"
73
+
74
+ ERROR_MESSAGE_TIMEOUT_READ =
75
+ "Timed out communicating with SimplyQ (%s). #{ERROR_MESSAGE_TIMEOUT_SUFFIX}"
76
+
77
+ NETWORK_ERROR_MESSAGES_MAP = {
78
+ EOFError => ERROR_MESSAGE_CONNECTION,
79
+ Errno::ECONNREFUSED => ERROR_MESSAGE_CONNECTION,
80
+ Errno::ECONNRESET => ERROR_MESSAGE_CONNECTION,
81
+ Errno::EHOSTUNREACH => ERROR_MESSAGE_CONNECTION,
82
+ Errno::ETIMEDOUT => ERROR_MESSAGE_TIMEOUT_CONNECT,
83
+ SocketError => ERROR_MESSAGE_CONNECTION,
84
+
85
+ Net::OpenTimeout => ERROR_MESSAGE_TIMEOUT_CONNECT,
86
+ Net::ReadTimeout => ERROR_MESSAGE_TIMEOUT_READ,
87
+
88
+ Faraday::TimeoutError => ERROR_MESSAGE_TIMEOUT_READ,
89
+ Faraday::ConnectionFailed => ERROR_MESSAGE_CONNECTION,
90
+
91
+ OpenSSL::SSL::SSLError => ERROR_MESSAGE_SSL,
92
+ Faraday::SSLError => ERROR_MESSAGE_SSL
93
+ }.freeze
94
+ private_constant :NETWORK_ERROR_MESSAGES_MAP
95
+
96
+ # Call an API with given options.
97
+ #
98
+ # @return [Array<(Object, Integer, Hash)>] an array of 3 elements:
99
+ # the data deserialized from response body (could be nil), response status code and response headers.
100
+ def call_api(http_method, path, opts = {})
101
+ check_api_key!
102
+
103
+ begin
104
+ response = connection.public_send(http_method.to_sym.downcase) do |req|
105
+ build_request(http_method, path, req, opts)
106
+ end
107
+
108
+ config.logger.debug "HTTP response body ~BEGIN~\n#{response.body}\n~END~\n" if config.debugging
109
+
110
+ unless response.success?
111
+ if response.status.zero?
112
+ # Errors from libcurl will be made visible here
113
+ raise ApiError.new(response.reason_phrase, http_status: 0)
114
+ else
115
+ raise specific_http_error(response, get_http_error_data(response).merge(params: opts[:query_params]))
116
+ end
117
+ end
118
+ rescue *NETWORK_ERROR_MESSAGES_MAP.keys => e
119
+ handle_network_error(e)
120
+ end
121
+
122
+ [response.body, response.status, response.headers]
123
+ end
124
+
125
+ def handle_network_error(error)
126
+ errors, message = NETWORK_ERROR_MESSAGES_MAP.detect do |(e, _)|
127
+ error.is_a?(e)
128
+ end
129
+
130
+ if errors.nil?
131
+ message = "Unexpected error #{error.class.name} communicating " \
132
+ "with SimplyQ. Please let us know at support@simplyq.io."
133
+ end
134
+
135
+ message = message % config.base_url
136
+ message += "\n\n(Network error: #{error.message})"
137
+
138
+ raise APIConnectionError.new(message, http_status: 0, error: error)
139
+ end
140
+
141
+ def get_http_error_data(response)
142
+ body = safe_json_parse_body(response)
143
+ if body.is_a?(Hash)
144
+ message = body[:error] || body[:message]
145
+
146
+ message = "Invalid request" if message.nil? && body[:errors]
147
+
148
+ return {
149
+ message: message,
150
+ errors: body[:errors],
151
+ code: body[:code]
152
+ }
153
+ end
154
+
155
+ { message: response.reason_phrase }
156
+ end
157
+
158
+ def safe_json_parse_body(response)
159
+ return nil if response.body.nil?
160
+
161
+ JSON.parse(response.body, symbolize_names: true)
162
+ rescue JSON::ParserError
163
+ nil
164
+ end
165
+
166
+ def specific_http_error(resp, error_data = {})
167
+ # The standard arguments that are passed to API exceptions
168
+ opts = {
169
+ http_body: resp.body,
170
+ http_headers: resp.headers,
171
+ http_status: resp.status,
172
+ code: error_data[:code]
173
+ }
174
+
175
+ case resp.status
176
+ when 400, 404, 422
177
+ case error_data[:type]
178
+ when "idempotency_error"
179
+ IdempotencyError.new(error_data[:message], **opts)
180
+ else
181
+ InvalidRequestError.new(
182
+ error_data[:message], error_data[:param],
183
+ **opts.merge(errors: error_data[:errors])
184
+ )
185
+ end
186
+ when 401
187
+ AuthenticationError.new(error_data[:message] || resp.reason_phrase, **opts)
188
+ when 402
189
+ PaymentRequiredError.new(error_data[:message] || resp.reason_phrase, **opts)
190
+ when 403
191
+ PermissionError.new(error_data[:message] || resp.reason_phrase, **opts)
192
+ when 429
193
+ RateLimitError.new(error_data[:message] || resp.reason_phrase, **opts)
194
+ else
195
+ APIError.new(error_data[:message] || resp.reason_phrase, **opts)
196
+ end
197
+ end
198
+
199
+ def build_request_url(path)
200
+ # Add leading and trailing slashes to path
201
+ path = "/#{path}".gsub(%r{/+}, "/")
202
+ @config.base_url + path
203
+ end
204
+
205
+ # Builds the HTTP request
206
+ #
207
+ # @param [String] http_method HTTP method/verb (e.g. POST)
208
+ # @param [String] path URL path (e.g. /account/new)
209
+ # @option opts [Hash] :header_params Header parameters
210
+ # @option opts [Hash] :query_params Query parameters
211
+ # @option opts [Hash] :form_params Query parameters
212
+ # @option opts [Object] :body HTTP body (JSON/XML)
213
+ # @return [Faraday::Request] A Faraday Request
214
+ def build_request(http_method, path, request, opts = {})
215
+ url = build_request_url(path)
216
+ http_method = http_method.to_sym.downcase
217
+
218
+ header_params = @default_headers.merge(opts[:header_params] || {})
219
+ query_params = opts[:query_params] || {}
220
+ form_params = opts[:form_params] || {}
221
+
222
+ header_params[HEADER_AUTHORIZATION] = config.auth_api_key
223
+
224
+ if %i[post patch put delete].include?(http_method)
225
+ req_body = build_request_body(header_params, form_params, opts[:body])
226
+ config.logger.debug "HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n" if config.debugging
227
+ end
228
+ request.headers = header_params
229
+ request.body = req_body
230
+
231
+ # Overload default options only if provided
232
+ request.options.timeout = config.timeout if config.timeout
233
+
234
+ request.url url
235
+ request.params = query_params
236
+ download_file(request) if opts[:return_type] == "File" || opts[:return_type] == "Binary"
237
+ request
238
+ end
239
+
240
+ # Builds the HTTP request body
241
+ #
242
+ # @param [Hash] header_params Header parameters
243
+ # @param [Hash] form_params Query parameters
244
+ # @param [Object] body HTTP body (JSON/XML)
245
+ # @return [String] HTTP body data in the form of string
246
+ def build_request_body(header_params, form_params, body)
247
+ # http form
248
+ if header_params["Content-Type"] == "application/x-www-form-urlencoded"
249
+ data = URI.encode_www_form(form_params)
250
+ elsif header_params["Content-Type"] == "multipart/form-data"
251
+ data = {}
252
+ form_params.each do |key, value|
253
+ data[key] = case value
254
+ when ::File, ::Tempfile
255
+ Faraday::FilePart.new(value.path, "application/octet-stream", value.path)
256
+ when ::Array, nil
257
+ # let Faraday handle Array and nil parameters
258
+ value
259
+ else
260
+ value.to_s
261
+ end
262
+ end
263
+ elsif body
264
+ data = body.is_a?(String) ? body : body.to_json
265
+ else
266
+ data = nil
267
+ end
268
+ data
269
+ end
270
+
271
+ def connection
272
+ @connection ||= build_connection
273
+ end
274
+
275
+ def build_connection
276
+ Faraday.new(url: config.base_url, ssl: ssl_options, proxy: config.proxy) do |conn|
277
+ basic_auth(conn)
278
+ config.configure_middleware(conn)
279
+ yield(conn) if block_given?
280
+ conn.adapter(Faraday.default_adapter)
281
+ end
282
+ end
283
+
284
+ def ssl_options
285
+ {
286
+ ca_file: config.ssl_ca_file,
287
+ verify: config.ssl_verify,
288
+ verify_mode: config.ssl_verify_mode,
289
+ client_cert: config.ssl_client_cert,
290
+ client_key: config.ssl_client_key
291
+ }
292
+ end
293
+
294
+ def basic_auth(conn)
295
+ if config.username && config.password
296
+ if Gem::Version.new(Faraday::VERSION) >= Gem::Version.new("2.0")
297
+ conn.request(:authorization, :basic, config.username, config.password)
298
+ else
299
+ conn.request(:basic_auth, config.username, config.password)
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Simplyq
6
+ class Configuration
7
+ # Defines API keys used with API Key authentications.
8
+ attr_accessor :api_key
9
+
10
+ # Defines the api version
11
+ attr_accessor :api_version
12
+
13
+ # Defines the logger used for debugging.
14
+ # Default to `Rails.logger` (when in Rails) or logging to STDOUT.
15
+ #
16
+ # @return [#debug]
17
+ attr_accessor :logger
18
+
19
+ # Defines the username used with HTTP basic authentication.
20
+ #
21
+ # @return [String]
22
+ attr_accessor :username
23
+
24
+ # Defines the password used with HTTP basic authentication.
25
+ #
26
+ # @return [String]
27
+ attr_accessor :password
28
+
29
+ # Set this to false to skip client side validation in the operation.
30
+ # Default to true.
31
+ # @return [true, false]
32
+ attr_accessor :client_side_validation
33
+
34
+ ### TLS/SSL setting
35
+ # Set this to false to skip verifying SSL certificate when calling API from https server.
36
+ # Default to true.
37
+ #
38
+ # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks.
39
+ #
40
+ # @return [true, false]
41
+ attr_accessor :ssl_verify
42
+
43
+ ### TLS/SSL setting
44
+ # Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html)
45
+ #
46
+ # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks.
47
+ #
48
+ attr_accessor :ssl_verify_mode
49
+
50
+ ### TLS/SSL setting
51
+ # Set this to customize the certificate file to verify the peer.
52
+ #
53
+ # @return [String] the path to the certificate file
54
+ attr_accessor :ssl_ca_file
55
+
56
+ ### TLS/SSL setting
57
+ # Client certificate file (for client certificate)
58
+ attr_accessor :ssl_client_cert
59
+
60
+ ### TLS/SSL setting
61
+ # Client private key file (for client certificate)
62
+ attr_accessor :ssl_client_key
63
+
64
+ ### Proxy setting
65
+ # HTTP Proxy settings
66
+ attr_accessor :proxy
67
+
68
+ attr_accessor :timeout
69
+
70
+ attr_accessor :base_url
71
+
72
+ attr_accessor :debugging
73
+
74
+ attr_reader :open_timeout
75
+ attr_reader :read_timeout
76
+ attr_reader :write_timeout
77
+
78
+ def self.setup
79
+ new.tap do |instance|
80
+ yield(instance) if block_given?
81
+ end
82
+ end
83
+
84
+ def initialize
85
+ @timeout = 30
86
+ @open_timeout = 30
87
+ @read_timeout = 80
88
+ @write_timeout = 30
89
+
90
+ @client_side_validation = true
91
+
92
+ @base_url = "https://api.simplyq.io"
93
+ @middlewares = Hash.new { |h, k| h[k] = [] }
94
+ @logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
95
+
96
+ yield(self) if block_given?
97
+ end
98
+
99
+ # The default Configuration object.
100
+ def self.default
101
+ Configuration.new
102
+ end
103
+
104
+ # Gets Basic Auth token string
105
+ def basic_auth_token
106
+ "Basic #{["#{username}:#{password}"].pack("m").delete("\r\n")}"
107
+ end
108
+
109
+ def auth_api_key
110
+ "Bearer #{@api_key}"
111
+ end
112
+
113
+ # TODO: Remove
114
+ # def base_path=(base_path)
115
+ # # Add leading and trailing slashes to base_path
116
+ # @base_path = "/#{base_path}".gsub(%r{/+}, "/")
117
+ # @base_path = "" if @base_path == "/"
118
+ # end
119
+
120
+ # TODO: Remove
121
+ # Returns base URL for specified operation based on server settings
122
+ # def base_url(operation = nil)
123
+ # index = server_operation_index.fetch(operation, server_index)
124
+ # return "#{scheme}://#{[host, base_path].join("/").gsub(%r{/+}, "/")}".sub(%r{/+\z}, "") if index.nil?
125
+
126
+ # server_url(index, server_operation_variables.fetch(operation, server_variables),
127
+ # operation_server_settings[operation])
128
+ # end
129
+
130
+ # Adds middleware to the stack
131
+ def use(*middleware)
132
+ set_faraday_middleware(:use, *middleware)
133
+ end
134
+
135
+ # Adds request middleware to the stack
136
+ def request(*middleware)
137
+ set_faraday_middleware(:request, *middleware)
138
+ end
139
+
140
+ # Adds response middleware to the stack
141
+ def response(*middleware)
142
+ set_faraday_middleware(:response, *middleware)
143
+ end
144
+
145
+ # Adds Faraday middleware setting information to the stack
146
+ #
147
+ # @example Use the `set_faraday_middleware` method to set middleware information
148
+ # config.set_faraday_middleware(:request, :retry, max: 3, methods: [:get, :post], retry_statuses: [503])
149
+ # config.set_faraday_middleware(:response, :logger, nil, { bodies: true, log_level: :debug })
150
+ # config.set_faraday_middleware(:use, Faraday::HttpCache, store: Rails.cache, shared_cache: false)
151
+ # config.set_faraday_middleware(:insert, 0, FaradayMiddleware::FollowRedirects, { standards_compliant: true, limit: 1 })
152
+ # config.set_faraday_middleware(:swap, 0, Faraday::Response::Logger)
153
+ # config.set_faraday_middleware(:delete, Faraday::Multipart::Middleware)
154
+ #
155
+ # @see https://github.com/lostisland/faraday/blob/v2.3.0/lib/faraday/rack_builder.rb#L92-L143
156
+ def set_faraday_middleware(operation, key, *args, &block)
157
+ unless %i[request response use insert insert_before insert_after swap delete].include?(operation)
158
+ raise ArgumentError, "Invalid faraday middleware operation #{operation}. Must be" \
159
+ " :request, :response, :use, :insert, :insert_before, :insert_after, :swap or :delete."
160
+ end
161
+
162
+ @middlewares[operation] << [key, args, block]
163
+ end
164
+ ruby2_keywords(:set_faraday_middleware) if respond_to?(:ruby2_keywords, true)
165
+
166
+ # Set up middleware on the connection
167
+ def configure_middleware(connection)
168
+ return if @middlewares.empty?
169
+
170
+ %i[request response use insert insert_before insert_after swap].each do |operation|
171
+ next unless @middlewares.key?(operation)
172
+
173
+ @middlewares[operation].each do |key, args, block|
174
+ connection.builder.send(operation, key, *args, &block)
175
+ end
176
+ end
177
+
178
+ if @middlewares.key?(:delete)
179
+ @middlewares[:delete].each do |key, _args, _block|
180
+ connection.builder.delete(key)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end