gaskit 0.1.0
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/.rspec +3 -0
- data/.rspec_status +75 -0
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +110 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/gasket-0.1.0.gem +0 -0
- data/gaskit.gemspec +42 -0
- data/lib/gaskit/boot/query.rb +30 -0
- data/lib/gaskit/boot/service.rb +28 -0
- data/lib/gaskit/configuration.rb +144 -0
- data/lib/gaskit/contract_registry.rb +75 -0
- data/lib/gaskit/core.rb +64 -0
- data/lib/gaskit/error.rb +16 -0
- data/lib/gaskit/flow.rb +227 -0
- data/lib/gaskit/flow_result.rb +35 -0
- data/lib/gaskit/helpers.rb +30 -0
- data/lib/gaskit/logger.rb +274 -0
- data/lib/gaskit/operation.rb +260 -0
- data/lib/gaskit/operation_exit.rb +39 -0
- data/lib/gaskit/operation_result.rb +157 -0
- data/lib/gaskit/railtie.rb +11 -0
- data/lib/gaskit/repository.rb +143 -0
- data/lib/gaskit/version.rb +5 -0
- data/lib/gaskit.rb +44 -0
- data/lib/generators/gaskit/operation/flow_generator.rb +34 -0
- data/lib/generators/gaskit/operation/operation_generator.rb +51 -0
- data/lib/generators/gaskit/operation/query_generator.rb +22 -0
- data/lib/generators/gaskit/operation/repository_generator.rb +37 -0
- data/lib/generators/gaskit/operation/service_generator.rb +22 -0
- data/lib/generators/gaskit/operation/templates/flow.rb.tt +7 -0
- data/lib/generators/gaskit/operation/templates/operation.rb.tt +16 -0
- data/lib/generators/gaskit/operation/templates/repository.rb.tt +7 -0
- data/sig/gaskit.rbs +4 -0
- metadata +140 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "json"
|
5
|
+
require "time"
|
6
|
+
require "English"
|
7
|
+
|
8
|
+
require_relative "helpers"
|
9
|
+
|
10
|
+
module Gaskit
|
11
|
+
# A logger class designed for structured logging with support for JSON, contextual data,
|
12
|
+
# and environment-aware formatting.
|
13
|
+
#
|
14
|
+
# This logger wraps a configurable `Logger` instance and supports:
|
15
|
+
#
|
16
|
+
# - Structured or human-readable log formatting
|
17
|
+
# - Inclusion of global and per-call context (with filtering of sensitive keys)
|
18
|
+
# - Support for custom log levels, formatters, and disabling output via `Gaskit.config`
|
19
|
+
#
|
20
|
+
# By default, logs are pretty-printed in development and JSON-formatted in production.
|
21
|
+
# These defaults can be overridden via configuration.
|
22
|
+
#
|
23
|
+
# @example Basic usage with default formatting
|
24
|
+
# logger = Gaskit::Logger.new('MyOperation', context: { user_id: 42 })
|
25
|
+
#
|
26
|
+
# logger.info("Operation started")
|
27
|
+
# logger.debug(context: { details: "debug info" }) { "Deferred message" }
|
28
|
+
#
|
29
|
+
# @example Using logger within a class
|
30
|
+
# class UserRepository
|
31
|
+
# def self.logger
|
32
|
+
# @logger ||= Gaskit::Logger.new(self)
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def self.find(id)
|
36
|
+
# logger.info("Looking up user", context: { user_id: id })
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Customizing the log formatter globally
|
41
|
+
# Gaskit.config do |c|
|
42
|
+
# c.log_formatter = ->(severity, time, _progname, msg) do
|
43
|
+
# message, ctx = msg.is_a?(Array) ? msg : [msg, {}]
|
44
|
+
# "[#{time.strftime('%T')}] #{severity}: #{message} #{ctx.to_json}\n"
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# @example Configuring JSON or pretty formatters
|
49
|
+
# Gaskit.config do |c|
|
50
|
+
# c.setup_logger(Logger.new($stdout), formatter: Gaskit::Logger.formatter(:json))
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # or
|
54
|
+
#
|
55
|
+
# Gaskit.config do |c|
|
56
|
+
# c.setup_logger(Logger.new($stdout), formatter: Gaskit::Logger.formatter(:pretty))
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# @example Disabling logs entirely
|
60
|
+
# Gaskit.config do |c|
|
61
|
+
# c.disable_logging = true
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @see Gaskit::Configuration
|
65
|
+
class Logger
|
66
|
+
SENSITIVE_KEYS = %i[email ip_address password auth_token secret ssn jwt token].freeze
|
67
|
+
|
68
|
+
class << self
|
69
|
+
# Returns a built-in log formatter.
|
70
|
+
#
|
71
|
+
# Use this method to obtain either the JSON or pretty formatter for logs.
|
72
|
+
# This is useful when customizing the logger via `Gaskit.config`.
|
73
|
+
#
|
74
|
+
# @example Use JSON formatter
|
75
|
+
# Gaskit.config do |c|
|
76
|
+
# c.setup_logger(Logger.new($stdout), formatter: Gaskit::Logger.formatter(:json))
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# @example Use pretty formatter
|
80
|
+
# Gaskit.config do |c|
|
81
|
+
# c.setup_logger(Logger.new($stdout), formatter: Gaskit::Logger.formatter(:pretty))
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# @param formatter [Symbol] The formatter type to use (`:json` or `:pretty`)
|
85
|
+
# @return [Proc] A formatter proc suitable for use with `Logger#formatter`
|
86
|
+
# @raise [ArgumentError] If an unknown formatter symbol is provided
|
87
|
+
def formatter(formatter = :pretty)
|
88
|
+
case formatter
|
89
|
+
when :json
|
90
|
+
json_formatter
|
91
|
+
when :pretty
|
92
|
+
pretty_formatter
|
93
|
+
else
|
94
|
+
raise ArgumentError, "Invalid log formatter: #{formatter}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# JSON formatter (for production or structured logs)
|
99
|
+
#
|
100
|
+
# @return [Proc] The formatter callable.
|
101
|
+
def json_formatter
|
102
|
+
lambda do |severity, time, _progname, msg|
|
103
|
+
message, context = extract_message_and_context(msg)
|
104
|
+
|
105
|
+
log_entry = {
|
106
|
+
timestamp: time.utc.iso8601,
|
107
|
+
level: severity.downcase,
|
108
|
+
class: context[:class],
|
109
|
+
message: message,
|
110
|
+
context: context&.reject { |k| k == :duration }
|
111
|
+
}.compact
|
112
|
+
|
113
|
+
log_entry[:duration] = context[:duration] if context&.key?(:duration)
|
114
|
+
|
115
|
+
"#{JSON.dump(log_entry)}\n"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Pretty formatter (for dev logs)
|
120
|
+
#
|
121
|
+
# @return [Proc] The formatter callable.
|
122
|
+
def pretty_formatter
|
123
|
+
lambda do |severity, time, _progname, msg|
|
124
|
+
message, context = extract_message_and_context(msg)
|
125
|
+
context ||= {}
|
126
|
+
|
127
|
+
tags = %W[[#{time.utc.iso8601}] [#{severity}]]
|
128
|
+
tags << "[#{context[:class]}]" if context[:class]
|
129
|
+
tags += context.map { |k, v| "[#{k}=#{v}]" }
|
130
|
+
|
131
|
+
"#{tags.join(" ")} #{message}\n"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# Extracts the message and context from a log payload.
|
138
|
+
#
|
139
|
+
# @param msg [Object] The payload passed to the logger.
|
140
|
+
# @return [Array] An array with the message and context.
|
141
|
+
def extract_message_and_context(msg)
|
142
|
+
return [msg[0], msg[1]] if msg.is_a?(Array) && msg.size == 2
|
143
|
+
|
144
|
+
[msg.to_s, {}]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
attr_reader :context
|
149
|
+
|
150
|
+
# Initializes a new logger instance.
|
151
|
+
#
|
152
|
+
# @param klass [Class] The name of the class being logged.
|
153
|
+
# @param context [Hash] Optional additional context to include in every log entry.
|
154
|
+
def initialize(klass, context: {})
|
155
|
+
@class_name = resolve_name(klass)
|
156
|
+
@context = apply_context(context)
|
157
|
+
|
158
|
+
@logger = Gaskit.configuration.logger || ::Logger.new($stdout)
|
159
|
+
rescue StandardError
|
160
|
+
::Logger.new($stdout).error "Failed to initialize logger: #{$ERROR_INFO}"
|
161
|
+
@logger = ::Logger.new(nil) # fallback null logger
|
162
|
+
end
|
163
|
+
|
164
|
+
# Logs a debug-level message.
|
165
|
+
#
|
166
|
+
# @param message [String, nil] The log message (or provide it via the block parameter).
|
167
|
+
# @param context [Hash, nil] Additional context for this specific log entry.
|
168
|
+
# @yield Block for deferred log message computation.
|
169
|
+
def debug(message = nil, context: {}, &block)
|
170
|
+
log(:debug, message, context: context, &block)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Logs an info-level message.
|
174
|
+
#
|
175
|
+
# @param message [String, nil] The log message (or provide it via the block parameter).
|
176
|
+
# @param context [Hash, nil] Additional context for this specific log entry.
|
177
|
+
# @yield Block for deferred log message computation.
|
178
|
+
def info(message = nil, context: {}, &block)
|
179
|
+
log(:info, message, context: context, &block)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Logs a warning-level message.
|
183
|
+
#
|
184
|
+
# @param message [String, nil] The log message (or provide it via the block parameter).
|
185
|
+
# @param context [Hash, nil] Additional context for this specific log entry.
|
186
|
+
# @yield Block for deferred log message computation.
|
187
|
+
def warn(message = nil, context: {}, &block)
|
188
|
+
log(:warn, message, context: context, &block)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Logs an error-level message.
|
192
|
+
#
|
193
|
+
# @param message [String, nil] The log message (or provide it via the block parameter).
|
194
|
+
# @param context [Hash, nil] Additional context for this specific log entry.
|
195
|
+
# @yield Block for deferred log message computation.
|
196
|
+
def error(message = nil, context: {}, &block)
|
197
|
+
log(:error, message, context: context, &block)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Logs a message at the specified level.
|
201
|
+
#
|
202
|
+
# @param level [Symbol] The log level (e.g., :debug, :info).
|
203
|
+
# @param message [String, nil] The log message (or provide it via the block parameter).
|
204
|
+
# @param context [Hash, nil] Additional context for this specific log entry.
|
205
|
+
# @yield Block for deferred log message computation.
|
206
|
+
def log(level, message, context: {}, &block)
|
207
|
+
return if Gaskit.configuration.disable_logging
|
208
|
+
|
209
|
+
@logger.public_send(level) do
|
210
|
+
combined_context = filtered_context(@context.merge(context))
|
211
|
+
combined_context[:class] ||= @class_name
|
212
|
+
|
213
|
+
msg = message || block&.call
|
214
|
+
|
215
|
+
[msg, combined_context]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Creates a new logger instance with additional merged context.
|
220
|
+
#
|
221
|
+
# @param extra_context [Hash] Additional context to include.
|
222
|
+
# @return [Gaskit::Logger] A new logger instance with the updated context.
|
223
|
+
def with_context(extra_context)
|
224
|
+
self.class.new(@class_name, context: @context.merge(extra_context))
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
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
|
+
def apply_context(context)
|
244
|
+
default_context = Gaskit.configuration.context_provider.call
|
245
|
+
context = default_context.merge(context)
|
246
|
+
|
247
|
+
Helpers.deep_compact(context)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Filters out sensitive values from the log context based on predefined keys.
|
251
|
+
#
|
252
|
+
# This method transforms all context keys to symbols and checks if each key is considered sensitive.
|
253
|
+
# If a key matches one of the predefined sensitive keys, its value is replaced with "[FILTERED]".
|
254
|
+
#
|
255
|
+
# @param context [Hash] The context hash to be filtered.
|
256
|
+
# @return [Hash] The filtered context hash with sensitive values masked.
|
257
|
+
def filtered_context(context)
|
258
|
+
filtered_context = context.transform_keys(&:to_sym).transform_values.with_index do |value, index|
|
259
|
+
key = context.keys[index].to_sym
|
260
|
+
sensitive_key?(key) ? "[FILTERED]" : value
|
261
|
+
end
|
262
|
+
|
263
|
+
Helpers.deep_compact(filtered_context)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Determines if a given key is considered sensitive and should be masked in logs.
|
267
|
+
#
|
268
|
+
# @param key [Symbol, String] The key to evaluate.
|
269
|
+
# @return [Boolean] True if the key is sensitive and should be filtered; false otherwise.
|
270
|
+
def sensitive_key?(key)
|
271
|
+
SENSITIVE_KEYS.include?(key)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "core"
|
4
|
+
require_relative "operation_result"
|
5
|
+
require_relative "operation_exit"
|
6
|
+
require_relative "helpers"
|
7
|
+
|
8
|
+
module Gaskit
|
9
|
+
# The Gaskit::Operation class defines a structured and extensible pattern for building application operations.
|
10
|
+
# It enforces consistent behavior across operations while supporting customization via contracts.
|
11
|
+
#
|
12
|
+
# # Features
|
13
|
+
# - Pluggable contracts via `use_contract`, allowing you to define or reference a `result` and
|
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.
|
17
|
+
# - Integrated duration tracking, structured logging, and early exits.
|
18
|
+
# - Supports `.call` (non-raising) and `.call!` (raising) styles.
|
19
|
+
#
|
20
|
+
# @example Using a registered contract
|
21
|
+
# class MyOperation < Gaskit::Operation
|
22
|
+
# use_contract :service
|
23
|
+
#
|
24
|
+
# def call
|
25
|
+
# # Do work
|
26
|
+
# "done"
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @example Overriding only part of the contract
|
31
|
+
# class MyCustomOp < Gaskit::Operation
|
32
|
+
# use_contract :service, result: MyCustomResult
|
33
|
+
#
|
34
|
+
# def call
|
35
|
+
# exit(:unauthorized, "User not allowed") if unauthorized?
|
36
|
+
# "okay"
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Fully manual contract
|
41
|
+
# class ManualOp < Gaskit::Operation
|
42
|
+
# use_contract result: MyResult, early_exit: MyExit
|
43
|
+
#
|
44
|
+
# def call
|
45
|
+
# do_work
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# result = ManualOp.call(context: { request_id: "abc123" })
|
50
|
+
#
|
51
|
+
# @abstract Subclass this and define `#call` or `#call!` to create a new operation.
|
52
|
+
class Operation
|
53
|
+
class << self
|
54
|
+
def result_class
|
55
|
+
return @result_class if defined?(@result_class) && @result_class
|
56
|
+
return superclass&.result_class if superclass.respond_to?(:result_class)
|
57
|
+
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Defines the result and early exit classes for this operation.
|
62
|
+
# Can reference a named contract registered in `Gaskit::Registry`, override either part manually,
|
63
|
+
# or define both without using a named contract.
|
64
|
+
#
|
65
|
+
# @example Use a registered contract
|
66
|
+
# use_contract :service
|
67
|
+
#
|
68
|
+
# @example Override only result class
|
69
|
+
# use_contract :service, result: CustomResult
|
70
|
+
#
|
71
|
+
# @example Define both without using a contract name
|
72
|
+
# use_contract result: CustomResult, early_exit: CustomExit
|
73
|
+
#
|
74
|
+
# @param contract [Symbol, nil] A registered contract name (e.g., `:service`)
|
75
|
+
# @param result [Class, nil] A class that inherits from `Gaskit::BaseResult`
|
76
|
+
# @raise [ArgumentError] if contract is not a symbol or unexpected args are passed
|
77
|
+
# @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
|
+
# @return [void]
|
80
|
+
def use_contract(contract = nil, result: nil)
|
81
|
+
if contract
|
82
|
+
raise ArgumentError, "use_contract must be called with a symbol or keyword args" unless contract.is_a?(Symbol)
|
83
|
+
|
84
|
+
result = Gaskit.fetch_contract(contract)
|
85
|
+
end
|
86
|
+
|
87
|
+
Gaskit::ContractRegistry.verify_result_class!(result)
|
88
|
+
@result_class = result
|
89
|
+
end
|
90
|
+
|
91
|
+
# Declares a symbolic error and message for use with `exit(:key)`
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# error :unauthorized, "You must be signed in", code: "AUTH-001"
|
95
|
+
#
|
96
|
+
# @param key [String, Symbol] The key used to declare the error.
|
97
|
+
# @param message [String] The error message.
|
98
|
+
# @param code [String, nil] Optional error code.
|
99
|
+
# @return [void]
|
100
|
+
def error(key, message, code: nil)
|
101
|
+
errors_registry[key.to_sym] = { message: message, code: code }
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the error registry for the operation class
|
105
|
+
#
|
106
|
+
# @return [void]
|
107
|
+
def errors_registry
|
108
|
+
@errors_registry ||= {}
|
109
|
+
end
|
110
|
+
|
111
|
+
# Execute the operation without raising an exception on failure.
|
112
|
+
#
|
113
|
+
# @param [Array] args Positional arguments passed.
|
114
|
+
# @param [Hash] kwargs Keyword arguments passed (with optional :context).
|
115
|
+
# @yield [Block] Additional block logic to pass during the operation.
|
116
|
+
# @return [OperationResult] The result of the operation.
|
117
|
+
def call(*args, **kwargs, &block)
|
118
|
+
invoke(false, *args, **kwargs, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Execute the operation with raising an exception on failure.
|
122
|
+
#
|
123
|
+
# @param [Array] args Positional arguments passed.
|
124
|
+
# @param [Hash] kwargs Keyword arguments passed (with optional :context).
|
125
|
+
# @yield [Block] Additional block logic to pass during the operation.
|
126
|
+
# @return [OperationResult] The result of the operation.
|
127
|
+
def call!(*args, **kwargs, &block)
|
128
|
+
invoke(true, *args, **kwargs, &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Core execution logic for operations, handling errors and timing.
|
134
|
+
#
|
135
|
+
# @param [Boolean] raise_on_failure Whether to raise exceptions on failure.
|
136
|
+
# @param [Array] args Positional arguments for the operation.
|
137
|
+
# @param [Hash] kwargs Keyword arguments, including optional :context.
|
138
|
+
# @yield [Block] Additional block logic during execution.
|
139
|
+
# @raise [NotImplementedError] If operation type is not set in subclasses.
|
140
|
+
# @return [OperationResult] The result of the operation.
|
141
|
+
def invoke(raise_on_failure, *args, **kwargs, &block)
|
142
|
+
unless result_class
|
143
|
+
raise NotImplementedError, "No result_class defined for #{name} or its ancestors. " \
|
144
|
+
"Did you forget to call `use_contract`?"
|
145
|
+
end
|
146
|
+
|
147
|
+
context = kwargs.delete(:context) || {}
|
148
|
+
operation = new(raise_on_failure, context: context)
|
149
|
+
duration, (result, error) = execute(operation, *args, **kwargs, &block)
|
150
|
+
|
151
|
+
operation.logger.debug(context: { duration: duration }) do
|
152
|
+
"Operation completed in #{duration} seconds"
|
153
|
+
end
|
154
|
+
|
155
|
+
result_class.new(error.nil?, result, error, duration: duration, context: context)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Executes the operation logic and handles potential exceptions.
|
159
|
+
#
|
160
|
+
# @param [Gaskit::Operation] operation The instance of the current operation.
|
161
|
+
# @param [Array] args Positional arguments passed.
|
162
|
+
# @param [Hash] kwargs Keyword arguments passed.
|
163
|
+
# @yield [Block] Additional block for the operation.
|
164
|
+
# @return [Array] The execution duration, result, and error if any.
|
165
|
+
def execute(operation, *args, **kwargs, &block)
|
166
|
+
Helpers.time_execution do
|
167
|
+
[operation.call(*args, **kwargs, &block), nil]
|
168
|
+
rescue StandardError => e
|
169
|
+
if e.is_a?(Gaskit::OperationExit)
|
170
|
+
log_exit(operation, e)
|
171
|
+
else
|
172
|
+
log_exception(operation, e)
|
173
|
+
raise e if operation.raise_on_failure
|
174
|
+
|
175
|
+
end
|
176
|
+
[nil, e]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Logs an early exit from the operation.
|
181
|
+
#
|
182
|
+
# @param [Gaskit::Operation] operation The operation instance.
|
183
|
+
# @param [StandardError] operation_exit The exit error raised.
|
184
|
+
# @return [void]
|
185
|
+
def log_exit(operation, operation_exit)
|
186
|
+
operation.logger.warn { "Exited early: #{operation_exit.key} – #{operation_exit.message}" }
|
187
|
+
end
|
188
|
+
|
189
|
+
# Logs any unhandled exception during the operation.
|
190
|
+
#
|
191
|
+
# @param [Gaskit::Operation] operation The operation instance.
|
192
|
+
# @param [Exception] exception The raised exception.
|
193
|
+
# @return [void]
|
194
|
+
def log_exception(operation, exception)
|
195
|
+
operation.logger.error { "[#{exception.class}] #{exception.message}" }
|
196
|
+
operation.logger.error { exception.backtrace&.join("\n") }
|
197
|
+
end
|
198
|
+
|
199
|
+
# @return [String] The name of the operation class (e.g., "MyOperation").
|
200
|
+
def operation_name
|
201
|
+
@operation_name ||= self.class.name.to_s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
attr_reader :raise_on_failure, :context, :logger
|
206
|
+
|
207
|
+
# Initializes a new Gaskit::Operation instance.
|
208
|
+
#
|
209
|
+
# @param [Boolean] raise_on_failure Whether to raise exceptions on failure.
|
210
|
+
# @param [Hash] context Context data for the operation.
|
211
|
+
# @return [void]
|
212
|
+
def initialize(raise_on_failure, context: {})
|
213
|
+
@raise_on_failure = raise_on_failure
|
214
|
+
@context = apply_context(context)
|
215
|
+
@logger = Gaskit::Logger.new(self.class, context: @context)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Applies global context, if set, from Gaskit.configuration.context_provider.
|
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))
|
225
|
+
end
|
226
|
+
|
227
|
+
# Executes the operation logic.
|
228
|
+
#
|
229
|
+
# @param [Array] args Positional arguments passed.
|
230
|
+
# @param [Hash] kwargs Keyword arguments passed.
|
231
|
+
# @return [void]
|
232
|
+
# @raise [NotImplementedError] Must be implemented by subclasses.
|
233
|
+
def call(*args, **kwargs)
|
234
|
+
raise NotImplementedError, "#{self.class.name} must implement `#call`"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Terminates the operation early with a symbolic key.
|
238
|
+
#
|
239
|
+
# If the key was previously registered via `self.error`, it uses the declared message and code.
|
240
|
+
# Otherwise, it uses the key as the message.
|
241
|
+
#
|
242
|
+
# @param exit_key [Symbol] The symbolic reason for exiting.
|
243
|
+
# @param message [String, nil] Optional message override.
|
244
|
+
# @raise [OperationExit] always raises an instance with message and optional code
|
245
|
+
def exit(exit_key, message = nil)
|
246
|
+
exit_key = exit_key.to_sym
|
247
|
+
definition = self.class.errors_registry[exit_key]
|
248
|
+
|
249
|
+
if definition
|
250
|
+
message ||= definition[:message]
|
251
|
+
code = definition[:code]
|
252
|
+
end
|
253
|
+
|
254
|
+
raise OperationExit.new(exit_key, message, code: code)
|
255
|
+
end
|
256
|
+
|
257
|
+
# @see #exit
|
258
|
+
alias abort exit
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gaskit
|
4
|
+
# OperationExit is a custom exception representing an early exit from an operation.
|
5
|
+
#
|
6
|
+
# It communicates intent-based flow interruption (e.g., authorization failure, validation issue)
|
7
|
+
# and includes a symbolic `key`, optional `message`, and optional `code`.
|
8
|
+
#
|
9
|
+
# @example Raising an OperationExit with just a key
|
10
|
+
# exit(:unauthorized)
|
11
|
+
#
|
12
|
+
# @example With a custom message and code
|
13
|
+
# exit(:unauthorized, "Access denied", code: "AUTH-001")
|
14
|
+
#
|
15
|
+
# @example Handling OperationExit in a flow
|
16
|
+
# begin
|
17
|
+
# MyFlow.call!
|
18
|
+
# rescue Gaskit::OperationExit => e
|
19
|
+
# puts "Exited: #{e.key} - #{e.message} (#{e.code})"
|
20
|
+
# end
|
21
|
+
class OperationExit < Gaskit::Error
|
22
|
+
# @return [Symbol, String] The symbolic or textual reason for the early exit
|
23
|
+
attr_reader :key
|
24
|
+
|
25
|
+
# @return [String, nil] Optional structured code (e.g., "AUTH-001")
|
26
|
+
attr_reader :code
|
27
|
+
|
28
|
+
# Initializes an OperationExit.
|
29
|
+
#
|
30
|
+
# @param key [Symbol, String] The symbolic exit key
|
31
|
+
# @param message [String, nil] A human-readable message (defaults to key if not provided)
|
32
|
+
# @param code [String, nil] A structured error code for analytics or debugging
|
33
|
+
def initialize(key, message = nil, code: nil)
|
34
|
+
super(message || key.to_s)
|
35
|
+
@key = key
|
36
|
+
@code = code
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|