gaskit 0.1.0 → 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 +4 -4
- data/.rspec_status +118 -47
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +12 -101
- data/README.md +32 -2
- data/gaskit-0.1.0.gem +0 -0
- data/lib/gaskit/boot/query.rb +1 -1
- data/lib/gaskit/boot/service.rb +1 -1
- data/lib/gaskit/configuration.rb +12 -31
- data/lib/gaskit/contract_registry.rb +2 -2
- data/lib/gaskit/core.rb +8 -25
- data/lib/gaskit/flow.rb +334 -130
- data/lib/gaskit/flow_result.rb +19 -12
- data/lib/gaskit/helpers.rb +15 -0
- data/lib/gaskit/hook_registry.rb +62 -0
- data/lib/gaskit/hookable.rb +133 -0
- data/lib/gaskit/logger.rb +31 -19
- data/lib/gaskit/operation.rb +123 -59
- data/lib/gaskit/operation_exit.rb +5 -1
- data/lib/gaskit/operation_result.rb +6 -6
- data/lib/gaskit/version.rb +1 -1
- data/lib/gaskit.rb +1 -1
- metadata +5 -2
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gaskit
|
4
|
+
# Gaskit::HookRegistry manages registration and retrieval of global hooks for
|
5
|
+
# operation-style lifecycles (e.g., `before`, `after`, `around`).
|
6
|
+
#
|
7
|
+
# Hooks are grouped by type and tag. They are used in classes that include `Gaskit::Hookable`.
|
8
|
+
class HookRegistry
|
9
|
+
def initialize
|
10
|
+
@hooks = {
|
11
|
+
before: Hash.new { |h, k| h[k] = [] },
|
12
|
+
after: Hash.new { |h, k| h[k] = [] },
|
13
|
+
around: Hash.new { |h, k| h[k] = [] }
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Registers a new hook under a given type and tag.
|
18
|
+
#
|
19
|
+
# @param type [Symbol] The lifecycle type: `:before`, `:after`, or `:around`
|
20
|
+
# @param tag [Symbol, String] A symbolic tag to group related hooks (e.g., :audit, :metrics)
|
21
|
+
# @param callable [#call, nil] A callable object (optional if block is given)
|
22
|
+
# @yield [hook] A block to be registered as a hook if no callable is passed
|
23
|
+
# @return [void]
|
24
|
+
# @raise [ArgumentError] If the hook is not callable or the type is invalid
|
25
|
+
def register(type, tag, callable = nil, &block)
|
26
|
+
hook = callable || block
|
27
|
+
raise ArgumentError, "Hook must respond to #call" unless hook.respond_to?(:call)
|
28
|
+
raise ArgumentError, "Unknown hook type: #{type}" unless @hooks.key?(type)
|
29
|
+
|
30
|
+
@hooks[type][tag.to_sym] << hook
|
31
|
+
end
|
32
|
+
|
33
|
+
# Checks if a hook tag is registered under a specific type.
|
34
|
+
#
|
35
|
+
# @param type [Symbol] The hook type (`:before`, `:after`, or `:around`)
|
36
|
+
# @param tag [Symbol, String] The tag to check
|
37
|
+
# @return [Boolean] Whether a hook with that tag exists for the given type
|
38
|
+
def registered?(type, tag)
|
39
|
+
@hooks[type].key?(tag.to_sym)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Fetches hooks for the given type, filtered by tags.
|
43
|
+
#
|
44
|
+
# @param type [Symbol] The hook type (`:before`, `:after`, or `:around`)
|
45
|
+
# @param tags [Array<Symbol, String>, nil] One or more tags to filter hooks by. If nil or empty,
|
46
|
+
# returns all hooks of that type.
|
47
|
+
# @return [Array<#call>] An array of callable hooks
|
48
|
+
def fetch(type, tags = nil)
|
49
|
+
return @hooks[type].values.flatten if tags.nil? || tags.empty?
|
50
|
+
|
51
|
+
(tags || []).flat_map { |tag| @hooks[type][tag.to_sym] }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns all registered tags for a given hook type.
|
55
|
+
#
|
56
|
+
# @param type [Symbol] The hook type (`:before`, `:after`, or `:around`)
|
57
|
+
# @return [Array<Symbol>] List of registered tags under that type
|
58
|
+
def registered_tags(type)
|
59
|
+
@hooks[type].keys
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
module Gaskit
|
6
|
+
module Hookable
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
VALID_HOOK_TYPES = %i[before after around].freeze
|
13
|
+
|
14
|
+
def use_hooks(*tags, all: tags.empty?)
|
15
|
+
registered_hooks.concat(tags.map(&:to_sym)).uniq!
|
16
|
+
|
17
|
+
@run_all_hooks = all
|
18
|
+
@enabled = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def use_hook(type, proc = nil, &block)
|
22
|
+
hook = block_given? ? block : proc
|
23
|
+
type, hook = validate_hook!(type, hook)
|
24
|
+
|
25
|
+
inline_hooks[type] << hook
|
26
|
+
@enabled = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def before(proc = nil, &block)
|
30
|
+
use_hook(:before, proc, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def around(proc = nil, &block)
|
34
|
+
use_hook(:around, proc, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def after(proc = nil, &block)
|
38
|
+
use_hook(:after, proc, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def hooks_enabled?
|
42
|
+
@enabled || false
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_all_hooks?
|
46
|
+
@run_all_hooks || false
|
47
|
+
end
|
48
|
+
|
49
|
+
def registered_hooks
|
50
|
+
@registered_hooks ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def inline_hooks
|
54
|
+
@inline_hooks ||= Concurrent::Hash.new { |h, k| h[k] = [] }
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def validate_hook!(type, hook)
|
60
|
+
type = type.to_sym
|
61
|
+
|
62
|
+
unless VALID_HOOK_TYPES.include?(type)
|
63
|
+
raise ArgumentError, "#{type} is not a valid hook type (valid types are #{VALID_HOOK_TYPES.join(", ")})"
|
64
|
+
end
|
65
|
+
|
66
|
+
raise ArgumentError, "Hook must be callable" unless hook.respond_to?(:call)
|
67
|
+
|
68
|
+
[type, hook]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_hooks(*types, &block)
|
73
|
+
return block.call unless self.class.hooks_enabled?
|
74
|
+
|
75
|
+
types = types.map(&:to_sym)
|
76
|
+
result = nil
|
77
|
+
|
78
|
+
apply_type_hooks(:before) if types.include?(:before)
|
79
|
+
result = apply_type_hooks(:around, &block) if types.include?(:around)
|
80
|
+
apply_type_hooks(:after, result: result) if types.include?(:after)
|
81
|
+
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
def apply_before_hooks
|
86
|
+
apply_type_hooks(:before)
|
87
|
+
end
|
88
|
+
|
89
|
+
def apply_around_hooks(&block)
|
90
|
+
apply_type_hooks(:around, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def apply_after_hooks(result)
|
94
|
+
apply_type_hooks(:after, result: result)
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def collect_hooks(type)
|
100
|
+
inline = self.class.inline_hooks[type]
|
101
|
+
registered =
|
102
|
+
if self.class.run_all_hooks?
|
103
|
+
Gaskit.hooks.fetch(type)
|
104
|
+
else
|
105
|
+
Gaskit.hooks.fetch(type, self.class.registered_hooks)
|
106
|
+
end
|
107
|
+
|
108
|
+
(registered + inline).uniq
|
109
|
+
end
|
110
|
+
|
111
|
+
def apply_type_hooks(type, result: nil, &block)
|
112
|
+
return result unless self.class.hooks_enabled?
|
113
|
+
|
114
|
+
hooks = collect_hooks(type)
|
115
|
+
process_hooks(type, hooks, result, &block)
|
116
|
+
end
|
117
|
+
|
118
|
+
def process_hooks(type, hooks, result, &block)
|
119
|
+
case type
|
120
|
+
when :before
|
121
|
+
hooks.each { |hook| instance_exec(&hook) }
|
122
|
+
when :around
|
123
|
+
result = hooks.reverse.inject(block) do |acc, hook|
|
124
|
+
proc { instance_exec(acc, &hook) }
|
125
|
+
end.call
|
126
|
+
when :after
|
127
|
+
hooks.each { |hook| instance_exec(result, &hook) }
|
128
|
+
end
|
129
|
+
|
130
|
+
result
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/gaskit/logger.rb
CHANGED
@@ -123,10 +123,11 @@ module Gaskit
|
|
123
123
|
lambda do |severity, time, _progname, msg|
|
124
124
|
message, context = extract_message_and_context(msg)
|
125
125
|
context ||= {}
|
126
|
+
class_name = context.delete(:class)
|
126
127
|
|
127
128
|
tags = %W[[#{time.utc.iso8601}] [#{severity}]]
|
128
|
-
tags << "[#{
|
129
|
-
tags += context.map { |k, v| "[#{k}=#{v}]" }
|
129
|
+
tags << "[#{class_name}]" if class_name
|
130
|
+
tags += flatten_context(context).map { |k, v| "[#{k}=#{v}]" }
|
130
131
|
|
131
132
|
"#{tags.join(" ")} #{message}\n"
|
132
133
|
end
|
@@ -143,18 +144,43 @@ module Gaskit
|
|
143
144
|
|
144
145
|
[msg.to_s, {}]
|
145
146
|
end
|
147
|
+
|
148
|
+
# Recursively flattens a nested hash by concatenating keys using underscores.
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# flatten_context({ a: { b: 1, c: { d: 2 } } })
|
152
|
+
# # => { "a_b" => 1, "a_c_d" => 2 }
|
153
|
+
#
|
154
|
+
# @param hash [Hash] The hash to flatten.
|
155
|
+
# @param prefix [String, nil] The prefix to prepend to keys (used during recursion).
|
156
|
+
# @return [Hash] A flat hash with underscore-separated keys.
|
157
|
+
def flatten_context(hash, prefix = nil)
|
158
|
+
result = {}
|
159
|
+
|
160
|
+
hash.each do |key, value|
|
161
|
+
full_key = prefix ? "#{prefix}_#{key}" : key.to_s
|
162
|
+
|
163
|
+
if value.is_a?(Hash)
|
164
|
+
result.merge!(flatten_context(value, full_key))
|
165
|
+
else
|
166
|
+
result[full_key] = value
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
result
|
171
|
+
end
|
146
172
|
end
|
147
173
|
|
148
174
|
attr_reader :context
|
149
175
|
|
150
176
|
# Initializes a new logger instance.
|
151
177
|
#
|
152
|
-
# @param klass [Class] The name of the class being logged.
|
178
|
+
# @param klass [Class, Object, String, Symbol] The name of the class being logged.
|
153
179
|
# @param context [Hash] Optional additional context to include in every log entry.
|
154
180
|
def initialize(klass, context: {})
|
155
|
-
@class_name = resolve_name(klass)
|
156
|
-
@context = apply_context(context)
|
181
|
+
@class_name = Gaskit::Helpers.resolve_name(klass)
|
157
182
|
|
183
|
+
@context = apply_context(context).merge(class: @class_name)
|
158
184
|
@logger = Gaskit.configuration.logger || ::Logger.new($stdout)
|
159
185
|
rescue StandardError
|
160
186
|
::Logger.new($stdout).error "Failed to initialize logger: #{$ERROR_INFO}"
|
@@ -226,20 +252,6 @@ module Gaskit
|
|
226
252
|
|
227
253
|
private
|
228
254
|
|
229
|
-
# Resolves the class name provided when instantiating the logger.
|
230
|
-
#
|
231
|
-
# @return [String] The resolved class name.
|
232
|
-
def resolve_name(source)
|
233
|
-
case source
|
234
|
-
when String, Symbol
|
235
|
-
source.to_s
|
236
|
-
when Class
|
237
|
-
source.name
|
238
|
-
else
|
239
|
-
source.class.name
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
255
|
def apply_context(context)
|
244
256
|
default_context = Gaskit.configuration.context_provider.call
|
245
257
|
context = default_context.merge(context)
|
data/lib/gaskit/operation.rb
CHANGED
@@ -4,16 +4,14 @@ require_relative "core"
|
|
4
4
|
require_relative "operation_result"
|
5
5
|
require_relative "operation_exit"
|
6
6
|
require_relative "helpers"
|
7
|
+
require_relative "hookable"
|
7
8
|
|
8
9
|
module Gaskit
|
9
10
|
# The Gaskit::Operation class defines a structured and extensible pattern for building application operations.
|
10
11
|
# It enforces consistent behavior across operations while supporting customization via contracts.
|
11
12
|
#
|
12
13
|
# # Features
|
13
|
-
# - Pluggable contracts via `use_contract`, allowing you to define or reference a `result`
|
14
|
-
# `early_exit` class.
|
15
|
-
# - A **contract** is a pairing of a result and early_exit class, either manually defined or registered
|
16
|
-
# under a symbol.
|
14
|
+
# - Pluggable contracts via `use_contract`, allowing you to define or reference a `result` class.
|
17
15
|
# - Integrated duration tracking, structured logging, and early exits.
|
18
16
|
# - Supports `.call` (non-raising) and `.call!` (raising) styles.
|
19
17
|
#
|
@@ -50,6 +48,8 @@ module Gaskit
|
|
50
48
|
#
|
51
49
|
# @abstract Subclass this and define `#call` or `#call!` to create a new operation.
|
52
50
|
class Operation
|
51
|
+
include Gaskit::Hookable
|
52
|
+
|
53
53
|
class << self
|
54
54
|
def result_class
|
55
55
|
return @result_class if defined?(@result_class) && @result_class
|
@@ -58,30 +58,26 @@ module Gaskit
|
|
58
58
|
nil
|
59
59
|
end
|
60
60
|
|
61
|
-
# Defines the result
|
62
|
-
# Can reference a named contract registered in `Gaskit::Registry`,
|
63
|
-
#
|
61
|
+
# Defines the result class for this operation.
|
62
|
+
# Can reference a named contract registered in `Gaskit::Registry`, or define one without
|
63
|
+
# using a registered contract.
|
64
64
|
#
|
65
65
|
# @example Use a registered contract
|
66
66
|
# use_contract :service
|
67
67
|
#
|
68
|
-
# @example
|
69
|
-
# use_contract
|
70
|
-
#
|
71
|
-
# @example Define both without using a contract name
|
72
|
-
# use_contract result: CustomResult, early_exit: CustomExit
|
68
|
+
# @example Define a contract that has not been registered
|
69
|
+
# use_contract result: CustomResult
|
73
70
|
#
|
74
71
|
# @param contract [Symbol, nil] A registered contract name (e.g., `:service`)
|
75
72
|
# @param result [Class, nil] A class that inherits from `Gaskit::BaseResult`
|
76
73
|
# @raise [ArgumentError] if contract is not a symbol or unexpected args are passed
|
77
74
|
# @raise [ResultTypeError] if `result` is not a subclass of `Gaskit::BaseResult`
|
78
|
-
# @raise [EarlyExitTypeError] if `early_exit` is not a subclass of `Gaskit::BaseExit`
|
79
75
|
# @return [void]
|
80
76
|
def use_contract(contract = nil, result: nil)
|
81
77
|
if contract
|
82
78
|
raise ArgumentError, "use_contract must be called with a symbol or keyword args" unless contract.is_a?(Symbol)
|
83
79
|
|
84
|
-
result = Gaskit.
|
80
|
+
result = Gaskit.contracts.fetch(contract)
|
85
81
|
end
|
86
82
|
|
87
83
|
Gaskit::ContractRegistry.verify_result_class!(result)
|
@@ -98,6 +94,10 @@ module Gaskit
|
|
98
94
|
# @param code [String, nil] Optional error code.
|
99
95
|
# @return [void]
|
100
96
|
def error(key, message, code: nil)
|
97
|
+
raise ArgumentError, "Error key must be a symbol or a string, received #{key}" unless key.is_a?(Symbol)
|
98
|
+
raise ArgumentError, "Error message must be a string" unless message.is_a?(String)
|
99
|
+
raise ArgumentError, "Error key :#{key} is already registered" if errors_registry.key?(key)
|
100
|
+
|
101
101
|
errors_registry[key.to_sym] = { message: message, code: code }
|
102
102
|
end
|
103
103
|
|
@@ -108,24 +108,24 @@ module Gaskit
|
|
108
108
|
@errors_registry ||= {}
|
109
109
|
end
|
110
110
|
|
111
|
-
#
|
111
|
+
# Executes the operation with soft-failure handling
|
112
112
|
#
|
113
|
-
# @param [Array]
|
114
|
-
# @param [Hash]
|
115
|
-
# @
|
116
|
-
# @return [OperationResult]
|
117
|
-
def call(*args, **kwargs, &block)
|
118
|
-
invoke(false, *args, **kwargs, &block)
|
113
|
+
# @param args [Array] Positional arguments for the first step
|
114
|
+
# @param context [Hash] Shared context across all steps
|
115
|
+
# @param kwargs [Hash] Keyword arguments for the first step
|
116
|
+
# @return [Gaskit::OperationResult]
|
117
|
+
def call(*args, context: {}, **kwargs, &block)
|
118
|
+
invoke(false, context, *args, **kwargs, &block)
|
119
119
|
end
|
120
120
|
|
121
|
-
#
|
121
|
+
# Executes the operation with hard-failure handling (raises on unhandled errors)
|
122
122
|
#
|
123
|
-
# @param [Array]
|
124
|
-
# @param [Hash]
|
125
|
-
# @
|
126
|
-
# @return [OperationResult]
|
127
|
-
def call!(*args, **kwargs, &block)
|
128
|
-
invoke(true, *args, **kwargs, &block)
|
123
|
+
# @param args [Array] Positional arguments for the first step
|
124
|
+
# @param context [Hash] Shared context across all steps
|
125
|
+
# @param kwargs [Hash] Keyword arguments for the first step
|
126
|
+
# @return [Gaskit::OperationResult]
|
127
|
+
def call!(*args, context: {}, **kwargs, &block)
|
128
|
+
invoke(true, context, *args, **kwargs, &block)
|
129
129
|
end
|
130
130
|
|
131
131
|
private
|
@@ -138,21 +138,26 @@ module Gaskit
|
|
138
138
|
# @yield [Block] Additional block logic during execution.
|
139
139
|
# @raise [NotImplementedError] If operation type is not set in subclasses.
|
140
140
|
# @return [OperationResult] The result of the operation.
|
141
|
-
def invoke(raise_on_failure, *args, **kwargs, &block)
|
141
|
+
def invoke(raise_on_failure, context, *args, **kwargs, &block)
|
142
142
|
unless result_class
|
143
143
|
raise NotImplementedError, "No result_class defined for #{name} or its ancestors. " \
|
144
144
|
"Did you forget to call `use_contract`?"
|
145
145
|
end
|
146
146
|
|
147
|
-
context = kwargs.delete(:context) || {}
|
148
147
|
operation = new(raise_on_failure, context: context)
|
149
148
|
duration, (result, error) = execute(operation, *args, **kwargs, &block)
|
150
149
|
|
151
|
-
|
152
|
-
|
150
|
+
result = build_result(result, error, duration, operation.context)
|
151
|
+
|
152
|
+
begin
|
153
|
+
operation.apply_after_hooks(result)
|
154
|
+
rescue StandardError => e
|
155
|
+
result = handle_after_hook_error(operation, result, e, duration)
|
153
156
|
end
|
154
157
|
|
155
|
-
|
158
|
+
log_execution_debug(operation, duration)
|
159
|
+
|
160
|
+
result
|
156
161
|
end
|
157
162
|
|
158
163
|
# Executes the operation logic and handles potential exceptions.
|
@@ -164,16 +169,40 @@ module Gaskit
|
|
164
169
|
# @return [Array] The execution duration, result, and error if any.
|
165
170
|
def execute(operation, *args, **kwargs, &block)
|
166
171
|
Helpers.time_execution do
|
167
|
-
|
172
|
+
operation.apply_hooks(:before, :around) do
|
173
|
+
[operation.call(*args, **kwargs, &block), nil]
|
174
|
+
end
|
168
175
|
rescue StandardError => e
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
log_exception(operation, e)
|
173
|
-
raise e if operation.raise_on_failure
|
176
|
+
handle_execution_error(operation, e)
|
177
|
+
end
|
178
|
+
end
|
174
179
|
|
175
|
-
|
176
|
-
|
180
|
+
# Builds an OperationResult instance.
|
181
|
+
#
|
182
|
+
# @param [Object, nil] result The result of the operation.
|
183
|
+
# @param [StandardError] error The error, if any.
|
184
|
+
# @param [Float] duration The duration of the operation.
|
185
|
+
# @param [Gaskit::Context] context The operation context.
|
186
|
+
# @return [OperationResult]
|
187
|
+
def build_result(result, error, duration, context)
|
188
|
+
result_class.new(
|
189
|
+
error.nil?,
|
190
|
+
result,
|
191
|
+
error,
|
192
|
+
duration: duration,
|
193
|
+
context: context
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Logs execution information about the operation if Gaskit.debug? == true.
|
198
|
+
#
|
199
|
+
# @param [Gaskit::Operation] operation The operation instance.
|
200
|
+
# @param [Float] duration The operation's duration.
|
201
|
+
def log_execution_debug(operation, duration)
|
202
|
+
return unless Gaskit.debug?
|
203
|
+
|
204
|
+
operation.logger.debug(context: { duration: duration }) do
|
205
|
+
"Operation completed in #{duration} seconds"
|
177
206
|
end
|
178
207
|
end
|
179
208
|
|
@@ -189,16 +218,40 @@ module Gaskit
|
|
189
218
|
# Logs any unhandled exception during the operation.
|
190
219
|
#
|
191
220
|
# @param [Gaskit::Operation] operation The operation instance.
|
192
|
-
# @param [
|
221
|
+
# @param [StandardError] exception The raised exception.
|
193
222
|
# @return [void]
|
194
223
|
def log_exception(operation, exception)
|
195
224
|
operation.logger.error { "[#{exception.class}] #{exception.message}" }
|
196
225
|
operation.logger.error { exception.backtrace&.join("\n") }
|
197
226
|
end
|
198
227
|
|
199
|
-
#
|
200
|
-
|
201
|
-
|
228
|
+
# Handles exceptions during execution.
|
229
|
+
#
|
230
|
+
# @param [Gaskit::Operation] operation The operation instance.
|
231
|
+
# @param [StandardError] error The raised error.
|
232
|
+
# @return [Array] The result and the error.
|
233
|
+
def handle_execution_error(operation, error)
|
234
|
+
if error.is_a?(Gaskit::OperationExit)
|
235
|
+
log_exit(operation, error)
|
236
|
+
else
|
237
|
+
log_exception(operation, error)
|
238
|
+
raise error if operation.raise_on_failure?
|
239
|
+
end
|
240
|
+
|
241
|
+
[nil, error]
|
242
|
+
end
|
243
|
+
|
244
|
+
# Handles errors raised after executing hooks.
|
245
|
+
#
|
246
|
+
# @param [Gaskit::Operation] operation The operation instance.
|
247
|
+
# @param [OperationResult] result The current operation result.
|
248
|
+
# @param [StandardError] error The encountered error.
|
249
|
+
# @param [Float] duration The execution duration.
|
250
|
+
def handle_after_hook_error(operation, result, error, duration)
|
251
|
+
log_exception(operation, error)
|
252
|
+
raise error if operation.raise_on_failure?
|
253
|
+
|
254
|
+
build_result(result, error, duration, operation.context)
|
202
255
|
end
|
203
256
|
end
|
204
257
|
|
@@ -212,16 +265,15 @@ module Gaskit
|
|
212
265
|
def initialize(raise_on_failure, context: {})
|
213
266
|
@raise_on_failure = raise_on_failure
|
214
267
|
@context = apply_context(context)
|
215
|
-
@logger = Gaskit::Logger.new(self
|
268
|
+
@logger = Gaskit::Logger.new(self, context: @context)
|
269
|
+
|
270
|
+
return unless self.class.result_class.nil?
|
271
|
+
|
272
|
+
raise Gaskit::Error, "No result_class defined for #{self.class.name} or its ancestors."
|
216
273
|
end
|
217
274
|
|
218
|
-
|
219
|
-
|
220
|
-
# @param context [Hash] The context provided directly to the Flow.
|
221
|
-
# @return [Hash] The fully applied context Hash.
|
222
|
-
def apply_context(context)
|
223
|
-
default_context = Gaskit.configuration.context_provider.call
|
224
|
-
Helpers.deep_compact(default_context.merge(context))
|
275
|
+
def raise_on_failure?
|
276
|
+
@raise_on_failure
|
225
277
|
end
|
226
278
|
|
227
279
|
# Executes the operation logic.
|
@@ -239,22 +291,34 @@ module Gaskit
|
|
239
291
|
# If the key was previously registered via `self.error`, it uses the declared message and code.
|
240
292
|
# Otherwise, it uses the key as the message.
|
241
293
|
#
|
242
|
-
# @param
|
294
|
+
# @param error_key [Symbol] The symbolic reason for exiting.
|
243
295
|
# @param message [String, nil] Optional message override.
|
296
|
+
# @param code [String, nil] Optional error code.
|
244
297
|
# @raise [OperationExit] always raises an instance with message and optional code
|
245
|
-
def exit(
|
246
|
-
|
247
|
-
definition = self.class.errors_registry
|
298
|
+
def exit(error_key, message = nil, code: nil)
|
299
|
+
error_key = error_key.to_sym
|
300
|
+
definition = self.class.errors_registry.fetch(error_key, nil)
|
248
301
|
|
249
302
|
if definition
|
250
303
|
message ||= definition[:message]
|
251
|
-
code
|
304
|
+
code ||= definition[:code]
|
252
305
|
end
|
253
306
|
|
254
|
-
raise OperationExit.new(
|
307
|
+
raise OperationExit.new(error_key, message, code: code)
|
255
308
|
end
|
256
309
|
|
257
310
|
# @see #exit
|
258
311
|
alias abort exit
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
# Applies global context, if set, from Gaskit.configuration.context_provider.
|
316
|
+
#
|
317
|
+
# @param context [Hash] The context provided directly to the Flow.
|
318
|
+
# @return [Hash] The fully applied context Hash.
|
319
|
+
def apply_context(context = {})
|
320
|
+
default_context = Gaskit.configuration.context_provider.call
|
321
|
+
Helpers.deep_compact(default_context.merge(context))
|
322
|
+
end
|
259
323
|
end
|
260
324
|
end
|
@@ -31,9 +31,13 @@ module Gaskit
|
|
31
31
|
# @param message [String, nil] A human-readable message (defaults to key if not provided)
|
32
32
|
# @param code [String, nil] A structured error code for analytics or debugging
|
33
33
|
def initialize(key, message = nil, code: nil)
|
34
|
-
super(message ||
|
34
|
+
super(message || "early exit")
|
35
35
|
@key = key
|
36
36
|
@code = code
|
37
37
|
end
|
38
|
+
|
39
|
+
def inspect
|
40
|
+
"#<#{self.class} key=#{key.inspect} message=#{message.inspect} code=#{code.inspect}>"
|
41
|
+
end
|
38
42
|
end
|
39
43
|
end
|
@@ -30,11 +30,11 @@ module Gaskit
|
|
30
30
|
|
31
31
|
# Initializes a new instance of OperationResult.
|
32
32
|
#
|
33
|
-
# @param [Boolean]
|
34
|
-
# @param [Object, nil]
|
35
|
-
# @param [
|
36
|
-
# @param [Float, String]
|
37
|
-
# @param [Hash]
|
33
|
+
# @param success [Boolean] Whether the operation was successful.
|
34
|
+
# @param value [Object, nil] The value obtained as a result of the operation.
|
35
|
+
# @param error [StandardError, nil] The error encountered during the operation.
|
36
|
+
# @param duration [Float, String] The time taken to complete the operation in seconds.
|
37
|
+
# @param context [Hash] Optional context metadata for this operation.
|
38
38
|
def initialize(success, value, error, duration:, context: {})
|
39
39
|
@success = success
|
40
40
|
@value = value
|
@@ -47,7 +47,7 @@ module Gaskit
|
|
47
47
|
#
|
48
48
|
# @return [String] The formatted inspection string.
|
49
49
|
def inspect
|
50
|
-
"#<#{self.class.name}
|
50
|
+
"#<#{self.class.name} status=#{status} value=#{value.inspect} duration=#{duration}>"
|
51
51
|
end
|
52
52
|
|
53
53
|
# Indicates whether the operation was successful.
|
data/lib/gaskit/version.rb
CHANGED
data/lib/gaskit.rb
CHANGED
@@ -27,7 +27,7 @@ require_relative "gaskit/railtie" if defined?(Rails::Railtie)
|
|
27
27
|
# end
|
28
28
|
#
|
29
29
|
# @example Registering a contract
|
30
|
-
# Gaskit.
|
30
|
+
# Gaskit.contracts.register(:service, MyResultClass)
|
31
31
|
#
|
32
32
|
# @example Defining a service
|
33
33
|
# class MyService < Gaskit::Service
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gaskit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Lucas
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- LICENSE.txt
|
84
84
|
- README.md
|
85
85
|
- gasket-0.1.0.gem
|
86
|
+
- gaskit-0.1.0.gem
|
86
87
|
- gaskit.gemspec
|
87
88
|
- lib/gaskit.rb
|
88
89
|
- lib/gaskit/boot/query.rb
|
@@ -94,6 +95,8 @@ files:
|
|
94
95
|
- lib/gaskit/flow.rb
|
95
96
|
- lib/gaskit/flow_result.rb
|
96
97
|
- lib/gaskit/helpers.rb
|
98
|
+
- lib/gaskit/hook_registry.rb
|
99
|
+
- lib/gaskit/hookable.rb
|
97
100
|
- lib/gaskit/logger.rb
|
98
101
|
- lib/gaskit/operation.rb
|
99
102
|
- lib/gaskit/operation_exit.rb
|