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