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,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resolvers/base_resolver'
4
+ require_relative 'resolvers/method_dispatch_wrapper'
5
+
6
+ module TaskerCore
7
+ module Registry
8
+ # TAS-93: Resolver Chain - Priority-Ordered Handler Resolution
9
+ #
10
+ # The ResolverChain orchestrates handler resolution by trying resolvers
11
+ # in priority order until one successfully resolves the handler.
12
+ #
13
+ # == Resolution Contract
14
+ #
15
+ # 1. If HandlerDefinition has `resolver:` hint, use ONLY that resolver
16
+ # 2. Otherwise, try resolvers in priority order (lower = first)
17
+ # 3. Wrap resolved handler with MethodDispatchWrapper if needed
18
+ # 4. Return nil if no resolver can handle the definition
19
+ #
20
+ # == Default Chain (when using .default)
21
+ #
22
+ # Priority 10: ExplicitMappingResolver - registered handlers
23
+ # Priority 100: ClassConstantResolver - class path inference
24
+ #
25
+ # == Usage
26
+ #
27
+ # # Create default chain
28
+ # chain = ResolverChain.default
29
+ #
30
+ # # Or build custom chain
31
+ # chain = ResolverChain.new
32
+ # .with_resolver(MyResolver.new)
33
+ # .with_resolver(AnotherResolver.new)
34
+ #
35
+ # # Resolve a handler
36
+ # handler = chain.resolve(definition, config)
37
+ #
38
+ # == Adding Custom Resolvers
39
+ #
40
+ # chain.add_resolver(CustomResolver.new)
41
+ #
42
+ # == Named Resolver Access
43
+ #
44
+ # # Add with name for resolver hint support
45
+ # chain.add_resolver(PaymentResolver.new)
46
+ #
47
+ # # Definition with resolver hint
48
+ # definition.resolver = 'payment_resolver'
49
+ # # Chain will ONLY use PaymentResolver
50
+ #
51
+ class ResolverChain
52
+ # Error raised when resolver hint points to unknown resolver
53
+ class ResolverNotFoundError < StandardError; end
54
+
55
+ # Error raised when resolution fails completely
56
+ class ResolutionError < StandardError; end
57
+
58
+ def initialize
59
+ @resolvers = []
60
+ @resolvers_by_name = {}
61
+ @logger = TaskerCore::Logger.instance
62
+ end
63
+
64
+ # Create a chain with default resolvers
65
+ #
66
+ # @return [ResolverChain] Chain with Explicit + ClassConstant resolvers
67
+ def self.default
68
+ require_relative 'resolvers/explicit_mapping_resolver'
69
+ require_relative 'resolvers/class_constant_resolver'
70
+
71
+ new.tap do |chain|
72
+ chain.add_resolver(Resolvers::ExplicitMappingResolver.new)
73
+ chain.add_resolver(Resolvers::ClassConstantResolver.new)
74
+ end
75
+ end
76
+
77
+ # Add a resolver to the chain
78
+ #
79
+ # @param resolver [Resolvers::BaseResolver] Resolver to add
80
+ # @return [self] For method chaining
81
+ def add_resolver(resolver)
82
+ @resolvers << resolver
83
+ @resolvers.sort_by!(&:priority)
84
+ @resolvers_by_name[resolver.name] = resolver
85
+ self
86
+ end
87
+
88
+ # Fluent API for adding resolvers
89
+ alias with_resolver add_resolver
90
+
91
+ # Remove a resolver by name
92
+ #
93
+ # @param name [String] Resolver name to remove
94
+ # @return [Resolvers::BaseResolver, nil] Removed resolver
95
+ def remove_resolver(name)
96
+ resolver = @resolvers_by_name.delete(name)
97
+ @resolvers.delete(resolver) if resolver
98
+ resolver
99
+ end
100
+
101
+ # Get a resolver by name
102
+ #
103
+ # @param name [String] Resolver name
104
+ # @return [Resolvers::BaseResolver, nil]
105
+ def get_resolver(name)
106
+ @resolvers_by_name[name]
107
+ end
108
+
109
+ # Get the explicit mapping resolver (convenience method)
110
+ #
111
+ # @return [Resolvers::ExplicitMappingResolver, nil]
112
+ def explicit_resolver
113
+ @resolvers_by_name['explicit_mapping']
114
+ end
115
+
116
+ # Resolve a handler from the definition
117
+ #
118
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
119
+ # @param config [Hash] Additional context
120
+ # @return [Object, nil] Resolved handler or nil
121
+ def resolve(definition, config = {})
122
+ # Contract: If resolver hint is present, use ONLY that resolver
123
+ return resolve_with_hint(definition, config) if definition.has_resolver_hint?
124
+
125
+ # Otherwise: Try inferential chain resolution
126
+ resolve_with_chain(definition, config)
127
+ end
128
+
129
+ # Check if any resolver can handle this definition
130
+ #
131
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
132
+ # @param config [Hash] Additional context
133
+ # @return [Boolean]
134
+ def can_resolve?(definition, config = {})
135
+ if definition.has_resolver_hint?
136
+ resolver = @resolvers_by_name[definition.resolver]
137
+ return resolver&.can_resolve?(definition, config) || false
138
+ end
139
+
140
+ @resolvers.any? { |r| r.can_resolve?(definition, config) }
141
+ end
142
+
143
+ # Get all registered callables across all resolvers
144
+ #
145
+ # @return [Array<String>] List of all registered callable identifiers
146
+ def registered_callables
147
+ @resolvers.flat_map(&:registered_callables).uniq
148
+ end
149
+
150
+ # Register a handler directly (convenience method for ExplicitMappingResolver)
151
+ #
152
+ # @param key [String] Handler key
153
+ # @param handler [Object] Handler class or instance
154
+ # @return [self]
155
+ def register(key, handler)
156
+ explicit = explicit_resolver
157
+ raise 'No ExplicitMappingResolver in chain' unless explicit
158
+
159
+ explicit.register(key, handler)
160
+ self
161
+ end
162
+
163
+ # Get chain info for debugging
164
+ #
165
+ # @return [Array<Hash>] Resolver info
166
+ def chain_info
167
+ @resolvers.map do |resolver|
168
+ {
169
+ name: resolver.name,
170
+ priority: resolver.priority,
171
+ callables: resolver.registered_callables.size
172
+ }
173
+ end
174
+ end
175
+
176
+ # @return [Integer] Number of resolvers in chain
177
+ def size
178
+ @resolvers.size
179
+ end
180
+
181
+ # @return [Array<String>] Names of resolvers in priority order
182
+ def resolver_names
183
+ @resolvers.map(&:name)
184
+ end
185
+
186
+ # Wrap handler for method dispatch if needed.
187
+ #
188
+ # This method is public because HandlerRegistry needs to wrap handlers
189
+ # that it instantiates directly (outside the resolver chain flow).
190
+ #
191
+ # @param handler [Object] Resolved handler instance
192
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
193
+ # @return [Object, nil] Handler (possibly wrapped), or nil if method not found
194
+ def wrap_for_method_dispatch(handler, definition)
195
+ # No wrapping needed if using default .call method
196
+ return handler unless definition.uses_method_dispatch?
197
+
198
+ effective_method = definition.effective_method.to_sym
199
+
200
+ # Verify handler responds to the target method
201
+ unless handler.respond_to?(effective_method)
202
+ @logger.warn("ResolverChain: Handler #{handler.class} doesn't respond to ##{effective_method}")
203
+ return nil
204
+ end
205
+
206
+ @logger.debug("ResolverChain: Wrapping #{handler.class} for method dispatch → ##{effective_method}")
207
+ Resolvers::MethodDispatchWrapper.new(handler, effective_method)
208
+ end
209
+
210
+ private
211
+
212
+ # Resolve using explicit resolver hint (bypasses chain)
213
+ #
214
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
215
+ # @param config [Hash] Additional context
216
+ # @return [Object, nil] Resolved handler
217
+ def resolve_with_hint(definition, config)
218
+ resolver_name = definition.resolver
219
+ resolver = @resolvers_by_name[resolver_name]
220
+
221
+ unless resolver
222
+ @logger.warn("ResolverChain: Unknown resolver hint '#{resolver_name}'")
223
+ raise ResolverNotFoundError, "Unknown resolver: '#{resolver_name}'"
224
+ end
225
+
226
+ handler = resolver.resolve(definition, config)
227
+
228
+ unless handler
229
+ @logger.warn("ResolverChain: Resolver '#{resolver_name}' failed to resolve '#{definition.callable}'")
230
+ return nil
231
+ end
232
+
233
+ @logger.debug("ResolverChain: Resolved '#{definition.callable}' via hint '#{resolver_name}'")
234
+ wrap_for_method_dispatch(handler, definition)
235
+ end
236
+
237
+ # Resolve using chain (tries resolvers in priority order)
238
+ #
239
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
240
+ # @param config [Hash] Additional context
241
+ # @return [Object, nil] Resolved handler
242
+ def resolve_with_chain(definition, config)
243
+ @resolvers.each do |resolver|
244
+ next unless resolver.can_resolve?(definition, config)
245
+
246
+ handler = resolver.resolve(definition, config)
247
+ next unless handler
248
+
249
+ @logger.debug("ResolverChain: Resolved '#{definition.callable}' via '#{resolver.name}'")
250
+ return wrap_for_method_dispatch(handler, definition)
251
+ end
252
+
253
+ @logger.debug("ResolverChain: No resolver could handle '#{definition.callable}'")
254
+ nil
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Registry
5
+ module Resolvers
6
+ # TAS-93: Abstract base class for step handler resolvers
7
+ #
8
+ # This class defines the contract that all resolvers must implement.
9
+ # Resolvers are tried in priority order by the ResolverChain until
10
+ # one successfully resolves the handler.
11
+ #
12
+ # == Resolution Contract
13
+ #
14
+ # 1. `name` - Human-readable identifier for logging/debugging
15
+ # 2. `priority` - Lower numbers = tried first (10 = explicit, 100 = inferential)
16
+ # 3. `can_resolve?` - Quick check if this resolver might handle the definition
17
+ # 4. `resolve` - Actually resolve the handler callable
18
+ #
19
+ # == Implementation Example
20
+ #
21
+ # class MyResolver < BaseResolver
22
+ # def name
23
+ # 'my_custom_resolver'
24
+ # end
25
+ #
26
+ # def priority
27
+ # 50 # Between explicit (10) and class constant (100)
28
+ # end
29
+ #
30
+ # def can_resolve?(definition, config)
31
+ # # Quick eligibility check
32
+ # definition.callable.start_with?('MyApp::')
33
+ # end
34
+ #
35
+ # def resolve(definition, config)
36
+ # # Return handler or nil
37
+ # # handler must respond to effective_method
38
+ # end
39
+ # end
40
+ #
41
+ class BaseResolver
42
+ # Human-readable name for this resolver (for logging/debugging)
43
+ #
44
+ # @return [String] The resolver name
45
+ def name
46
+ raise NotImplementedError, "#{self.class}#name must be implemented"
47
+ end
48
+
49
+ # Resolution priority (lower = tried first)
50
+ #
51
+ # Standard priorities:
52
+ # - 10: Explicit mapping (registered handlers)
53
+ # - 100: Class constant (inferential)
54
+ #
55
+ # @return [Integer] The priority value
56
+ def priority
57
+ raise NotImplementedError, "#{self.class}#priority must be implemented"
58
+ end
59
+
60
+ # Quick eligibility check (called before resolve)
61
+ #
62
+ # This should be a fast check to determine if this resolver
63
+ # is even worth trying for the given definition.
64
+ #
65
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
66
+ # @param config [Hash] Additional configuration context
67
+ # @return [Boolean] true if this resolver might be able to resolve the handler
68
+ def can_resolve?(definition, config)
69
+ raise NotImplementedError, "#{self.class}#can_resolve? must be implemented"
70
+ end
71
+
72
+ # Resolve the handler callable from the definition
73
+ #
74
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
75
+ # @param config [Hash] Additional configuration context
76
+ # @return [Object, nil] Handler object or nil if cannot resolve
77
+ def resolve(definition, config)
78
+ raise NotImplementedError, "#{self.class}#resolve must be implemented"
79
+ end
80
+
81
+ # Return all callable keys this resolver knows about (for debugging/introspection)
82
+ #
83
+ # @return [Array<String>] List of registered callable identifiers
84
+ def registered_callables
85
+ []
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_resolver'
4
+
5
+ module TaskerCore
6
+ module Registry
7
+ module Resolvers
8
+ # TAS-93: Class Constant Resolver (Inferential)
9
+ #
10
+ # This resolver uses Ruby's `const_get` to resolve class paths
11
+ # to actual handler classes. It's the fallback resolver with
12
+ # lowest priority (100), tried after explicit mappings.
13
+ #
14
+ # == Resolution Behavior
15
+ #
16
+ # Given callable: "MyApp::Handlers::PaymentHandler"
17
+ # 1. Attempts `Object.const_get('MyApp::Handlers::PaymentHandler')`
18
+ # 2. Verifies the result is a Class
19
+ # 3. Verifies the class responds to effective_method (from HandlerDefinition)
20
+ # 4. Instantiates with config if supported
21
+ #
22
+ # == Usage
23
+ #
24
+ # resolver = ClassConstantResolver.new
25
+ #
26
+ # # This will find any loaded Ruby class
27
+ # handler = resolver.resolve(definition, config)
28
+ #
29
+ # == When This Resolver is Used
30
+ #
31
+ # - Standard handler class paths (e.g., "MyApp::PaymentHandler")
32
+ # - Handler classes already loaded in memory
33
+ # - Conventional Ruby handler patterns
34
+ #
35
+ class ClassConstantResolver < BaseResolver
36
+ PRIORITY = 100 # Lowest priority - tried last
37
+
38
+ def initialize
39
+ super
40
+ @logger = TaskerCore::Logger.instance
41
+ end
42
+
43
+ # @return [String] Human-readable resolver name
44
+ def name
45
+ 'class_constant'
46
+ end
47
+
48
+ # @return [Integer] Resolution priority (100 = lowest)
49
+ def priority
50
+ PRIORITY
51
+ end
52
+
53
+ # Check if callable looks like a Ruby class constant path.
54
+ #
55
+ # This method checks the STRING FORMAT, not whether the class exists.
56
+ # Actual class existence is verified in resolve() - this allows the
57
+ # resolver chain to skip this resolver for callables that are clearly
58
+ # not class paths (e.g., "payment_handler", "./path/to/handler").
59
+ #
60
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
61
+ # @param config [Hash] Additional context
62
+ # @return [Boolean] true if callable looks like a class constant
63
+ def can_resolve?(definition, _config)
64
+ callable = definition.callable.to_s
65
+
66
+ # Basic validation: should look like a class constant
67
+ # (starts with uppercase, may contain ::)
68
+ callable.match?(/\A[A-Z]/)
69
+ end
70
+
71
+ # Resolve handler class from constant path
72
+ #
73
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
74
+ # @param config [Hash] Additional context
75
+ # @return [Object, nil] Handler instance or nil
76
+ def resolve(definition, _config)
77
+ callable = definition.callable.to_s
78
+ effective_method = definition.effective_method.to_sym
79
+
80
+ # Resolve the class constant
81
+ klass = resolve_constant(callable)
82
+ return nil unless klass
83
+
84
+ # Verify it's a class
85
+ unless klass.is_a?(Class)
86
+ @logger.debug("ClassConstantResolver: '#{callable}' is not a Class (#{klass.class})")
87
+ return nil
88
+ end
89
+
90
+ # Verify it responds to the effective method
91
+ unless responds_to_method?(klass, effective_method)
92
+ @logger.debug("ClassConstantResolver: '#{callable}' does not respond to ##{effective_method}")
93
+ return nil
94
+ end
95
+
96
+ # Instantiate with configuration
97
+ instantiate_handler(klass, definition.initialization)
98
+ rescue StandardError => e
99
+ @logger.debug("ClassConstantResolver: Failed to resolve '#{callable}': #{e.message}")
100
+ nil
101
+ end
102
+
103
+ private
104
+
105
+ # Resolve constant path to class
106
+ #
107
+ # @param callable [String] Class constant path
108
+ # @return [Class, nil] Resolved class or nil
109
+ def resolve_constant(callable)
110
+ # Use constantize from ActiveSupport if available
111
+ if callable.respond_to?(:constantize)
112
+ callable.constantize
113
+ else
114
+ Object.const_get(callable)
115
+ end
116
+ rescue NameError => e
117
+ @logger.debug("ClassConstantResolver: Constant '#{callable}' not found: #{e.message}")
118
+ nil
119
+ end
120
+
121
+ # Check if class or its instances respond to the method
122
+ #
123
+ # @param klass [Class] Handler class
124
+ # @param method_name [Symbol] Method to check
125
+ # @return [Boolean] true if responds to method
126
+ def responds_to_method?(klass, method_name)
127
+ # Check instance methods (most common case)
128
+ klass.instance_methods.include?(method_name) ||
129
+ # Check if the class itself responds (for class methods)
130
+ klass.respond_to?(method_name)
131
+ end
132
+
133
+ # Instantiate a handler with configuration
134
+ #
135
+ # @param klass [Class] Handler class
136
+ # @param initialization [Hash] Configuration to pass
137
+ # @return [Object] Handler instance
138
+ def instantiate_handler(klass, initialization)
139
+ arity = klass.instance_method(:initialize).arity
140
+
141
+ if arity.positive? || (arity.negative? && accepts_config_kwarg?(klass))
142
+ klass.new(config: initialization || {})
143
+ else
144
+ klass.new
145
+ end
146
+ end
147
+
148
+ # Check if the class accepts config: keyword argument
149
+ def accepts_config_kwarg?(klass)
150
+ params = klass.instance_method(:initialize).parameters
151
+ params.any? { |type, name| %i[key keyreq].include?(type) && name == :config }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_resolver'
4
+
5
+ module TaskerCore
6
+ module Registry
7
+ module Resolvers
8
+ # TAS-93: Explicit Key-to-Handler Mapping Resolver
9
+ #
10
+ # This resolver handles explicitly registered handlers with direct
11
+ # key → handler mappings. It has the highest priority (10) because
12
+ # explicit registrations should always take precedence.
13
+ #
14
+ # == Usage
15
+ #
16
+ # resolver = ExplicitMappingResolver.new
17
+ # resolver.register('payment.process', PaymentHandler)
18
+ # resolver.register('payment.refund', RefundHandler.new)
19
+ #
20
+ # # Later, during resolution
21
+ # handler = resolver.resolve(definition, config)
22
+ #
23
+ # == When to Use
24
+ #
25
+ # - Handlers registered at application boot
26
+ # - Proc/Lambda handlers that can't be resolved by class path
27
+ # - Overriding inferential resolution with specific handlers
28
+ # - Testing with mock handlers
29
+ #
30
+ class ExplicitMappingResolver < BaseResolver
31
+ PRIORITY = 10 # Highest priority - explicit mappings always win
32
+
33
+ def initialize(resolver_name = 'explicit_mapping')
34
+ super()
35
+ @resolver_name = resolver_name
36
+ @handlers = Concurrent::Hash.new
37
+ @logger = TaskerCore::Logger.instance
38
+ end
39
+
40
+ # @return [String] Human-readable resolver name
41
+ def name
42
+ @resolver_name
43
+ end
44
+
45
+ # @return [Integer] Resolution priority (10 = highest)
46
+ def priority
47
+ PRIORITY
48
+ end
49
+
50
+ # Check if we have an explicit mapping for this callable
51
+ #
52
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
53
+ # @param config [Hash] Additional context
54
+ # @return [Boolean] true if callable is registered
55
+ def can_resolve?(definition, _config)
56
+ @handlers.key?(definition.callable.to_s)
57
+ end
58
+
59
+ # Resolve handler from explicit registration
60
+ #
61
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
62
+ # @param config [Hash] Additional context
63
+ # @return [Object, nil] Handler instance or nil
64
+ def resolve(definition, _config)
65
+ callable = definition.callable.to_s
66
+ handler = @handlers[callable]
67
+
68
+ return nil unless handler
69
+
70
+ instantiate_handler(handler, definition.initialization)
71
+ rescue StandardError => e
72
+ @logger.warn("ExplicitMappingResolver: Failed to instantiate '#{callable}': #{e.message}")
73
+ nil
74
+ end
75
+
76
+ # @return [Array<String>] List of registered callable keys
77
+ def registered_callables
78
+ @handlers.keys
79
+ end
80
+
81
+ # Register a handler with an explicit key
82
+ #
83
+ # @param key [String] Handler key (usually the callable string)
84
+ # @param handler [Class, Object, Proc] Handler class, instance, or callable
85
+ # @return [void]
86
+ def register(key, handler)
87
+ @handlers[key.to_s] = handler
88
+ @logger.debug("ExplicitMappingResolver: Registered '#{key}'")
89
+ end
90
+
91
+ # Unregister a handler
92
+ #
93
+ # @param key [String] Handler key to remove
94
+ # @return [Object, nil] Removed handler or nil
95
+ def unregister(key)
96
+ @handlers.delete(key.to_s)
97
+ end
98
+
99
+ # Check if a handler is registered
100
+ #
101
+ # @param key [String] Handler key
102
+ # @return [Boolean] true if registered
103
+ def registered?(key)
104
+ @handlers.key?(key.to_s)
105
+ end
106
+
107
+ # Clear all registered handlers
108
+ def clear!
109
+ @handlers.clear
110
+ end
111
+
112
+ # @return [Integer] Number of registered handlers
113
+ def size
114
+ @handlers.size
115
+ end
116
+
117
+ private
118
+
119
+ # Instantiate a handler with configuration
120
+ #
121
+ # @param handler [Class, Object, Proc] Handler class, instance, or callable
122
+ # @param initialization [Hash] Configuration to pass
123
+ # @return [Object] Handler instance or callable
124
+ def instantiate_handler(handler, initialization)
125
+ # Already an instance or Proc - return as-is
126
+ return handler if handler.is_a?(Proc) || !handler.is_a?(Class)
127
+
128
+ # Class - instantiate with config if supported
129
+ arity = handler.instance_method(:initialize).arity
130
+
131
+ if arity.positive? || (arity.negative? && accepts_config_kwarg?(handler))
132
+ handler.new(config: initialization || {})
133
+ else
134
+ handler.new
135
+ end
136
+ end
137
+
138
+ # Check if the class accepts config: keyword argument
139
+ def accepts_config_kwarg?(klass)
140
+ params = klass.instance_method(:initialize).parameters
141
+ params.any? { |type, name| %i[key keyreq].include?(type) && name == :config }
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end