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,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
|