flows 0.0.2 → 0.1.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.
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # rubocop:disable all
3
+
4
+ require 'bundler/setup'
5
+ require 'json'
6
+ require 'ruby-prof'
7
+ require 'stackprof'
8
+
9
+ require_relative './examples'
10
+
11
+ flows_ten_steps = FlowsTenSteps.new
12
+
13
+ build_output_name = '10steps_build_10k_times'
14
+ exec_output_name = '10steps_execution_10k_times'
15
+
16
+ #
17
+ # RubyProf
18
+ #
19
+ RubyProf.measure_mode = RubyProf::WALL_TIME
20
+
21
+ puts 'Build with RubyProf...'
22
+ result = RubyProf.profile do
23
+ 10_000.times do
24
+ FlowsTenSteps.new
25
+ end
26
+ end
27
+ printer = RubyProf::MultiPrinter.new(result)
28
+ printer.print(path: 'profile', profile: build_output_name)
29
+
30
+ puts 'Execution with RubyProf...'
31
+ result = RubyProf.profile do
32
+ 10_000.times {
33
+ flows_ten_steps.call
34
+ }
35
+ end
36
+ printer = RubyProf::MultiPrinter.new(result)
37
+ printer.print(path: 'profile', profile: exec_output_name)
38
+
39
+ #
40
+ # StackProf
41
+ #
42
+
43
+ puts 'Build with StackProf...'
44
+ result = StackProf.run(mode: :wall, raw: true) do
45
+ 10_000.times do
46
+ FlowsTenSteps.new
47
+ end
48
+ end
49
+ File.write("profile/#{build_output_name}.json", JSON.generate(result))
50
+
51
+ puts 'Execution with StackProf...'
52
+ result = StackProf.run(mode: :wall, raw: true) do
53
+ 10_000.times do
54
+ flows_ten_steps.call
55
+ end
56
+ end
57
+ File.write("profile/#{exec_output_name}.json", JSON.generate(result))
58
+
59
+ puts
60
+ puts 'Install speedscope:'
61
+ puts ' npm i -g speedscope'
62
+ puts
63
+ puts "speedscope profile/#{build_output_name}.json"
64
+ puts "speedscope profile/#{exec_output_name}.json"
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # rubocop:disable all
3
+
4
+ require 'bundler/setup'
5
+ require 'benchmark/ips'
6
+
7
+ puts '-' * 50
8
+ puts '- method execution'
9
+ puts '-' * 50
10
+
11
+ class OneMethod
12
+ def meth
13
+ :ok
14
+ end
15
+ end
16
+
17
+ one_method = OneMethod.new
18
+ method_obj = one_method.method(:meth)
19
+
20
+ Benchmark.ips do |b|
21
+ b.report('native call') { one_method.meth }
22
+ b.report('send(...)') { one_method.send(:meth) }
23
+ b.report('Method#call') { method_obj.call }
24
+
25
+ b.compare!
26
+ end
data/flows.gemspec CHANGED
@@ -2,7 +2,7 @@ lib = File.expand_path('lib', __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require 'flows/version'
4
4
 
5
- Gem::Specification.new do |spec|
5
+ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
6
6
  spec.name = 'flows'
7
7
  spec.version = Flows::VERSION
8
8
  spec.authors = ['Roman Kolesnev']
@@ -33,4 +33,13 @@ Gem::Specification.new do |spec|
33
33
 
34
34
  spec.add_development_dependency 'codecov'
35
35
  spec.add_development_dependency 'simplecov'
36
+
37
+ # benchmarking tools
38
+ spec.add_development_dependency 'benchmark-ips'
39
+ spec.add_development_dependency 'ruby-prof'
40
+ spec.add_development_dependency 'stackprof'
41
+
42
+ # alternatives for comparison in benchmarking
43
+ spec.add_development_dependency 'dry-transaction'
44
+ spec.add_development_dependency 'trailblazer-operation'
36
45
  end
@@ -0,0 +1,37 @@
1
+ module Flows
2
+ module Operation
3
+ class Builder
4
+ # Router builder
5
+ module BuildRouter
6
+ class << self
7
+ def call(custom_routes, next_step, step_names)
8
+ if custom_routes
9
+ custom_router(custom_routes, next_step, step_names)
10
+ else
11
+ Flows::ResultRouter.new(next_step, :term)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def custom_router(custom_routes, next_step, step_names)
18
+ check_custom_routes(custom_routes, step_names)
19
+
20
+ custom_routes[Flows::Result::Ok] ||= next_step
21
+ custom_routes[Flows::Result::Err] ||= :term
22
+
23
+ Flows::Router.new(custom_routes)
24
+ end
25
+
26
+ def check_custom_routes(custom_routes, step_names)
27
+ custom_routes.values.each do |target|
28
+ next if step_names.include?(target) || target == :term
29
+
30
+ raise(::Flows::Operation::NoStepDefinedError, target)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,10 +1,17 @@
1
+ require_relative './builder/build_router'
2
+
1
3
  module Flows
2
4
  module Operation
3
5
  # Flow builder
4
6
  class Builder
5
- def initialize(steps:, method_source:)
7
+ attr_reader :steps, :method_source, :deps
8
+
9
+ def initialize(steps:, method_source:, deps:)
6
10
  @method_source = method_source
7
11
  @steps = steps
12
+ @deps = deps
13
+
14
+ @step_names = @steps.map { |s| s[:name] }
8
15
  end
9
16
 
10
17
  def call
@@ -17,39 +24,96 @@ module Flows
17
24
 
18
25
  private
19
26
 
20
- def resolve_wiring!
21
- @steps = @steps.map.with_index do |step, index|
22
- step.merge(
23
- next_step: @steps.dig(index + 1, :name) || :term
24
- )
27
+ def resolve_wiring! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
28
+ # we have to disable some linters for performance reasons
29
+ # this method can be simplified using `map.with_index`, but while loops is about
30
+ # 2x faster for such cases.
31
+ index = 0
32
+
33
+ while index < @steps.length
34
+ current_step = @steps[index]
35
+ next_step_name = nil
36
+
37
+ inner_index = index + 1
38
+ while inner_index < @steps.length
39
+ candidate = @steps[inner_index]
40
+ candidate_last_track = candidate[:track_path].last
41
+
42
+ if candidate[:track_path] == [] || current_step[:track_path].include?(candidate_last_track)
43
+ next_step_name = candidate[:name]
44
+ break
45
+ end
46
+
47
+ inner_index += 1
48
+ end
49
+
50
+ current_step[:next_step] = next_step_name || :term
51
+
52
+ index += 1
25
53
  end
26
54
  end
27
55
 
28
56
  def resolve_bodies!
29
57
  @steps.map! do |step|
30
- unless @method_source.respond_to?(step[:name])
31
- raise ::Flows::Operation::NoStepImplementationError, step[:name]
32
- end
33
-
34
58
  step.merge(
35
- body: @method_source.method(step[:name])
59
+ body: step[:custom_body] || resolve_body_from_source(step[:name])
36
60
  )
37
61
  end
38
62
  end
39
63
 
64
+ def resolve_body_from_source(name)
65
+ return @deps[name] if @deps.key?(name)
66
+
67
+ raise(::Flows::Operation::NoStepImplementationError, name) unless @method_source.respond_to?(name)
68
+
69
+ @method_source.method(name)
70
+ end
71
+
40
72
  def build_nodes
41
73
  @nodes = @steps.map do |step|
42
74
  Flows::Node.new(
43
75
  name: step[:name],
44
- body: step[:body],
76
+ body: build_final_body(step),
45
77
  preprocessor: method(:node_preprocessor),
46
78
  postprocessor: method(:node_postprocessor),
47
- router: make_router(step),
48
- meta: { name: step[:name] }
79
+ router: BuildRouter.call(step[:custom_routes], step[:next_step], @step_names),
80
+ meta: build_meta(step)
49
81
  )
50
82
  end
51
83
  end
52
84
 
85
+ def build_final_body(step)
86
+ case step[:type]
87
+ when :step
88
+ step[:body]
89
+ when :wrapper
90
+ build_wrapper_body(step[:body], step[:block])
91
+ end
92
+ end
93
+
94
+ def build_wrapper_body(wrapper, block)
95
+ suboperation_class = Class.new do
96
+ include ::Flows::Operation
97
+ end
98
+
99
+ suboperation_class.instance_exec(&block)
100
+ suboperation_class.no_shape
101
+
102
+ suboperation = suboperation_class.new(method_source: @method_source, deps: @deps)
103
+
104
+ lambda do |**options|
105
+ wrapper.call(**options) { suboperation.call(**options) }
106
+ end
107
+ end
108
+
109
+ def build_meta(step)
110
+ {
111
+ type: step[:type],
112
+ name: step[:name],
113
+ track_path: step[:track_path]
114
+ }
115
+ end
116
+
53
117
  def node_preprocessor(_input, context, _meta)
54
118
  context[:data]
55
119
  end
@@ -61,15 +125,6 @@ module Flows
61
125
 
62
126
  output
63
127
  end
64
-
65
- def make_router(step_definition)
66
- routes = step_definition[:custom_routes]
67
-
68
- routes[Flows::Result::Ok] ||= step_definition[:next_step]
69
- routes[Flows::Result::Err] ||= :term
70
-
71
- Flows::Router.new(routes)
72
- end
73
128
  end
74
129
  end
75
130
  end
@@ -2,31 +2,70 @@ module Flows
2
2
  module Operation
3
3
  # DSL methods for operation
4
4
  module DSL
5
- attr_reader :steps, :success_shapes, :failure_shapes
5
+ attr_reader :steps, :ok_shapes, :err_shapes
6
6
 
7
7
  include Flows::Result::Helpers
8
8
 
9
- def step(name, custom_routes = {})
10
- @steps << {
11
- name: name,
12
- custom_routes: custom_routes
13
- }
9
+ def step(name, custom_body_or_routes = nil, custom_routes = nil)
10
+ if custom_routes
11
+ custom_body = custom_body_or_routes
12
+ elsif custom_body_or_routes.is_a? Hash
13
+ custom_routes = custom_body_or_routes
14
+ custom_body = nil
15
+ else
16
+ custom_routes = nil
17
+ custom_body = custom_body_or_routes
18
+ end
19
+
20
+ @steps << make_step(name, custom_routes: custom_routes, custom_body: custom_body)
21
+ end
22
+
23
+ def track(name, &block)
24
+ track_path_before = @track_path
25
+ @track_path += [name]
26
+
27
+ @steps << make_step(name, custom_body: ->(**) { ok })
28
+ instance_exec(&block)
29
+
30
+ @track_path = track_path_before
31
+ end
32
+
33
+ def wrap(name, custom_body = nil, &block)
34
+ @steps << make_step(name, type: :wrapper, custom_body: custom_body, block: block)
35
+ end
36
+
37
+ def ok_shape(*keys, **code_keys_map)
38
+ @ok_shapes = if keys.empty?
39
+ code_keys_map
40
+ else
41
+ { success: keys }
42
+ end
43
+ end
44
+
45
+ def err_shape(*keys, **code_keys_map)
46
+ @err_shapes = if keys.empty?
47
+ code_keys_map
48
+ else
49
+ { failure: keys }
50
+ end
14
51
  end
15
52
 
16
- def success(*keys, **code_keys_map)
17
- @success_shapes = if keys.empty?
18
- code_keys_map
19
- else
20
- { success: keys }
21
- end
53
+ def no_shape
54
+ @ok_shapes = :no_shapes
55
+ @err_shapes = :no_shapes
22
56
  end
23
57
 
24
- def failure(*keys, **code_keys_map)
25
- @failure_shapes = if keys.empty?
26
- code_keys_map
27
- else
28
- { failure: keys }
29
- end
58
+ private
59
+
60
+ def make_step(name, type: :step, custom_routes: {}, custom_body: nil, block: nil)
61
+ {
62
+ type: type,
63
+ name: name,
64
+ custom_routes: custom_routes,
65
+ custom_body: custom_body,
66
+ block: block,
67
+ track_path: @track_path
68
+ }
30
69
  end
31
70
  end
32
71
  end
@@ -29,6 +29,16 @@ module Flows
29
29
  end
30
30
  end
31
31
 
32
+ class NoStepDefinedError < ::Flows::Error
33
+ def initialize(step_name)
34
+ @step_name = step_name
35
+ end
36
+
37
+ def message
38
+ "Missing step or track definition: #{@step_name}"
39
+ end
40
+ end
41
+
32
42
  class MissingOutputError < ::Flows::Error
33
43
  def initialize(required_keys, actual_keys)
34
44
  @missing_keys = required_keys - actual_keys
@@ -4,11 +4,11 @@ module Flows
4
4
  class Executor
5
5
  include ::Flows::Result::Helpers
6
6
 
7
- def initialize(flow:, success_shapes:, failure_shapes:, class_name:)
7
+ def initialize(flow:, ok_shapes:, err_shapes:, class_name:)
8
8
  @flow = flow
9
9
 
10
- @success_shapes = success_shapes
11
- @failure_shapes = failure_shapes
10
+ @ok_shapes = ok_shapes
11
+ @err_shapes = err_shapes
12
12
  @operation_class_name = class_name
13
13
  end
14
14
 
@@ -31,8 +31,10 @@ module Flows
31
31
  end
32
32
 
33
33
  def build_success_result(status, context)
34
- shape = @success_shapes[status]
35
- raise ::Flows::Operation::UnexpectedSuccessStatusError.new(status, @success_shapes.keys) if shape.nil?
34
+ return ok(status, context[:data]) if @ok_shapes == :no_shapes
35
+
36
+ shape = @ok_shapes[status]
37
+ raise ::Flows::Operation::UnexpectedSuccessStatusError.new(status, @ok_shapes.keys) if shape.nil?
36
38
 
37
39
  data = extract_data(context[:data], shape)
38
40
 
@@ -40,13 +42,16 @@ module Flows
40
42
  end
41
43
 
42
44
  def build_failure_result(status, context, last_result)
43
- raise ::Flows::Operation::NoFailureShapeError if @failure_shapes.nil?
45
+ raise ::Flows::Operation::NoFailureShapeError if @err_shapes.nil?
46
+
47
+ meta = build_meta(context, last_result)
44
48
 
45
- shape = @failure_shapes[status]
46
- raise ::Flows::Operation::UnexpectedFailureStatusError.new(status, @failure_shapes.keys) if shape.nil?
49
+ return Flows::Result::Err.new(context[:data], status: status, meta: meta) if @err_shapes == :no_shapes
50
+
51
+ shape = @err_shapes[status]
52
+ raise ::Flows::Operation::UnexpectedFailureStatusError.new(status, @err_shapes.keys) if shape.nil?
47
53
 
48
54
  data = extract_data(context[:data], shape)
49
- meta = build_meta(context, last_result)
50
55
 
51
56
  Flows::Result::Err.new(data, status: status, meta: meta)
52
57
  end
@@ -9,15 +9,18 @@ module Flows
9
9
  module Operation
10
10
  def self.included(mod)
11
11
  mod.instance_variable_set(:@steps, [])
12
+ mod.instance_variable_set(:@track_path, [])
12
13
  mod.extend ::Flows::Operation::DSL
13
14
  end
14
15
 
15
16
  include ::Flows::Result::Helpers
16
17
 
17
- def initialize
18
+ def initialize(method_source: nil, deps: {})
18
19
  _flows_do_checks
19
20
 
20
- @_flows_executor = _flows_make_executor(_flows_make_flow)
21
+ flow = _flows_make_flow(method_source || self, deps)
22
+
23
+ @_flows_executor = _flows_make_executor(flow)
21
24
  end
22
25
 
23
26
  def call(**params)
@@ -28,21 +31,22 @@ module Flows
28
31
 
29
32
  def _flows_do_checks
30
33
  raise NoStepsError if self.class.steps.empty?
31
- raise NoSuccessShapeError, self if self.class.success_shapes.nil?
34
+ raise NoSuccessShapeError, self if self.class.ok_shapes.nil?
32
35
  end
33
36
 
34
- def _flows_make_flow
37
+ def _flows_make_flow(method_source, deps)
35
38
  ::Flows::Operation::Builder.new(
36
39
  steps: self.class.steps,
37
- method_source: self
40
+ method_source: method_source,
41
+ deps: deps
38
42
  ).call
39
43
  end
40
44
 
41
45
  def _flows_make_executor(flow)
42
46
  ::Flows::Operation::Executor.new(
43
47
  flow: flow,
44
- success_shapes: self.class.success_shapes,
45
- failure_shapes: self.class.failure_shapes,
48
+ ok_shapes: self.class.ok_shapes,
49
+ err_shapes: self.class.err_shapes,
46
50
  class_name: self.class.name
47
51
  )
48
52
  end
@@ -2,8 +2,12 @@ module Flows
2
2
  class Result
3
3
  # Wrapper for failure results
4
4
  class Err < Result
5
+ attr_reader :error
6
+
5
7
  def initialize(data, status: :failure, meta: {})
6
- super
8
+ @error = data
9
+ @status = status
10
+ @meta = meta
7
11
  end
8
12
 
9
13
  def ok?
@@ -17,10 +21,6 @@ module Flows
17
21
  def unwrap
18
22
  raise UnwrapError.new(@status, @data, @meta)
19
23
  end
20
-
21
- def error
22
- @data
23
- end
24
24
  end
25
25
  end
26
26
  end
@@ -2,8 +2,12 @@ module Flows
2
2
  class Result
3
3
  # Wrapper for successful results
4
4
  class Ok < Result
5
+ attr_reader :unwrap
6
+
5
7
  def initialize(data, status: :success, meta: {})
6
- super
8
+ @unwrap = data
9
+ @status = status
10
+ @meta = meta
7
11
  end
8
12
 
9
13
  def ok?
@@ -14,10 +18,6 @@ module Flows
14
18
  false
15
19
  end
16
20
 
17
- def unwrap
18
- @data
19
- end
20
-
21
21
  def error
22
22
  raise NoErrorError.new(@status, @data)
23
23
  end
data/lib/flows/result.rb CHANGED
@@ -3,12 +3,8 @@ module Flows
3
3
  class Result
4
4
  attr_reader :status, :meta
5
5
 
6
- def initialize(data, status:, meta: {})
7
- @data = data
8
- @status = status
9
- @meta = meta
10
-
11
- raise 'Use Flows::Result::Ok or Flows::Result::Err for build result objects' if self.class == Result
6
+ def initialize(**)
7
+ raise 'Use Flows::Result::Ok or Flows::Result::Err for build result objects'
12
8
  end
13
9
  end
14
10
  end
@@ -0,0 +1,14 @@
1
+ module Flows
2
+ # Node router for simple case when result must be a `Flows::Result`
3
+ # and we don't care about resukt status key
4
+ class ResultRouter
5
+ def initialize(success_route, failure_route)
6
+ @success_route = success_route
7
+ @failure_route = failure_route
8
+ end
9
+
10
+ def call(output, **)
11
+ output.ok? ? @success_route : @failure_route
12
+ end
13
+ end
14
+ end
data/lib/flows/router.rb CHANGED
@@ -4,23 +4,19 @@ module Flows
4
4
  class Error < Flows::Error; end
5
5
  class NoRouteError < Error; end
6
6
 
7
- DEFAULT_PREPROCESSOR = ->(output, _context, _meta) { output }
8
-
9
- def initialize(route_hash, preprocessor: DEFAULT_PREPROCESSOR)
7
+ def initialize(route_hash, preprocessor: nil)
10
8
  @route_def = route_hash
11
9
  @preprocessor = preprocessor
12
10
  end
13
11
 
14
12
  def call(output, context:, meta:)
15
- data = @preprocessor.call(output, context, meta)
13
+ data = @preprocessor ? @preprocessor.call(output, context, meta) : output
16
14
 
17
- matched_entry = @route_def.find do |predicate, _|
18
- predicate === data # rubocop:disable Style/CaseEquality
15
+ @route_def.each_pair do |predicate, route|
16
+ return route if predicate === data # rubocop:disable Style/CaseEquality
19
17
  end
20
18
 
21
- raise NoRouteError, "no route found found for output: #{output.inspect}" unless matched_entry
22
-
23
- matched_entry[1]
19
+ raise NoRouteError, "no route found found for output: #{output.inspect}"
24
20
  end
25
21
  end
26
22
  end
data/lib/flows/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Flows
2
- VERSION = '0.0.2'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
data/lib/flows.rb CHANGED
@@ -5,6 +5,7 @@ end
5
5
  require 'flows/version'
6
6
 
7
7
  require 'flows/router'
8
+ require 'flows/result_router'
8
9
  require 'flows/node'
9
10
  require 'flows/flow'
10
11
 
data/profile/.keep ADDED
File without changes