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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ # require_relative '../types' # Temporarily disabled due to dry-types version issues
7
+
8
+ module TaskerCore
9
+ module StepHandler
10
+ class Base
11
+ attr_reader :config, :logger, :rust_integration, :orchestration_system
12
+
13
+ def initialize(config: {}, logger: nil)
14
+ @config = config || {}
15
+ @logger = logger || TaskerCore::Logger.instance
16
+ orchestration_manager = TaskerCore::Internal::OrchestrationManager.instance
17
+ @orchestration_system = orchestration_manager.orchestration_system
18
+ end
19
+
20
+ # ========================================================================
21
+ # MAIN STEP EXECUTION INTERFACE (implemented by subclasses)
22
+ # ========================================================================
23
+
24
+ # Main business logic method - cross-language standard signature
25
+ #
26
+ # Subclasses implement the actual business logic in this method.
27
+ # The context provides unified access to all step execution data.
28
+ #
29
+ # @param context [TaskerCore::Types::StepContext] Unified step execution context
30
+ # @return [TaskerCore::Types::StepHandlerCallResult] Step execution result
31
+ #
32
+ # @example Basic handler
33
+ # def call(context)
34
+ # even_number = context.get_task_field('even_number')
35
+ # result = even_number * 2
36
+ # success(result: result)
37
+ # end
38
+ #
39
+ # @example Handler with dependencies
40
+ # def call(context)
41
+ # prev_result = context.get_dependency_result('step_1')
42
+ # success(result: prev_result + 1)
43
+ # end
44
+ #
45
+ # @example Accessing context fields
46
+ # def call(context)
47
+ # # Cross-language standard fields
48
+ # context.task_uuid # Task UUID
49
+ # context.step_uuid # Step UUID
50
+ # context.input_data # Step inputs
51
+ # context.step_config # Handler configuration
52
+ # context.retry_count # Current retry attempt
53
+ # context.max_retries # Maximum retries allowed
54
+ #
55
+ # # Ruby-specific accessors (backward compat)
56
+ # context.task # TaskWrapper
57
+ # context.workflow_step # WorkflowStepWrapper
58
+ # end
59
+ def call(context)
60
+ raise NotImplementedError, 'Subclasses must implement #call(context)'
61
+ end
62
+
63
+ # ========================================================================
64
+ # CONFIGURATION AND METADATA
65
+ # ========================================================================
66
+
67
+ # Validate step handler configuration
68
+ # @param config_hash [Hash] Configuration to validate
69
+ # @return [Boolean] Whether configuration is valid
70
+ def validate_config(config_hash)
71
+ # Basic validation - subclasses can override for specific requirements
72
+ config_hash.is_a?(Hash)
73
+ end
74
+
75
+ # Get handler name for registration and logging
76
+ # @return [String] Handler name
77
+ def handler_name
78
+ class_name = self.class.name.split('::').last
79
+ # Convert CamelCase to snake_case (like Rails underscore method)
80
+ class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
81
+ end
82
+
83
+ # Get handler metadata for monitoring and introspection
84
+ # @return [Hash] Handler metadata
85
+ def metadata
86
+ {
87
+ handler_name: handler_name,
88
+ handler_class: self.class.name,
89
+ version: begin
90
+ self.class.const_get(:VERSION)
91
+ rescue StandardError
92
+ '1.0.0'
93
+ end,
94
+ capabilities: capabilities,
95
+ config_schema: config_schema,
96
+ ruby_version: RUBY_VERSION,
97
+ created_at: Time.now.iso8601
98
+ }
99
+ end
100
+
101
+ # Get handler capabilities
102
+ # @return [Array<String>] List of capabilities
103
+ def capabilities
104
+ caps = ['process']
105
+ caps << 'process_results' if respond_to?(:process_results, true)
106
+ caps << 'async' if respond_to?(:process_async, true)
107
+ caps << 'streaming' if respond_to?(:process_stream, true)
108
+ caps
109
+ end
110
+
111
+ # Get configuration schema for validation
112
+ # @return [Hash] JSON schema describing expected configuration
113
+ def config_schema
114
+ {
115
+ type: 'object',
116
+ properties: {
117
+ timeout: { type: 'integer', minimum: 1, default: 300 },
118
+ retries: { type: 'integer', minimum: 0, default: 3 },
119
+ log_level: { type: 'string', enum: %w[debug info warn error], default: 'info' }
120
+ },
121
+ additionalProperties: true
122
+ }
123
+ end
124
+
125
+ # ========================================================================
126
+ # RESULT HELPER METHODS
127
+ # ========================================================================
128
+
129
+ # Return a standardized success result from a step handler
130
+ #
131
+ # This wraps TaskerCore::Types::StepHandlerCallResult.success() to provide
132
+ # a convenient API for handlers to return properly structured results.
133
+ #
134
+ # @param result [Object] The result data (alias: result_data)
135
+ # @param result_data [Object] Alternative name for result parameter
136
+ # @param metadata [Hash] Optional metadata for observability
137
+ # @return [TaskerCore::Types::StepHandlerCallResult::Success] Success result
138
+ #
139
+ # @example Basic usage
140
+ # success(result: { order_id: "123", total: 100.00 })
141
+ #
142
+ # @example With metadata
143
+ # success(
144
+ # result: { validated: true },
145
+ # metadata: { processing_time_ms: 125 }
146
+ # )
147
+ #
148
+ # @example Using result_data alias (batch processing pattern)
149
+ # success(
150
+ # result_data: { batch_processing_outcome: outcome },
151
+ # metadata: { worker_count: 5 }
152
+ # )
153
+ def success(result: nil, result_data: nil, metadata: {})
154
+ # Accept either result: or result_data: for flexibility
155
+ actual_result = result || result_data
156
+
157
+ raise ArgumentError, 'result or result_data required' if actual_result.nil?
158
+
159
+ TaskerCore::Types::StepHandlerCallResult.success(
160
+ result: actual_result,
161
+ metadata: metadata
162
+ )
163
+ end
164
+
165
+ # Return a standardized error result from a step handler
166
+ #
167
+ # This wraps TaskerCore::Types::StepHandlerCallResult.error() to provide
168
+ # a convenient API for handlers to return error results.
169
+ #
170
+ # Note: In most cases, you should raise TaskerCore exceptions instead
171
+ # of returning error results. This method is provided for cases where
172
+ # you need to return an error without raising an exception.
173
+ #
174
+ # @param message [String] Human-readable error message
175
+ # @param error_type [String] Type of error (PermanentError, RetryableError, etc.)
176
+ # @param error_code [String, nil] Optional error code
177
+ # @param retryable [Boolean] Whether to retry this error
178
+ # @param metadata [Hash] Additional error context
179
+ # @return [TaskerCore::Types::StepHandlerCallResult::Error] Error result
180
+ #
181
+ # @example Permanent error
182
+ # failure(
183
+ # message: "Invalid order total",
184
+ # error_type: "PermanentError",
185
+ # error_code: "INVALID_TOTAL",
186
+ # retryable: false,
187
+ # metadata: { total: -50 }
188
+ # )
189
+ #
190
+ # @example Retryable error
191
+ # failure(
192
+ # message: "API temporarily unavailable",
193
+ # error_type: "RetryableError",
194
+ # retryable: true,
195
+ # metadata: { service: "payment_gateway" }
196
+ # )
197
+ def failure(message:, error_type: 'UnexpectedError', error_code: nil, retryable: false, metadata: {})
198
+ TaskerCore::Types::StepHandlerCallResult.error(
199
+ error_type: error_type,
200
+ message: message,
201
+ error_code: error_code,
202
+ retryable: retryable,
203
+ metadata: metadata
204
+ )
205
+ end
206
+
207
+ # ========================================================================
208
+ # RAILS ENGINE INTEGRATION NOTES
209
+ # ========================================================================
210
+
211
+ # NOTE: In the Rails engine execution flow, step handlers are called via:
212
+ # 1. TaskHandler.handle(task) → WorkflowCoordinator
213
+ # 2. WorkflowCoordinator → TaskHandler.handle_one_step(task, sequence, step)
214
+ # 3. handle_one_step → get_step_handler(step) → step_handler.call(task, sequence, step)
215
+ #
216
+ # The Rust foundation provides the same flow, so there's no need for a separate
217
+ # execute_step method. The Rails engine signature process(task, sequence, step)
218
+ # is the main entry point that frameworks call.
219
+ #
220
+ # The lifecycle management (events, error handling, retries) is handled by:
221
+ # - Rust orchestration layer with TaskHandler/WorkflowCoordinator foundation
222
+ # - Ruby subclasses provide business logic hooks via call()
223
+
224
+ # ========================================================================
225
+ # INTERNAL PROCESSING METHODS
226
+ # ========================================================================
227
+
228
+ private
229
+
230
+ # Validate step configuration before execution
231
+ def validate_step_configuration(config_hash)
232
+ # Basic validation - subclasses can override for specific requirements
233
+ config_hash.is_a?(Hash)
234
+ end
235
+
236
+ # Extract step information for logging
237
+ def extract_step_info(step)
238
+ {
239
+ step_id: extract_attribute(step, :id) || extract_attribute(step, :workflow_step_uuid),
240
+ step_name: extract_attribute(step, :name),
241
+ step_class: step.class.name
242
+ }
243
+ end
244
+
245
+ # Extract task information for logging
246
+ def extract_task_info(task)
247
+ {
248
+ task_uuid: extract_attribute(task, :task_uuid) || extract_attribute(task, :id),
249
+ task_class: task.class.name
250
+ }
251
+ end
252
+
253
+ # Safe attribute extraction from Ruby objects
254
+ def extract_attribute(object, attribute)
255
+ object.respond_to?(attribute) ? object.send(attribute) : nil
256
+ rescue StandardError
257
+ nil
258
+ end
259
+
260
+ # Validate process method result
261
+ def validate_process_result(result)
262
+ unless result.is_a?(Hash)
263
+ raise TaskerCore::PermanentError.new(
264
+ "Step process method must return Hash, got #{result.class}",
265
+ error_code: 'INVALID_PROCESS_RESULT',
266
+ error_category: 'validation'
267
+ )
268
+ end
269
+
270
+ return if result.key?(:status) || result.key?('status')
271
+
272
+ logger.warn('Step process result missing status field - assuming success')
273
+ result[:status] = 'completed'
274
+ end
275
+
276
+ # Classify unexpected errors into permanent vs retryable
277
+ def classify_unexpected_error(error, _context)
278
+ case error
279
+ when ArgumentError, TypeError, NoMethodError
280
+ # Programming errors are permanent
281
+ TaskerCore::PermanentError.new(
282
+ "Programming error in step handler: #{error.message}",
283
+ error_code: 'PROGRAMMING_ERROR',
284
+ error_category: 'validation',
285
+ context: { original_error: error.class.name, backtrace: error.backtrace&.first(5) }
286
+ )
287
+ when Timeout::Error
288
+ # Timeouts are retryable
289
+ TaskerCore::RetryableError.new(
290
+ "Step handler timeout: #{error.message}",
291
+ error_category: 'timeout',
292
+ context: { timeout_class: error.class.name }
293
+ )
294
+ when IOError, SystemCallError
295
+ # IO errors are usually retryable
296
+ TaskerCore::RetryableError.new(
297
+ "IO error in step handler: #{error.message}",
298
+ error_category: 'network',
299
+ context: { io_error_class: error.class.name }
300
+ )
301
+ else
302
+ # Unknown errors - assume retryable for safety
303
+ logger.error("Unexpected error in step handler: #{error.class} - #{error.message}")
304
+ TaskerCore::RetryableError.new(
305
+ "Unexpected error in step handler: #{error.message}",
306
+ error_category: 'unknown',
307
+ context: {
308
+ original_error: error.class.name,
309
+ backtrace: error.backtrace&.first(5)
310
+ }
311
+ )
312
+ end
313
+ end
314
+
315
+ # Format successful result for Rust layer
316
+ def format_success_result(result, duration)
317
+ {
318
+ status: 'completed',
319
+ success: true,
320
+ data: result,
321
+ duration_seconds: duration,
322
+ handler: handler_name,
323
+ timestamp: Time.now.iso8601
324
+ }
325
+ end
326
+
327
+ # Format error result for Rust layer
328
+ def format_error_result(error, duration, permanent:)
329
+ {
330
+ status: 'error',
331
+ success: false,
332
+ error: {
333
+ message: error.message,
334
+ type: error.class.name,
335
+ permanent: permanent,
336
+ retry_after: error.respond_to?(:retry_after) ? error.retry_after : nil,
337
+ error_code: error.respond_to?(:error_code) ? error.error_code : nil,
338
+ error_category: error.respond_to?(:error_category) ? error.error_category : 'unknown',
339
+ context: error.respond_to?(:context) ? error.context : {}
340
+ },
341
+ duration_seconds: duration,
342
+ handler: handler_name,
343
+ timestamp: Time.now.iso8601
344
+ }
345
+ end
346
+
347
+ # Default Rust integration
348
+ def default_rust_integration
349
+ # Return nil - integration is optional
350
+ nil
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'mixins'
5
+
6
+ module TaskerCore
7
+ module StepHandler
8
+ # Batchable step handler for batch processing handlers
9
+ #
10
+ # ## TAS-112: Composition Pattern (DEPRECATED CLASS)
11
+ #
12
+ # This class is provided for backward compatibility. For new code, use the mixin pattern:
13
+ #
14
+ # ```ruby
15
+ # class CsvBatchProcessorHandler < TaskerCore::StepHandler::Base
16
+ # include TaskerCore::StepHandler::Mixins::Batchable
17
+ #
18
+ # def call(context)
19
+ # batch_ctx = get_batch_context(context)
20
+ #
21
+ # # Handle no-op placeholder
22
+ # no_op_result = handle_no_op_worker(batch_ctx)
23
+ # return no_op_result if no_op_result
24
+ #
25
+ # # Handler-specific processing...
26
+ # end
27
+ # end
28
+ # ```
29
+ #
30
+ # ## TAS-112: 0-Indexed Cursors (BREAKING CHANGE)
31
+ #
32
+ # As of TAS-112, cursor indexing is 0-based to match Python, TypeScript, and Rust.
33
+ # Previously Ruby used 1-based indexing.
34
+ #
35
+ # ## IMPORTANT: Outcome Helper Methods Return Success Objects
36
+ #
37
+ # The outcome helper methods return fully-wrapped Success objects:
38
+ #
39
+ # ```ruby
40
+ # def call(context)
41
+ # if dataset_empty?
42
+ # return no_batches_outcome(reason: 'empty_dataset') # Returns Success
43
+ # end
44
+ # end
45
+ # ```
46
+ class Batchable < Base
47
+ include Mixins::Batchable
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'mixins'
5
+ require_relative '../types/decision_point_outcome'
6
+ require_relative '../types/step_handler_call_result'
7
+
8
+ module TaskerCore
9
+ module StepHandler
10
+ # Decision step handler for TAS-53 Dynamic Workflow Decision Points
11
+ #
12
+ # ## TAS-112: Composition Pattern (DEPRECATED CLASS)
13
+ #
14
+ # This class is provided for backward compatibility. For new code, use the mixin pattern:
15
+ #
16
+ # ```ruby
17
+ # class MyDecisionHandler < TaskerCore::StepHandler::Base
18
+ # include TaskerCore::StepHandler::Mixins::Decision
19
+ #
20
+ # def call(context)
21
+ # amount = context.get_task_field('amount')
22
+ #
23
+ # if amount < 1000
24
+ # decision_success(
25
+ # steps: ['auto_approve'],
26
+ # result_data: { route_type: 'auto', amount: amount }
27
+ # )
28
+ # else
29
+ # decision_success(
30
+ # steps: ['manager_approval', 'finance_review'],
31
+ # result_data: { route_type: 'dual', amount: amount }
32
+ # )
33
+ # end
34
+ # end
35
+ # end
36
+ # ```
37
+ #
38
+ # ## No-Branch Pattern
39
+ #
40
+ # ```ruby
41
+ # def call(context)
42
+ # if context.get_task_field('skip_approval')
43
+ # decision_no_branches(result_data: { reason: 'skipped' })
44
+ # else
45
+ # decision_success(steps: ['standard_approval'])
46
+ # end
47
+ # end
48
+ # ```
49
+ class Decision < Base
50
+ include Mixins::Decision
51
+ end
52
+ end
53
+ end