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,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module Gaskit
|
6
|
+
# Gaskit::Configuration holds global configuration for the Gaskit gem.
|
7
|
+
#
|
8
|
+
# It allows customization of logging behavior, including:
|
9
|
+
# - Log level (`log_level`)
|
10
|
+
# - Custom logger (`logger`)
|
11
|
+
# - Disabling logging entirely (`disable_logging`)
|
12
|
+
# - Structured or custom log formatting (`log_formatter`)
|
13
|
+
# - Debug mode (`debug`)
|
14
|
+
#
|
15
|
+
# @example Configuring Gaskit in an initializer
|
16
|
+
# Gaskit.config do |c|
|
17
|
+
# c.debug = true
|
18
|
+
# c.disable_logging = false
|
19
|
+
#
|
20
|
+
# c.context_provider = -> {
|
21
|
+
# {
|
22
|
+
# tenant_id: Current.tenant_id,
|
23
|
+
# user_id: Current.user_id
|
24
|
+
# }
|
25
|
+
# }
|
26
|
+
#
|
27
|
+
# # Optionally replace the logger
|
28
|
+
# custom_logger = Logger.new("log/gaskit.log")
|
29
|
+
# c.setup_logger(custom_logger, level: :info, formatter: ->(severity, time, _progname, msg) {
|
30
|
+
# message, context = msg.is_a?(Array) ? msg : [msg, {}]
|
31
|
+
# "[#{time.strftime('%Y-%m-%d %H:%M:%S')} #{severity}] #{message} (#{context.inspect})\n"
|
32
|
+
# })
|
33
|
+
# end
|
34
|
+
class Configuration
|
35
|
+
# @return [Boolean] Whether debug mode is enabled.
|
36
|
+
attr_accessor :debug
|
37
|
+
|
38
|
+
# @return [Boolean] Whether to completely suppress log output.
|
39
|
+
attr_accessor :disable_logging
|
40
|
+
|
41
|
+
# @return [Logger] The logger instance used internally by Gaskit.
|
42
|
+
attr_reader :logger
|
43
|
+
|
44
|
+
# @return[#call] A callable to apply a global context used for all log entries.
|
45
|
+
attr_reader :context_provider
|
46
|
+
|
47
|
+
# Initializes the configuration with default settings.
|
48
|
+
#
|
49
|
+
# The configuration includes:
|
50
|
+
# - Default environment set to `"development"`
|
51
|
+
# - Debug mode disabled
|
52
|
+
# - Logging to stdout
|
53
|
+
# - Default log level set to ::Logger::DEBUG
|
54
|
+
# - An empty base context hash
|
55
|
+
# - A new `ContractRegistry` instance for contract management
|
56
|
+
def initialize
|
57
|
+
@debug = false
|
58
|
+
@disable_logging = false
|
59
|
+
@context_provider = -> { {} }
|
60
|
+
@contract_registry = ContractRegistry.new
|
61
|
+
|
62
|
+
setup_logger
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sets the logger, formatter, and level in one go.
|
66
|
+
#
|
67
|
+
# @param custom_logger [::Logger, nil] An optional custom logger.
|
68
|
+
# @param level [Symbol, Integer] Log level (e.g., :debug, Logger::WARN)
|
69
|
+
# @param formatter [Proc] Custom formatter for log entries.
|
70
|
+
def setup_logger(custom_logger = nil, level: :debug, formatter: nil)
|
71
|
+
@logger = custom_logger || ::Logger.new($stdout)
|
72
|
+
|
73
|
+
effective_formatter = formatter || @logger&.formatter || Gaskit::Logger.formatter(:pretty)
|
74
|
+
self.log_formatter = effective_formatter if effective_formatter.respond_to?(:call)
|
75
|
+
self.log_level = level
|
76
|
+
end
|
77
|
+
|
78
|
+
# Sets the logging level.
|
79
|
+
#
|
80
|
+
# @param level [Symbol, Integer] The log level (e.g., :info, :debug, or Logger::WARN).
|
81
|
+
# @raise [NameError] If the provided level is a symbol, and it does not map to a valid Logger constant.
|
82
|
+
def log_level=(level)
|
83
|
+
level = ::Logger.const_get(level.upcase) if level.is_a?(Symbol)
|
84
|
+
@logger.level = level
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sets a custom log formatter.
|
88
|
+
#
|
89
|
+
# @param formatter [#call] A callable object that receives log arguments (severity, time, progname, msg).
|
90
|
+
# @raise [ArgumentError] If the provided formatter is not callable.
|
91
|
+
def log_formatter=(formatter)
|
92
|
+
raise ArgumentError, "Formatter must be callable" unless formatter.respond_to?(:call)
|
93
|
+
|
94
|
+
@logger.formatter = formatter
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sets the global context provider used for all log entries.
|
98
|
+
#
|
99
|
+
# @param provider [#call] A proc or lambda returning a Hash of context values.
|
100
|
+
# @raise [ArgumentError] If the provided callable is not callable.
|
101
|
+
def context_provider=(provider)
|
102
|
+
raise ArgumentError, "Provider must be callable" unless provider.respond_to?(:call)
|
103
|
+
|
104
|
+
@context_provider = provider
|
105
|
+
end
|
106
|
+
|
107
|
+
# Registers a contract with a name and associated result class.
|
108
|
+
#
|
109
|
+
# @param name [Symbol, String] The name of the contract.
|
110
|
+
# @param result_class [Class] The class that represents the result for the contract.
|
111
|
+
# @param override [Boolean] Whether to override an existing contract (default: false).
|
112
|
+
# @raise [Gaskit::ContractError] If the contract is already registered and override is not allowed.
|
113
|
+
# @raise [Gaskit::ResultTypeError] If the result_class does not inherit from `Gaskit::OperationResult`.
|
114
|
+
# @return [void]
|
115
|
+
def register_contract(name, result_class, override: false)
|
116
|
+
@contract_registry.register(name, result_class, override: override)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Fetches a registered contract's result class by its name.
|
120
|
+
#
|
121
|
+
# @param name [Symbol, String] The name of the contract.
|
122
|
+
# @return [Class] The result class associated with the contract.
|
123
|
+
# @raise [Gaskit::ContractError] If the contract is not registered.
|
124
|
+
def fetch_contract(name)
|
125
|
+
@contract_registry.fetch(name)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Checks if a contract is registered.
|
129
|
+
#
|
130
|
+
# @param name [Symbol, String] The name of the contract.
|
131
|
+
# @return [Boolean] true if the contract is registered, otherwise false.
|
132
|
+
def contract_registered?(name)
|
133
|
+
@contract_registry.registered?(name)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Lists all registered contracts.
|
137
|
+
#
|
138
|
+
# @return [Hash<Symbol, Class>] A hash of all registered contracts where keys are
|
139
|
+
# contract names and values are their corresponding result classes.
|
140
|
+
def registered_contracts
|
141
|
+
@contract_registry.all
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "core"
|
4
|
+
|
5
|
+
module Gaskit
|
6
|
+
# Represents a registry for managing contracts and their result classes
|
7
|
+
#
|
8
|
+
# This class allows registering contracts with associated result classes,
|
9
|
+
# checking if they are registered, and fetching them for use.
|
10
|
+
# It also includes validation to ensure result classes adhere to the expected base class.
|
11
|
+
class ContractRegistry
|
12
|
+
class << self
|
13
|
+
# Verifies that the given class is a subclass of `Gaskit::BaseResult`
|
14
|
+
#
|
15
|
+
# @param result_class [Class] The class to verify
|
16
|
+
# @raise [Gaskit::ResultTypeError] if the class does not inherit from `Gaskit::BaseResult`
|
17
|
+
def verify_result_class!(result_class)
|
18
|
+
raise Gaskit::ResultTypeError, result_class unless result_class <= Gaskit::OperationResult
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Initializes a new instance of `ContractRegistry`
|
23
|
+
#
|
24
|
+
# Sets up the internal hash for storing contracts.
|
25
|
+
def initialize
|
26
|
+
@contracts = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Registers a contract with an associated result class
|
30
|
+
#
|
31
|
+
# @param name [Symbol, String] The name of the contract
|
32
|
+
# @param result_class [Class] The class that represents the result for the contract
|
33
|
+
# @param override [Boolean] Whether to override an existing registration (default: false)
|
34
|
+
# @raise [Gaskit::ContractError] if the contract is already registered and override is not allowed
|
35
|
+
# @raise [Gaskit::ResultTypeError] if the result_class does not inherit from `Gaskit::BaseResult`
|
36
|
+
# @return [void]
|
37
|
+
def register(name, result_class, override: false)
|
38
|
+
name = name.to_sym
|
39
|
+
raise Gaskit::ContractError, "Contract #{name} already registered" if @contracts.key?(name) && !override
|
40
|
+
|
41
|
+
ContractRegistry.verify_result_class!(result_class)
|
42
|
+
@contracts[name] = result_class.freeze
|
43
|
+
|
44
|
+
Gaskit.configuration.logger.debug { "[Gaskit] Registered contract #{name}" } if Gaskit.debug?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Checks if a contract is registered
|
48
|
+
#
|
49
|
+
# @param name [Symbol, String] The name of the contract
|
50
|
+
# @return [Boolean] true if the contract is registered, otherwise false
|
51
|
+
def registered?(name)
|
52
|
+
@contracts.key?(name.to_sym)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Fetches a registered contract's result class
|
56
|
+
#
|
57
|
+
# @param name [Symbol, String] The name of the contract
|
58
|
+
# @return [Class] The result class for the contract
|
59
|
+
# @raise [Gaskit::ContractError] if the contract is not registered
|
60
|
+
def fetch(name)
|
61
|
+
@contracts.fetch(name.to_sym) do
|
62
|
+
raise Gaskit::ContractError, "Contract #{name} not registered, register it with " \
|
63
|
+
"Gaskit.configuration.register_contract(name, result_class)"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a duplicate of all registered contracts
|
68
|
+
#
|
69
|
+
# @return [Hash<Symbol, Class>] A hash of all registered contracts where keys are
|
70
|
+
# contract names and values are their corresponding result classes
|
71
|
+
def all
|
72
|
+
@contracts.dup
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/gaskit/core.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "configuration"
|
4
|
+
require_relative "contract_registry"
|
5
|
+
|
6
|
+
module Gaskit
|
7
|
+
class << self
|
8
|
+
# Configures the Gaskit system.
|
9
|
+
#
|
10
|
+
# This yields the configuration instance, allowing you to modify settings such as logger,
|
11
|
+
# global context, log level, and formatting.
|
12
|
+
#
|
13
|
+
# @yieldparam [Gaskit::Configuration] configuration
|
14
|
+
# @return [void]
|
15
|
+
def config
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Retrieves the global Gaskit configuration.
|
20
|
+
#
|
21
|
+
# @return [Gaskit::Configuration] the configuration instance
|
22
|
+
def configuration
|
23
|
+
@configuration ||= Configuration.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns configuration.debug.
|
27
|
+
#
|
28
|
+
# @return [Boolean] `true` is Gaskit is set to debug, `false` otherwise.
|
29
|
+
def debug?
|
30
|
+
Gaskit.configuration.debug
|
31
|
+
end
|
32
|
+
|
33
|
+
# Registers a new operation contract.
|
34
|
+
#
|
35
|
+
# @param name [Symbol, String] Contract name
|
36
|
+
# @param result_class [Class<Gaskit::OperationResult>] Result class for the operation
|
37
|
+
def register_contract(name, result_class, override: false)
|
38
|
+
configuration.register_contract(name, result_class, override: override)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Fetches the result and exit classes for the given contract name.
|
42
|
+
#
|
43
|
+
# @param name [Symbol, String]
|
44
|
+
# @return [Class]
|
45
|
+
def fetch_contract(name)
|
46
|
+
configuration.fetch_contract(name)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns whether the contract is registered.
|
50
|
+
#
|
51
|
+
# @param name [Symbol, String]
|
52
|
+
# @return [Boolean]
|
53
|
+
def contract_registered?(name)
|
54
|
+
configuration.contract_registered?(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns all registered contracts.
|
58
|
+
#
|
59
|
+
# @return [Hash{Symbol=>Hash}]
|
60
|
+
def registered_contracts
|
61
|
+
configuration.registered_contracts
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/gaskit/error.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gaskit
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ContractError < Error; end
|
7
|
+
|
8
|
+
# Raised when an operation contract supplies an incorrect result type.
|
9
|
+
class ResultTypeError < Error
|
10
|
+
# @param [Class] klazz The class that failed the type check.
|
11
|
+
def initialize(klazz)
|
12
|
+
message = "Expected result class to inherit from Gaskit::BaseResult, got: #{klazz}"
|
13
|
+
super(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/gaskit/flow.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require_relative "core"
|
5
|
+
require_relative "flow_result"
|
6
|
+
require_relative "helpers"
|
7
|
+
|
8
|
+
module Gaskit
|
9
|
+
# Base class for defining and executing multi-step operation pipelines
|
10
|
+
#
|
11
|
+
# @example Inline (block-based) flow
|
12
|
+
# result = Gaskit::Flow.call(1, 2, context: {}) do
|
13
|
+
# step AddOp
|
14
|
+
# step MultOp, multiplier: 2
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Class-based flow
|
18
|
+
# class MyFlow < Gaskit::Flow
|
19
|
+
# step AddOp
|
20
|
+
# step MultOp, multiplier: 1.5
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# result = MyFlow.call(5, 5, context: { request_id: "abc123" })
|
24
|
+
class Flow
|
25
|
+
class << self
|
26
|
+
# Inherited hook to initialize step DSL
|
27
|
+
#
|
28
|
+
# @param subclass [Class] The subclass inheriting from Flow
|
29
|
+
# @return [void]
|
30
|
+
def inherited(subclass)
|
31
|
+
subclass.instance_variable_set(:@defined_steps, [])
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns defined steps for the flow class
|
36
|
+
#
|
37
|
+
# @return [Array<Array>] An array of [operation, args, kwargs] tuples
|
38
|
+
def defined_steps
|
39
|
+
@defined_steps ||= []
|
40
|
+
end
|
41
|
+
|
42
|
+
# Adds a step to the flow
|
43
|
+
#
|
44
|
+
# @param operation [Class<Gaskit::Operation>] The operation class
|
45
|
+
# @param args [Array] Positional arguments for the step
|
46
|
+
# @param context [Hash] Optional context overrides
|
47
|
+
# @param kwargs [Hash] Keyword arguments for the step
|
48
|
+
# @return [void]
|
49
|
+
def step(operation, *args, context: {}, **kwargs)
|
50
|
+
kwargs = kwargs.merge(context: context)
|
51
|
+
defined_steps << [operation, args, kwargs]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Executes the flow with soft-failure handling
|
55
|
+
#
|
56
|
+
# @param args [Array] Positional arguments for the first step
|
57
|
+
# @param context [Hash] Shared context across all steps
|
58
|
+
# @param kwargs [Hash] Keyword arguments for the first step
|
59
|
+
# @return [FlowResult]
|
60
|
+
def call(*args, context: {}, **kwargs, &block)
|
61
|
+
invoke(false, context, *args, **kwargs, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Executes the flow with hard-failure handling (raises on unhandled errors)
|
65
|
+
#
|
66
|
+
# @param args [Array] Positional arguments for the first step
|
67
|
+
# @param context [Hash] Shared context across all steps
|
68
|
+
# @param kwargs [Hash] Keyword arguments for the first step
|
69
|
+
# @return [FlowResult]
|
70
|
+
def call!(*args, context: {}, **kwargs, &block)
|
71
|
+
invoke(true, context, *args, **kwargs, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Internal flow initializer
|
77
|
+
def invoke(raise_on_failure, context, *args, **kwargs, &block)
|
78
|
+
flow = new(raise_on_failure, context, [args, kwargs])
|
79
|
+
flow.execute(&block)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Hash] Execution context
|
84
|
+
attr_reader :context
|
85
|
+
|
86
|
+
# @return [Gaskit::OperationResult] Most recent result
|
87
|
+
attr_reader :result
|
88
|
+
|
89
|
+
# @return [Array<Hash>] List of step metadata
|
90
|
+
attr_reader :steps
|
91
|
+
|
92
|
+
# Executes a single step of the flow
|
93
|
+
#
|
94
|
+
# @param operation [Class<Gaskit::Operation>]
|
95
|
+
# @param args [Array] Additional positional arguments
|
96
|
+
# @param context [Hash] Step-local context
|
97
|
+
# @param kwargs [Hash] Additional keyword arguments
|
98
|
+
# @return [void]
|
99
|
+
def step(operation, *args, context: {}, **kwargs, &block)
|
100
|
+
raise ArgumentError, "Operation must be a subclass of Gaskit::Operation" unless operation <= Gaskit::Operation
|
101
|
+
|
102
|
+
return if result&.early_exit?
|
103
|
+
|
104
|
+
kwargs = kwargs.merge(context: context)
|
105
|
+
@result = execute_step(operation, *args, **kwargs, &block)
|
106
|
+
@steps << compile_step_entry(operation, *args, **kwargs)
|
107
|
+
|
108
|
+
update_input(result)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Executes the flow either via block or pre-defined DSL
|
112
|
+
#
|
113
|
+
# @return [FlowResult]
|
114
|
+
def execute(&block)
|
115
|
+
duration, = Gaskit::Helpers.time_execution do
|
116
|
+
if block_given?
|
117
|
+
instance_eval(&block)
|
118
|
+
else
|
119
|
+
self.class.defined_steps.each { |(op, args, kwargs)| step(op, *args, **kwargs) }
|
120
|
+
end
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
FlowResult.new(@result, @steps, duration: duration, context: @context)
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
# Initializes a flow instance
|
131
|
+
#
|
132
|
+
# @param raise_on_failure [Boolean] Whether to raise on unexpected errors
|
133
|
+
# @param context [Hash] Flow context
|
134
|
+
# @param input [Array] Initial args/kwargs input bundle
|
135
|
+
def initialize(raise_on_failure, context, input)
|
136
|
+
@raise_on_failure = raise_on_failure
|
137
|
+
@context = apply_context(context)
|
138
|
+
@input = input
|
139
|
+
@steps = []
|
140
|
+
@result = nil
|
141
|
+
end
|
142
|
+
|
143
|
+
# Applies global context, if set, from Gaskit.configuration.context_provider
|
144
|
+
# and injects the `gaskit_flow` key to indicate to operations they are a part
|
145
|
+
# of a flow.
|
146
|
+
#
|
147
|
+
# @param context [Hash] The context provided directly to the Flow.
|
148
|
+
# @return [Hash] The fully applied context Hash.
|
149
|
+
def apply_context(context)
|
150
|
+
default_context = Gaskit.configuration.context_provider.call
|
151
|
+
context = default_context.merge(
|
152
|
+
gaskit_flow: { id: SecureRandom.uuid, name: self.class.name },
|
153
|
+
**context
|
154
|
+
)
|
155
|
+
|
156
|
+
Helpers.deep_compact(context)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Executes a single operation step and handles errors
|
160
|
+
#
|
161
|
+
# @param operation [Class<Gaskit::Operation>]
|
162
|
+
# @param kwargs [Hash] Merged args/kwargs/context
|
163
|
+
# @return [Gaskit::OperationResult]
|
164
|
+
def execute_step(operation, **kwargs, &block)
|
165
|
+
input_args, input_kwargs = @input
|
166
|
+
kwargs = (input_kwargs || {}).merge(kwargs).merge(context: @context)
|
167
|
+
|
168
|
+
return operation.call!(*input_args, **kwargs, &block) if @raise_on_failure
|
169
|
+
|
170
|
+
operation.call(*input_args, **kwargs, &block)
|
171
|
+
rescue StandardError => e
|
172
|
+
raise e if @raise_on_failure
|
173
|
+
|
174
|
+
result_class = operation.class.result_class
|
175
|
+
result_class.new(false, nil, e, duration: 0.0, context: @context)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Logs a step’s full input and output
|
179
|
+
#
|
180
|
+
# @param operation [Class]
|
181
|
+
# @param args [Array]
|
182
|
+
# @param kwargs [Hash]
|
183
|
+
# @return [Hash] Step metadata
|
184
|
+
def compile_step_entry(operation, *args, **kwargs)
|
185
|
+
args, kwargs = step_input(*args, **kwargs)
|
186
|
+
|
187
|
+
{
|
188
|
+
operation: operation,
|
189
|
+
args: args,
|
190
|
+
kwargs: kwargs,
|
191
|
+
result: result.to_h
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
# Combines current flow input with explicit args for logging
|
196
|
+
#
|
197
|
+
# @param args [Array]
|
198
|
+
# @param kwargs [Hash]
|
199
|
+
# @return [Array<Array, Hash>]
|
200
|
+
def step_input(*args, **kwargs)
|
201
|
+
input_args, input_kwargs = @input
|
202
|
+
args = (input_args || []).concat(args)
|
203
|
+
kwargs = input_kwargs.merge(kwargs)
|
204
|
+
|
205
|
+
[args, kwargs]
|
206
|
+
end
|
207
|
+
|
208
|
+
# Set the input used to call the next operation. Do not set input if the result
|
209
|
+
# is a failure or has a nil value.
|
210
|
+
#
|
211
|
+
# @param result [Gaskit::OperationResult] The result of the operation.
|
212
|
+
# @return [void]
|
213
|
+
def update_input(result)
|
214
|
+
return if result&.failure? || result&.value.nil?
|
215
|
+
|
216
|
+
@input =
|
217
|
+
case result.value
|
218
|
+
when Array
|
219
|
+
[result.value, {}]
|
220
|
+
when Hash
|
221
|
+
[[], result.value]
|
222
|
+
else
|
223
|
+
[[result.value], {}]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gaskit
|
4
|
+
# Represents the result of a flow execution, including step-by-step trace
|
5
|
+
#
|
6
|
+
# @example Checking result and accessing steps
|
7
|
+
# result = MyFlow.call(1, 2)
|
8
|
+
# if result.success?
|
9
|
+
# puts "Total: #{result.value}"
|
10
|
+
# else
|
11
|
+
# puts "Failed at: #{result.steps.last[:operation]}"
|
12
|
+
# end
|
13
|
+
class FlowResult < OperationResult
|
14
|
+
# @return [Array<Hash>] A list of step data executed during the flow
|
15
|
+
attr_reader :steps
|
16
|
+
|
17
|
+
# Initializes a new FlowResult
|
18
|
+
#
|
19
|
+
# @param result [Gaskit::OperationResult] The final operation result
|
20
|
+
# @param steps [Array<Hash>] Step-by-step execution details
|
21
|
+
# @param duration [Float, String] Total flow duration
|
22
|
+
# @param context [Hash] Execution context
|
23
|
+
def initialize(result, steps, duration:, context: {})
|
24
|
+
super(
|
25
|
+
result.success?,
|
26
|
+
result.value,
|
27
|
+
result.error,
|
28
|
+
duration: duration,
|
29
|
+
context: context
|
30
|
+
)
|
31
|
+
|
32
|
+
@steps = steps
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gaskit
|
4
|
+
module Helpers
|
5
|
+
class << self
|
6
|
+
# Measures the time taken for execution.
|
7
|
+
#
|
8
|
+
# @yield The block containing the logic to time.
|
9
|
+
# @return [Array<Float, Object>] The duration and the result.
|
10
|
+
def time_execution
|
11
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
12
|
+
result = yield
|
13
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time)
|
14
|
+
|
15
|
+
[format("%.6f", duration), result]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Applies deep compat on the provided hash.
|
19
|
+
#
|
20
|
+
# @param hash [Hash] The original hash to compact.
|
21
|
+
# @return [Hash] The compacted hash.
|
22
|
+
def deep_compact(hash)
|
23
|
+
hash.each_with_object({}) do |(k, v), result|
|
24
|
+
compacted = v.is_a?(Hash) ? deep_compact(v) : v
|
25
|
+
result[k.to_sym] = compacted unless compacted.nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|