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,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Registry
5
+ module Resolvers
6
+ # TAS-93: Method Dispatch Wrapper
7
+ #
8
+ # Wraps a handler to redirect the standard `.call()` interface to
9
+ # a specified method name. This enables handlers with multiple
10
+ # entry points (e.g., process, refund, validate) to be invoked
11
+ # through the unified `.call()` interface.
12
+ #
13
+ # == Background
14
+ #
15
+ # The worker execution system always invokes `handler.call(context)`.
16
+ # When a HandlerDefinition specifies `method: refund`, we need to
17
+ # redirect that call to `handler.refund(context)`.
18
+ #
19
+ # == Usage
20
+ #
21
+ # # Original handler
22
+ # class PaymentHandler
23
+ # def process(context); end
24
+ # def refund(context); end
25
+ # end
26
+ #
27
+ # # Wrap for refund dispatch
28
+ # handler = PaymentHandler.new
29
+ # wrapped = MethodDispatchWrapper.new(handler, :refund)
30
+ #
31
+ # # Now call() invokes refund()
32
+ # wrapped.call(context) # => handler.refund(context)
33
+ #
34
+ # == When Wrapping Occurs
35
+ #
36
+ # The ResolverChain automatically wraps handlers when:
37
+ # - HandlerDefinition has `method` set to something other than 'call'
38
+ # - The resolved handler doesn't already respond to `.call()`
39
+ #
40
+ class MethodDispatchWrapper
41
+ attr_reader :handler, :target_method
42
+
43
+ # Create a method dispatch wrapper
44
+ #
45
+ # @param handler [Object] The underlying handler instance
46
+ # @param target_method [Symbol, String] Method to invoke on call()
47
+ def initialize(handler, target_method)
48
+ @handler = handler
49
+ @target_method = target_method.to_sym
50
+ validate!
51
+ end
52
+
53
+ # Invoke the target method on the wrapped handler
54
+ #
55
+ # @param args [Array] Arguments to pass through
56
+ # @param kwargs [Hash] Keyword arguments to pass through
57
+ # @param block [Proc] Block to pass through
58
+ # @return [Object] Result from the target method
59
+ def call(*, **kwargs, &)
60
+ if kwargs.empty?
61
+ @handler.public_send(@target_method, *, &)
62
+ else
63
+ @handler.public_send(@target_method, *, **kwargs, &)
64
+ end
65
+ end
66
+
67
+ # Delegate respond_to? to the underlying handler
68
+ #
69
+ # @param method_name [Symbol] Method to check
70
+ # @param include_private [Boolean] Include private methods
71
+ # @return [Boolean]
72
+ # rubocop:disable Style/OptionalBooleanParameter -- Must match Ruby's respond_to? signature
73
+ def respond_to?(method_name, include_private = false)
74
+ method_name.to_sym == :call || @handler.respond_to?(method_name, include_private)
75
+ end
76
+ # rubocop:enable Style/OptionalBooleanParameter
77
+
78
+ # Delegate respond_to_missing? to the underlying handler
79
+ def respond_to_missing?(method_name, include_private = false)
80
+ @handler.respond_to_missing?(method_name, include_private)
81
+ end
82
+
83
+ # Forward method_missing to underlying handler for delegation
84
+ #
85
+ # @param method_name [Symbol] Method name
86
+ # @param args [Array] Arguments
87
+ # @param block [Proc] Block
88
+ def method_missing(method_name, *, &)
89
+ if @handler.respond_to?(method_name)
90
+ @handler.public_send(method_name, *, &)
91
+ else
92
+ super
93
+ end
94
+ end
95
+
96
+ # String representation for debugging
97
+ #
98
+ # @return [String]
99
+ def to_s
100
+ "#<MethodDispatchWrapper handler=#{@handler.class} method=#{@target_method}>"
101
+ end
102
+
103
+ # Inspect for debugging
104
+ #
105
+ # @return [String]
106
+ def inspect
107
+ to_s
108
+ end
109
+
110
+ # Access the underlying handler class
111
+ #
112
+ # @return [Class]
113
+ def handler_class
114
+ @handler.class
115
+ end
116
+
117
+ # Check if this is wrapping a particular class
118
+ #
119
+ # @param klass [Class] Class to check
120
+ # @return [Boolean]
121
+ def wraps?(klass)
122
+ @handler.is_a?(klass)
123
+ end
124
+
125
+ # Unwrap to get the original handler
126
+ #
127
+ # @return [Object] The underlying handler
128
+ def unwrap
129
+ @handler
130
+ end
131
+
132
+ private
133
+
134
+ # Validate the wrapper can dispatch to target method
135
+ def validate!
136
+ return if @handler.respond_to?(@target_method)
137
+
138
+ raise ArgumentError,
139
+ "Handler #{@handler.class} does not respond to ##{@target_method}"
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_resolver'
4
+
5
+ module TaskerCore
6
+ module Registry
7
+ module Resolvers
8
+ # TAS-93: Developer-Friendly Base Class for Custom Resolvers
9
+ #
10
+ # RegistryResolver provides a DSL for creating custom handler resolvers
11
+ # with pattern matching, prefix matching, and explicit handler registration.
12
+ #
13
+ # == Usage Examples
14
+ #
15
+ # === Pattern-Based Resolution
16
+ #
17
+ # class PaymentResolver < RegistryResolver
18
+ # # Match handlers by pattern
19
+ # handles_pattern(/^Payments::.*Handler$/)
20
+ #
21
+ # # Custom resolution logic
22
+ # def resolve_handler(definition, config)
23
+ # klass = definition.callable.constantize
24
+ # klass.new(config: definition.initialization)
25
+ # end
26
+ # end
27
+ #
28
+ # === Prefix-Based Resolution
29
+ #
30
+ # class LegacyResolver < RegistryResolver
31
+ # handles_prefix 'Legacy::'
32
+ # priority_value 50
33
+ #
34
+ # def resolve_handler(definition, config)
35
+ # # Custom instantiation for legacy handlers
36
+ # LegacyAdapter.wrap(definition.callable.constantize)
37
+ # end
38
+ # end
39
+ #
40
+ # === Explicit Handler Registration
41
+ #
42
+ # class ExplicitResolver < RegistryResolver
43
+ # register_handler 'payment.process', PaymentHandler
44
+ # register_handler 'payment.refund', RefundHandler
45
+ # end
46
+ #
47
+ # == DSL Methods
48
+ #
49
+ # - `handles_pattern(regex)` - Match callable names by regex
50
+ # - `handles_prefix(prefix)` - Match callable names by prefix
51
+ # - `register_handler(key, handler)` - Explicit key → handler mapping
52
+ # - `priority_value(n)` - Set resolver priority (default: 50)
53
+ # - `resolver_name(name)` - Set human-readable name
54
+ #
55
+ class RegistryResolver < BaseResolver
56
+ class << self
57
+ # DSL: Set pattern to match callable names
58
+ def handles_pattern(pattern)
59
+ @pattern = pattern
60
+ end
61
+
62
+ # DSL: Set prefix to match callable names
63
+ def handles_prefix(prefix)
64
+ @prefix = prefix
65
+ end
66
+
67
+ # DSL: Register explicit handler mapping
68
+ def register_handler(key, handler_or_class)
69
+ @handlers ||= {}
70
+ @handlers[key.to_s] = handler_or_class
71
+ end
72
+
73
+ # DSL: Set resolver priority
74
+ def priority_value(value)
75
+ @priority = value
76
+ end
77
+
78
+ # DSL: Set resolver name
79
+ def resolver_name(value)
80
+ @resolver_name = value
81
+ end
82
+
83
+ # Accessors for DSL values
84
+ attr_reader :pattern, :prefix, :handlers
85
+
86
+ def configured_priority
87
+ @priority || 50 # Default: between explicit (10) and inferential (100)
88
+ end
89
+
90
+ def configured_name
91
+ @resolver_name || name.demodulize.underscore
92
+ end
93
+
94
+ # Inherit DSL configuration in subclasses
95
+ def inherited(subclass)
96
+ super
97
+ subclass.instance_variable_set(:@handlers, @handlers&.dup || {})
98
+ end
99
+ end
100
+
101
+ def initialize
102
+ super
103
+ @handlers = self.class.handlers&.dup || {}
104
+ @logger = TaskerCore::Logger.instance
105
+ end
106
+
107
+ # @return [String] Human-readable resolver name
108
+ def name
109
+ self.class.configured_name
110
+ end
111
+
112
+ # @return [Integer] Resolution priority
113
+ def priority
114
+ self.class.configured_priority
115
+ end
116
+
117
+ # Check if this resolver can handle the definition
118
+ #
119
+ # Resolution order:
120
+ # 1. Check explicit handler registration
121
+ # 2. Check pattern match
122
+ # 3. Check prefix match
123
+ #
124
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
125
+ # @param config [Hash] Additional context
126
+ # @return [Boolean]
127
+ def can_resolve?(definition, _config)
128
+ callable = definition.callable.to_s
129
+
130
+ # Check explicit registration
131
+ return true if @handlers.key?(callable)
132
+
133
+ # Check pattern
134
+ return true if self.class.pattern && callable.match?(self.class.pattern)
135
+
136
+ # Check prefix
137
+ return true if self.class.prefix && callable.start_with?(self.class.prefix)
138
+
139
+ false
140
+ end
141
+
142
+ # Resolve the handler from the definition
143
+ #
144
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
145
+ # @param config [Hash] Additional context
146
+ # @return [Object, nil] Handler instance or nil
147
+ def resolve(definition, config)
148
+ callable = definition.callable.to_s
149
+
150
+ # Try explicit registration first
151
+ if @handlers.key?(callable)
152
+ handler = @handlers[callable]
153
+ return instantiate_handler(handler, definition.initialization)
154
+ end
155
+
156
+ # Otherwise, delegate to subclass implementation
157
+ resolve_handler(definition, config)
158
+ rescue StandardError => e
159
+ @logger.debug("RegistryResolver #{name}: Failed to resolve '#{callable}': #{e.message}")
160
+ nil
161
+ end
162
+
163
+ # @return [Array<String>] List of explicitly registered callables
164
+ def registered_callables
165
+ @handlers.keys
166
+ end
167
+
168
+ # Register a handler at runtime
169
+ #
170
+ # @param key [String] Handler key/callable name
171
+ # @param handler [Object] Handler class or instance
172
+ def register(key, handler)
173
+ @handlers[key.to_s] = handler
174
+ end
175
+
176
+ # Unregister a handler at runtime
177
+ #
178
+ # @param key [String] Handler key to remove
179
+ # @return [Object, nil] Removed handler or nil
180
+ def unregister(key)
181
+ @handlers.delete(key.to_s)
182
+ end
183
+
184
+ protected
185
+
186
+ # Override in subclass to provide custom resolution logic
187
+ #
188
+ # This is called when no explicit handler is registered.
189
+ #
190
+ # @param definition [TaskerCore::Types::HandlerDefinition] Handler configuration
191
+ # @param config [Hash] Additional context
192
+ # @return [Object, nil] Handler instance or nil
193
+ def resolve_handler(_definition, _config)
194
+ nil # Default: no custom resolution
195
+ end
196
+
197
+ # Instantiate a handler with configuration
198
+ #
199
+ # Handles:
200
+ # - Classes with (config:) keyword arg
201
+ # - Classes with no-arg constructor
202
+ # - Already-instantiated objects
203
+ #
204
+ # @param handler [Class, Object] Handler class or instance
205
+ # @param initialization [Hash] Configuration to pass
206
+ # @return [Object] Handler instance
207
+ def instantiate_handler(handler, initialization)
208
+ return handler unless handler.is_a?(Class)
209
+
210
+ arity = handler.instance_method(:initialize).arity
211
+
212
+ if arity.positive? || (arity.negative? && accepts_config_kwarg?(handler))
213
+ handler.new(config: initialization || {})
214
+ else
215
+ handler.new
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ # Check if the class accepts config: keyword argument
222
+ def accepts_config_kwarg?(klass)
223
+ params = klass.instance_method(:initialize).parameters
224
+ params.any? { |type, name| %i[key keyreq].include?(type) && name == :config }
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TAS-93: Step Handler Resolver Infrastructure
4
+ #
5
+ # This module provides the resolver chain pattern for step handler resolution.
6
+ # Resolvers are tried in priority order until one successfully resolves.
7
+ #
8
+ # == Built-in Resolvers
9
+ #
10
+ # - ExplicitMappingResolver (priority 10): Explicit key → handler mappings
11
+ # - ClassConstantResolver (priority 100): Class path inference via const_get
12
+ #
13
+ # == Custom Resolvers
14
+ #
15
+ # Extend RegistryResolver for developer-friendly custom resolution:
16
+ #
17
+ # class PaymentResolver < TaskerCore::Registry::Resolvers::RegistryResolver
18
+ # handles_pattern(/^Payments::/)
19
+ # priority_value 50
20
+ #
21
+ # def resolve_handler(definition, config)
22
+ # # Custom resolution logic
23
+ # end
24
+ # end
25
+ #
26
+ # == Method Dispatch
27
+ #
28
+ # When HandlerDefinition specifies `method: refund`, the chain automatically
29
+ # wraps the handler with MethodDispatchWrapper to redirect .call() → .refund()
30
+ #
31
+ require_relative 'resolvers/base_resolver'
32
+ require_relative 'resolvers/registry_resolver'
33
+ require_relative 'resolvers/explicit_mapping_resolver'
34
+ require_relative 'resolvers/class_constant_resolver'
35
+ require_relative 'resolvers/method_dispatch_wrapper'
36
+
37
+ module TaskerCore
38
+ module Registry
39
+ module Resolvers
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TAS-93: Resolver chain infrastructure (load first - used by HandlerRegistry)
4
+ require_relative 'registry/resolvers'
5
+ require_relative 'registry/resolver_chain'
6
+
7
+ require_relative 'registry/handler_registry'
8
+
9
+ module TaskerCore
10
+ module Registry
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'mixins'
5
+
6
+ module TaskerCore
7
+ module StepHandler
8
+ # API step handler that provides HTTP functionality.
9
+ #
10
+ # ## TAS-112: Composition Pattern (DEPRECATED CLASS)
11
+ #
12
+ # This class is provided for backward compatibility. For new code, use the mixin pattern:
13
+ #
14
+ # ```ruby
15
+ # class MyApiHandler < TaskerCore::StepHandler::Base
16
+ # include TaskerCore::StepHandler::Mixins::API
17
+ #
18
+ # def call(context)
19
+ # response = get('/users')
20
+ # success(result: response.body)
21
+ # end
22
+ # end
23
+ # ```
24
+ #
25
+ # ## Key Features
26
+ # - Faraday HTTP client with full configuration support
27
+ # - Automatic error classification (RetryableError vs PermanentError)
28
+ # - Retry-After header support for server-requested backoff
29
+ # - SSL configuration, headers, query parameters
30
+ class Api < Base
31
+ include Mixins::API
32
+
33
+ # Legacy process method for Rails engine compatibility
34
+ # New handlers should implement call(context) instead
35
+ #
36
+ # @param _task [Object] Task (unused, for Rails compatibility)
37
+ # @param _sequence [Object] Sequence (unused, for Rails compatibility)
38
+ # @param _step [Object] Step (unused, for Rails compatibility)
39
+ # @return [Faraday::Response] HTTP response object
40
+ def process(_task, _sequence, _step)
41
+ url = config[:url] || config['url']
42
+ response = connection.get(url)
43
+ process_response(response)
44
+ response
45
+ end
46
+ end
47
+ end
48
+ end