flows 0.3.0 → 0.4.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +1 -0
  4. data/.reek.yml +42 -0
  5. data/.rubocop.yml +20 -7
  6. data/.ruby-version +1 -1
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +42 -0
  9. data/Gemfile +0 -6
  10. data/Gemfile.lock +139 -74
  11. data/README.md +158 -364
  12. data/Rakefile +35 -1
  13. data/bin/.rubocop.yml +5 -0
  14. data/bin/all_the_errors +47 -0
  15. data/bin/benchmark +73 -105
  16. data/bin/benchmark_cli/compare.rb +118 -0
  17. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  18. data/bin/benchmark_cli/compare/base.rb +45 -0
  19. data/bin/benchmark_cli/compare/command.rb +47 -0
  20. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  21. data/bin/benchmark_cli/examples.rb +23 -0
  22. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  38. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  39. data/bin/benchmark_cli/helpers.rb +12 -0
  40. data/bin/benchmark_cli/ruby.rb +15 -0
  41. data/bin/benchmark_cli/ruby/command.rb +38 -0
  42. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  43. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  44. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  45. data/bin/console +1 -0
  46. data/bin/docserver +7 -0
  47. data/bin/errors +118 -0
  48. data/bin/errors_cli/contract_error_demo.rb +49 -0
  49. data/bin/errors_cli/di_error_demo.rb +38 -0
  50. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  51. data/bin/errors_cli/oc_error_demo.rb +40 -0
  52. data/bin/errors_cli/railway_error_demo.rb +10 -0
  53. data/bin/errors_cli/result_error_demo.rb +13 -0
  54. data/bin/errors_cli/scp_error_demo.rb +17 -0
  55. data/docs/README.md +2 -186
  56. data/docs/_sidebar.md +0 -24
  57. data/docs/index.html +1 -1
  58. data/flows.gemspec +25 -2
  59. data/forspell.dict +9 -0
  60. data/lefthook.yml +9 -0
  61. data/lib/flows.rb +11 -5
  62. data/lib/flows/contract.rb +402 -0
  63. data/lib/flows/contract/array.rb +55 -0
  64. data/lib/flows/contract/case_eq.rb +41 -0
  65. data/lib/flows/contract/compose.rb +77 -0
  66. data/lib/flows/contract/either.rb +53 -0
  67. data/lib/flows/contract/error.rb +25 -0
  68. data/lib/flows/contract/hash.rb +75 -0
  69. data/lib/flows/contract/hash_of.rb +70 -0
  70. data/lib/flows/contract/helpers.rb +22 -0
  71. data/lib/flows/contract/predicate.rb +34 -0
  72. data/lib/flows/contract/transformer.rb +50 -0
  73. data/lib/flows/contract/tuple.rb +70 -0
  74. data/lib/flows/flow.rb +75 -7
  75. data/lib/flows/flow/node.rb +131 -0
  76. data/lib/flows/flow/router.rb +25 -0
  77. data/lib/flows/flow/router/custom.rb +54 -0
  78. data/lib/flows/flow/router/errors.rb +11 -0
  79. data/lib/flows/flow/router/simple.rb +20 -0
  80. data/lib/flows/plugin.rb +13 -0
  81. data/lib/flows/plugin/dependency_injector.rb +159 -0
  82. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  83. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  84. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  85. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  86. data/lib/flows/plugin/implicit_init.rb +45 -0
  87. data/lib/flows/plugin/output_contract.rb +84 -0
  88. data/lib/flows/plugin/output_contract/dsl.rb +36 -0
  89. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  90. data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
  91. data/lib/flows/railway.rb +140 -37
  92. data/lib/flows/railway/dsl.rb +8 -19
  93. data/lib/flows/railway/errors.rb +8 -12
  94. data/lib/flows/railway/step.rb +24 -0
  95. data/lib/flows/railway/step_list.rb +38 -0
  96. data/lib/flows/result.rb +188 -2
  97. data/lib/flows/result/do.rb +160 -16
  98. data/lib/flows/result/err.rb +12 -6
  99. data/lib/flows/result/errors.rb +29 -17
  100. data/lib/flows/result/helpers.rb +25 -3
  101. data/lib/flows/result/ok.rb +12 -6
  102. data/lib/flows/shared_context_pipeline.rb +216 -0
  103. data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
  104. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  105. data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
  106. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  107. data/lib/flows/shared_context_pipeline/step.rb +46 -0
  108. data/lib/flows/shared_context_pipeline/track.rb +67 -0
  109. data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
  110. data/lib/flows/util.rb +17 -0
  111. data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
  112. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
  113. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
  114. data/lib/flows/util/prepend_to_class.rb +145 -0
  115. data/lib/flows/version.rb +1 -1
  116. metadata +233 -37
  117. data/bin/demo +0 -66
  118. data/bin/examples.rb +0 -195
  119. data/bin/profile_10steps +0 -106
  120. data/bin/ruby_benchmarks +0 -26
  121. data/docs/CNAME +0 -1
  122. data/docs/contributing/benchmarks_profiling.md +0 -3
  123. data/docs/contributing/local_development.md +0 -3
  124. data/docs/flow/direct_usage.md +0 -3
  125. data/docs/flow/general_idea.md +0 -3
  126. data/docs/operation/basic_usage.md +0 -1
  127. data/docs/operation/inject_steps.md +0 -3
  128. data/docs/operation/lambda_steps.md +0 -3
  129. data/docs/operation/result_shapes.md +0 -3
  130. data/docs/operation/routing_tracks.md +0 -3
  131. data/docs/operation/wrapping_steps.md +0 -3
  132. data/docs/overview/performance.md +0 -336
  133. data/docs/railway/basic_usage.md +0 -232
  134. data/docs/result_objects/basic_usage.md +0 -196
  135. data/docs/result_objects/do_notation.md +0 -139
  136. data/lib/flows/implicit_build.rb +0 -16
  137. data/lib/flows/node.rb +0 -27
  138. data/lib/flows/operation.rb +0 -55
  139. data/lib/flows/operation/builder.rb +0 -130
  140. data/lib/flows/operation/builder/build_router.rb +0 -37
  141. data/lib/flows/operation/dsl.rb +0 -93
  142. data/lib/flows/operation/errors.rb +0 -75
  143. data/lib/flows/operation/executor.rb +0 -78
  144. data/lib/flows/railway/builder.rb +0 -68
  145. data/lib/flows/railway/executor.rb +0 -23
  146. data/lib/flows/result_router.rb +0 -14
  147. data/lib/flows/router.rb +0 -22
@@ -1,16 +0,0 @@
1
- module Flows
2
- # Module to extend Operation and Railway. Adds implicit building feature.
3
- module ImplicitBuild
4
- attr_reader :default_build
5
-
6
- def self.extended(mod)
7
- mod.instance_variable_set(:@default_build, nil)
8
- end
9
-
10
- def call(**params)
11
- @default_build ||= new
12
-
13
- default_build.call(**params)
14
- end
15
- end
16
- end
@@ -1,27 +0,0 @@
1
- module Flows
2
- # Representation of FSM node.
3
- class Node
4
- attr_reader :name, :meta
5
-
6
- def initialize(name:, body:, router:, meta: {}, preprocessor: nil, postprocessor: nil)
7
- @name = name
8
- @body = body
9
- @router = router
10
-
11
- @meta = meta.freeze
12
-
13
- @preprocessor = preprocessor
14
- @postprocessor = postprocessor
15
- end
16
-
17
- def call(input, context:)
18
- input = @preprocessor.call(input, context, @meta) if @preprocessor
19
- output = @body.call(input)
20
- output = @postprocessor.call(output, context, @meta) if @postprocessor
21
-
22
- route = @router.call(output, context: context, meta: @meta)
23
-
24
- [output, route]
25
- end
26
- end
27
- end
@@ -1,55 +0,0 @@
1
- require_relative 'operation/errors'
2
-
3
- require_relative 'operation/dsl'
4
- require_relative 'operation/builder'
5
- require_relative 'operation/executor'
6
-
7
- require_relative 'implicit_build'
8
-
9
- module Flows
10
- # Operation DSL
11
- module Operation
12
- def self.included(mod)
13
- mod.extend ::Flows::Operation::DSL
14
- mod.extend ::Flows::ImplicitBuild
15
- end
16
-
17
- include ::Flows::Result::Helpers
18
-
19
- def initialize(method_source: nil, deps: {})
20
- _flows_do_checks
21
-
22
- flow = _flows_make_flow(method_source || self, deps)
23
-
24
- @_flows_executor = _flows_make_executor(flow)
25
- end
26
-
27
- def call(**params)
28
- @_flows_executor.call(**params)
29
- end
30
-
31
- private
32
-
33
- def _flows_do_checks
34
- raise NoStepsError if self.class.steps.empty?
35
- raise NoSuccessShapeError, self if self.class.ok_shapes.nil?
36
- end
37
-
38
- def _flows_make_flow(method_source, deps)
39
- ::Flows::Operation::Builder.new(
40
- steps: self.class.steps,
41
- method_source: method_source,
42
- deps: deps
43
- ).call
44
- end
45
-
46
- def _flows_make_executor(flow)
47
- ::Flows::Operation::Executor.new(
48
- flow: flow,
49
- ok_shapes: self.class.ok_shapes,
50
- err_shapes: self.class.err_shapes,
51
- class_name: self.class.name
52
- )
53
- end
54
- end
55
- end
@@ -1,130 +0,0 @@
1
- require_relative './builder/build_router'
2
-
3
- module Flows
4
- module Operation
5
- # Flow builder
6
- class Builder
7
- attr_reader :steps, :method_source, :deps
8
-
9
- def initialize(steps:, method_source:, deps:)
10
- @method_source = method_source
11
- @steps = steps
12
- @deps = deps
13
-
14
- @step_names = @steps.map { |s| s[:name] }
15
- end
16
-
17
- def call
18
- resolve_wiring!
19
- resolve_bodies!
20
-
21
- nodes = build_nodes
22
- Flows::Flow.new(start_node: nodes.first.name, nodes: nodes)
23
- end
24
-
25
- private
26
-
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
53
- end
54
- end
55
-
56
- def resolve_bodies!
57
- @steps.each do |step|
58
- step.merge!(
59
- body: step[:custom_body] || resolve_body_from_source(step[:name])
60
- )
61
- end
62
- end
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
-
72
- def build_nodes
73
- @nodes = @steps.map do |step|
74
- Flows::Node.new(
75
- name: step[:name],
76
- body: build_final_body(step),
77
- preprocessor: method(:node_preprocessor),
78
- postprocessor: method(:node_postprocessor),
79
- router: BuildRouter.call(step[:custom_routes], step[:next_step], @step_names),
80
- meta: build_meta(step)
81
- )
82
- end
83
- end
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
-
117
- def node_preprocessor(_input, context, _meta)
118
- context[:data]
119
- end
120
-
121
- def node_postprocessor(output, context, meta)
122
- output_data = output.ok? ? output.unwrap : output.error
123
- context[:data].merge!(output_data)
124
- context[:last_step] = meta[:name]
125
-
126
- output
127
- end
128
- end
129
- end
130
- end
@@ -1,37 +0,0 @@
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,93 +0,0 @@
1
- module Flows
2
- module Operation
3
- # DSL methods for operation
4
- module DSL
5
- attr_reader :steps, :ok_shapes, :err_shapes
6
-
7
- def self.extended(mod, steps = nil, ok_shapes = nil, err_shapes = nil)
8
- mod.instance_variable_set(:@steps, steps || [])
9
- mod.instance_variable_set(:@track_path, [])
10
- mod.instance_variable_set(:@ok_shapes, ok_shapes)
11
- mod.instance_variable_set(:@err_shapes, err_shapes)
12
-
13
- mod.class_exec do
14
- def self.inherited(subclass)
15
- ::Flows::Operation::DSL.extended(subclass, steps.map(&:dup), ok_shapes, err_shapes)
16
- super
17
- end
18
- end
19
- end
20
-
21
- include Flows::Result::Helpers
22
-
23
- def step(name, custom_body_or_routes = nil, custom_routes = nil)
24
- if custom_routes
25
- custom_body = custom_body_or_routes
26
- elsif custom_body_or_routes.is_a? Hash
27
- custom_routes = custom_body_or_routes
28
- custom_body = nil
29
- else
30
- custom_routes = nil
31
- custom_body = custom_body_or_routes
32
- end
33
-
34
- @steps << make_step(name, custom_routes: custom_routes, custom_body: custom_body)
35
- end
36
-
37
- def track(name, &block)
38
- track_path_before = @track_path
39
- @track_path += [name]
40
-
41
- @steps << make_step(name, custom_body: ->(**) { ok })
42
- instance_exec(&block)
43
-
44
- @track_path = track_path_before
45
- end
46
-
47
- def routes(routes_hash)
48
- routes_hash
49
- end
50
-
51
- alias when_ok match_ok
52
- alias when_err match_err
53
-
54
- def wrap(name, custom_body = nil, &block)
55
- @steps << make_step(name, type: :wrapper, custom_body: custom_body, block: block)
56
- end
57
-
58
- def ok_shape(*keys, **code_keys_map)
59
- @ok_shapes = if keys.empty?
60
- code_keys_map
61
- else
62
- { success: keys }
63
- end
64
- end
65
-
66
- def err_shape(*keys, **code_keys_map)
67
- @err_shapes = if keys.empty?
68
- code_keys_map
69
- else
70
- { failure: keys }
71
- end
72
- end
73
-
74
- def no_shape
75
- @ok_shapes = :no_shapes
76
- @err_shapes = :no_shapes
77
- end
78
-
79
- private
80
-
81
- def make_step(name, type: :step, custom_routes: {}, custom_body: nil, block: nil)
82
- {
83
- type: type,
84
- name: name,
85
- custom_routes: custom_routes,
86
- custom_body: custom_body,
87
- block: block,
88
- track_path: @track_path
89
- }
90
- end
91
- end
92
- end
93
- end
@@ -1,75 +0,0 @@
1
- module Flows
2
- module Operation
3
- # rubocop:disable Style/Documentation
4
- class NoSuccessShapeError < ::Flows::Error
5
- def message
6
- 'Missing success output shapes'
7
- end
8
- end
9
-
10
- class NoFailureShapeError < ::Flows::Error
11
- def message
12
- 'Missing failure output shape'
13
- end
14
- end
15
-
16
- class NoStepsError < ::Flows::Error
17
- def message
18
- 'No steps defined'
19
- end
20
- end
21
-
22
- class NoStepImplementationError < ::Flows::Error
23
- def initialize(step_name)
24
- @step_name = step_name
25
- end
26
-
27
- def message
28
- "Missing step implementation for #{@step_name}"
29
- end
30
- end
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
-
42
- class MissingOutputError < ::Flows::Error
43
- def initialize(required_keys, actual_keys)
44
- @missing_keys = required_keys - actual_keys
45
- end
46
-
47
- def message
48
- "Missing keys in output: #{@missing_keys.join(', ')}"
49
- end
50
- end
51
-
52
- class UnexpectedSuccessStatusError < ::Flows::Error
53
- def initialize(actual_status, supported_statuses)
54
- @actual_status = actual_status.inspect
55
- @supported_statuses = supported_statuses.map(&:inspect).join(', ')
56
- end
57
-
58
- def message
59
- "Unexpeted success result status: `#{@actual_status}`, supported statuses: `#{@supported_statuses}`"
60
- end
61
- end
62
-
63
- class UnexpectedFailureStatusError < ::Flows::Error
64
- def initialize(actual_status, supported_statuses)
65
- @actual_status = actual_status.inspect
66
- @supported_statuses = supported_statuses.map(&:inspect).join(', ')
67
- end
68
-
69
- def message
70
- "Unexpeted failure result status: `#{@actual_status}`, supported statuses: `#{@supported_statuses}`"
71
- end
72
- end
73
- # rubocop:enable Style/Documentation
74
- end
75
- end
@@ -1,78 +0,0 @@
1
- module Flows
2
- module Operation
3
- # Runner for operation steps
4
- class Executor
5
- include ::Flows::Result::Helpers
6
-
7
- def initialize(flow:, ok_shapes:, err_shapes:, class_name:)
8
- @flow = flow
9
-
10
- @ok_shapes = ok_shapes
11
- @err_shapes = err_shapes
12
- @operation_class_name = class_name
13
- end
14
-
15
- def call(**params)
16
- context = { data: params }
17
- last_result = @flow.call(nil, context: context)
18
-
19
- build_result(last_result, context)
20
- end
21
-
22
- private
23
-
24
- def build_result(last_result, context)
25
- status = last_result.status
26
-
27
- case last_result
28
- when Flows::Result::Ok then build_success_result(status, context)
29
- when Flows::Result::Err then build_failure_result(status, context, last_result)
30
- end
31
- end
32
-
33
- def build_success_result(status, context)
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?
38
-
39
- data = extract_data(context[:data], shape)
40
-
41
- ok(status, data)
42
- end
43
-
44
- def build_failure_result(status, context, last_result)
45
- raise ::Flows::Operation::NoFailureShapeError if @err_shapes.nil?
46
-
47
- meta = build_meta(context, last_result)
48
-
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?
53
-
54
- data = extract_data(context[:data], shape)
55
-
56
- Flows::Result::Err.new(data, status: status, meta: meta)
57
- end
58
-
59
- def extract_data(output, keys)
60
- raise ::Flows::Operation::MissingOutputError.new(keys, output.keys) unless keys.all? { |key| output.key?(key) }
61
-
62
- output.slice(*keys)
63
- end
64
-
65
- def build_meta(context, last_result)
66
- meta = {
67
- operation: @operation_class_name,
68
- step: context[:last_step],
69
- context_data: context[:data]
70
- }
71
-
72
- meta[:nested_metadata] = last_result.meta if last_result.meta.any?
73
-
74
- meta
75
- end
76
- end
77
- end
78
- end