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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +28 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +86 -0
  8. data/Rakefile +12 -0
  9. data/VERSION +1 -0
  10. data/docs/colorize.md +22 -0
  11. data/docs/configuration.md +63 -0
  12. data/docs/context_definition.md +21 -0
  13. data/docs/duration.md +37 -0
  14. data/docs/flow.md +25 -0
  15. data/docs/jobify.md +38 -0
  16. data/docs/light_context.md +24 -0
  17. data/docs/light_context_definition.md +19 -0
  18. data/docs/loggable.md +17 -0
  19. data/docs/operation.md +34 -0
  20. data/docs/organize.md +20 -0
  21. data/docs/populate.md +34 -0
  22. data/lib/interactor/colorize.rb +61 -0
  23. data/lib/interactor/context_definition.rb +156 -0
  24. data/lib/interactor/contextable.rb +18 -0
  25. data/lib/interactor/duration/base_formatter.rb +32 -0
  26. data/lib/interactor/duration/color_string_formatter.rb +22 -0
  27. data/lib/interactor/duration/json_formatter.rb +43 -0
  28. data/lib/interactor/duration/string_formatter.rb +57 -0
  29. data/lib/interactor/duration.rb +97 -0
  30. data/lib/interactor/extended/configuration.rb +51 -0
  31. data/lib/interactor/extended/error.rb +8 -0
  32. data/lib/interactor/extended/helpers.rb +54 -0
  33. data/lib/interactor/extended/version.rb +30 -0
  34. data/lib/interactor/extended.rb +84 -0
  35. data/lib/interactor/flow.rb +21 -0
  36. data/lib/interactor/jobify.rb +121 -0
  37. data/lib/interactor/light_context.rb +169 -0
  38. data/lib/interactor/light_context_definition.rb +53 -0
  39. data/lib/interactor/loggable.rb +28 -0
  40. data/lib/interactor/operation.rb +21 -0
  41. data/lib/interactor/organize.rb +49 -0
  42. data/lib/interactor/populate.rb +63 -0
  43. data/lib/interactor/threadable.rb +13 -0
  44. data/sig/interactor/colorize.rbs +9 -0
  45. data/sig/interactor/context_definition.rbs +20 -0
  46. data/sig/interactor/contextable.rbs +6 -0
  47. data/sig/interactor/duration/base_formatter.rbs +12 -0
  48. data/sig/interactor/duration/color_string_formatter.rbs +9 -0
  49. data/sig/interactor/duration/json_formatter.rbs +7 -0
  50. data/sig/interactor/duration/string_formatter.rbs +7 -0
  51. data/sig/interactor/duration.rbs +7 -0
  52. data/sig/interactor/extended/configuration.rbs +14 -0
  53. data/sig/interactor/extended/error.rbs +6 -0
  54. data/sig/interactor/extended/helpers.rbs +9 -0
  55. data/sig/interactor/extended/version.rbs +10 -0
  56. data/sig/interactor/extended.rbs +12 -0
  57. data/sig/interactor/flow.rbs +8 -0
  58. data/sig/interactor/jobify.rbs +32 -0
  59. data/sig/interactor/light_context.rbs +28 -0
  60. data/sig/interactor/light_context_definition.rbs +13 -0
  61. data/sig/interactor/loggable.rbs +13 -0
  62. data/sig/interactor/operation.rbs +8 -0
  63. data/sig/interactor/organize.rbs +21 -0
  64. data/sig/interactor/populate.rbs +20 -0
  65. data/sig/interactor/threadable.rbs +5 -0
  66. 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactor
4
+ module Extended
5
+ # Base error class for the Extended gem.
6
+ class Error < StandardError; end
7
+ end
8
+ 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