tasker-rb 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/DEVELOPMENT.md +548 -0
- data/README.md +87 -0
- data/ext/tasker_core/Cargo.lock +4720 -0
- data/ext/tasker_core/Cargo.toml +76 -0
- data/ext/tasker_core/extconf.rb +38 -0
- data/ext/tasker_core/src/CLAUDE.md +7 -0
- data/ext/tasker_core/src/bootstrap.rs +320 -0
- data/ext/tasker_core/src/bridge.rs +400 -0
- data/ext/tasker_core/src/client_ffi.rs +173 -0
- data/ext/tasker_core/src/conversions.rs +131 -0
- data/ext/tasker_core/src/diagnostics.rs +57 -0
- data/ext/tasker_core/src/event_handler.rs +179 -0
- data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
- data/ext/tasker_core/src/ffi_logging.rs +245 -0
- data/ext/tasker_core/src/global_event_system.rs +16 -0
- data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
- data/ext/tasker_core/src/lib.rs +41 -0
- data/ext/tasker_core/src/observability_ffi.rs +339 -0
- data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
- data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
- data/lib/tasker_core/bootstrap.rb +394 -0
- data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
- data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
- data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
- data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
- data/lib/tasker_core/domain_events.rb +43 -0
- data/lib/tasker_core/errors/CLAUDE.md +7 -0
- data/lib/tasker_core/errors/common.rb +305 -0
- data/lib/tasker_core/errors/error_classifier.rb +61 -0
- data/lib/tasker_core/errors.rb +4 -0
- data/lib/tasker_core/event_bridge.rb +330 -0
- data/lib/tasker_core/handlers.rb +159 -0
- data/lib/tasker_core/internal.rb +31 -0
- data/lib/tasker_core/logger.rb +234 -0
- data/lib/tasker_core/models.rb +337 -0
- data/lib/tasker_core/observability/types.rb +158 -0
- data/lib/tasker_core/observability.rb +292 -0
- data/lib/tasker_core/registry/handler_registry.rb +453 -0
- data/lib/tasker_core/registry/resolver_chain.rb +258 -0
- data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
- data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
- data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
- data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
- data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
- data/lib/tasker_core/registry/resolvers.rb +42 -0
- data/lib/tasker_core/registry.rb +12 -0
- data/lib/tasker_core/step_handler/api.rb +48 -0
- data/lib/tasker_core/step_handler/base.rb +354 -0
- data/lib/tasker_core/step_handler/batchable.rb +50 -0
- data/lib/tasker_core/step_handler/decision.rb +53 -0
- data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
- data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
- data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
- data/lib/tasker_core/step_handler/mixins.rb +66 -0
- data/lib/tasker_core/subscriber.rb +212 -0
- data/lib/tasker_core/task_handler/base.rb +254 -0
- data/lib/tasker_core/tasker_rb.so +0 -0
- data/lib/tasker_core/template_discovery.rb +181 -0
- data/lib/tasker_core/tracing.rb +166 -0
- data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
- data/lib/tasker_core/types/client_types.rb +145 -0
- data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
- data/lib/tasker_core/types/error_types.rb +72 -0
- data/lib/tasker_core/types/simple_message.rb +151 -0
- data/lib/tasker_core/types/step_context.rb +328 -0
- data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
- data/lib/tasker_core/types/step_message.rb +112 -0
- data/lib/tasker_core/types/step_types.rb +207 -0
- data/lib/tasker_core/types/task_template.rb +240 -0
- data/lib/tasker_core/types/task_types.rb +148 -0
- data/lib/tasker_core/types.rb +132 -0
- data/lib/tasker_core/version.rb +13 -0
- data/lib/tasker_core/worker/CLAUDE.md +7 -0
- data/lib/tasker_core/worker/event_poller.rb +224 -0
- data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
- data/lib/tasker_core.rb +160 -0
- metadata +322 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module TaskerCore
|
|
6
|
+
module StepHandler
|
|
7
|
+
module Mixins
|
|
8
|
+
# API mixin that provides HTTP functionality for step handlers.
|
|
9
|
+
# Use this mixin with Base to add HTTP capabilities to any handler.
|
|
10
|
+
#
|
|
11
|
+
# ## TAS-112: Composition Pattern
|
|
12
|
+
#
|
|
13
|
+
# This module follows the composition-over-inheritance pattern. Instead of
|
|
14
|
+
# inheriting from a specialized API handler class, include this mixin in
|
|
15
|
+
# your Base handler.
|
|
16
|
+
#
|
|
17
|
+
# ## Usage
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# class MyApiHandler < TaskerCore::StepHandler::Base
|
|
21
|
+
# include TaskerCore::StepHandler::Mixins::API
|
|
22
|
+
#
|
|
23
|
+
# def call(context)
|
|
24
|
+
# response = get('/users', params: { limit: 10 })
|
|
25
|
+
# success(result: response.body)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# ## Key Features
|
|
31
|
+
#
|
|
32
|
+
# - Faraday HTTP client with full configuration support
|
|
33
|
+
# - Automatic error classification (RetryableError vs PermanentError)
|
|
34
|
+
# - Retry-After header support for server-requested backoff
|
|
35
|
+
# - SSL configuration, headers, query parameters
|
|
36
|
+
module API
|
|
37
|
+
# Hook called when module is included
|
|
38
|
+
def self.included(base)
|
|
39
|
+
base.extend(ClassMethods)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Class methods added to including class
|
|
43
|
+
module ClassMethods
|
|
44
|
+
# No class methods needed for now
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Override capabilities to include API-specific features
|
|
48
|
+
def capabilities
|
|
49
|
+
super + %w[http_client error_classification retry_headers faraday_connection]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Enhanced configuration schema for API handlers
|
|
53
|
+
def config_schema
|
|
54
|
+
super.merge({ properties: get_merged_api_config(super[:properties]) })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ========================================================================
|
|
58
|
+
# HTTP CONNECTION
|
|
59
|
+
# ========================================================================
|
|
60
|
+
|
|
61
|
+
# Access to the configured Faraday connection
|
|
62
|
+
# @return [Faraday::Connection] Configured HTTP connection
|
|
63
|
+
def connection
|
|
64
|
+
@connection ||= build_faraday_connection
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Allow connection customization with block
|
|
68
|
+
# @yield [connection] Faraday connection for customization
|
|
69
|
+
def configure_connection(&)
|
|
70
|
+
@connection = build_faraday_connection(&)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ========================================================================
|
|
74
|
+
# CONVENIENCE HTTP METHODS
|
|
75
|
+
# ========================================================================
|
|
76
|
+
|
|
77
|
+
# Perform HTTP GET request with automatic error classification
|
|
78
|
+
# @param path [String] API endpoint path
|
|
79
|
+
# @param params [Hash] Query parameters
|
|
80
|
+
# @param headers [Hash] Additional headers
|
|
81
|
+
# @return [Faraday::Response] Raw response object
|
|
82
|
+
def get(path, params: {}, headers: {})
|
|
83
|
+
response = connection.get(path, params, headers)
|
|
84
|
+
process_response(response)
|
|
85
|
+
response
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Perform HTTP POST request with automatic error classification
|
|
89
|
+
# @param path [String] API endpoint path
|
|
90
|
+
# @param data [Hash] Request body data
|
|
91
|
+
# @param headers [Hash] Additional headers
|
|
92
|
+
# @return [Faraday::Response] Raw response object
|
|
93
|
+
def post(path, data: {}, headers: {})
|
|
94
|
+
response = connection.post(path, data, headers)
|
|
95
|
+
process_response(response)
|
|
96
|
+
response
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Perform HTTP PUT request with automatic error classification
|
|
100
|
+
# @param path [String] API endpoint path
|
|
101
|
+
# @param data [Hash] Request body data
|
|
102
|
+
# @param headers [Hash] Additional headers
|
|
103
|
+
# @return [Faraday::Response] Raw response object
|
|
104
|
+
def put(path, data: {}, headers: {})
|
|
105
|
+
response = connection.put(path, data, headers)
|
|
106
|
+
process_response(response)
|
|
107
|
+
response
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Perform HTTP DELETE request with automatic error classification
|
|
111
|
+
# @param path [String] API endpoint path
|
|
112
|
+
# @param params [Hash] Query parameters
|
|
113
|
+
# @param headers [Hash] Additional headers
|
|
114
|
+
# @return [Faraday::Response] Raw response object
|
|
115
|
+
def delete(path, params: {}, headers: {})
|
|
116
|
+
response = connection.delete(path, params, headers)
|
|
117
|
+
process_response(response)
|
|
118
|
+
response
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ========================================================================
|
|
122
|
+
# SPECIALIZED HTTP METHODS
|
|
123
|
+
# ========================================================================
|
|
124
|
+
|
|
125
|
+
# Upload file via multipart form data
|
|
126
|
+
# @param path [String] Upload endpoint path
|
|
127
|
+
# @param file_path [String] Path to file to upload
|
|
128
|
+
# @param field_name [String] Form field name for file
|
|
129
|
+
# @param additional_fields [Hash] Additional form fields
|
|
130
|
+
# @return [Hash] Response data
|
|
131
|
+
def api_upload_file(path, file_path, field_name: 'file', additional_fields: {})
|
|
132
|
+
unless File.exist?(file_path)
|
|
133
|
+
raise TaskerCore::PermanentError.new(
|
|
134
|
+
"File not found: #{file_path}",
|
|
135
|
+
error_code: 'FILE_NOT_FOUND',
|
|
136
|
+
error_category: 'validation'
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
payload = additional_fields.merge({
|
|
141
|
+
field_name => Faraday::Multipart::FilePart.new(file_path,
|
|
142
|
+
File.open(file_path).content_type || 'application/octet-stream')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
http_request(:post, path, multipart: payload)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Perform paginated request that handles cursor/offset pagination
|
|
149
|
+
# @param path [String] API endpoint path
|
|
150
|
+
# @param method [Symbol] HTTP method (:get, :post)
|
|
151
|
+
# @param pagination_key [String] Key for pagination parameter
|
|
152
|
+
# @param limit_key [String] Key for limit parameter
|
|
153
|
+
# @param max_pages [Integer] Maximum pages to fetch (safety limit)
|
|
154
|
+
# @yield [page_data] Block to process each page of data
|
|
155
|
+
# @return [Array] All collected results
|
|
156
|
+
def api_paginated_request(path, method: :get, pagination_key: 'cursor',
|
|
157
|
+
limit_key: 'limit', max_pages: 100)
|
|
158
|
+
results = []
|
|
159
|
+
pagination_value = nil
|
|
160
|
+
page_count = 0
|
|
161
|
+
|
|
162
|
+
loop do
|
|
163
|
+
page_count += 1
|
|
164
|
+
if page_count > max_pages
|
|
165
|
+
logger.warn("Pagination limit reached (#{max_pages} pages) for #{path}")
|
|
166
|
+
break
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
params = { limit_key => @config.dig(:pagination, :page_size) || 100 }
|
|
170
|
+
params[pagination_key] = pagination_value if pagination_value
|
|
171
|
+
|
|
172
|
+
response = http_request(method, path, params: params)
|
|
173
|
+
page_data = response[:data] || response['data'] || []
|
|
174
|
+
|
|
175
|
+
# Process page with block if provided
|
|
176
|
+
if block_given?
|
|
177
|
+
yield(page_data)
|
|
178
|
+
else
|
|
179
|
+
results.concat(Array(page_data))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check for next page
|
|
183
|
+
pagination_value = extract_pagination_cursor(response, pagination_key)
|
|
184
|
+
break unless pagination_value && page_data.any?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
results
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# ========================================================================
|
|
191
|
+
# RESULT HELPERS
|
|
192
|
+
# ========================================================================
|
|
193
|
+
|
|
194
|
+
# Return a successful API response result
|
|
195
|
+
#
|
|
196
|
+
# @param data [Object] Response data
|
|
197
|
+
# @param status [Integer] HTTP status code
|
|
198
|
+
# @param headers [Hash] Response headers
|
|
199
|
+
# @param execution_time_ms [Integer] Execution time in milliseconds
|
|
200
|
+
# @param metadata [Hash] Additional metadata
|
|
201
|
+
# @return [StepHandlerCallResult] Success result
|
|
202
|
+
def api_success(data:, status: 200, headers: {}, execution_time_ms: nil, metadata: {})
|
|
203
|
+
result = {
|
|
204
|
+
'data' => data,
|
|
205
|
+
'status' => status,
|
|
206
|
+
'headers' => headers
|
|
207
|
+
}
|
|
208
|
+
result['execution_time_ms'] = execution_time_ms if execution_time_ms
|
|
209
|
+
|
|
210
|
+
success(result: result, metadata: metadata.merge(api_call: true))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Return a failed API response result
|
|
214
|
+
#
|
|
215
|
+
# @param message [String] Error message
|
|
216
|
+
# @param status [Integer] HTTP status code
|
|
217
|
+
# @param error_type [String] Error type classification
|
|
218
|
+
# @param headers [Hash] Response headers
|
|
219
|
+
# @param execution_time_ms [Integer] Execution time in milliseconds
|
|
220
|
+
# @param metadata [Hash] Additional metadata
|
|
221
|
+
# @return [StepHandlerCallResult] Error result
|
|
222
|
+
def api_failure(message:, status: nil, error_type: nil, headers: {}, execution_time_ms: nil, metadata: {})
|
|
223
|
+
# Classify error type based on status code if not provided
|
|
224
|
+
classified_error_type = error_type || classify_error_type(status)
|
|
225
|
+
retryable = retryable_status?(status)
|
|
226
|
+
|
|
227
|
+
failure(
|
|
228
|
+
message: message,
|
|
229
|
+
error_type: classified_error_type,
|
|
230
|
+
retryable: retryable,
|
|
231
|
+
metadata: metadata.merge(
|
|
232
|
+
api_call: true,
|
|
233
|
+
status: status,
|
|
234
|
+
headers: headers,
|
|
235
|
+
execution_time_ms: execution_time_ms
|
|
236
|
+
).compact
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# ========================================================================
|
|
241
|
+
# RESPONSE PROCESSING
|
|
242
|
+
# ========================================================================
|
|
243
|
+
|
|
244
|
+
# Process HTTP response and classify errors
|
|
245
|
+
# @param response [Faraday::Response] HTTP response to process
|
|
246
|
+
def process_response(response)
|
|
247
|
+
return response if response.success?
|
|
248
|
+
|
|
249
|
+
case response.status
|
|
250
|
+
when 400, 401, 403, 404, 422
|
|
251
|
+
# Client errors - permanent failures (don't retry)
|
|
252
|
+
raise TaskerCore::PermanentError.new(
|
|
253
|
+
"HTTP #{response.status}: #{response.reason_phrase}",
|
|
254
|
+
error_code: "HTTP_#{response.status}",
|
|
255
|
+
error_category: classify_client_error(response.status),
|
|
256
|
+
context: {
|
|
257
|
+
status: response.status,
|
|
258
|
+
body: response.body,
|
|
259
|
+
headers: response.headers.to_h
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
when 429
|
|
263
|
+
# Rate limiting - retryable with server-suggested backoff
|
|
264
|
+
retry_after = extract_retry_after_header(response.headers)
|
|
265
|
+
raise TaskerCore::RetryableError.new(
|
|
266
|
+
"Rate limited: #{response.reason_phrase}",
|
|
267
|
+
retry_after: retry_after,
|
|
268
|
+
error_category: 'rate_limit',
|
|
269
|
+
context: {
|
|
270
|
+
status: response.status,
|
|
271
|
+
retry_after: retry_after,
|
|
272
|
+
body: response.body,
|
|
273
|
+
headers: response.headers.to_h
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
when 503
|
|
277
|
+
# Service unavailable - retryable with server-suggested backoff
|
|
278
|
+
retry_after = extract_retry_after_header(response.headers)
|
|
279
|
+
raise TaskerCore::RetryableError.new(
|
|
280
|
+
"Service unavailable: #{response.reason_phrase}",
|
|
281
|
+
retry_after: retry_after,
|
|
282
|
+
error_category: 'service_unavailable',
|
|
283
|
+
context: {
|
|
284
|
+
status: response.status,
|
|
285
|
+
retry_after: retry_after,
|
|
286
|
+
body: response.body,
|
|
287
|
+
headers: response.headers.to_h
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
when 500..599
|
|
291
|
+
# Other server errors - retryable without forced backoff
|
|
292
|
+
raise TaskerCore::RetryableError.new(
|
|
293
|
+
"Server error: HTTP #{response.status} #{response.reason_phrase}",
|
|
294
|
+
error_category: 'server_error',
|
|
295
|
+
context: {
|
|
296
|
+
status: response.status,
|
|
297
|
+
body: response.body,
|
|
298
|
+
headers: response.headers.to_h
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
else
|
|
302
|
+
# Unknown status codes - treat as retryable for safety
|
|
303
|
+
raise TaskerCore::RetryableError.new(
|
|
304
|
+
"Unknown HTTP status: #{response.status} #{response.reason_phrase}",
|
|
305
|
+
error_category: 'unknown',
|
|
306
|
+
context: {
|
|
307
|
+
status: response.status,
|
|
308
|
+
body: response.body,
|
|
309
|
+
headers: response.headers.to_h
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private
|
|
316
|
+
|
|
317
|
+
# Classify error type based on HTTP status code
|
|
318
|
+
def classify_error_type(status)
|
|
319
|
+
case status
|
|
320
|
+
when 400..499 then 'PermanentError'
|
|
321
|
+
when 500..599 then 'RetryableError'
|
|
322
|
+
else 'UnexpectedError'
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Determine if status code indicates retryable error
|
|
327
|
+
def retryable_status?(status)
|
|
328
|
+
return false if status.nil?
|
|
329
|
+
|
|
330
|
+
status >= 500 || status == 429
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def get_merged_api_config(config)
|
|
334
|
+
::TaskerCore::ConfigSchemas::API_CONFIG_SCHEMA.dup.merge(config)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Build Faraday connection with configuration
|
|
338
|
+
def build_faraday_connection
|
|
339
|
+
base_url = config[:url] || config['url']
|
|
340
|
+
|
|
341
|
+
Faraday.new(base_url) do |conn|
|
|
342
|
+
# Apply configuration
|
|
343
|
+
apply_connection_config(conn)
|
|
344
|
+
|
|
345
|
+
# Apply custom configuration block if provided
|
|
346
|
+
yield(conn) if block_given?
|
|
347
|
+
|
|
348
|
+
# Default adapter (must be last)
|
|
349
|
+
conn.adapter Faraday.default_adapter unless conn.builder.handlers.any? { |h| h.klass < Faraday::Adapter }
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Apply configuration to Faraday connection
|
|
354
|
+
def apply_connection_config(conn)
|
|
355
|
+
# Get API timeouts from configuration
|
|
356
|
+
api_timeouts = TaskerCore::Config.instance.api_timeouts
|
|
357
|
+
|
|
358
|
+
# Timeouts - use config values or TaskerCore configuration defaults
|
|
359
|
+
conn.options.timeout = config[:timeout] || config['timeout'] || api_timeouts[:timeout]
|
|
360
|
+
conn.options.open_timeout = config[:open_timeout] || config['open_timeout'] || api_timeouts[:open_timeout]
|
|
361
|
+
|
|
362
|
+
# SSL configuration
|
|
363
|
+
if (ssl_config = config[:ssl] || config['ssl'])
|
|
364
|
+
conn.ssl.merge!(ssl_config.transform_keys(&:to_sym))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Headers
|
|
368
|
+
if (headers = config[:headers] || config['headers'])
|
|
369
|
+
headers.each { |key, value| conn.headers[key] = value }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Query parameters
|
|
373
|
+
if (params = config[:params] || config['params'])
|
|
374
|
+
conn.params.merge!(params)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Authentication
|
|
378
|
+
apply_authentication(conn)
|
|
379
|
+
|
|
380
|
+
# Request/response middleware
|
|
381
|
+
conn.request :json
|
|
382
|
+
conn.response :json
|
|
383
|
+
|
|
384
|
+
# Logging (only in debug mode)
|
|
385
|
+
return unless logger.level <= Logger::DEBUG
|
|
386
|
+
|
|
387
|
+
conn.response :logger, logger, { headers: false, bodies: false }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Apply authentication to connection
|
|
391
|
+
def apply_authentication(conn)
|
|
392
|
+
auth_config = config[:auth] || config['auth'] || {}
|
|
393
|
+
|
|
394
|
+
case auth_config[:type] || auth_config['type']
|
|
395
|
+
when 'bearer'
|
|
396
|
+
token = auth_config[:token] || auth_config['token']
|
|
397
|
+
conn.request :authorization, 'Bearer', token if token
|
|
398
|
+
when 'basic'
|
|
399
|
+
username = auth_config[:username] || auth_config['username']
|
|
400
|
+
password = auth_config[:password] || auth_config['password']
|
|
401
|
+
conn.request :authorization, :basic, username, password if username && password
|
|
402
|
+
when 'api_key'
|
|
403
|
+
token = auth_config[:token] || auth_config['token']
|
|
404
|
+
header = auth_config[:api_key_header] || auth_config['api_key_header'] || 'X-API-Key'
|
|
405
|
+
conn.headers[header] = token if token
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Extract retry-after header value
|
|
410
|
+
def extract_retry_after_header(headers)
|
|
411
|
+
TaskerCore::ErrorClassification.extract_retry_after(headers)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Extract pagination cursor from response
|
|
415
|
+
def extract_pagination_cursor(response, pagination_key)
|
|
416
|
+
data = response[:data] || response['data']
|
|
417
|
+
|
|
418
|
+
# Check for cursor in response metadata
|
|
419
|
+
meta = response[:meta] || response['meta'] || response[:pagination] || response['pagination']
|
|
420
|
+
return meta[pagination_key] || meta[pagination_key.to_s] if meta
|
|
421
|
+
|
|
422
|
+
# Check for cursor in response body
|
|
423
|
+
return data[pagination_key] || data[pagination_key.to_s] if data.is_a?(Hash)
|
|
424
|
+
|
|
425
|
+
# Check for next URL in headers (Link header)
|
|
426
|
+
if (headers = response[:headers] || response['headers'])
|
|
427
|
+
link_header = headers['Link'] || headers['link']
|
|
428
|
+
return extract_param_from_link_header(link_header, pagination_key) if link_header&.include?('rel="next"')
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
nil
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Simple link header parsing for pagination
|
|
435
|
+
def extract_param_from_link_header(link_header, param_key)
|
|
436
|
+
match = link_header.match(/[?&]#{param_key}=([^&>]+)/)
|
|
437
|
+
match ? match[1] : nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Classify client error category by status code
|
|
441
|
+
def classify_client_error(status_code)
|
|
442
|
+
case status_code
|
|
443
|
+
when 400, 422 then 'validation'
|
|
444
|
+
when 401, 403 then 'authorization'
|
|
445
|
+
when 404 then 'not_found'
|
|
446
|
+
else 'client_error'
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|