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,453 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TaskerCore
6
+ module Registry
7
+ # Handler registry with TAS-93 ResolverChain support
8
+ #
9
+ # Manages the discovery, registration, and instantiation of step handlers
10
+ # throughout the worker lifecycle. Uses a ResolverChain for flexible
11
+ # handler resolution with support for:
12
+ # - Explicit registration (highest priority)
13
+ # - Class constant resolution (inferential)
14
+ # - Method dispatch (handler_method redirects .call())
15
+ # - Resolver hints (bypass chain with specific resolver)
16
+ #
17
+ # == Resolution Priority
18
+ #
19
+ # 1. **Explicit Registration** (priority 10): Handlers registered via `register_handler`
20
+ # 2. **Class Constant** (priority 100): Ruby class lookup via `Object.const_get`
21
+ #
22
+ # == Method Dispatch (TAS-93)
23
+ #
24
+ # When resolving with a HandlerDefinition that specifies `handler_method`,
25
+ # the returned handler is wrapped to redirect `.call()` to the specified method:
26
+ #
27
+ # # Template specifies: handler_method: "refund"
28
+ # handler = registry.resolve_handler(handler_definition)
29
+ # handler.call(context) # Actually calls handler.refund(context)
30
+ #
31
+ # == Usage Examples
32
+ #
33
+ # @example Resolving by class name (string)
34
+ # handler = registry.resolve_handler("PaymentHandler")
35
+ #
36
+ # @example Resolving with HandlerDefinition (full TAS-93 support)
37
+ # definition = TaskerCore::Types::HandlerDefinition.new(
38
+ # callable: "PaymentHandler",
39
+ # handler_method: "refund"
40
+ # )
41
+ # handler = registry.resolve_handler(definition)
42
+ # handler.call(context) # Calls PaymentHandler#refund
43
+ #
44
+ # @example Resolving from FFI HandlerWrapper
45
+ # handler = registry.resolve_handler(step_data.step_definition.handler)
46
+ #
47
+ # @see TaskerCore::Registry::ResolverChain For resolution chain details
48
+ # @see TaskerCore::Registry::Resolvers::MethodDispatchWrapper For method dispatch
49
+ class HandlerRegistry
50
+ include Singleton
51
+
52
+ attr_reader :logger, :handlers, :resolver_chain
53
+
54
+ def initialize
55
+ @logger = TaskerCore::Logger.instance
56
+ @handlers = {} # Legacy compatibility - also populated for direct access
57
+ @resolver_chain = ResolverChain.default
58
+ bootstrap_handlers!
59
+ end
60
+
61
+ # Resolve handler by class name, HandlerDefinition, or HandlerWrapper
62
+ #
63
+ # TAS-93: Uses ResolverChain for flexible resolution with method dispatch support.
64
+ #
65
+ # @param handler_spec [String, Types::HandlerDefinition, Models::HandlerWrapper]
66
+ # Handler specification - can be:
67
+ # - String: Class name to resolve (e.g., "PaymentHandler")
68
+ # - HandlerDefinition: Full definition with method/resolver hints
69
+ # - HandlerWrapper: FFI wrapper from step_definition.handler
70
+ # @return [Object, nil] Handler instance ready for .call(), or nil if not found
71
+ def resolve_handler(handler_spec)
72
+ # Convert to HandlerDefinition for unified resolution
73
+ definition = normalize_to_definition(handler_spec)
74
+
75
+ # Use resolver chain for resolution
76
+ handler = @resolver_chain.resolve(definition)
77
+ return handler if handler
78
+
79
+ # Fallback: try legacy @handlers hash for backward compatibility
80
+ handler_class = @handlers[definition.callable]
81
+ return nil unless handler_class
82
+
83
+ instantiate_handler(handler_class, definition)
84
+ rescue StandardError => e
85
+ logger.error("💥 Failed to resolve handler '#{extract_callable(handler_spec)}': #{e.message}")
86
+ nil
87
+ end
88
+
89
+ # Register handler class
90
+ #
91
+ # @param class_name [String] Handler identifier (typically class name)
92
+ # @param handler_class [Class] Handler class to register
93
+ def register_handler(class_name, handler_class)
94
+ # Register in both resolver chain and legacy hash
95
+ @resolver_chain.register(class_name, handler_class)
96
+ @handlers[class_name] = handler_class
97
+ logger.debug("✅ Registered handler: #{class_name}")
98
+ end
99
+
100
+ # Check if handler is available
101
+ #
102
+ # @param class_name [String] Handler class name
103
+ # @return [Boolean] true if handler can be resolved
104
+ def handler_available?(class_name)
105
+ definition = TaskerCore::Types::HandlerDefinition.new(callable: class_name)
106
+ @resolver_chain.can_resolve?(definition) || @handlers.key?(class_name)
107
+ end
108
+
109
+ # Get all registered handler names
110
+ #
111
+ # @return [Array<String>] Sorted list of registered handler names
112
+ def registered_handlers
113
+ (@resolver_chain.registered_callables + @handlers.keys).uniq.sort
114
+ end
115
+
116
+ # ========================================================================
117
+ # CROSS-LANGUAGE STANDARD ALIASES (TAS-96)
118
+ # ========================================================================
119
+
120
+ # Cross-language standard: register(name, handler_class)
121
+ # @see #register_handler
122
+ alias register register_handler
123
+
124
+ # Cross-language standard: is_registered(name)
125
+ # @see #handler_available?
126
+ alias is_registered handler_available?
127
+
128
+ # Cross-language standard: list_handlers
129
+ # @see #registered_handlers
130
+ alias list_handlers registered_handlers
131
+
132
+ # Cross-language standard: resolve(name)
133
+ # @see #resolve_handler
134
+ alias resolve resolve_handler
135
+
136
+ # Get template discovery information for debugging
137
+ def template_discovery_info
138
+ template_path = TaskerCore::TemplateDiscovery::TemplatePath.find_template_config_directory
139
+
140
+ {
141
+ template_path: template_path,
142
+ template_files: template_path ? TaskerCore::TemplateDiscovery::TemplatePath.discover_template_files(template_path) : [],
143
+ discovered_handlers: template_path ? TaskerCore::TemplateDiscovery::HandlerDiscovery.discover_all_handlers(template_path) : [],
144
+ handlers_by_namespace: template_path ? TaskerCore::TemplateDiscovery::HandlerDiscovery.discover_handlers_by_namespace(template_path) : {},
145
+ environment: ENV['TASKER_ENV'] || ENV['RAILS_ENV'] || 'development'
146
+ }
147
+ end
148
+
149
+ # TAS-93: Add custom resolver to the chain
150
+ #
151
+ # @param resolver [Resolvers::BaseResolver] Resolver to add
152
+ def add_resolver(resolver)
153
+ @resolver_chain.add_resolver(resolver)
154
+ logger.info("✅ Added resolver '#{resolver.name}' (priority #{resolver.priority})")
155
+ end
156
+
157
+ private
158
+
159
+ # TAS-93: Normalize handler spec to HandlerDefinition for unified resolution
160
+ #
161
+ # @param handler_spec [String, Types::HandlerDefinition, Models::HandlerWrapper]
162
+ # @return [Types::HandlerDefinition]
163
+ def normalize_to_definition(handler_spec)
164
+ case handler_spec
165
+ when String
166
+ # Simple string callable - create minimal definition
167
+ TaskerCore::Types::HandlerDefinition.new(callable: handler_spec)
168
+ when TaskerCore::Types::HandlerDefinition
169
+ # Already a HandlerDefinition - use as-is
170
+ handler_spec
171
+ when TaskerCore::Models::HandlerWrapper
172
+ # FFI HandlerWrapper - convert to HandlerDefinition
173
+ # TAS-93: HandlerWrapper now includes handler_method and resolver from Rust FFI
174
+ TaskerCore::Types::HandlerDefinition.new(
175
+ callable: handler_spec.callable,
176
+ initialization: handler_spec.initialization.to_h,
177
+ handler_method: handler_spec.handler_method,
178
+ resolver: handler_spec.resolver
179
+ )
180
+ else
181
+ # Try to extract callable from object (duck typing)
182
+ unless handler_spec.respond_to?(:callable)
183
+ raise ArgumentError, "Cannot normalize #{handler_spec.class} to HandlerDefinition"
184
+ end
185
+
186
+ attrs = { callable: handler_spec.callable.to_s }
187
+ attrs[:initialization] = handler_spec.initialization.to_h if handler_spec.respond_to?(:initialization)
188
+ attrs[:handler_method] = handler_spec.handler_method if handler_spec.respond_to?(:handler_method)
189
+ attrs[:resolver] = handler_spec.resolver if handler_spec.respond_to?(:resolver)
190
+ TaskerCore::Types::HandlerDefinition.new(**attrs)
191
+
192
+ end
193
+ end
194
+
195
+ # Extract callable string from handler spec for error messages
196
+ #
197
+ # @param handler_spec [Object] Handler specification
198
+ # @return [String] Callable name
199
+ def extract_callable(handler_spec)
200
+ case handler_spec
201
+ when String then handler_spec
202
+ when TaskerCore::Types::HandlerDefinition then handler_spec.callable
203
+ else
204
+ handler_spec.respond_to?(:callable) ? handler_spec.callable.to_s : handler_spec.to_s
205
+ end
206
+ end
207
+
208
+ # Instantiate handler with method dispatch support
209
+ #
210
+ # @param handler_class [Class] Handler class
211
+ # @param definition [Types::HandlerDefinition] Handler definition
212
+ # @return [Object] Handler instance (possibly wrapped for method dispatch)
213
+ def instantiate_handler(handler_class, definition)
214
+ # Check constructor arity for config support
215
+ arity = handler_class.instance_method(:initialize).arity
216
+ handler = if arity.positive? || (arity.negative? && accepts_config_kwarg?(handler_class))
217
+ handler_class.new(config: definition.initialization || {})
218
+ else
219
+ handler_class.new
220
+ end
221
+
222
+ # Wrap for method dispatch if needed
223
+ @resolver_chain.wrap_for_method_dispatch(handler, definition)
224
+ end
225
+
226
+ # Check if class accepts config: keyword argument
227
+ #
228
+ # @param klass [Class] Class to check
229
+ # @return [Boolean]
230
+ def accepts_config_kwarg?(klass)
231
+ params = klass.instance_method(:initialize).parameters
232
+ params.any? { |type, name| %i[key keyreq].include?(type) && name == :config }
233
+ end
234
+
235
+ def bootstrap_handlers!
236
+ logger.info('🔧 Bootstrapping Ruby handler registry with template-driven discovery')
237
+
238
+ registered_count = 0
239
+
240
+ # Check if test environment has already loaded handlers
241
+ if test_environment_active? && test_handlers_preloaded?
242
+ logger.info('🧪 Test environment detected with preloaded handlers')
243
+ registered_count = register_preloaded_handlers
244
+ end
245
+
246
+ # If no handlers registered yet, discover from templates
247
+ if registered_count.zero?
248
+ # Discover handlers from YAML templates
249
+ discovered_handlers = discover_handlers_from_templates
250
+
251
+ # Load and register discovered handlers
252
+ discovered_handlers.each do |handler_class_name|
253
+ # Try to load and register the handler
254
+ handler_class = find_and_load_handler_class(handler_class_name)
255
+ if handler_class
256
+ register_handler(handler_class_name, handler_class)
257
+ registered_count += 1
258
+ else
259
+ logger.debug("⚠️ Handler class not found for: #{handler_class_name}")
260
+ end
261
+ rescue StandardError => e
262
+ logger.warn("❌ Failed to register handler #{handler_class_name}: #{e.message}")
263
+ end
264
+ end
265
+
266
+ logger.info("✅ Handler registry bootstrapped with #{registered_count} handlers")
267
+ end
268
+
269
+ def load_example_handlers!
270
+ # Load all example handler files from spec/handlers/examples/
271
+ spec_dir = File.expand_path('../../../spec/handlers/examples', __dir__)
272
+ return unless Dir.exist?(spec_dir)
273
+
274
+ Dir.glob("#{spec_dir}/**/*_handler.rb").each do |handler_file|
275
+ require handler_file
276
+ logger.debug("✅ Loaded handler file: #{handler_file}")
277
+ rescue StandardError => e
278
+ logger.warn("❌ Failed to load handler file #{handler_file}: #{e.message}")
279
+ end
280
+ end
281
+
282
+ # Discover handlers from YAML templates using the template discovery system
283
+ def discover_handlers_from_templates
284
+ template_path = determine_template_path
285
+
286
+ if template_path
287
+ logger.debug("🔍 Discovering handlers from template path: #{template_path}")
288
+ TaskerCore::TemplateDiscovery::HandlerDiscovery.discover_all_handlers(template_path)
289
+ else
290
+ logger.warn('⚠️ No template directory found, no handlers will be discovered')
291
+ []
292
+ end
293
+ end
294
+
295
+ # Determine the template path based on environment and configuration
296
+ def determine_template_path
297
+ # 1. Explicit override takes highest priority
298
+ return ENV['TASKER_TEMPLATE_PATH'] if ENV['TASKER_TEMPLATE_PATH']
299
+
300
+ # 2. Test environment: default to spec/fixtures/templates
301
+ if test_environment?
302
+ fixtures_path = File.expand_path('../../../spec/fixtures/templates', __dir__)
303
+ return fixtures_path if Dir.exist?(fixtures_path)
304
+ end
305
+
306
+ # 3. Production/development: use standard discovery
307
+ TaskerCore::TemplateDiscovery::TemplatePath.find_template_config_directory
308
+ end
309
+
310
+ # Check if we're in a test environment
311
+ def test_environment?
312
+ ENV['TASKER_ENV'] == 'test' || ENV['RAILS_ENV'] == 'test'
313
+ end
314
+
315
+ # Find and dynamically load handler class by name
316
+ def find_and_load_handler_class(handler_class_name)
317
+ # First try to find if it's already loaded
318
+ existing_class = find_loaded_handler_class(handler_class_name)
319
+ return existing_class if existing_class
320
+
321
+ # Try to load from common handler file patterns
322
+ load_handler_file(handler_class_name)
323
+
324
+ # Try to find it again after loading
325
+ find_loaded_handler_class(handler_class_name)
326
+ end
327
+
328
+ # Find a handler class that's already loaded in memory
329
+ def find_loaded_handler_class(handler_class_name)
330
+ # Search through ObjectSpace for classes that match the handler name
331
+ ObjectSpace.each_object(Class).find do |klass|
332
+ klass.name&.end_with?(handler_class_name) &&
333
+ klass < TaskerCore::StepHandler::Base
334
+ end
335
+ end
336
+
337
+ # Attempt to load handler file using common naming conventions
338
+ def load_handler_file(handler_class_name)
339
+ # Convert CamelCase to snake_case for file names
340
+ file_name = "#{handler_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')}.rb"
341
+
342
+ # Try various common locations
343
+ search_paths = handler_search_paths
344
+
345
+ search_paths.each do |search_path|
346
+ potential_file = File.join(search_path, file_name)
347
+ next unless File.exist?(potential_file)
348
+
349
+ begin
350
+ require potential_file
351
+ logger.debug("✅ Loaded handler file: #{potential_file}")
352
+ return true
353
+ rescue StandardError => e
354
+ logger.debug("❌ Failed to load #{potential_file}: #{e.message}")
355
+ end
356
+ end
357
+
358
+ false
359
+ end
360
+
361
+ # Get search paths for handler files
362
+ def handler_search_paths
363
+ paths = []
364
+
365
+ # Add current working directory patterns
366
+ %w[
367
+ app/handlers
368
+ lib/handlers
369
+ handlers
370
+ app/tasker/handlers
371
+ lib/tasker/handlers
372
+ ].each do |relative_path|
373
+ full_path = File.expand_path(relative_path)
374
+ paths << full_path if Dir.exist?(full_path)
375
+ end
376
+
377
+ # In test environment, add spec/handlers/examples for test fixtures
378
+ if test_environment?
379
+ spec_dir = File.expand_path('../../../spec/handlers/examples', __dir__)
380
+ Dir.glob("#{spec_dir}/**/").each { |dir| paths << dir } if Dir.exist?(spec_dir)
381
+ end
382
+
383
+ paths
384
+ end
385
+
386
+ # Check if test environment is active
387
+ def test_environment_active?
388
+ return false unless defined?(TaskerCore::TestEnvironment)
389
+
390
+ TaskerCore::TestEnvironment.loaded?
391
+ end
392
+
393
+ # Check if test environment has preloaded handler classes
394
+ def test_handlers_preloaded?
395
+ return false unless test_environment_active?
396
+
397
+ # Check if we have handler classes loaded in ObjectSpace
398
+ handler_count = 0
399
+ ObjectSpace.each_object(Class) do |klass|
400
+ if klass.name&.end_with?('Handler') &&
401
+ klass.ancestors.any? { |ancestor| ancestor.name&.include?('StepHandler') }
402
+ handler_count += 1
403
+ break if handler_count.positive? # We just need to know if any exist
404
+ end
405
+ rescue StandardError
406
+ next # Skip classes that can't be introspected
407
+ end
408
+
409
+ handler_count.positive?
410
+ end
411
+
412
+ # Register preloaded handlers from test environment
413
+ def register_preloaded_handlers
414
+ return 0 unless test_environment_active?
415
+
416
+ registered_count = 0
417
+ handler_names = TaskerCore::TestEnvironment.handler_names
418
+
419
+ handler_names.each do |handler_class_name|
420
+ # Find the loaded class - use the full class name from ObjectSpace
421
+ handler_class = find_loaded_handler_class_by_full_name(handler_class_name)
422
+ if handler_class
423
+ # Register with FULL class name so templates can reference it properly
424
+ register_handler(handler_class_name, handler_class)
425
+ registered_count += 1
426
+ logger.debug("✅ Registered preloaded test handler: #{handler_class_name}")
427
+ else
428
+ logger.debug("⚠️ Preloaded handler class not found: #{handler_class_name}")
429
+ end
430
+ rescue StandardError => e
431
+ logger.warn("❌ Failed to register preloaded handler #{handler_class_name}: #{e.message}")
432
+ end
433
+
434
+ logger.info("📚 Registered #{registered_count} preloaded test handlers")
435
+ registered_count
436
+ end
437
+
438
+ # Find a handler class by its full class name (e.g., "LinearWorkflow::StepHandlers::LinearStep1Handler")
439
+ def find_loaded_handler_class_by_full_name(full_class_name)
440
+ # Try to constantize the full class name
441
+ full_class_name.split('::').reduce(Object) do |mod, const_name|
442
+ mod.const_get(const_name)
443
+ end
444
+ rescue NameError
445
+ # If not found by constantize, search ObjectSpace for a match
446
+ ObjectSpace.each_object(Class).find do |klass|
447
+ klass.name == full_class_name &&
448
+ klass < TaskerCore::StepHandler::Base
449
+ end
450
+ end
451
+ end
452
+ end
453
+ end