flows 0.3.0 → 0.4.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 +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +1 -0
- data/.reek.yml +42 -0
- data/.rubocop.yml +20 -7
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +0 -6
- data/Gemfile.lock +139 -74
- data/README.md +158 -364
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +47 -0
- data/bin/benchmark +73 -105
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +118 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/README.md +2 -186
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +25 -2
- data/forspell.dict +9 -0
- data/lefthook.yml +9 -0
- data/lib/flows.rb +11 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +41 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +25 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +75 -7
- data/lib/flows/flow/node.rb +131 -0
- data/lib/flows/flow/router.rb +25 -0
- data/lib/flows/flow/router/custom.rb +54 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +20 -0
- data/lib/flows/plugin.rb +13 -0
- data/lib/flows/plugin/dependency_injector.rb +159 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/output_contract.rb +84 -0
- data/lib/flows/plugin/output_contract/dsl.rb +36 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -37
- data/lib/flows/railway/dsl.rb +8 -19
- data/lib/flows/railway/errors.rb +8 -12
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +188 -2
- data/lib/flows/result/do.rb +160 -16
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +216 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +46 -0
- data/lib/flows/shared_context_pipeline/track.rb +67 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
- data/lib/flows/util/prepend_to_class.rb +145 -0
- data/lib/flows/version.rb +1 -1
- metadata +233 -37
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -195
- data/bin/profile_10steps +0 -106
- data/bin/ruby_benchmarks +0 -26
- data/docs/CNAME +0 -1
- data/docs/contributing/benchmarks_profiling.md +0 -3
- data/docs/contributing/local_development.md +0 -3
- data/docs/flow/direct_usage.md +0 -3
- data/docs/flow/general_idea.md +0 -3
- data/docs/operation/basic_usage.md +0 -1
- data/docs/operation/inject_steps.md +0 -3
- data/docs/operation/lambda_steps.md +0 -3
- data/docs/operation/result_shapes.md +0 -3
- data/docs/operation/routing_tracks.md +0 -3
- data/docs/operation/wrapping_steps.md +0 -3
- data/docs/overview/performance.md +0 -336
- data/docs/railway/basic_usage.md +0 -232
- data/docs/result_objects/basic_usage.md +0 -196
- data/docs/result_objects/do_notation.md +0 -139
- data/lib/flows/implicit_build.rb +0 -16
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -55
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
@@ -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,46 @@
|
|
1
|
+
module Flows
|
2
|
+
class SharedContextPipeline
|
3
|
+
EMPTY_ARRAY = [].freeze
|
4
|
+
|
5
|
+
# @api private
|
6
|
+
Step = Struct.new(:name, :lambda, :router_def, :next_step, keyword_init: true) do
|
7
|
+
def to_node(pipeline_class)
|
8
|
+
klass = self.class
|
9
|
+
|
10
|
+
Flows::Flow::Node.new(
|
11
|
+
body: lambda || pipeline_class.method(name),
|
12
|
+
router: router_def.to_router(next_step),
|
13
|
+
meta: { name: name },
|
14
|
+
preprocessor: klass::NODE_PREPROCESSOR,
|
15
|
+
postprocessor: klass::NODE_POSTPROCESSOR
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Step.const_set(
|
21
|
+
:NODE_PREPROCESSOR,
|
22
|
+
lambda do |_input, context, meta|
|
23
|
+
context[:last_step] = meta[:name]
|
24
|
+
|
25
|
+
context[:class].before_each_callbacks.each do |callback|
|
26
|
+
callback.call(context[:class], meta[:name], context[:data])
|
27
|
+
end
|
28
|
+
|
29
|
+
[EMPTY_ARRAY, context[:data]]
|
30
|
+
end
|
31
|
+
)
|
32
|
+
|
33
|
+
Step.const_set(
|
34
|
+
:NODE_POSTPROCESSOR,
|
35
|
+
lambda do |output, context, meta|
|
36
|
+
context[:data].merge!(output.instance_variable_get(:@data))
|
37
|
+
|
38
|
+
context[:class].after_each_callbacks.each do |callback|
|
39
|
+
callback.call(context[:class], meta[:name], context[:data], output)
|
40
|
+
end
|
41
|
+
|
42
|
+
output
|
43
|
+
end
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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(name:, lambda: nil, router_def:)
|
20
|
+
step = Step.new(name: name, lambda: lambda, router_def: router_def)
|
21
|
+
|
22
|
+
last_step = @step_list.last
|
23
|
+
last_step.next_step = name if last_step
|
24
|
+
|
25
|
+
@step_list << step
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_mutation_step(name:, lambda: nil, router_def:)
|
31
|
+
step = MutationStep.new(name: name, lambda: lambda, router_def: router_def)
|
32
|
+
|
33
|
+
last_step = @step_list.last
|
34
|
+
last_step.next_step = name if last_step
|
35
|
+
|
36
|
+
@step_list << step
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def first_step_name
|
42
|
+
@step_list.first.name
|
43
|
+
end
|
44
|
+
|
45
|
+
def empty?
|
46
|
+
@step_list.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_node_map(method_source)
|
50
|
+
@step_list.each_with_object(@name => make_track_entry_node) do |step, node_map|
|
51
|
+
node_map[step.name] = step.to_node(method_source)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def make_track_entry_node
|
58
|
+
MutationStep.new(
|
59
|
+
name: @name,
|
60
|
+
lambda: proc { true },
|
61
|
+
router_def: TRACK_ENTRY_ROUTER_DEF,
|
62
|
+
next_step: first_step_name
|
63
|
+
).to_node(nil)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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(name:, lambda:, router_def:)
|
22
|
+
@tracks[@current_track].add_step(name: name, lambda: lambda, router_def: router_def)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_mutation_step(name:, lambda:, router_def:)
|
26
|
+
@tracks[@current_track].add_mutation_step(name: name, lambda: lambda, router_def: router_def)
|
27
|
+
end
|
28
|
+
|
29
|
+
def first_step_name
|
30
|
+
@tracks[:main].first_step_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def main_track_empty?
|
34
|
+
@tracks[:main].empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_node_map(method_source)
|
38
|
+
@tracks.reduce({}) do |node_map, (_, track)|
|
39
|
+
node_map.merge!(
|
40
|
+
track.to_node_map(method_source)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/flows/util.rb
ADDED
@@ -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,79 @@
|
|
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
|
+
# Flows::Util::InheritableSingletonVars::SomeStrategy.call(
|
45
|
+
# self,
|
46
|
+
# **rest_of_the_options_here
|
47
|
+
# )
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# In case of extensions and mixins:
|
51
|
+
#
|
52
|
+
# module MyExtension
|
53
|
+
# def self.extended(mod)
|
54
|
+
# Flows::Util::InheritableSingletonVars::SomeStrategy.call(
|
55
|
+
# mod,
|
56
|
+
# **rest_of_the_options_here
|
57
|
+
# )
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# module MyMixin
|
62
|
+
# def self.included(mod)
|
63
|
+
# Flows::Util::InheritableSingletonVars::SomeStrategy.call(
|
64
|
+
# mod,
|
65
|
+
# **rest_of_the_options_here
|
66
|
+
# )
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# Moreover, you can use multiple strategies in the same class.
|
71
|
+
#
|
72
|
+
# @since 0.4.0
|
73
|
+
module InheritableSingletonVars
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
require_relative './inheritable_singleton_vars/dup_strategy'
|
79
|
+
require_relative './inheritable_singleton_vars/isolation_strategy'
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Flows
|
2
|
+
module Util
|
3
|
+
module InheritableSingletonVars
|
4
|
+
# Strategy which uses `#dup` to copy variables to a child class.
|
5
|
+
#
|
6
|
+
# Can be applied several times to the same class.
|
7
|
+
#
|
8
|
+
# Can be applied in the middle of inheritance chain.
|
9
|
+
#
|
10
|
+
# When your value is a custom class you may need to adjust `#dup` behaviour.
|
11
|
+
# It can be done using `initialize_dup` method.
|
12
|
+
# Unfortunately it's not documented well in the standard library.
|
13
|
+
# So, [this will help you](https://blog.appsignal.com/2019/02/26/diving-into-dup-and-clone.html).
|
14
|
+
#
|
15
|
+
# @note If you change variables in a parent class after a child being defined
|
16
|
+
# it will have no effect on a child. Remember this when working in environments
|
17
|
+
# with tricky or experimental autoload mechanism.
|
18
|
+
#
|
19
|
+
# @see InheritableSingletonVars the parent module's documentation describes the problem this module solves.
|
20
|
+
#
|
21
|
+
# @since 0.4.0
|
22
|
+
module DupStrategy
|
23
|
+
VAR_LIST_VAR_NAME = :@inheritable_vars_with_dup
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
module InheritanceCallback
|
27
|
+
def inherited(child_class)
|
28
|
+
DupStrategy.migrate(self, child_class)
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def included(child_mod)
|
34
|
+
DupStrategy.migrate(self, child_mod)
|
35
|
+
|
36
|
+
child_mod.singleton_class.prepend(InheritanceCallback)
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def extended(child_mod)
|
42
|
+
DupStrategy.migrate(self, child_mod)
|
43
|
+
|
44
|
+
child_mod.singleton_class.prepend(InheritanceCallback)
|
45
|
+
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
# Applies behaviour and defaults for singleton variables.
|
52
|
+
#
|
53
|
+
# @note Variable names should look like `:@var` or `'@var'`.
|
54
|
+
#
|
55
|
+
# @param klass [Class] target class.
|
56
|
+
# @param attrs_with_default [Hash<Symbol, String => Object>] keys are variable names,
|
57
|
+
# values are default values.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# class MyClass
|
61
|
+
# Flows::Util::InheritableSingletonVars::DupStrategy.call(
|
62
|
+
# self,
|
63
|
+
# :@my_list => []
|
64
|
+
# )
|
65
|
+
# end
|
66
|
+
def call(klass, attrs_with_default = {})
|
67
|
+
init_variables_with_default_values(klass, attrs_with_default)
|
68
|
+
|
69
|
+
var_names = attrs_with_default.keys.map(&:to_sym)
|
70
|
+
add_var_list(klass, var_names)
|
71
|
+
|
72
|
+
inject_inheritance_hook(klass)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Moves variables between modules
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
def migrate(from_mod, to_mod)
|
79
|
+
var_list = from_mod.instance_variable_get(VAR_LIST_VAR_NAME)
|
80
|
+
to_mod.instance_variable_set(VAR_LIST_VAR_NAME, var_list.dup)
|
81
|
+
|
82
|
+
var_list.each do |name|
|
83
|
+
to_mod.instance_variable_set(name, from_mod.instance_variable_get(name).dup)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def init_variables_with_default_values(klass, attrs_with_default)
|
90
|
+
attrs_with_default.each do |name, default_value|
|
91
|
+
klass.instance_variable_set(name, default_value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_var_list(klass, var_names)
|
96
|
+
watch_list = klass.instance_variable_get(VAR_LIST_VAR_NAME) || []
|
97
|
+
watch_list.concat(var_names)
|
98
|
+
klass.instance_variable_set(VAR_LIST_VAR_NAME, watch_list)
|
99
|
+
end
|
100
|
+
|
101
|
+
def inject_inheritance_hook(klass)
|
102
|
+
singleton = klass.singleton_class
|
103
|
+
singleton.prepend(InheritanceCallback) unless singleton.is_a?(InheritanceCallback)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Flows
|
2
|
+
module Util
|
3
|
+
module InheritableSingletonVars
|
4
|
+
# Strategy which uses procs to generate initial values in target class and children.
|
5
|
+
#
|
6
|
+
# This strategy designed to make fully isolated singleton variables between classes.
|
7
|
+
#
|
8
|
+
# Can be applied several times to the same class.
|
9
|
+
#
|
10
|
+
# Can be applied in the middle of inheritance chain.
|
11
|
+
#
|
12
|
+
# @see InheritableSingletonVars the parent module's documentation describes the problem this module solves.
|
13
|
+
#
|
14
|
+
# @since 0.4.0
|
15
|
+
module IsolationStrategy
|
16
|
+
VAR_MAP_VAR_NAME = :@inheritable_vars_with_isolation
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
module InheritanceCallback
|
20
|
+
def inherited(child_class)
|
21
|
+
IsolationStrategy.migrate(self, child_class)
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def included(child_mod)
|
27
|
+
IsolationStrategy.migrate(self, child_mod)
|
28
|
+
|
29
|
+
child_mod.singleton_class.prepend InheritanceCallback
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def extended(child_mod)
|
35
|
+
IsolationStrategy.migrate(self, child_mod)
|
36
|
+
|
37
|
+
child_mod.singleton_class.prepend InheritanceCallback
|
38
|
+
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class << self
|
44
|
+
# Applies behaviour and defaults for singleton variables.
|
45
|
+
#
|
46
|
+
# @note Variable names should look like `:@var` or `'@var'`.
|
47
|
+
#
|
48
|
+
# @param klass [Class] target class.
|
49
|
+
# @param attrs_with_default [Hash<Symbol, String => Proc>] keys are variable names,
|
50
|
+
# values are procs or lambdas which return default values.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# class MyClass
|
54
|
+
# Flows::Util::InheritableSingletonVars::IsolationStrategy.call(
|
55
|
+
# self,
|
56
|
+
# :@my_list => -> { [] }
|
57
|
+
# )
|
58
|
+
# end
|
59
|
+
def call(klass, attrs_with_default = {})
|
60
|
+
init_variables_with_default_values(klass, attrs_with_default)
|
61
|
+
|
62
|
+
var_defaults = attrs_with_default
|
63
|
+
add_variables_to_store(klass, var_defaults)
|
64
|
+
|
65
|
+
inject_inheritance_hook(klass)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Moves variables between modules
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
def migrate(from_mod, to_mod)
|
72
|
+
new_var_map = from_mod.instance_variable_get(VAR_MAP_VAR_NAME).dup
|
73
|
+
|
74
|
+
to_mod.instance_variable_set(VAR_MAP_VAR_NAME, new_var_map)
|
75
|
+
|
76
|
+
new_var_map.each do |name, default_value_proc|
|
77
|
+
to_mod.instance_variable_set(name, default_value_proc.call)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def init_variables_with_default_values(klass, attrs_with_default)
|
84
|
+
attrs_with_default.each do |name, default_value_proc|
|
85
|
+
klass.instance_variable_set(name, default_value_proc.call)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_variables_to_store(klass, var_defaults)
|
90
|
+
store = klass.instance_variable_get(VAR_MAP_VAR_NAME) || {}
|
91
|
+
next_store = store.merge(var_defaults)
|
92
|
+
|
93
|
+
klass.instance_variable_set(VAR_MAP_VAR_NAME, next_store)
|
94
|
+
end
|
95
|
+
|
96
|
+
def inject_inheritance_hook(klass)
|
97
|
+
singleton = klass.singleton_class
|
98
|
+
singleton.prepend(InheritanceCallback) unless singleton.is_a?(InheritanceCallback)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|