interactor-extended 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 +2 -0
- data/.rubocop.yml +28 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/docs/colorize.md +22 -0
- data/docs/configuration.md +63 -0
- data/docs/context_definition.md +21 -0
- data/docs/duration.md +37 -0
- data/docs/flow.md +25 -0
- data/docs/jobify.md +38 -0
- data/docs/light_context.md +24 -0
- data/docs/light_context_definition.md +19 -0
- data/docs/loggable.md +17 -0
- data/docs/operation.md +34 -0
- data/docs/organize.md +20 -0
- data/docs/populate.md +34 -0
- data/lib/interactor/colorize.rb +61 -0
- data/lib/interactor/context_definition.rb +156 -0
- data/lib/interactor/contextable.rb +18 -0
- data/lib/interactor/duration/base_formatter.rb +32 -0
- data/lib/interactor/duration/color_string_formatter.rb +22 -0
- data/lib/interactor/duration/json_formatter.rb +43 -0
- data/lib/interactor/duration/string_formatter.rb +57 -0
- data/lib/interactor/duration.rb +97 -0
- data/lib/interactor/extended/configuration.rb +51 -0
- data/lib/interactor/extended/error.rb +8 -0
- data/lib/interactor/extended/helpers.rb +54 -0
- data/lib/interactor/extended/version.rb +30 -0
- data/lib/interactor/extended.rb +84 -0
- data/lib/interactor/flow.rb +21 -0
- data/lib/interactor/jobify.rb +121 -0
- data/lib/interactor/light_context.rb +169 -0
- data/lib/interactor/light_context_definition.rb +53 -0
- data/lib/interactor/loggable.rb +28 -0
- data/lib/interactor/operation.rb +21 -0
- data/lib/interactor/organize.rb +49 -0
- data/lib/interactor/populate.rb +63 -0
- data/lib/interactor/threadable.rb +13 -0
- data/sig/interactor/colorize.rbs +9 -0
- data/sig/interactor/context_definition.rbs +20 -0
- data/sig/interactor/contextable.rbs +6 -0
- data/sig/interactor/duration/base_formatter.rbs +12 -0
- data/sig/interactor/duration/color_string_formatter.rbs +9 -0
- data/sig/interactor/duration/json_formatter.rbs +7 -0
- data/sig/interactor/duration/string_formatter.rbs +7 -0
- data/sig/interactor/duration.rbs +7 -0
- data/sig/interactor/extended/configuration.rbs +14 -0
- data/sig/interactor/extended/error.rbs +6 -0
- data/sig/interactor/extended/helpers.rbs +9 -0
- data/sig/interactor/extended/version.rbs +10 -0
- data/sig/interactor/extended.rbs +12 -0
- data/sig/interactor/flow.rbs +8 -0
- data/sig/interactor/jobify.rbs +32 -0
- data/sig/interactor/light_context.rbs +28 -0
- data/sig/interactor/light_context_definition.rbs +13 -0
- data/sig/interactor/loggable.rbs +13 -0
- data/sig/interactor/operation.rbs +8 -0
- data/sig/interactor/organize.rbs +21 -0
- data/sig/interactor/populate.rbs +20 -0
- data/sig/interactor/threadable.rbs +5 -0
- metadata +127 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
# Provides colorization utilities for strings.
|
|
5
|
+
module Colorize
|
|
6
|
+
class << self
|
|
7
|
+
COLORS = {
|
|
8
|
+
black: 30,
|
|
9
|
+
red: 31,
|
|
10
|
+
green: 32,
|
|
11
|
+
yellow: 33,
|
|
12
|
+
blue: 34,
|
|
13
|
+
magenta: 35,
|
|
14
|
+
cyan: 36,
|
|
15
|
+
white: 37,
|
|
16
|
+
default: 39,
|
|
17
|
+
light_black: 90,
|
|
18
|
+
light_red: 91,
|
|
19
|
+
light_green: 92,
|
|
20
|
+
light_yellow: 93,
|
|
21
|
+
light_blue: 94,
|
|
22
|
+
light_magenta: 95,
|
|
23
|
+
light_cyan: 96,
|
|
24
|
+
light_white: 97
|
|
25
|
+
}.freeze
|
|
26
|
+
MODES = {
|
|
27
|
+
default: 0, # Turn off all attributes
|
|
28
|
+
bold: 1,
|
|
29
|
+
dim: 2,
|
|
30
|
+
italic: 3,
|
|
31
|
+
underline: 4,
|
|
32
|
+
blink: 5,
|
|
33
|
+
blink_slow: 5,
|
|
34
|
+
blink_fast: 6,
|
|
35
|
+
invert: 7,
|
|
36
|
+
hide: 8,
|
|
37
|
+
strike: 9,
|
|
38
|
+
double_underline: 20,
|
|
39
|
+
reveal: 28,
|
|
40
|
+
overlined: 53
|
|
41
|
+
}.freeze
|
|
42
|
+
# Colorizes a string with the given options.
|
|
43
|
+
# @param msg [String] The message to colorize.
|
|
44
|
+
# @param color_value [Symbol, nil] The color value.
|
|
45
|
+
# @param color [Symbol, nil] The color.
|
|
46
|
+
# @param mode [Symbol, nil] The mode.
|
|
47
|
+
# @param bg [Symbol, nil] The background color.
|
|
48
|
+
# @return [String] The colorized string.
|
|
49
|
+
def colorize(msg, color_value = nil, color: nil, mode: nil, bg: nil) # rubocop:disable Naming/MethodParameterName
|
|
50
|
+
"\033[#{MODES[mode] || MODES[:bold]};#{COLORS[color_value || color] || COLORS[:default]};#{(COLORS[bg] || COLORS[:default]) + 10}m#{msg}\033[0m" # rubocop:disable Layout/LineLength
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Colorizes the receiver using the Colorize module.
|
|
55
|
+
# @param args [untyped] Arguments to pass to Colorize.colorize.
|
|
56
|
+
# @return [String]
|
|
57
|
+
def colorize(*)
|
|
58
|
+
::Interactor::Colorize.colorize(*)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
# Defines inputs and outputs for an interactor's context.
|
|
5
|
+
module ContextDefinition
|
|
6
|
+
include Interactor::Extended::Helpers
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def included(base)
|
|
10
|
+
base.extend(ClassMethods)
|
|
11
|
+
base.remove_all_attributes!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
attr_accessor :attributes
|
|
17
|
+
|
|
18
|
+
# Defines an attribute for the context.
|
|
19
|
+
# @param methods [Symbol, String] The attribute names.
|
|
20
|
+
# @param array [bool] Whether the attribute is an array.
|
|
21
|
+
# @param default [untyped, nil] Default value for the attribute.
|
|
22
|
+
# @param required [bool] Whether the attribute is required.
|
|
23
|
+
# @param direction [Symbol] The direction of the attribute (:input or :output).
|
|
24
|
+
def attribute(*methods, array: false, default: nil, required: false, direction: :output) # rubocop:disable Metrics
|
|
25
|
+
self.attributes ||= {}
|
|
26
|
+
if !(methods.last.is_a?(String) || methods.last.is_a?(Symbol)) || methods.last == :boolean
|
|
27
|
+
type = methods.pop
|
|
28
|
+
type = [TrueClass, FalseClass] if type == :boolean
|
|
29
|
+
type = type.is_a?(Array) ? type.map(&:name) : type.name
|
|
30
|
+
methods.each do |method|
|
|
31
|
+
attributes[method] = {
|
|
32
|
+
type:,
|
|
33
|
+
direction:,
|
|
34
|
+
array:,
|
|
35
|
+
default:,
|
|
36
|
+
required:
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
methods.each do |method|
|
|
41
|
+
define_method(method) { context[method] }
|
|
42
|
+
end
|
|
43
|
+
return if direction != :output
|
|
44
|
+
|
|
45
|
+
methods.each do |method|
|
|
46
|
+
define_method(:"#{method}=") { |value| set_attribute(method, value) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Defines an input attribute.
|
|
51
|
+
# @param methods [Symbol, String] The input names.
|
|
52
|
+
# @param array [bool] Whether the input is an array.
|
|
53
|
+
# @param default [untyped, nil] Default value.
|
|
54
|
+
# @param required [bool] Whether the input is required.
|
|
55
|
+
# @param writer [bool] Whether to define a writer method.
|
|
56
|
+
def input(*methods, array: false, default: nil, required: false, writer: false)
|
|
57
|
+
attribute(*methods, array:, default:, required:, direction: writer ? :output : :input)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Defines an output attribute.
|
|
61
|
+
# @param methods [Symbol, String] The output names.
|
|
62
|
+
# @param array [bool] Whether the output is an array.
|
|
63
|
+
# @param default [untyped, nil] Default value.
|
|
64
|
+
# @param required [bool] Whether the output is required.
|
|
65
|
+
def output(*methods, array: false, default: nil, required: false)
|
|
66
|
+
attribute(*methods, array:, default:, required:)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns all attributes defined in the class hierarchy.
|
|
70
|
+
# @return [Hash[Symbol, Hash[Symbol, untyped]]]
|
|
71
|
+
def all_attributes
|
|
72
|
+
return @all_attributes if defined?(@all_attributes)
|
|
73
|
+
|
|
74
|
+
klass = self
|
|
75
|
+
attributes_arr = []
|
|
76
|
+
loop do
|
|
77
|
+
attributes_arr << klass.attributes
|
|
78
|
+
klass = klass.superclass
|
|
79
|
+
break if klass == Object
|
|
80
|
+
end
|
|
81
|
+
@all_attributes ||= attributes_arr.reverse.compact.each_with_object({}) { |attrs, result| result.merge!(attrs) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Removes all cached attributes.
|
|
85
|
+
# @return [void]
|
|
86
|
+
def remove_all_attributes!
|
|
87
|
+
remove_instance_variable(:@all_attributes) if defined?(@all_attributes)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def initialize(*)
|
|
92
|
+
super
|
|
93
|
+
initialize_default_values
|
|
94
|
+
errors = check_attributes
|
|
95
|
+
context.fail!(errors:, interactor: self.class.name) unless errors.empty?
|
|
96
|
+
rescue Failure # rubocop:disable Lint/SuppressedException
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Runs the interactor, raising a Failure if the context has failed.
|
|
100
|
+
# @return [untyped]
|
|
101
|
+
def run!
|
|
102
|
+
raise Failure, context if context.failure?
|
|
103
|
+
|
|
104
|
+
super
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def initialize_default_values
|
|
110
|
+
self.class.all_attributes
|
|
111
|
+
.select { |key, options| !options[:default].nil? && !context.respond_to?(key) }
|
|
112
|
+
.each do |key, options|
|
|
113
|
+
value =
|
|
114
|
+
if options[:default].is_a?(Proc)
|
|
115
|
+
instance_exec(*[self, context][0, options[:default].arity], &options[:default])
|
|
116
|
+
else
|
|
117
|
+
deep_dup(options[:default])
|
|
118
|
+
end
|
|
119
|
+
set_attribute(key, value)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def set_attribute(key, value)
|
|
124
|
+
error = check_attribute(key, value)
|
|
125
|
+
context.fail!(errors: { key => [error] }) if error
|
|
126
|
+
context[key] = value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_attributes
|
|
130
|
+
self.class.all_attributes.filter_map do |key, _options|
|
|
131
|
+
error = check_attribute(key)
|
|
132
|
+
[key, [error]] if error
|
|
133
|
+
end.to_h
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def check_attribute(key, value = nil) # rubocop:disable Metrics
|
|
137
|
+
options = self.class.all_attributes[key]
|
|
138
|
+
return unless options
|
|
139
|
+
|
|
140
|
+
value ||= context[key]
|
|
141
|
+
types = [options[:type]].flatten.map { safe_constantize(_1) }
|
|
142
|
+
if value.nil?
|
|
143
|
+
return options[:required] ? "#{options[:direction].capitalize} param \"#{key}\" is required" : nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if options[:array]
|
|
147
|
+
return if value.is_a?(Array) && value.all? { |item| types.any? { |type| item.is_a?(type) } }
|
|
148
|
+
|
|
149
|
+
return "#{options[:direction].capitalize} param \"#{key}\" is not kind of Array of #{types.join(', ')}"
|
|
150
|
+
end
|
|
151
|
+
return if types.any? { |type| value.is_a?(type) }
|
|
152
|
+
|
|
153
|
+
"#{options[:direction].capitalize} param \"#{key}\" is not kind of #{types.join(', ')}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
# Provides context management for interactors.
|
|
5
|
+
module Contextable
|
|
6
|
+
# Initializes the contextable with a context hash.
|
|
7
|
+
# @param context [Hash] The context data.
|
|
8
|
+
def initialize(context = {})
|
|
9
|
+
@context = LightContext.build(context)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns the context object.
|
|
13
|
+
# @return [LightContext]
|
|
14
|
+
def ctx
|
|
15
|
+
@context
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Duration
|
|
5
|
+
# Base class for duration formatters.
|
|
6
|
+
class BaseFormatter
|
|
7
|
+
# Initializes the formatter with duration data.
|
|
8
|
+
# @param duration Array[String, Array[Float, Float]] The duration data.
|
|
9
|
+
def initialize(duration, options = {})
|
|
10
|
+
@duration = duration
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the duration data.
|
|
15
|
+
# @return Array[String, Array[Float, Float]]
|
|
16
|
+
def value
|
|
17
|
+
@duration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def value(data)
|
|
22
|
+
format = ::Interactor::Extended.configuration.duration_format&.to_sym
|
|
23
|
+
case format
|
|
24
|
+
when :json, :flat_json then JsonFormatter
|
|
25
|
+
when :string, :flat_string then StringFormatter
|
|
26
|
+
else ColorStringFormatter
|
|
27
|
+
end.new(data, flat: format.to_s.match(/^flat_/)).value
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Duration
|
|
5
|
+
# Formats duration data into a colorized string.
|
|
6
|
+
class ColorStringFormatter < StringFormatter
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def message(node, max)
|
|
10
|
+
[
|
|
11
|
+
::Interactor::Colorize.colorize(node[:name].ljust(max - (node[:level] * 2)), :yellow),
|
|
12
|
+
[
|
|
13
|
+
"count: #{node[:count]}".ljust(10),
|
|
14
|
+
"time: #{::Interactor::Colorize.colorize(node[:time].to_s.ljust(25), :green)}",
|
|
15
|
+
"own: #{::Interactor::Colorize.colorize(node[:own].to_s.ljust(25), :green)}",
|
|
16
|
+
"max: #{::Interactor::Colorize.colorize(node[:max].to_s.ljust(25), :green)}"
|
|
17
|
+
].join("\t")
|
|
18
|
+
].join("\t")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Duration
|
|
5
|
+
# Formats duration data into a JSON-compatible array of hashes.
|
|
6
|
+
class JsonFormatter < BaseFormatter
|
|
7
|
+
def value
|
|
8
|
+
tree.then { @options[:flat] ? to_flat(_1) : _1 }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def tree
|
|
14
|
+
@tree ||= process_duration(@duration.then { @options[:flat] ? process_flat_duration(_1) : _1 })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def process_duration(data, level = 0)
|
|
18
|
+
data.group_by(&:first).map do |name, values|
|
|
19
|
+
{
|
|
20
|
+
name:,
|
|
21
|
+
level:,
|
|
22
|
+
count: values.count,
|
|
23
|
+
time: values.sum { _1[3] - _1[2] }.fdiv(values.count),
|
|
24
|
+
own: values.sum { _1[4] }.fdiv(values.count),
|
|
25
|
+
max: values.max_by { _1[4] }.last,
|
|
26
|
+
children: process_duration(values.flat_map { _1[1] }, level + 1)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def process_flat_duration(data)
|
|
32
|
+
data.flat_map { [_1.dup.tap { |value| value[1] = [] }] + process_flat_duration(_1[1]) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_flat(data)
|
|
36
|
+
data.each_with_object([]) do |item, result|
|
|
37
|
+
result << item.except(:children)
|
|
38
|
+
result.concat(to_flat(item[:children]))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Duration
|
|
5
|
+
# Formats duration data into a string.
|
|
6
|
+
class StringFormatter < JsonFormatter
|
|
7
|
+
# Returns the string representation of the duration data.
|
|
8
|
+
# @return String
|
|
9
|
+
def value
|
|
10
|
+
super
|
|
11
|
+
flat_data = to_flat(tree)
|
|
12
|
+
|
|
13
|
+
if @options[:flat]
|
|
14
|
+
max = flat_data.map { _1[:name].length }.max
|
|
15
|
+
return flat_data.map { message(_1, max) }.join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
max = flat_data.map { (_1[:level] * 2) + _1[:name].length }.max
|
|
19
|
+
tree_value(tree, max).strip
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def tree_value(node, max, prefix = '', is_last: true, connector: nil, new_prefix: nil) # rubocop:disable Metrics
|
|
25
|
+
if node.is_a?(Array)
|
|
26
|
+
return node.map.with_index do |child, i|
|
|
27
|
+
tree_value(child, max, '', is_last: i == node.size - 1, connector: '', new_prefix: '')
|
|
28
|
+
end.join
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
children = node[:children].to_a
|
|
32
|
+
new_prefix ||= prefix + (is_last ? ' ' : '│ ')
|
|
33
|
+
[
|
|
34
|
+
prefix,
|
|
35
|
+
connector || (is_last ? '└ ' : '├ '),
|
|
36
|
+
message(node, max),
|
|
37
|
+
"\n",
|
|
38
|
+
children.map.with_index do |child, i|
|
|
39
|
+
tree_value(child, max, new_prefix, is_last: i == children.size - 1)
|
|
40
|
+
end.join
|
|
41
|
+
].join
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def message(node, max)
|
|
45
|
+
[
|
|
46
|
+
node[:name].ljust(max - (@options[:flat] ? 0 : node[:level] * 2)),
|
|
47
|
+
[
|
|
48
|
+
"count: #{node[:count]}".ljust(10),
|
|
49
|
+
"time: #{node[:time].to_s.ljust(25)}",
|
|
50
|
+
"own: #{node[:own].to_s.ljust(25)}",
|
|
51
|
+
"max: #{node[:max].to_s.ljust(25)}"
|
|
52
|
+
].join("\t")
|
|
53
|
+
].join("\t")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
# Provides duration tracking for interactors.
|
|
5
|
+
module Duration
|
|
6
|
+
Storage = Struct.new(:data, :total)
|
|
7
|
+
class << self
|
|
8
|
+
# Includes the duration tracking functionality into a base class.
|
|
9
|
+
# @param base [Class] The class to include the module into.
|
|
10
|
+
def included(base)
|
|
11
|
+
base.extend(ClassMethods)
|
|
12
|
+
base.class_eval do # rubocop:disable Metrics/BlockLength
|
|
13
|
+
around :_process_duration
|
|
14
|
+
|
|
15
|
+
def self.inherited(subclass)
|
|
16
|
+
super
|
|
17
|
+
subclass.around :_process_duration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def _duration
|
|
23
|
+
Thread.current.thread_variable_get('interactor.duration')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _duration=(value)
|
|
27
|
+
Thread.current.thread_variable_set('interactor.duration', value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def _duration_current
|
|
31
|
+
Thread.current.thread_variable_get('interactor.duration_current')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def _duration_current=(value)
|
|
35
|
+
Thread.current.thread_variable_set('interactor.duration_current', value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def _duration_clock
|
|
39
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def _process_duration(interactor, key = self.class.name)
|
|
43
|
+
self._duration ||= nil
|
|
44
|
+
initial_operation = _duration.nil?
|
|
45
|
+
self._duration = Storage.new(data: [], total: 0) if _duration.nil?
|
|
46
|
+
parent = _duration.data << (current = [key, []])
|
|
47
|
+
_duration.data = current[1]
|
|
48
|
+
duration_total = _duration.total.clone
|
|
49
|
+
current[2] = _duration_clock
|
|
50
|
+
interactor.call
|
|
51
|
+
ensure
|
|
52
|
+
current[3] = _duration_clock
|
|
53
|
+
current[4] = current[3] - current[2] - (_duration.total - duration_total)
|
|
54
|
+
_duration.data = parent
|
|
55
|
+
_duration.total += current[4]
|
|
56
|
+
_duration_print if initial_operation
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def _duration_print
|
|
60
|
+
data = _duration.data
|
|
61
|
+
self._duration = nil
|
|
62
|
+
return data.each { _duration_current.push(_1) } if _duration_current
|
|
63
|
+
|
|
64
|
+
::Interactor::Extended.configuration.on_duration&.call(BaseFormatter.value(data))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module ClassMethods
|
|
71
|
+
# Enables duration tracking for the next defined method (or all methods if `all` is true).
|
|
72
|
+
# @param all [bool] When true, tracks every subsequently defined method.
|
|
73
|
+
# @return [void]
|
|
74
|
+
def duration(all = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
75
|
+
@__duration_all_methods = true if all
|
|
76
|
+
@__duration_next_method = true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Wraps any method defined after a `duration` call with duration tracking.
|
|
80
|
+
# @param name [Symbol] The name of the method that was just added.
|
|
81
|
+
# @return [void]
|
|
82
|
+
def method_added(name)
|
|
83
|
+
super
|
|
84
|
+
return unless @__duration_next_method
|
|
85
|
+
|
|
86
|
+
@__duration_next_method = false
|
|
87
|
+
original = instance_method(name)
|
|
88
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
89
|
+
key = "#{self.class.name}##{name}"
|
|
90
|
+
interactor = -> { original.bind_call(self, *args, **kwargs, &block) }
|
|
91
|
+
_process_duration(interactor, key)
|
|
92
|
+
end
|
|
93
|
+
@__duration_next_method = true if @__duration_all_methods
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Extended
|
|
5
|
+
# Manages configuration for the Interactor Extended gem.
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :logger,
|
|
8
|
+
:type, # ctx|light|legacy|all|all_debug
|
|
9
|
+
:duration_format, # json|string|color_string
|
|
10
|
+
:on_duration
|
|
11
|
+
attr_reader :types
|
|
12
|
+
|
|
13
|
+
# Initializes the configuration with default values.
|
|
14
|
+
def initialize
|
|
15
|
+
@logger = defined?(Rails) ? Rails.logger : ::Logger.new($stdout)
|
|
16
|
+
@type = :all
|
|
17
|
+
@duration_format = :color_string
|
|
18
|
+
@on_duration = -> { @logger.debug("\n#{_1}") }
|
|
19
|
+
@types = {}
|
|
20
|
+
add_type!(:base)
|
|
21
|
+
add_type!(:ctx, [::Interactor::Contextable])
|
|
22
|
+
add_type!(:light, [
|
|
23
|
+
::Interactor::LightContextDefinition,
|
|
24
|
+
::Interactor::Populate,
|
|
25
|
+
::Interactor::Organize,
|
|
26
|
+
::Interactor::Threadable
|
|
27
|
+
], :ctx)
|
|
28
|
+
add_type!(:legacy, [
|
|
29
|
+
::Interactor::ContextDefinition,
|
|
30
|
+
::Interactor::Populate,
|
|
31
|
+
::Interactor::Organize,
|
|
32
|
+
::Interactor::Threadable,
|
|
33
|
+
::Interactor::Jobify
|
|
34
|
+
], :ctx)
|
|
35
|
+
add_type!(:all, [::Interactor::Colorize, ::Interactor::Loggable], :legacy)
|
|
36
|
+
add_type!(:all_debug, [::Interactor::Duration], :all)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Adds a new type to the configuration.
|
|
40
|
+
# @param type [Symbol] The name of the type.
|
|
41
|
+
# @param modules [Array<Module>] The modules to include.
|
|
42
|
+
# @param inherit [Symbol, nil] The type to inherit from.
|
|
43
|
+
# @raise [RuntimeError] If the type already exists.
|
|
44
|
+
def add_type!(type, modules = [], inherit = nil)
|
|
45
|
+
raise 'Can\'t replace' if @types.key?(type.to_sym)
|
|
46
|
+
|
|
47
|
+
@types[type.to_sym] = (inherit ? @types[inherit.to_sym].to_a : []) + modules
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Extended
|
|
5
|
+
# Provides helper methods for the Extended gem.
|
|
6
|
+
module Helpers
|
|
7
|
+
# Deeply duplicates an object.
|
|
8
|
+
# @param obj [untyped] The object to duplicate.
|
|
9
|
+
# @return [untyped]
|
|
10
|
+
def deep_dup(obj)
|
|
11
|
+
case obj
|
|
12
|
+
when Hash
|
|
13
|
+
obj.to_h { |k, v| [deep_dup(k), deep_dup(v)] }
|
|
14
|
+
when Array
|
|
15
|
+
obj.map { deep_dup(_1) }
|
|
16
|
+
when NilClass, FalseClass, TrueClass, Symbol, Numeric
|
|
17
|
+
obj
|
|
18
|
+
else
|
|
19
|
+
obj.dup
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Deeply transforms keys of an object.
|
|
24
|
+
# @param object [untyped] The object to transform.
|
|
25
|
+
# @yield [Symbol] The key to transform.
|
|
26
|
+
# @return [untyped]
|
|
27
|
+
def deep_transform_keys(object, &block)
|
|
28
|
+
case object
|
|
29
|
+
when Hash
|
|
30
|
+
object.each_with_object({}) do |(key, value), result|
|
|
31
|
+
result[yield(key)] = deep_transform_keys(value, &block)
|
|
32
|
+
end
|
|
33
|
+
when Array
|
|
34
|
+
object.map { deep_transform_keys(_1, &block) }
|
|
35
|
+
else
|
|
36
|
+
object
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Safely constantizes a string.
|
|
41
|
+
# @param camel_cased_word [String] The string to constantize.
|
|
42
|
+
# @return [untyped]
|
|
43
|
+
def safe_constantize(camel_cased_word)
|
|
44
|
+
Object.const_get(camel_cased_word)
|
|
45
|
+
rescue NameError => e
|
|
46
|
+
raise if e.name && !(camel_cased_word.to_s.split('::').include?(e.name.to_s) ||
|
|
47
|
+
e.name.to_s == camel_cased_word.to_s)
|
|
48
|
+
rescue LoadError => e
|
|
49
|
+
message = e.respond_to?(:original_message) ? e.original_message : e.message
|
|
50
|
+
raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(message)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Extended
|
|
5
|
+
# The version string for the gem.
|
|
6
|
+
VERSION =
|
|
7
|
+
if File.exist?(File.expand_path(File.join(__dir__, '../../../VERSION')))
|
|
8
|
+
File.read(File.expand_path(File.join(__dir__, '../../../VERSION')))
|
|
9
|
+
else
|
|
10
|
+
''
|
|
11
|
+
end.strip.freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Returns the full version string including revision.
|
|
15
|
+
# @return [String]
|
|
16
|
+
def version
|
|
17
|
+
[VERSION, revision].compact.join('-')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the git revision if available.
|
|
21
|
+
# @return [String, nil]
|
|
22
|
+
def revision
|
|
23
|
+
source = Gem::Specification.find_by_name('interactor-extended')&.source
|
|
24
|
+
source.revision if source.is_a?(Bundler::Source::Git)
|
|
25
|
+
rescue Gem::MissingSpecError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|