flows 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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