flows 0.2.0 → 0.6.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +9 -1
  4. data/.mdlrc +1 -1
  5. data/.reek.yml +54 -0
  6. data/.rubocop.yml +26 -7
  7. data/.rubocop_todo.yml +27 -0
  8. data/.ruby-version +1 -1
  9. data/.yardopts +1 -0
  10. data/CHANGELOG.md +81 -0
  11. data/Gemfile +0 -6
  12. data/README.md +167 -363
  13. data/Rakefile +35 -1
  14. data/bin/.rubocop.yml +5 -0
  15. data/bin/all_the_errors +55 -0
  16. data/bin/benchmark +73 -105
  17. data/bin/benchmark_cli/compare.rb +118 -0
  18. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  19. data/bin/benchmark_cli/compare/base.rb +45 -0
  20. data/bin/benchmark_cli/compare/command.rb +47 -0
  21. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  22. data/bin/benchmark_cli/examples.rb +23 -0
  23. data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  31. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  33. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  38. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  39. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  40. data/bin/benchmark_cli/helpers.rb +12 -0
  41. data/bin/benchmark_cli/ruby.rb +15 -0
  42. data/bin/benchmark_cli/ruby/command.rb +38 -0
  43. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  44. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  45. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  46. data/bin/console +1 -0
  47. data/bin/docserver +7 -0
  48. data/bin/errors +138 -0
  49. data/bin/errors_cli/contract_error_demo.rb +49 -0
  50. data/bin/errors_cli/di_error_demo.rb +38 -0
  51. data/bin/errors_cli/flow_error_demo.rb +22 -0
  52. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  53. data/bin/errors_cli/interface_error_demo.rb +17 -0
  54. data/bin/errors_cli/oc_error_demo.rb +40 -0
  55. data/bin/errors_cli/railway_error_demo.rb +10 -0
  56. data/bin/errors_cli/result_error_demo.rb +13 -0
  57. data/bin/errors_cli/scp_error_demo.rb +17 -0
  58. data/docs/README.md +3 -187
  59. data/docs/_sidebar.md +0 -24
  60. data/docs/index.html +1 -1
  61. data/flows.gemspec +27 -2
  62. data/forspell.dict +9 -0
  63. data/lefthook.yml +9 -0
  64. data/lib/flows.rb +11 -5
  65. data/lib/flows/contract.rb +402 -0
  66. data/lib/flows/contract/array.rb +55 -0
  67. data/lib/flows/contract/case_eq.rb +43 -0
  68. data/lib/flows/contract/compose.rb +77 -0
  69. data/lib/flows/contract/either.rb +53 -0
  70. data/lib/flows/contract/error.rb +24 -0
  71. data/lib/flows/contract/hash.rb +75 -0
  72. data/lib/flows/contract/hash_of.rb +70 -0
  73. data/lib/flows/contract/helpers.rb +22 -0
  74. data/lib/flows/contract/predicate.rb +34 -0
  75. data/lib/flows/contract/transformer.rb +50 -0
  76. data/lib/flows/contract/tuple.rb +70 -0
  77. data/lib/flows/flow.rb +96 -7
  78. data/lib/flows/flow/errors.rb +29 -0
  79. data/lib/flows/flow/node.rb +132 -0
  80. data/lib/flows/flow/router.rb +29 -0
  81. data/lib/flows/flow/router/custom.rb +59 -0
  82. data/lib/flows/flow/router/errors.rb +11 -0
  83. data/lib/flows/flow/router/simple.rb +25 -0
  84. data/lib/flows/plugin.rb +15 -0
  85. data/lib/flows/plugin/dependency_injector.rb +170 -0
  86. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  87. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  88. data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
  89. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  90. data/lib/flows/plugin/implicit_init.rb +45 -0
  91. data/lib/flows/plugin/interface.rb +84 -0
  92. data/lib/flows/plugin/output_contract.rb +85 -0
  93. data/lib/flows/plugin/output_contract/dsl.rb +48 -0
  94. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  95. data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
  96. data/lib/flows/plugin/profiler.rb +114 -0
  97. data/lib/flows/plugin/profiler/injector.rb +35 -0
  98. data/lib/flows/plugin/profiler/report.rb +48 -0
  99. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  100. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  101. data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
  102. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  103. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  104. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  105. data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
  106. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  107. data/lib/flows/railway.rb +140 -34
  108. data/lib/flows/railway/dsl.rb +8 -18
  109. data/lib/flows/railway/errors.rb +8 -12
  110. data/lib/flows/railway/step.rb +24 -0
  111. data/lib/flows/railway/step_list.rb +38 -0
  112. data/lib/flows/result.rb +188 -2
  113. data/lib/flows/result/do.rb +158 -16
  114. data/lib/flows/result/err.rb +12 -6
  115. data/lib/flows/result/errors.rb +29 -17
  116. data/lib/flows/result/helpers.rb +25 -3
  117. data/lib/flows/result/ok.rb +12 -6
  118. data/lib/flows/shared_context_pipeline.rb +342 -0
  119. data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
  120. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
  121. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  122. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  123. data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
  124. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  125. data/lib/flows/shared_context_pipeline/step.rb +55 -0
  126. data/lib/flows/shared_context_pipeline/track.rb +54 -0
  127. data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
  128. data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
  129. data/lib/flows/util.rb +17 -0
  130. data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
  131. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
  132. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
  133. data/lib/flows/util/prepend_to_class.rb +191 -0
  134. data/lib/flows/version.rb +1 -1
  135. metadata +253 -38
  136. data/Gemfile.lock +0 -174
  137. data/bin/demo +0 -66
  138. data/bin/examples.rb +0 -195
  139. data/bin/profile_10steps +0 -106
  140. data/bin/ruby_benchmarks +0 -26
  141. data/docs/CNAME +0 -1
  142. data/docs/contributing/benchmarks_profiling.md +0 -3
  143. data/docs/contributing/local_development.md +0 -3
  144. data/docs/flow/direct_usage.md +0 -3
  145. data/docs/flow/general_idea.md +0 -3
  146. data/docs/operation/basic_usage.md +0 -1
  147. data/docs/operation/inject_steps.md +0 -3
  148. data/docs/operation/lambda_steps.md +0 -3
  149. data/docs/operation/result_shapes.md +0 -3
  150. data/docs/operation/routing_tracks.md +0 -3
  151. data/docs/operation/wrapping_steps.md +0 -3
  152. data/docs/overview/performance.md +0 -336
  153. data/docs/railway/basic_usage.md +0 -232
  154. data/docs/result_objects/basic_usage.md +0 -196
  155. data/docs/result_objects/do_notation.md +0 -139
  156. data/lib/flows/node.rb +0 -27
  157. data/lib/flows/operation.rb +0 -52
  158. data/lib/flows/operation/builder.rb +0 -130
  159. data/lib/flows/operation/builder/build_router.rb +0 -37
  160. data/lib/flows/operation/dsl.rb +0 -93
  161. data/lib/flows/operation/errors.rb +0 -75
  162. data/lib/flows/operation/executor.rb +0 -78
  163. data/lib/flows/railway/builder.rb +0 -68
  164. data/lib/flows/railway/executor.rb +0 -23
  165. data/lib/flows/result_router.rb +0 -14
  166. data/lib/flows/router.rb +0 -22
@@ -0,0 +1,12 @@
1
+ require_relative 'dsl/tracks'
2
+ require_relative 'dsl/callbacks'
3
+
4
+ module Flows
5
+ class SharedContextPipeline
6
+ # @api private
7
+ module DSL
8
+ include Tracks
9
+ include Callbacks
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ module DSL
4
+ # @api private
5
+ module Callbacks
6
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
7
+ '@before_all_callbacks' => [],
8
+ '@after_all_callbacks' => [],
9
+ '@before_each_callbacks' => [],
10
+ '@after_each_callbacks' => []
11
+ )
12
+
13
+ include SingletonVarsSetup
14
+
15
+ attr_reader :before_all_callbacks, :after_all_callbacks, :before_each_callbacks, :after_each_callbacks
16
+
17
+ def before_all(&callback)
18
+ before_all_callbacks << callback
19
+ end
20
+
21
+ def after_all(&callback)
22
+ after_all_callbacks << callback
23
+ end
24
+
25
+ def before_each(&callback)
26
+ before_each_callbacks << callback
27
+ end
28
+
29
+ def after_each(&callback)
30
+ after_each_callbacks << callback
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ module DSL
4
+ # @api private
5
+ module Tracks
6
+ DEFAULT_ROUTER_DEF = RouterDefinition.new(
7
+ Flows::Result::Ok => :next,
8
+ Flows::Result::Err => :end
9
+ )
10
+
11
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
12
+ '@tracks' => TrackList.new
13
+ )
14
+
15
+ include SingletonVarsSetup
16
+
17
+ attr_reader :tracks
18
+
19
+ def step(name, router_def = DEFAULT_ROUTER_DEF, body: nil)
20
+ tracks.add_step(
21
+ Step.new(name: name, body: body, router_def: router_def)
22
+ )
23
+ end
24
+
25
+ def mut_step(name, router_def = DEFAULT_ROUTER_DEF, body: nil)
26
+ tracks.add_step(
27
+ MutationStep.new(name: name, body: body, router_def: router_def)
28
+ )
29
+ end
30
+
31
+ def wrap(name, router_def = DEFAULT_ROUTER_DEF, &tracks_definitions)
32
+ tracks.add_step(
33
+ Wrap.new(method_name: name, router_def: router_def, &tracks_definitions)
34
+ )
35
+ end
36
+
37
+ def track(name, &block)
38
+ track_before = tracks.current_track
39
+
40
+ tracks.switch_track(name)
41
+ instance_exec(&block)
42
+ tracks.switch_track(track_before)
43
+ end
44
+
45
+ # :reek:UtilityFunction is allowed here
46
+ def routes(routes_def)
47
+ RouterDefinition.new(routes_def)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ # Base error class for {SharedContextPipeline} errors.
4
+ class Error < StandardError; end
5
+
6
+ # Raised when initializing {SharedContextPipeline} with no steps.
7
+ class NoStepsError < Error
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+ def message
13
+ "No steps defined for main track in #{@klass}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ EMPTY_HASH = {}.freeze
4
+ EMPTY_OK = Flows::Result::Ok.new({}.freeze).freeze
5
+ EMPTY_ERR = Flows::Result::Err.new({}.freeze).freeze
6
+
7
+ # @api private
8
+ class MutationStep < Step
9
+ NODE_PREPROCESSOR = lambda do |_input, context, node_meta|
10
+ context[:class].before_each_callbacks.each do |callback|
11
+ context[:instance].instance_exec(context[:class], node_meta[:name], context[:data], context[:meta], &callback)
12
+ end
13
+
14
+ [[context[:data]], EMPTY_HASH]
15
+ end
16
+
17
+ NODE_POSTPROCESSOR = lambda do |output, context, node_meta|
18
+ case output
19
+ when Flows::Result then output
20
+ else output ? EMPTY_OK : EMPTY_ERR
21
+ end.tap do |result|
22
+ context[:class].after_each_callbacks.each do |callback|
23
+ context[:instance]
24
+ .instance_exec(context[:class], node_meta[:name], result, context[:data], context[:meta], &callback)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ # @api private
4
+ class RouterDefinition
5
+ def initialize(routes = {})
6
+ @routes = routes
7
+ end
8
+
9
+ # :reek:ControlParameter is false positive here
10
+ def to_router(next_step)
11
+ final_routes = @routes.transform_values do |route|
12
+ next route unless route == :next
13
+
14
+ next_step || :end
15
+ end
16
+
17
+ ::Flows::Flow::Router::Custom.new(final_routes)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ EMPTY_ARRAY = [].freeze
4
+
5
+ # @api private
6
+ Step = Struct.new(:name, :body, :router_def, :next_step, keyword_init: true) do
7
+ # :reek:ManualDispatch
8
+ def initialize(name:, body: nil, **rest)
9
+ if name.respond_to?(:call)
10
+ body = name
11
+ name = "#{body}+Step-Object-ID-#{object_id}"
12
+ end
13
+
14
+ super(name: name, body: body, **rest)
15
+ end
16
+
17
+ def to_node(pipeline_class)
18
+ klass = self.class
19
+
20
+ Flows::Flow::Node.new(
21
+ body: body || pipeline_class.method(name),
22
+ router: router_def.to_router(next_step),
23
+ meta: { name: name },
24
+ preprocessor: klass::NODE_PREPROCESSOR,
25
+ postprocessor: klass::NODE_POSTPROCESSOR
26
+ )
27
+ end
28
+ end
29
+
30
+ Step.const_set(
31
+ :NODE_PREPROCESSOR,
32
+ lambda do |_input, context, node_meta|
33
+ context[:class].before_each_callbacks.each do |callback|
34
+ context[:instance].instance_exec(context[:class], node_meta[:name], context[:data], context[:meta], &callback)
35
+ end
36
+
37
+ [EMPTY_ARRAY, context[:data]]
38
+ end
39
+ )
40
+
41
+ Step.const_set(
42
+ :NODE_POSTPROCESSOR,
43
+ lambda do |result, context, node_meta|
44
+ context[:data].merge!(result.instance_variable_get(:@data))
45
+
46
+ context[:class].after_each_callbacks.each do |callback|
47
+ context[:instance]
48
+ .instance_exec(context[:class], node_meta[:name], result, context[:data], context[:meta], &callback)
49
+ end
50
+
51
+ result
52
+ end
53
+ )
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ # @api private
4
+ class Track
5
+ TRACK_ENTRY_ROUTER_DEF = RouterDefinition.new(
6
+ Flows::Result::Ok => :next,
7
+ Flows::Result::Err => :end
8
+ )
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @step_list = []
13
+ end
14
+
15
+ def initialize_dup(_other)
16
+ @step_list = @step_list.map(&:dup)
17
+ end
18
+
19
+ def add_step(step)
20
+ last_step = @step_list.last
21
+ last_step.next_step = step.name if last_step
22
+
23
+ @step_list << step
24
+
25
+ self
26
+ end
27
+
28
+ def first_step_name
29
+ @step_list.first.name
30
+ end
31
+
32
+ def empty?
33
+ @step_list.empty?
34
+ end
35
+
36
+ def to_node_map(method_source)
37
+ @step_list.each_with_object(@name => make_track_entry_node) do |step, node_map|
38
+ node_map[step.name] = step.to_node(method_source)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def make_track_entry_node
45
+ MutationStep.new(
46
+ name: @name,
47
+ body: proc { true },
48
+ router_def: TRACK_ENTRY_ROUTER_DEF,
49
+ next_step: first_step_name
50
+ ).to_node(nil)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ # @api private
4
+ class TrackList
5
+ attr_reader :current_track
6
+
7
+ def initialize
8
+ @tracks = { main: Track.new(:main) }
9
+ @current_track = :main
10
+ end
11
+
12
+ def initialize_dup(_other)
13
+ @tracks = @tracks.transform_values(&:dup)
14
+ end
15
+
16
+ def switch_track(track_name)
17
+ @tracks[track_name] ||= Track.new(track_name)
18
+ @current_track = track_name
19
+ end
20
+
21
+ def add_step(step)
22
+ @tracks[@current_track].add_step(step)
23
+ end
24
+
25
+ def first_step_name
26
+ @tracks[:main].first_step_name
27
+ end
28
+
29
+ def main_track_empty?
30
+ @tracks[:main].empty?
31
+ end
32
+
33
+ def to_node_map(method_source)
34
+ @tracks.reduce({}) do |node_map, (_, track)|
35
+ node_map.merge!(
36
+ track.to_node_map(method_source)
37
+ )
38
+ end
39
+ end
40
+
41
+ def to_flow(method_source)
42
+ raise NoStepsError, method_source if main_track_empty?
43
+
44
+ Flows::Flow.new(
45
+ start_node: first_step_name,
46
+ node_map: to_node_map(method_source)
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,73 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ # @api private
4
+ class Wrap
5
+ attr_reader :router_def, :tracks_definitions
6
+
7
+ # :reek:Attribute
8
+ attr_accessor :next_step
9
+
10
+ EMPTY_HASH = {}.freeze
11
+
12
+ NODE_PREPROCESSOR = lambda do |_input, context, _node_meta|
13
+ [[context], EMPTY_HASH]
14
+ end
15
+
16
+ NODE_POSTPROCESSOR = lambda do |result, context, _node_meta|
17
+ context[:data].merge!(result.instance_variable_get(:@data))
18
+
19
+ result
20
+ end
21
+
22
+ def initialize(method_name:, router_def:, &tracks_definitions)
23
+ @method_name = method_name
24
+ @router_def = router_def
25
+ @tracks_definitions = tracks_definitions
26
+
27
+ singleton_class.extend DSL::Tracks
28
+ singleton_class.extend Result::Helpers
29
+
30
+ singleton_class.instance_exec(&tracks_definitions)
31
+ end
32
+
33
+ # on `#dup` we're getting new empty singleton class
34
+ # so we need to initialize it like original one
35
+ def initialize_dup(other)
36
+ singleton_class.extend DSL::Tracks
37
+ singleton_class.extend Result::Helpers
38
+ singleton_class.instance_exec(&other.tracks_definitions)
39
+ end
40
+
41
+ def name
42
+ singleton_class.tracks.first_step_name
43
+ end
44
+
45
+ def to_node(method_source)
46
+ Flows::Flow::Node.new(
47
+ body: make_body(method_source),
48
+ router: router_def.to_router(next_step),
49
+ meta: { wrap_name: @method_name },
50
+ preprocessor: NODE_PREPROCESSOR,
51
+ postprocessor: NODE_POSTPROCESSOR
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def make_flow(method_source)
58
+ singleton_class.tracks.to_flow(method_source)
59
+ end
60
+
61
+ def make_body(method_source)
62
+ flow = make_flow(method_source)
63
+ wrapper = method_source.method(@method_name)
64
+
65
+ lambda do |context|
66
+ wrapper.call(context[:data], context[:meta]) do
67
+ flow.call(nil, context: context)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ module Flows
2
+ # Namespace for low-level purely technical OOP helpers.
3
+ #
4
+ # Implementations here are relatively complex and require
5
+ # advanced understanding of Ruby's OOP and runtime.
6
+ #
7
+ # This module implements "hidden complexity" approach:
8
+ # hide most non-trivial parts of implementation inside
9
+ # small well-documented classes and modules.
10
+ #
11
+ # @since 0.4.0
12
+ module Util
13
+ end
14
+ end
15
+
16
+ require_relative 'util/prepend_to_class'
17
+ require_relative 'util/inheritable_singleton_vars'
@@ -0,0 +1,86 @@
1
+ module Flows
2
+ module Util
3
+ # Namespace for utility classes which allows you to define specific behaviour for
4
+ # [singleton variables](https://medium.com/@leo_hetsch/demystifying-singleton-classes-in-ruby-caf3fa4c9d91)
5
+ # in the context of inheritance.
6
+ #
7
+ # When you're writing some abstraction in Ruby one of the ways is to provide some base class and
8
+ # allow child classes to configure behaviour through class-level DSL. Something like that:
9
+ #
10
+ # class UserModel < BaseModel
11
+ # field :name
12
+ # field :username
13
+ # end
14
+ #
15
+ # The first problem here is where to store configuration values?
16
+ # In the most cases of such DSL it's singleton variables.
17
+ #
18
+ # But what will happen if we do something like this:
19
+ #
20
+ # class AdminModel < UserModel
21
+ # field :pgp_key
22
+ # end
23
+ #
24
+ # Which fields are defined for admin? `:name`, `:username` and `:pgp_key`?
25
+ # Or `:pgp_key` only? Both options are possible and can be implemented.
26
+ # But working with singleton variables is confusing and related code is confusing also.
27
+ # So, it's better to implement set of utility modules to define expected behaviour
28
+ # in a human-friendly format.
29
+ #
30
+ # The second problem is default values for singleton variables.
31
+ # In case of instance variables everything is simple:
32
+ # you have a constructor (`#initializer`) and it's the right place to set instance variables defaults.
33
+ # In case of singleton variables you can do it in `.extended` or `.included` callbacks.
34
+ # But this callback will not be executed on child classes. So, we have to add `.inherited` callback to the mix.
35
+ # Confusing? Yes. So, it's better to not think about it each time and
36
+ # use some helpers to explicitly define behaviour.
37
+ #
38
+ # Modules under this namespace provide helpers for defining defaults and inheritance strategy for your
39
+ # singleton variables.
40
+ #
41
+ # Each strategy here is using following way of injecting into yours abstract classes:
42
+ #
43
+ # class BaseSomething
44
+ # SingletonVarsSetup = Flows::Util::InheritableSingletonVars::SomeStrategy.make_module(
45
+ # **options_here
46
+ # )
47
+ #
48
+ # include SingeltonVarsSetup # extend also will work
49
+ # end
50
+ #
51
+ # In case of extensions and mixins:
52
+ #
53
+ # module MyExtension
54
+ # SingletonVarsSetup = Flows::Util::InheritableSingletonVars::SomeStrategy.make_module(
55
+ # **options_here
56
+ # )
57
+ #
58
+ # include SingeltonVarsSetup # extend also will work
59
+ # end
60
+ #
61
+ # class SomethingA
62
+ # extend MyExtension
63
+ # end
64
+ #
65
+ # module MyMixin
66
+ # SingletonVarsSetup = Flows::Util::InheritableSingletonVars::SomeStrategy.make_module(
67
+ # **options_here
68
+ # )
69
+ #
70
+ # include SingeltonVarsSetup # extend also will work
71
+ # end
72
+ #
73
+ # class SomethingB
74
+ # include MyMixin
75
+ # end
76
+ #
77
+ # Moreover, you can use multiple strategies in the same class.
78
+ #
79
+ # @since 0.4.0
80
+ module InheritableSingletonVars
81
+ end
82
+ end
83
+ end
84
+
85
+ require_relative './inheritable_singleton_vars/dup_strategy'
86
+ require_relative './inheritable_singleton_vars/isolation_strategy'