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,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