flows 0.4.0 → 0.5.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.mdlrc +1 -1
  3. data/.reek.yml +12 -0
  4. data/CHANGELOG.md +16 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +12 -2
  7. data/Rakefile +1 -1
  8. data/bin/all_the_errors +8 -0
  9. data/bin/errors +12 -0
  10. data/bin/errors_cli/flow_error_demo.rb +22 -0
  11. data/docs/README.md +1 -1
  12. data/lib/flows/contract/case_eq.rb +3 -1
  13. data/lib/flows/flow/errors.rb +29 -0
  14. data/lib/flows/flow/node.rb +1 -0
  15. data/lib/flows/flow/router/custom.rb +5 -0
  16. data/lib/flows/flow/router/simple.rb +5 -0
  17. data/lib/flows/flow/router.rb +4 -0
  18. data/lib/flows/flow.rb +21 -0
  19. data/lib/flows/plugin/dependency_injector.rb +5 -5
  20. data/lib/flows/plugin/output_contract/dsl.rb +15 -3
  21. data/lib/flows/plugin/output_contract/wrapper.rb +14 -12
  22. data/lib/flows/plugin/output_contract.rb +1 -0
  23. data/lib/flows/plugin/profiler/injector.rb +35 -0
  24. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  25. data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
  26. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  27. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  28. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  29. data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
  30. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  31. data/lib/flows/plugin/profiler/report.rb +48 -0
  32. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  33. data/lib/flows/plugin/profiler.rb +114 -0
  34. data/lib/flows/plugin.rb +1 -0
  35. data/lib/flows/railway/dsl.rb +3 -2
  36. data/lib/flows/result/do.rb +6 -8
  37. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
  38. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  39. data/lib/flows/shared_context_pipeline/dsl.rb +5 -56
  40. data/lib/flows/shared_context_pipeline/mutation_step.rb +6 -8
  41. data/lib/flows/shared_context_pipeline/step.rb +6 -8
  42. data/lib/flows/shared_context_pipeline/track.rb +2 -15
  43. data/lib/flows/shared_context_pipeline/track_list.rb +11 -6
  44. data/lib/flows/shared_context_pipeline/wrap.rb +64 -0
  45. data/lib/flows/shared_context_pipeline.rb +109 -26
  46. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +40 -51
  47. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +39 -52
  48. data/lib/flows/util/inheritable_singleton_vars.rb +22 -15
  49. data/lib/flows/util/prepend_to_class.rb +43 -9
  50. data/lib/flows/version.rb +1 -1
  51. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3408a7cfa09415262d76e3cdc1d2d28529103928bbbc379dc0c02a14b1f25eee
4
- data.tar.gz: 55b10a5f99d6c0b33185a9e0ba193ab39abd3f43ae88b39b5cb7b48c30fbe23d
3
+ metadata.gz: 8910a47890a3e15f9a79dbb3f7e6255f57cdf2232b9257e05dbdfd39b252d4f7
4
+ data.tar.gz: bb71db93b1128f3c9f7a7dfb5e705c4ec4e6bd3615a19479bb5f68ba97d63b05
5
5
  SHA512:
6
- metadata.gz: 23ef64c88317d77233cf9eb837d3318a73523fd8ac270ec7166eb42c57b0310a015af887bab0018423290eba49f7fb9d735b4c079940bbb1c2cb394857155779
7
- data.tar.gz: fa7baaa93dc8836ab470e873624feaa15a3a6a29a25e483d2efe2b34e228bb2ad88c1c31ed0905c5635aa51b8cb9760fcd76bc2fdc62806ff61ff72eba737e6d
6
+ metadata.gz: ea2894cb2a0472d84968d89c8f7ea3d0013e89fa1d7fc0864ba0cec223683c29950e24cff43b161204da0aa3aadf3969eb848539e828471de82a6b4beef500f1
7
+ data.tar.gz: b12a300213344b4baab7a0f112931018a3a6f74bfee143687111261d692478bd6f2edf1436fb4f8819aae0afca4d4172d194b22e672a1cf9b3838d1b2b749002
data/.mdlrc CHANGED
@@ -1 +1 @@
1
- rules "~MD013", "~MD033"
1
+ rules "~MD013", "~MD033", "~MD024"
data/.reek.yml CHANGED
@@ -22,6 +22,7 @@ detectors:
22
22
  DataClump:
23
23
  exclude:
24
24
  - Flows::Result::Helpers
25
+ - Flows::SharedContextPipeline::DSL::Tracks
25
26
  IrresponsibleModule:
26
27
  exclude:
27
28
  - Flows::SharedContextPipeline
@@ -29,14 +30,25 @@ detectors:
29
30
  exclude:
30
31
  - Flows::SharedContextPipeline#call
31
32
  - Flows::Contract # too many false positives here
33
+ - Flows::Plugin::Profiler::Report#events
32
34
  TooManyStatements:
33
35
  exclude:
34
36
  - Flows::SharedContextPipeline#call
37
+ - Flows::Plugin::Profiler::Wrapper#make_instance_wrapper
38
+ - Flows::Plugin::Profiler::Wrapper#make_singleton_wrapper
35
39
  - initialize
36
40
  DuplicateMethodCall:
37
41
  exclude:
38
42
  - Flows::SharedContextPipeline#call
43
+ - Flows::Plugin::Profiler::Wrapper#make_instance_wrapper
44
+ - Flows::Plugin::Profiler::Wrapper#make_singleton_wrapper
39
45
  - 'length'
40
46
  MissingSafeMethod:
41
47
  exclude:
42
48
  - Flows::Contract
49
+ BooleanParameter:
50
+ exclude:
51
+ - Flows::Plugin::OutputContract::DSL
52
+ TooManyMethods:
53
+ exclude:
54
+ - Flows::Plugin::Profiler::Report::Tree::Node
data/CHANGELOG.md CHANGED
@@ -16,6 +16,21 @@ Types of changes:
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.5.0] - 2020-05-18
20
+
21
+ ### Added
22
+
23
+ * `Flows::SharedContextPipeline` wrap DSL, [issue](https://github.com/ffloyd/flows/issues/7)
24
+ * `Flows::Flow` routing integrity check on initialization
25
+ * `Flows::Plugin::OutputContract` skip contract DSL method
26
+ * `Flows::Plugin::Profiler` introduced. Report types: raw, tree and flat.
27
+
28
+ ### Changed
29
+
30
+ * `Flows::SharedContextPipeline` callback API, [issue](https://github.com/ffloyd/flows/issues/6)
31
+ * `Flows::Util` modules API, [issue](https://github.com/ffloyd/flows/issues/11)
32
+ * `Flows::Contract::CaseEq` default error expanded to present more context
33
+
19
34
  ## [0.4.0] - 2020-04-21
20
35
 
21
36
  ### Added
@@ -39,4 +54,5 @@ Types of changes:
39
54
  target module did not included directly into class.
40
55
 
41
56
  [unreleased]: https://github.com/ffloyd/flows/compare/v0.4.0...HEAD
57
+ [0.5.0]: https://github.com/ffloyd/flows/compare/v0.4.0...v0.5.0
42
58
  [0.4.0]: https://github.com/ffloyd/flows/compare/v0.3.0...v0.4.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flows (0.4.0)
4
+ flows (0.5.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Flows
2
2
 
3
- [![Build Status](https://github.com/ffloyd/flows/workflows/Build/badge.svg)](https://github.com/ffloyd/flows/actions)
3
+ [![Build Status](https://github.com/ffloyd/flows/workflows/Test/badge.svg)](https://github.com/ffloyd/flows/actions)
4
4
  [![codecov](https://codecov.io/gh/ffloyd/flows/branch/master/graph/badge.svg)](https://codecov.io/gh/ffloyd/flows)
5
5
  [![Gem Version](https://badge.fury.io/rb/flows.svg)](https://badge.fury.io/rb/flows)
6
6
 
@@ -19,7 +19,7 @@ Also `flows` is faster than Ruby's alternatives.
19
19
  Add this line to your application's Gemfile:
20
20
 
21
21
  ```ruby
22
- gem 'flows', '~> 0.4'
22
+ gem 'flows', '~> 0.5'
23
23
  ```
24
24
 
25
25
  And then execute:
@@ -251,6 +251,16 @@ version. _No exceptions._
251
251
 
252
252
  Commit with a version bump should contain _only_ version bump and CHANGELOG.md update.
253
253
 
254
+ ### GitHub Flow
255
+
256
+ Since `v0.4.0` this repo strictly follow [GitHub
257
+ Flow](https://guides.github.com/introduction/flow/) with some additions:
258
+
259
+ * branch naming using dash: `improved-contexts`
260
+ * use [references to
261
+ issues](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
262
+ in commit messages and make links to issues in CHANGELOG.md
263
+
254
264
  ### Planned features for v1.0.0
255
265
 
256
266
  * validation framework
data/Rakefile CHANGED
@@ -14,7 +14,7 @@ Reek::Rake::Task.new
14
14
  Inch::Rake::Suggest.new
15
15
 
16
16
  PATHS_TO_SPELLCHECK = ['.'].freeze
17
- PATHS_FOR_MDL = ['README.md', Dir.glob('docs/**/*.md')].flatten.freeze
17
+ PATHS_FOR_MDL = ['README.md', 'CHANGELOG.md', Dir.glob('docs/**/*.md')].flatten.freeze
18
18
 
19
19
  desc 'Run self spellchecking'
20
20
  task :spellcheck do |_task|
data/bin/all_the_errors CHANGED
@@ -45,3 +45,11 @@ echo "Shared Context Pipeline"
45
45
  echo "---------------------------"
46
46
  "${BASH_SOURCE%/*}/errors" scp no_steps
47
47
  "${BASH_SOURCE%/*}/errors" scp no_step_impl
48
+
49
+ echo
50
+ echo
51
+ echo "---------------------------"
52
+ echo "Flow"
53
+ echo "---------------------------"
54
+ "${BASH_SOURCE%/*}/errors" flow invalid_node_route
55
+ "${BASH_SOURCE%/*}/errors" flow no_first_node
data/bin/errors CHANGED
@@ -16,6 +16,7 @@ require_relative 'errors_cli/oc_error_demo'
16
16
  require_relative 'errors_cli/railway_error_demo'
17
17
  require_relative 'errors_cli/result_error_demo'
18
18
  require_relative 'errors_cli/scp_error_demo'
19
+ require_relative 'errors_cli/flow_error_demo'
19
20
 
20
21
  class ErrorsCLI
21
22
  extend GLI::App
@@ -113,6 +114,17 @@ class ErrorsCLI
113
114
  SCPErrorDemo.no_step_impl
114
115
  end
115
116
  end
117
+
118
+ desc 'Flow errors'
119
+ command :flow do |cmd|
120
+ make_cmd cmd, 'No first node', :no_first_node do
121
+ FlowErrorDemo.no_first_node
122
+ end
123
+
124
+ make_cmd cmd, 'Invalid Node route', :invalid_node_route do
125
+ FlowErrorDemo.invalid_node_route
126
+ end
127
+ end
116
128
  end
117
129
 
118
130
  exit ErrorsCLI.run(ARGV)
@@ -0,0 +1,22 @@
1
+ module FlowErrorDemo
2
+ class << self
3
+ def no_first_node
4
+ Flows::Flow.new(
5
+ start_node: :first,
6
+ node_map: {}
7
+ )
8
+ end
9
+
10
+ def invalid_node_route
11
+ Flows::Flow.new(
12
+ start_node: :first,
13
+ node_map: {
14
+ first: Flows::Flow::Node.new(
15
+ body: ->(_) {},
16
+ router: Flows::Flow::Router::Custom.new(a: :b)
17
+ )
18
+ }
19
+ )
20
+ end
21
+ end
22
+ end
data/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Flows
2
2
 
3
- [![Build Status](https://github.com/ffloyd/flows/workflows/Build/badge.svg)](https://github.com/ffloyd/flows/actions)
3
+ [![Build Status](https://github.com/ffloyd/flows/workflows/Test/badge.svg)](https://github.com/ffloyd/flows/actions)
4
4
  [![codecov](https://codecov.io/gh/ffloyd/flows/branch/master/graph/badge.svg)](https://codecov.io/gh/ffloyd/flows)
5
5
  [![Gem Version](https://badge.fury.io/rb/flows.svg)](https://badge.fury.io/rb/flows)
6
6
 
@@ -30,7 +30,9 @@ module Flows
30
30
  # @see Contract#check!
31
31
  def check!(other)
32
32
  unless @object === other
33
- value_error = @error_message || "must match `#{@object.inspect}`"
33
+ value_error =
34
+ @error_message ||
35
+ "must match `#{@object.inspect}`, but has class `#{other.class.inspect}` and value `#{other.inspect}`"
34
36
  raise Error.new(other, value_error)
35
37
  end
36
38
 
@@ -0,0 +1,29 @@
1
+ module Flows
2
+ class Flow
3
+ # Base class for {Flow} error
4
+ class Error < StandardError; end
5
+
6
+ # Raised when router has an impossible route.
7
+ class InvalidNodeRouteError < Error
8
+ def initialize(node_name, route_destination)
9
+ @node_name = node_name.inspect
10
+ @route_destination = route_destination.inspect
11
+ end
12
+
13
+ def message
14
+ "Node `#{@node_name}` has a route to `#{@route_destination}`, but node `#{@route_destination}` is missing."
15
+ end
16
+ end
17
+
18
+ # Raised when router has an impossible route.
19
+ class InvalidFirstNodeError < Error
20
+ def initialize(node_name)
21
+ @node_name = node_name.inspect
22
+ end
23
+
24
+ def message
25
+ "`#{@node_name}` is a first node name, but node `#{@node_name}` is missing."
26
+ end
27
+ end
28
+ end
29
+ end
@@ -82,6 +82,7 @@ module Flows
82
82
  class Node
83
83
  # Node metadata, a frozen Ruby Hash.
84
84
  attr_reader :meta
85
+ attr_reader :router
85
86
 
86
87
  # @param body [Proc] node body
87
88
  # @param router [Router] node router
@@ -48,6 +48,11 @@ module Flows
48
48
 
49
49
  raise NoRouteError, "no route found for: `#{result.inspect}`"
50
50
  end
51
+
52
+ # @see Flows::Flow::Router#destinations
53
+ def destinations
54
+ @routes.values
55
+ end
51
56
  end
52
57
  end
53
58
  end
@@ -14,6 +14,11 @@ module Flows
14
14
  def call(result)
15
15
  result.ok? ? @success_route : @failure_route
16
16
  end
17
+
18
+ # @see Flows::Flow::Router#destinations
19
+ def destinations
20
+ [@success_route, @failure_route]
21
+ end
17
22
  end
18
23
  end
19
24
  end
@@ -15,6 +15,10 @@ module Flows
15
15
  # @param result [Flows::Result] Result Object, output of a {Node} execution.
16
16
  # @return [Symbol] name of the next node or a special symbol `:end`.
17
17
  # @raise [NoRouteError] if cannot determine a route.
18
+ #
19
+ # @!method destinations
20
+ # @abstract
21
+ # @return [Array<Symbol>] names of all the possible destination nodes
18
22
  class Router
19
23
  end
20
24
  end
data/lib/flows/flow.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative 'flow/errors'
1
2
  require_relative 'flow/node'
2
3
  require_relative 'flow/router'
3
4
 
@@ -66,9 +67,13 @@ module Flows
66
67
  class Flow
67
68
  # @param start_node [Symbol] name of the entry node.
68
69
  # @param node_map [Hash<Symbol, Node>] keys are node names, values are nodes.
70
+ # @raise [Flows::Flow::InvalidNodeRouteError] when some node has invalid routing destination.
71
+ # @raise [Flows::Flow::InvalidFirstNodeError] when first node is not presented in node map.
69
72
  def initialize(start_node:, node_map:)
70
73
  @start_node = start_node
71
74
  @node_map = node_map
75
+
76
+ check_routing_integrity
72
77
  end
73
78
 
74
79
  # Executes a flow.
@@ -85,5 +90,21 @@ module Flows
85
90
 
86
91
  input
87
92
  end
93
+
94
+ private
95
+
96
+ def check_routing_integrity
97
+ raise InvalidFirstNodeError, @start_node unless @node_map.key?(@start_node)
98
+
99
+ @node_map.each { |node_name, node| check_node_routing_integrity(node_name, node) }
100
+ end
101
+
102
+ def check_node_routing_integrity(node_name, node)
103
+ node.router.destinations.each do |destination_name|
104
+ if destination_name != :end && !@node_map.key?(destination_name)
105
+ raise InvalidNodeRouteError.new(node_name, destination_name)
106
+ end
107
+ end
108
+ end
88
109
  end
89
110
  end
@@ -88,11 +88,12 @@ module Flows
88
88
  # Placeholder for empty value. We cannot use `nil` because value can be `nil`.
89
89
  NO_VALUE = :__no_value__
90
90
 
91
- Flows::Util::InheritableSingletonVars::DupStrategy.call(
92
- self,
91
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
93
92
  '@dependencies' => {}
94
93
  )
95
94
 
95
+ include SingletonVarsSetup
96
+
96
97
  # @api private
97
98
  module DSL
98
99
  attr_reader :dependencies
@@ -133,8 +134,7 @@ module Flows
133
134
 
134
135
  singleton_class.prepend InheritanceCallback
135
136
 
136
- # @api private
137
- module InitializePatch
137
+ InitializerWrapper = Util::PrependToClass.make_module do
138
138
  def initialize(*args, **kwargs, &block) # rubocop:disable Metrics/MethodLength
139
139
  klass = self.class
140
140
  DependencyList.new(
@@ -153,7 +153,7 @@ module Flows
153
153
  end
154
154
  end
155
155
 
156
- Flows::Util::PrependToClass.call(self, InitializePatch)
156
+ include InitializerWrapper
157
157
  end
158
158
  end
159
159
  end
@@ -9,12 +9,17 @@ module Flows
9
9
  # Hash of contracts for failure results.
10
10
  attr_reader :failure_contracts
11
11
 
12
- Flows::Util::InheritableSingletonVars::DupStrategy.call(
13
- self,
12
+ # Is contract check and transformation disabled
13
+ attr_reader :skip_output_contract_flag
14
+
15
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
14
16
  '@success_contracts' => {},
15
- '@failure_contracts' => {}
17
+ '@failure_contracts' => {},
18
+ '@skip_output_contract_flag' => false
16
19
  )
17
20
 
21
+ include SingletonVarsSetup
22
+
18
23
  # Defines a contract for a successful result with specific status.
19
24
  #
20
25
  # @param status [Symbol] Corresponding result status.
@@ -30,6 +35,13 @@ module Flows
30
35
  def failure_with(status, &contract_block)
31
36
  failure_contracts[status] = Flows::Contract.make(&contract_block)
32
37
  end
38
+
39
+ # Disables contract check and transformation for current class and children.
40
+ #
41
+ # @param enable [Boolean] if true - contracts are disabled
42
+ def skip_output_contract(enable = true)
43
+ @skip_output_contract_flag = enable
44
+ end
33
45
  end
34
46
  end
35
47
  end
@@ -8,16 +8,16 @@ module Flows
8
8
  def initialize(*args, &block)
9
9
  super(*args, &block)
10
10
  klass = self.class
11
- raise NoContractError, klass if klass.success_contracts.empty?
11
+ raise NoContractError, klass if klass.success_contracts.empty? && !klass.skip_output_contract_flag
12
12
  end
13
13
 
14
14
  def call(*args, &block)
15
15
  result = super(*args, &block)
16
16
  klass = self.class
17
17
 
18
- contract = Util.contract_for(klass, result)
18
+ return result if klass.skip_output_contract_flag
19
19
 
20
- Util.transform_result(klass, contract, result)
20
+ Util.transform_result(klass, result)
21
21
 
22
22
  result
23
23
  end
@@ -28,16 +28,9 @@ module Flows
28
28
  # @api private
29
29
  module Util
30
30
  class << self
31
- def contract_for(klass, result)
32
- raise ResultTypeError.new(klass, result) unless result.is_a?(Flows::Result)
33
-
34
- status = result.status
35
- contracts = result.ok? ? klass.success_contracts : klass.failure_contracts
31
+ def transform_result(klass, result)
32
+ contract = Util.contract_for(klass, result)
36
33
 
37
- contracts[status] || raise(StatusError.new(klass, result, contracts.keys))
38
- end
39
-
40
- def transform_result(klass, contract, result)
41
34
  data = result.send(:data)
42
35
 
43
36
  transformed_result = contract.transform(data)
@@ -45,6 +38,15 @@ module Flows
45
38
 
46
39
  result.send(:'data=', transformed_result.unwrap)
47
40
  end
41
+
42
+ def contract_for(klass, result)
43
+ raise ResultTypeError.new(klass, result) unless result.is_a?(Flows::Result)
44
+
45
+ status = result.status
46
+ contracts = result.ok? ? klass.success_contracts : klass.failure_contracts
47
+
48
+ contracts[status] || raise(StatusError.new(klass, result, contracts.keys))
49
+ end
48
50
  end
49
51
  end
50
52
  end