trailblazer-operation 0.0.13 → 0.4.1

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 (60) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +5 -3
  3. data/CHANGES.md +100 -0
  4. data/Gemfile +4 -2
  5. data/Rakefile +8 -6
  6. data/lib/trailblazer/operation.rb +80 -13
  7. data/lib/trailblazer/operation/callable.rb +42 -0
  8. data/lib/trailblazer/operation/class_dependencies.rb +25 -0
  9. data/lib/trailblazer/operation/deprecated_macro.rb +19 -0
  10. data/lib/trailblazer/operation/heritage.rb +30 -0
  11. data/lib/trailblazer/operation/inject.rb +36 -0
  12. data/lib/trailblazer/operation/inspect.rb +79 -0
  13. data/lib/trailblazer/operation/public_call.rb +55 -0
  14. data/lib/trailblazer/operation/railway.rb +32 -0
  15. data/lib/trailblazer/operation/railway/fast_track.rb +13 -0
  16. data/lib/trailblazer/operation/railway/macaroni.rb +23 -0
  17. data/lib/trailblazer/operation/railway/normalizer.rb +58 -0
  18. data/lib/trailblazer/operation/railway/task_builder.rb +37 -0
  19. data/lib/trailblazer/operation/result.rb +6 -4
  20. data/lib/trailblazer/operation/trace.rb +46 -0
  21. data/lib/trailblazer/operation/version.rb +1 -1
  22. data/test/call_test.rb +27 -8
  23. data/test/callable_test.rb +147 -0
  24. data/test/class_dependencies_test.rb +16 -0
  25. data/test/docs/doormat_test.rb +189 -0
  26. data/test/docs/macaroni_test.rb +33 -0
  27. data/test/docs/operation_test.rb +23 -0
  28. data/test/docs/wiring_test.rb +559 -0
  29. data/test/dry_container_test.rb +4 -0
  30. data/test/fast_track_test.rb +197 -0
  31. data/test/gemfiles/Gemfile.ruby-2.0 +1 -2
  32. data/test/gemfiles/Gemfile.ruby-2.0.lock +40 -0
  33. data/test/inheritance_test.rb +1 -1
  34. data/test/inspect_test.rb +43 -0
  35. data/test/introspect_test.rb +51 -0
  36. data/test/macro_test.rb +60 -0
  37. data/test/operation_test.rb +94 -0
  38. data/test/result_test.rb +14 -8
  39. data/test/ruby-2.0.0/operation_test.rb +61 -0
  40. data/test/ruby-2.0.0/step_test.rb +136 -0
  41. data/test/skill_test.rb +66 -48
  42. data/test/step_test.rb +228 -0
  43. data/test/task_wrap_test.rb +97 -0
  44. data/test/test_helper.rb +37 -0
  45. data/test/trace_test.rb +57 -0
  46. data/test/wire_test.rb +113 -0
  47. data/test/wiring/defaults_test.rb +197 -0
  48. data/test/wiring/subprocess_test.rb +70 -0
  49. data/trailblazer-operation.gemspec +3 -5
  50. metadata +68 -37
  51. data/lib/trailblazer/operation/1.9.3/option.rb +0 -36
  52. data/lib/trailblazer/operation/generic.rb +0 -12
  53. data/lib/trailblazer/operation/option.rb +0 -54
  54. data/lib/trailblazer/operation/pipetree.rb +0 -142
  55. data/lib/trailblazer/operation/skill.rb +0 -41
  56. data/lib/trailblazer/skill.rb +0 -70
  57. data/test/2.0.0-pipetree_test.rb +0 -100
  58. data/test/2.1.0-pipetree_test.rb +0 -100
  59. data/test/operation_skill_test.rb +0 -89
  60. data/test/pipetree_test.rb +0 -185
@@ -0,0 +1,55 @@
1
+ module Trailblazer
2
+ module Operation::PublicCall
3
+ # This is the outer-most public `call` method that gets invoked when calling `Create.()`.
4
+ # The signature of this is `params, options, *containers`. This was a mistake, as the
5
+ # first argument could've been part of `options` hash in the first place.
6
+ #
7
+ # Create.(params, runtime_data, *containers)
8
+ # #=> Result<Context...>
9
+ #
10
+ # In workflows/Nested compositions, this method is not used anymore and it might probably
11
+ # get removed in future versions of TRB. Currently, we use Operation::__call__ as an alternative.
12
+ #
13
+ #
14
+ # @note Do not override this method as it will be removed in future versions. Also, you will break tracing.
15
+ # @return Operation::Railway::Result binary result object
16
+ def call(*args)
17
+ return call_with_circuit_interface(*args) if args.any? && args[0].is_a?(Array) # This is kind of a hack that could be well hidden if Ruby had method overloading. Goal is to simplify the call/__call__ thing as we're fading out Operation::call anyway.
18
+ call_with_public_interface(*args)
19
+ end
20
+
21
+ def call_with_public_interface(*args)
22
+ ctx = Operation::PublicCall.options_for_public_call(*args)
23
+
24
+ # call the activity.
25
+ # This will result in invoking {::call_with_circuit_interface}.
26
+ last_signal, (options, flow_options) = Activity::TaskWrap.invoke(self, [ctx, {}], {})
27
+
28
+ # Result is successful if the activity ended with an End event derived from Railway::End::Success.
29
+ Operation::Railway::Result(last_signal, options, flow_options)
30
+ end
31
+
32
+ # This interface is used for all nested OPs (and the outer-most, too).
33
+ def call_with_circuit_interface(args, circuit_options)
34
+ @activity.(
35
+ args,
36
+ circuit_options.merge(
37
+ exec_context: new
38
+ )
39
+ )
40
+ end
41
+
42
+ # Compile a Context object to be passed into the Activity::call.
43
+ # @private
44
+ def self.options_for_public_call(options={}, *containers)
45
+ # generate the skill hash that embraces runtime options plus potential containers, the so called Runtime options.
46
+ # This wrapping is supposed to happen once in the entire system.
47
+
48
+ hash_transformer = ->(containers) { containers[0].to_hash } # FIXME: don't transform any containers into kw args.
49
+
50
+ immutable_options = Trailblazer::Context::ContainerChain.new( [options, *containers], to_hash: hash_transformer ) # Runtime options, immutable.
51
+
52
+ ctx = Trailblazer::Context(immutable_options)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ module Trailblazer
2
+ # Operations is simply a thin API to define, inherit and run circuits by passing the options object.
3
+ # It encourages the linear railway style (http://trb.to/gems/workflow/circuit.html#operation) but can
4
+ # easily be extend for more complex workflows.
5
+ class Operation
6
+ # End event: All subclasses of End:::Success are interpreted as "success".
7
+ module Railway
8
+ # @param options Context
9
+ # @param end_event The last emitted signal in a circuit is usually the end event.
10
+ def self.Result(end_event, options, *)
11
+ Result.new(end_event.kind_of?(End::Success), options, end_event)
12
+ end
13
+
14
+ # The Railway::Result knows about its binary state, the context (data), and the last event in the circuit.
15
+ class Result < Result # Operation::Result
16
+ def initialize(success, data, event)
17
+ super(success, data)
18
+
19
+ @event = event
20
+ end
21
+
22
+ attr_reader :event
23
+ end
24
+
25
+ module End
26
+ class Success < Activity::End; end
27
+ class Failure < Activity::End; end
28
+ end
29
+
30
+ end # Railway
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ def self.fail! ; Activity::Left end
4
+ def self.pass! ; Activity::Right end
5
+ def self.fail_fast!; Activity::FastTrack::FailFast end
6
+ def self.pass_fast!; Activity::FastTrack::PassFast end
7
+
8
+ module End
9
+ FailFast = Class.new(Operation::Railway::End::Failure)
10
+ PassFast = Class.new(Operation::Railway::End::Success)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ # Call the user's steps with a differing API (inspired by Maciej Mensfeld) that
4
+ # only receives keyword args. The `options` keyword is the stateful context object
5
+ #
6
+ # def my_step( params:, ** )
7
+ # def my_step( params:, options:, ** )
8
+ module Macaroni
9
+ def self.call(user_proc)
10
+ Activity::TaskBuilder::Task.new( Trailblazer::Option.build( Macaroni::Option, user_proc ), user_proc )
11
+ end
12
+
13
+ class Option < Trailblazer::Option
14
+ # The Option#call! method prepares the arguments.
15
+ def self.call!(proc, options, *)
16
+ proc.( **options.to_hash.merge( options: options ) )
17
+ end
18
+ end
19
+ end
20
+
21
+ KwSignature = Macaroni
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ # The {Normalizer} is called for every DSL call (step/pass/fail etc.) and normalizes/defaults
4
+ # the user options, such as setting `:id`, connecting the task's outputs or wrapping the user's
5
+ # task via {TaskBuilder} in order to translate true/false to `Right` or `Left`.
6
+ #
7
+ # The Normalizer sits in the `@builder`, which receives all DSL calls from the Operation subclass.
8
+ module Normalizer
9
+ Pipeline = Activity::Magnetic::Normalizer::Pipeline.clone
10
+
11
+ Pipeline.module_eval do
12
+ # Handle the :override option which is specific to Operation.
13
+ def self.override(ctx, task:, options:, sequence_options:, **)
14
+ options, locals = Activity::Magnetic::Options.normalize(options, [:override])
15
+ sequence_options = sequence_options.merge( replace: options[:id] ) if locals[:override]
16
+
17
+ ctx[:options], ctx[:sequence_options] = options, sequence_options
18
+ end
19
+
20
+ # TODO remove in 2.2
21
+ def self.deprecate_macro_with_two_args(ctx, task:, **)
22
+ return true unless task.is_a?(Array) # TODO remove in 2.2
23
+
24
+ ctx[:options] = Operation::DeprecatedMacro.( *task )
25
+ end
26
+
27
+ # TODO remove in 2.2
28
+ def self.deprecate_name(ctx, local_options:, connection_options:, **)
29
+ connection_options, deprecated_options = Activity::Magnetic::Options.normalize(connection_options, [:name])
30
+ local_options, _deprecated_options = Activity::Magnetic::Options.normalize(local_options, [:name])
31
+
32
+ deprecated_options = deprecated_options.merge(_deprecated_options)
33
+
34
+ local_options = local_options.merge( name: deprecated_options[:name] ) if deprecated_options[:name]
35
+
36
+ local_options, locals = Activity::Magnetic::Options.normalize(local_options, [:name])
37
+ if locals[:name]
38
+ warn "[Trailblazer] The :name option for #step, #success and #failure has been renamed to :id."
39
+ local_options = local_options.merge(id: locals[:name])
40
+ end
41
+
42
+ ctx[:local_options], ctx[:connection_options] = local_options, connection_options
43
+ end
44
+
45
+ def self.raise_on_missing_id(ctx, local_options:, **)
46
+ raise "No :id given for #{local_options[:task]}" unless local_options[:id]
47
+ true
48
+ end
49
+
50
+ # add more normalization tasks to the existing Magnetic::Normalizer::Pipeline
51
+ task Activity::TaskBuilder::Binary( method(:deprecate_macro_with_two_args) ), before: "split_options"
52
+ task Activity::TaskBuilder::Binary( method(:deprecate_name) )
53
+ task Activity::TaskBuilder::Binary( method(:override) )
54
+ task Activity::TaskBuilder::Binary( method(:raise_on_missing_id) )
55
+ end
56
+ end # Normalizer
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ # every step is wrapped by this proc/decider. this is executed in the circuit as the actual task.
4
+ # Step calls step.(options, **options, flow_options)
5
+ # Output direction binary: true=>Right, false=>Left.
6
+ # Passes through all subclasses of Direction.~~~~~~~~~~~~~~~~~
7
+ module TaskBuilder
8
+ def self.call(user_proc)
9
+ Task.new( Trailblazer::Option::KW( user_proc ), user_proc )
10
+ end
11
+
12
+ # Translates the return value of the user step into a valid signal.
13
+ # Note that it passes through subclasses of {Signal}.
14
+ def self.binary_direction_for(result, on_true, on_false)
15
+ result.is_a?(Class) && result < Activity::Signal ? result : (result ? on_true : on_false)
16
+ end
17
+ end
18
+
19
+ class Task
20
+ def initialize(task, user_proc)
21
+ @task = task
22
+ @user_proc = user_proc
23
+ freeze
24
+ end
25
+
26
+ def call( (options, *args), **circuit_args )
27
+ # Execute the user step with TRB's kw args.
28
+ result = @task.( options, **circuit_args ) # circuit_args contains :exec_context.
29
+
30
+ # Return an appropriate signal which direction to go next.
31
+ direction = TaskBuilder.binary_direction_for( result, Activity::Right, Activity::Left )
32
+
33
+ [ direction, [ options, *args ], **circuit_args ]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,12 +1,11 @@
1
1
  class Trailblazer::Operation
2
2
  class Result
3
+ # @param success Boolean validity of the result object
4
+ # @param data Context
3
5
  def initialize(success, data)
4
- @success, @data = success, data # @data is a Skill instance.
6
+ @success, @data = success, data
5
7
  end
6
8
 
7
- extend Forwardable
8
- def_delegators :@data, :[] # DISCUSS: make it a real delegator? see Nested.
9
-
10
9
  def success?
11
10
  @success
12
11
  end
@@ -15,6 +14,9 @@ class Trailblazer::Operation
15
14
  ! success?
16
15
  end
17
16
 
17
+ extend Forwardable
18
+ def_delegators :@data, :[] # DISCUSS: make it a real delegator? see Nested.
19
+
18
20
  # DISCUSS: the two methods below are more for testing.
19
21
  def inspect(*slices)
20
22
  return "<Result:#{success?} #{slice(*slices).inspect} >" if slices.any?
@@ -0,0 +1,46 @@
1
+ module Trailblazer
2
+ class Operation
3
+ module Trace
4
+ # @note The problem in this method is, we have redundancy with Operation::PublicCall
5
+ def self.call(operation, *args)
6
+ ctx = PublicCall.options_for_public_call(*args) # redundant with PublicCall::call.
7
+
8
+ # Prepare the tracing-specific arguments. This is only run once for the entire circuit!
9
+ operation, *args = Trailblazer::Activity::Trace.arguments_for_call( operation, [ctx, {}], {} )
10
+
11
+ last_signal, (ctx, flow_options) = Activity::TaskWrap.invoke(operation, *args )
12
+
13
+ result = Railway::Result(last_signal, ctx) # redundant with PublicCall::call.
14
+
15
+ Result.new(result, flow_options[:stack].to_a)
16
+ end
17
+
18
+ # `Operation::trace` is included for simple tracing of the flow.
19
+ # It simply forwards all arguments to `Trace.call`.
20
+ #
21
+ # @public
22
+ #
23
+ # Operation.trace(params, "current_user" => current_user).wtf
24
+ def trace(*args)
25
+ Trace.(self, *args)
26
+ end
27
+
28
+ # Presentation of the traced stack via the returned result object.
29
+ # This object is wrapped around the original result in {Trace.call}.
30
+ class Result < SimpleDelegator
31
+ def initialize(result, stack)
32
+ super(result)
33
+ @stack = stack
34
+ end
35
+
36
+ def wtf
37
+ Trailblazer::Activity::Trace::Present.(@stack)
38
+ end
39
+
40
+ def wtf?
41
+ puts wtf
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
2
  class Operation
3
- VERSION = "0.0.13"
3
+ VERSION = "0.4.1"
4
4
  end
5
5
  end
@@ -1,28 +1,47 @@
1
1
  require "test_helper"
2
2
 
3
- # DISCUSS: do we need this test?
4
3
  class CallTest < Minitest::Spec
5
4
  describe "::call" do
6
5
  class Create < Trailblazer::Operation
6
+ step ->(*) { true }
7
7
  def inspect
8
8
  "#{@skills.inspect}"
9
9
  end
10
10
  end
11
11
 
12
- it { Create.().must_be_instance_of Trailblazer::Operation::Result }
12
+ it { Create.().must_be_instance_of Trailblazer::Operation::Railway::Result }
13
13
 
14
- it { Create.({}).inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{}} {\"pipetree\"=>[>operation.new]}> >} }
15
- it { Create.(name: "Jacob").inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
16
- it { Create.({ name: "Jacob" }, { policy: Object }).inspect.must_equal %{<Result:true <Skill {} {:policy=>Object, \"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
14
+ # it { Create.({}).inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{}} {\"pipetree\"=>[>operation.new]}> >} }
15
+ # it { Create.(name: "Jacob").inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
16
+ # it { Create.({ name: "Jacob" }, { policy: Object }).inspect.must_equal %{<Result:true <Skill {} {:policy=>Object, \"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
17
17
 
18
18
  #---
19
19
  # success?
20
20
  class Update < Trailblazer::Operation
21
- step ->(options) { options["params"] }, after: "operation.new"
21
+ step ->(options, **) { options[:result] }
22
+ end
23
+
24
+ # operation success
25
+ it do
26
+ result = Update.(result: true)
27
+
28
+ result.success?.must_equal true
29
+
30
+ result.event.must_be_instance_of Trailblazer::Operation::Railway::End::Success
31
+ result.event.must_equal Update.outputs[:success].signal
32
+ end
33
+
34
+ # operation failure
35
+ it do
36
+ result = Update.(result: false)
37
+
38
+ result.success?.must_equal false
39
+ result.failure?.must_equal true
40
+
41
+ result.event.must_be_instance_of Trailblazer::Operation::Railway::End::Failure
42
+ result.event.must_equal Update.outputs[:failure].signal
22
43
  end
23
44
 
24
- it { Update.(true).success?.must_equal true }
25
- it { Update.(false).success?.must_equal false }
26
45
  end
27
46
  end
28
47
 
@@ -0,0 +1,147 @@
1
+ require "test_helper"
2
+
3
+ class CallableHelper < Minitest::Spec
4
+ Operation = Trailblazer::Operation
5
+ Activity = Trailblazer::Activity
6
+
7
+ module Blog
8
+ Read = ->((options, *args), *) { options["Read"] = 1; [ Activity::Right, [options, *args] ] }
9
+ Next = ->((options, *args), *) { options["NextPage"] = []; [ options["return"], [options, *args] ] }
10
+ Comment = ->((options, *args), *) { options["Comment"] = 2; [ Activity::Right, [options, *args] ] }
11
+ end
12
+
13
+ module User
14
+ Relax = ->((options, *args), *) { options["Relax"]=true; [ Activity::Right, [options, *args] ] }
15
+ end
16
+
17
+ ### Callable( )
18
+ ###
19
+ describe "circuit with 1 level of nesting" do # TODO: test this kind of configuration in dsl_tests somewhere.
20
+ let(:blog) do
21
+ Module.new do
22
+ extend Activity::Path()
23
+
24
+ task task: Blog::Read
25
+ task task: Blog::Next, Output(Activity::Right, :done) => "End.success", Output(Activity::Left, :success) => Track(:success)
26
+ task task: Blog::Comment
27
+ end
28
+ end
29
+
30
+ let(:user) do
31
+ _blog = blog
32
+
33
+ Module.new do
34
+ extend Activity::Path()
35
+
36
+ task task: _blog, _blog.outputs[:success] => Track(:success)
37
+ task task: User::Relax
38
+ end
39
+ end
40
+
41
+ it "ends before comment, on next_page" do
42
+ user.( [options = { "return" => Activity::Right }] ).must_equal(
43
+ [user.outputs[:success].signal, [{"return"=>Trailblazer::Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true}]]
44
+ )
45
+
46
+ options.must_equal({"return"=>Trailblazer::Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true})
47
+ end
48
+ end
49
+
50
+ ### Callable( End1, End2 )
51
+ ###
52
+ describe "circuit with 2 end events in the nested process" do
53
+ let(:blog) do
54
+ Module.new do
55
+ extend Activity::Path()
56
+
57
+ task task: Blog::Read
58
+ task task: Blog::Next, Output(Activity::Right, :success___) => :__success, Output(Activity::Left, :retry___) => _retry=End(:retry)
59
+ end
60
+ end
61
+
62
+ let(:user) do
63
+ _blog = blog
64
+
65
+ Module.new do
66
+ extend Activity::Path()
67
+
68
+ task task: _blog, _blog.outputs[:success] => Track(:success), _blog.outputs[:retry] => "End.success"
69
+ task task: User::Relax
70
+ end
71
+ end
72
+
73
+ it "runs from Callable->default to Relax" do
74
+ user.( [ options = { "return" => Activity::Right } ] ).must_equal [
75
+ user.outputs[:success].signal,
76
+ [ {"return"=>Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true} ]
77
+ ]
78
+
79
+ options.must_equal({"return"=>Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true})
80
+ end
81
+
82
+ it "runs from other Callable end" do
83
+ user.( [ options = { "return" => Activity::Left } ] ).must_equal [
84
+ user.outputs[:success].signal,
85
+ [ {"return"=>Activity::Left, "Read"=>1, "NextPage"=>[]} ]
86
+ ]
87
+
88
+ options.must_equal({"return"=>Activity::Left, "Read"=>1, "NextPage"=>[]})
89
+ end
90
+
91
+ #---
92
+ #- Callable( activity, start_at )
93
+ let(:with_nested_and_start_at) do
94
+ _blog = blog
95
+
96
+ Module.new do
97
+ extend Activity::Path()
98
+
99
+ task task: Operation::Callable( _blog, start_task: Blog::Next ), _blog.outputs[:success] => Track(:success)
100
+ task task: User::Relax
101
+ end
102
+ end
103
+
104
+ it "runs Callable from alternative start" do
105
+ with_nested_and_start_at.( [options = { "return" => Activity::Right }] ).
106
+ must_equal [
107
+ with_nested_and_start_at.outputs[:success].signal,
108
+ [ {"return"=>Activity::Right, "NextPage"=>[], "Relax"=>true} ]
109
+ ]
110
+
111
+ options.must_equal({"return"=>Activity::Right, "NextPage"=>[], "Relax"=>true})
112
+ end
113
+
114
+ #---
115
+ #- Callable( activity, call: :__call__ ) { ... }
116
+ describe "Callable with :call option" do
117
+ let(:process) do
118
+ class Workout
119
+ def self.__call__((options, *args), *)
120
+ options[:workout] = 9
121
+
122
+ return Activity::Right, [options, *args]
123
+ end
124
+ end
125
+
126
+ subprocess = Operation::Callable( Workout, call: :__call__ )
127
+
128
+ Module.new do
129
+ extend Activity::Path()
130
+
131
+ task task: subprocess
132
+ task task: User::Relax
133
+ end
134
+ end
135
+
136
+ it "runs Callable process with __call__" do
137
+ process.( [options = { "return" => Activity::Right }] ).
138
+ must_equal [
139
+ process.outputs[:success].signal,
140
+ [{"return"=>Activity::Right, :workout=>9, "Relax"=>true}]
141
+ ]
142
+
143
+ options.must_equal({"return"=>Activity::Right, :workout=>9, "Relax"=>true})
144
+ end
145
+ end
146
+ end
147
+ end