flows 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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,25 @@
1
+ module Flows
2
+ class Flow
3
+ class Router
4
+ # Router with static paths for successful and failure results.
5
+ class Simple < Router
6
+ # @param success_route [Symbol] route for any successful results.
7
+ # @param failure_route [Symbol] route for any failure results.
8
+ def initialize(success_route, failure_route)
9
+ @success_route = success_route
10
+ @failure_route = failure_route
11
+ end
12
+
13
+ # @see Flows::Flow::Router#call
14
+ def call(result)
15
+ result.ok? ? @success_route : @failure_route
16
+ end
17
+
18
+ # @see Flows::Flow::Router#destinations
19
+ def destinations
20
+ [@success_route, @failure_route]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ module Flows
2
+ # Namespace for class behaviour extensions.
3
+ #
4
+ # Feel free to use it to empower your abstractions.
5
+ #
6
+ # @since 0.4.0
7
+ module Plugin
8
+ end
9
+ end
10
+
11
+ require_relative 'plugin/dependency_injector'
12
+ require_relative 'plugin/implicit_init'
13
+ require_relative 'plugin/output_contract'
14
+ require_relative 'plugin/profiler'
15
+ require_relative 'plugin/interface'
@@ -0,0 +1,170 @@
1
+ require_relative 'dependency_injector/errors'
2
+ require_relative 'dependency_injector/dependency'
3
+ require_relative 'dependency_injector/dependency_definition'
4
+ require_relative 'dependency_injector/dependency_list'
5
+
6
+ module Flows
7
+ module Plugin
8
+ # Allows to inject dependencies on the initialization step.
9
+ #
10
+ # After including this module you inject dependencies by providing `:dependencies` key
11
+ # to your initializer:
12
+ #
13
+ # x = MyClass.new(dependencies: { my_dep: -> { 'Hi' } })
14
+ # x.my_dep
15
+ # # => 'Hi'
16
+ #
17
+ # Keys are dependency names. Dependency will be injected as
18
+ # a public method with dependency name. Values are dependencies itself.
19
+ #
20
+ # You can also require some dependencies to be present.
21
+ # If required dependency is missed - {MissingDependencyError} will be raised.
22
+ #
23
+ # If an optional dependency has no default - {MissingDependencyDefaultError} will be raised.
24
+ #
25
+ # For an optional dependency default value must be provided.
26
+ #
27
+ # You can provide a type for the dependency.
28
+ # Type check uses case equality (`===`).
29
+ # So, it works like Ruby's `case`.
30
+ # In case of type mismatch {UnexpectedDependencyTypeError} will be raised.
31
+ #
32
+ # dependency :name, type: String # name should be a string
33
+ #
34
+ # # by the way, you can use lambdas like in Ruby's `case`
35
+ # dependency :age, type: ->(x) { x.is_a?(Number) && x > 0 && x < 100 }
36
+ #
37
+ # If you're trying to inject undeclared dependency - {UnexpectedDependencyError} will be raised.
38
+ #
39
+ # Inheritance is supported and dependency definitions will be inherited into child classes.
40
+ #
41
+ # @example
42
+ #
43
+ # class MyClass
44
+ # include Flows::Plugin::DependencyInjector
45
+ #
46
+ # dependency :logger, required: true
47
+ # dependency :name, default: 'Boris', type: String # by default dependency is optional.
48
+ #
49
+ # attr_reader :data
50
+ #
51
+ # def initializer(data)
52
+ # @data = data
53
+ # end
54
+ #
55
+ # def log_the_name
56
+ # logger.call(name)
57
+ # end
58
+ # end
59
+ #
60
+ # class Logger
61
+ # def self.call(msg)
62
+ # puts msg
63
+ # end
64
+ # end
65
+ #
66
+ # x = MyClass.new('DATA', dependencies: {
67
+ # logger: Logger
68
+ # })
69
+ #
70
+ # x.data
71
+ # # => 'DATA'
72
+ #
73
+ # x.name
74
+ # # => 'Boris'
75
+ #
76
+ # x.logger.call('Hello')
77
+ # # prints 'Hello'
78
+ #
79
+ # x.log_the_name
80
+ # # prints 'Boris'
81
+ module DependencyInjector
82
+ # Placeholder for empty type. We cannot use `nil` because value can be `nil`.
83
+ NO_TYPE = :__no_type__
84
+
85
+ # Placeholder for empty default. We cannot use `nil` because value can be `nil`.
86
+ NO_DEFAULT = :__no_default__
87
+
88
+ # Placeholder for empty value. We cannot use `nil` because value can be `nil`.
89
+ NO_VALUE = :__no_value__
90
+
91
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
92
+ '@dependencies' => {}
93
+ )
94
+
95
+ include SingletonVarsSetup
96
+
97
+ # @api private
98
+ module DSL
99
+ attr_reader :dependencies
100
+
101
+ # `:reek:BooleanParameter` disabled here because it's not applicable for DSLs
102
+ def dependency(name, required: false, default: NO_DEFAULT, type: NO_TYPE)
103
+ dependencies[name] = DependencyDefinition.new(
104
+ name: name,
105
+ required: required,
106
+ default: default,
107
+ type: type,
108
+ klass: self
109
+ )
110
+ end
111
+ end
112
+
113
+ # @api private
114
+ #
115
+ # `:reek:UtilityFunction` and `:reek:FeatureEnvy` are disabled here because Reek does not
116
+ # know about inheritance callback stuff.
117
+ module InheritanceCallback
118
+ def included(mod)
119
+ mod.extend(DSL)
120
+
121
+ mod.singleton_class.prepend(InheritanceCallback) if mod.class == Module
122
+
123
+ super
124
+ end
125
+
126
+ def extended(mod)
127
+ mod.extend(DSL)
128
+
129
+ mod.singleton_class.prepend(InheritanceCallback) if mod.class == Module
130
+
131
+ super
132
+ end
133
+ end
134
+
135
+ singleton_class.prepend InheritanceCallback
136
+
137
+ InitializerWrapper = Util::PrependToClass.make_module do
138
+ def initialize(*args, **kwargs, &block) # rubocop:disable Metrics/MethodLength
139
+ if @__dependencies_injected__
140
+ if kwargs.empty? # https://bugs.ruby-lang.org/issues/14415
141
+ super(*args, &block)
142
+ else
143
+ super(*args, **kwargs, &block)
144
+ end
145
+
146
+ return
147
+ end
148
+ @__dependencies_injected__ = true
149
+
150
+ klass = self.class
151
+ DependencyList.new(
152
+ klass: klass,
153
+ definitions: klass.dependencies,
154
+ provided_values: kwargs[:dependencies].dup || {}
155
+ ).inject_to(self)
156
+
157
+ filtered_kwargs = kwargs.reject { |key, _| key == :dependencies }
158
+
159
+ if filtered_kwargs.empty? # https://bugs.ruby-lang.org/issues/14415
160
+ super(*args, &block)
161
+ else
162
+ super(*args, **filtered_kwargs, &block)
163
+ end
164
+ end
165
+ end
166
+
167
+ include InitializerWrapper
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,24 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Resolves dependency on initialization and can inject it into class instance.
5
+ #
6
+ # @api private
7
+ Dependency = Struct.new(:name, :definition, :provided_value, :value, :klass, keyword_init: true) do
8
+ def initialize(*)
9
+ super
10
+
11
+ self.value = provided_value == NO_VALUE ? definition.default : provided_value
12
+ type = definition.type
13
+
14
+ raise UnexpectedDependencyTypeError.new(klass, name, value, type) if type != NO_TYPE && !(type === value) # rubocop:disable Style/CaseEquality
15
+ end
16
+
17
+ def inject_to(instance)
18
+ value = self.value
19
+ instance.define_singleton_method(name) { value }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Struct for storing dependency definitions.
5
+ #
6
+ # @api private
7
+ DependencyDefinition = Struct.new(:name, :required, :default, :type, :klass, keyword_init: true) do
8
+ def initialize(*)
9
+ super
10
+
11
+ raise MissingDependencyDefaultError.new(klass, name) if !required && (default == NO_DEFAULT)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,55 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Resolves dependencies on initialization and can inject it into class instance.
5
+ #
6
+ # @api private
7
+ class DependencyList
8
+ attr_reader :definitions, :provided_values, :dependencies
9
+
10
+ def initialize(klass:, definitions:, provided_values:)
11
+ @klass = klass
12
+ @definitions = definitions
13
+ @provided_values = provided_values.dup.tap { |pv| pv.default = NO_VALUE }
14
+
15
+ check_missing_dependencies
16
+ check_unexpected_dependencies
17
+ resolve_dependencies
18
+ end
19
+
20
+ def inject_to(instance)
21
+ dependencies.each { |dep| dep.inject_to(instance) }
22
+ end
23
+
24
+ private
25
+
26
+ def required_dependencies
27
+ definitions.select { |_, definition| definition.required }.keys
28
+ end
29
+
30
+ def check_missing_dependencies
31
+ missing = required_dependencies - provided_values.keys
32
+
33
+ raise MissingDependencyError.new(@klass, missing) if missing.any?
34
+ end
35
+
36
+ def check_unexpected_dependencies
37
+ unexpected = provided_values.keys - definitions.keys
38
+
39
+ raise UnexpectedDependencyError.new(@klass, unexpected) if unexpected.any?
40
+ end
41
+
42
+ def resolve_dependencies
43
+ @dependencies = definitions.map do |name, definition|
44
+ Dependency.new(
45
+ klass: @klass,
46
+ name: name,
47
+ definition: definition,
48
+ provided_value: provided_values[name]
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,58 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Base error class for dependency injection errors.
5
+ class Error < ::Flows::Error; end
6
+
7
+ # Raised when you're missed some dependency.
8
+ class MissingDependencyError < Error
9
+ def initialize(klass, names)
10
+ @klass = klass
11
+ @names = names
12
+ end
13
+
14
+ def message
15
+ "Missing dependency(ies) for #{@klass}: #{@names.map(&:to_s).join(', ')}"
16
+ end
17
+ end
18
+
19
+ # Raised when you're providing undeclared dependency.
20
+ class UnexpectedDependencyError < Error
21
+ def initialize(klass, names)
22
+ @klass = klass
23
+ @names = names
24
+ end
25
+
26
+ def message
27
+ "Unexpected dependency(ies) for #{@klass}: #{@names.map(&:to_s).join(', ')}"
28
+ end
29
+ end
30
+
31
+ # Raised when dependency has unexpected type.
32
+ class UnexpectedDependencyTypeError < Error
33
+ def initialize(klass, name, value, type)
34
+ @klass = klass
35
+ @_name = name
36
+ @value = value
37
+ @_type = type
38
+ end
39
+
40
+ def message
41
+ "#{@_name} dependency for #{@klass} has wrong type, must conform `#{@_type.inspect}`: `#{@value.inspect}`"
42
+ end
43
+ end
44
+
45
+ # Raised when an optional dependency has no default value.
46
+ class MissingDependencyDefaultError < Error
47
+ def initialize(klass, name)
48
+ @klass = klass
49
+ @_name = name
50
+ end
51
+
52
+ def message
53
+ "Optional dependency #{@_name} for #{@klass} has no default value"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,45 @@
1
+ module Flows
2
+ module Plugin
3
+ # Class extension with method `MyClass.call` which works like `MyClass.new.call`.
4
+ #
5
+ # @note This module must be injected into target class using `extend`, not `include`.
6
+ #
7
+ # @note Class inheritance is supported: each child class will inherit behaviour, but not data.
8
+ #
9
+ # @example Extending a class
10
+ # class SomeClass
11
+ # extend Flows::Plugin::ImplicitInit
12
+ #
13
+ # def initialize(param: 'default')
14
+ # @param = param
15
+ # end
16
+ #
17
+ # def call
18
+ # @param
19
+ # end
20
+ # end
21
+ #
22
+ # SomeClass.call
23
+ # # => 'default'
24
+ #
25
+ # SomeClass.default_instance.call
26
+ # # => 'default'
27
+ # @since 0.4.0
28
+ module ImplicitInit
29
+ # Contains memoized instance of a host class or `nil`.
30
+ attr_reader :default_instance
31
+
32
+ # Creates an instance of a host class by calling `new` without arguments and
33
+ # calls `#call` method on the instance with provided parameters and block.
34
+ #
35
+ # After first invocation the instance will be memoized in {.default_instance}.
36
+ #
37
+ # Child classes have separate default instances.
38
+ def call(*args, **kwargs, &block)
39
+ @default_instance ||= new
40
+
41
+ default_instance.call(*args, **kwargs, &block)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,84 @@
1
+ module Flows
2
+ module Plugin
3
+ # Class extension to define Java/C#-like interfaces in Ruby.
4
+ #
5
+ # On target class initialization will check defined methods for existence.
6
+ #
7
+ # **Currently interface composition is not supported.** You cannot define
8
+ # 2 interface modules and include it into one class.
9
+ #
10
+ # @example Simple interface
11
+ # class MyAction
12
+ # extend Flows::Plugin::Interface
13
+ #
14
+ # defmethod :perform
15
+ # end
16
+ #
17
+ # class InvalidAction < MyAction; end
18
+ # InvalidAction.new
19
+ # # will raise an error
20
+ #
21
+ # class ValidAction < MyAction
22
+ # def perfrom
23
+ # puts 'Hello!'
24
+ # end
25
+ # end
26
+ # ValidAction.new.perform
27
+ # # => Hello!
28
+ #
29
+ # @example Interface as module
30
+ # module MyBehavior
31
+ # extend Flows::Plugin::Interface
32
+ #
33
+ # defmethod :my_method
34
+ # end
35
+ #
36
+ # class MyImplementation
37
+ # include MyBehaviour
38
+ #
39
+ # def my_method; end
40
+ # end
41
+ module Interface
42
+ # Base error class for interface errors.
43
+ class Error < ::Flows::Error; end
44
+
45
+ # Raised when you're missed some dependency.
46
+ class MissingMethodsError < Error
47
+ def initialize(klass, names)
48
+ @klass = klass
49
+ @names = names
50
+ end
51
+
52
+ def message
53
+ "Methods required by interface for #{@klass} are missing: #{@names.map(&:to_s).join(', ')}"
54
+ end
55
+ end
56
+
57
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
58
+ '@interface_methods' => {}
59
+ )
60
+
61
+ include SingletonVarsSetup
62
+
63
+ InitializePatch = Flows::Util::PrependToClass.make_module do
64
+ def initialize(*)
65
+ klass = self.class
66
+
67
+ required_methods = klass.instance_variable_get(:@interface_methods).keys
68
+ missing_methods = required_methods - methods
69
+
70
+ raise MissingMethodsError.new(klass, missing_methods) if missing_methods.any?
71
+
72
+ super
73
+ end
74
+ end
75
+
76
+ include InitializePatch
77
+
78
+ def defmethod(method_name)
79
+ method_list = instance_variable_get(:@interface_methods)
80
+ method_list[method_name.to_sym] = { required_by: self }
81
+ end
82
+ end
83
+ end
84
+ end