trailblazer-operation 0.0.13 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c4a0c7253d802e980c74cd74a538803a76fee4a4
4
- data.tar.gz: 0e44522d1373cfa7733fdc5591bad5df8b94dd57
2
+ SHA256:
3
+ metadata.gz: d3347429bc2a448f6f41390b5d70aea2076d32235acb78f51563fbcb8f935f8e
4
+ data.tar.gz: 68200b340b34a5965cd773921b8282a4eb0f0ad56ae87e1b7ad2585a362cc02a
5
5
  SHA512:
6
- metadata.gz: 1a6187f6f92373ed7dc908ca819ff8b72503e3dfdf72addc8aa2c3140c05a5502d241e8109482fe51e3ea34f6aaa012f3c8751df898d3eb7aac4ec62641a0a23
7
- data.tar.gz: 7486af11d465c14f21506bcf48201d2b917766e9906e72c610d14f6d6598c1a498a7558f578c37f562def2cbe0b7929e6313d5714e584f63a048153be7770e90
6
+ metadata.gz: 6824c50e6c8f48b587afc1e8a2f08b3b1813379c811f6b1017c6088bdd9836c445839a20cec3ac1407b33290ea5c4a1d3ee47da77854de8bfde7bf24a4230662
7
+ data.tar.gz: b0fb9530502238b3bfab54311c93793a4c4f028091ba72a7f9e71a3eb1f760edb077577d2e542d06d6650c392b160f464a44c6f1c7d90d79b9447e468f6935c3
@@ -3,13 +3,15 @@ before_install:
3
3
  - gem install bundler
4
4
  matrix:
5
5
  include:
6
- - rvm: 1.9.3
7
- gemfile: "test/gemfiles/Gemfile.ruby-1.9"
8
6
  # - rvm: 2.0.0
9
7
  # gemfile: "test/gemfiles/Gemfile.ruby-2.0"
10
8
  - rvm: 2.1
11
9
  gemfile: Gemfile
12
10
  - rvm: 2.2.4
13
11
  gemfile: Gemfile
14
- - rvm: 2.3.1
12
+ - rvm: 2.3.3
15
13
  gemfile: Gemfile
14
+ - rvm: 2.4.0
15
+ gemfile: Gemfile
16
+ - rvm: jruby-9.1.13.0
17
+ env: JRUBY_OPTS="--profile.api"
data/CHANGES.md CHANGED
@@ -1,3 +1,103 @@
1
+ TODO:
2
+ * api to add your own task.
3
+
4
+ lots of work on the DSL specific parts.
5
+ Graph and Sequence to make it easier to wire anything.
6
+ macros can now add/modify the wiring, e.g. their end to the our end or the next task.
7
+ [ use circuit for actual step_args/initialize process, too? ]
8
+ You can now add an unlimited number of "your own" end events, which can then be interpreted on the outside (e.g. Endpoint)
9
+ * Introduced the `fast_track: true` option for steps. If you were returning `Railway.fail_fast!` and the like, you now need to declare that option, e.g.
10
+
11
+ ```ruby
12
+ step :my_validate!, fast_track: true
13
+ ```
14
+
15
+ params:, rest: ..
16
+
17
+
18
+ ## 0.4.1
19
+
20
+ * Use `activity-0.7.1`.
21
+
22
+ ## 0.4.0
23
+
24
+ * Use `activity-0.7.0`.
25
+
26
+ ## 0.3.1
27
+
28
+ * Moved `VariableMapping` to the `activity` gem.
29
+
30
+ ## 0.3.0
31
+
32
+ * Use `activity` 0.6.0.
33
+ * Remove `Operation::__call__` in favor of one `call` that dispatches to either
34
+ * `call_with_public_interface` this implements the complicated public `Operation.()` semantic and will be faded out with the rise of workflow engines.
35
+ * `call_with_circuit_interface` is the circuit-compatible version that will be invoked on nested operations.
36
+
37
+ This might seem a bit "magical" but simplifies the interface a lot. In better languages, you could use method overloading for that, in Ruby, we have to
38
+ do that ourselves. This decision was made with the deprecation of `Operation.()` in mind. In the future, operations will mostly be invoked from
39
+ workflow engines and not directly, where the engine takes care of applying the correct interface.
40
+
41
+ ## 0.2.5
42
+
43
+ * Minor fixes for activity 0.5.2.
44
+
45
+ ## 0.2.4
46
+
47
+ * Use `Activity::FastTrack` signals.
48
+
49
+ ## 0.2.2
50
+
51
+ * Use `activity-0.4.2`.
52
+
53
+ ## 0.2.1
54
+
55
+ * Use `activity-0.4.1`.
56
+
57
+ ## 0.2.0
58
+
59
+ * Cleanly separate `Activity` and `Operation` responsibilities. An operation is nothing more but a class around an activity, hosting instance methods and implementing inheritance.
60
+
61
+ ## 0.1.4
62
+
63
+ * `TaskWrap.arguments_for_call` now returns the correct `circuit_options` where the `:runner` etc.'s already merged.
64
+
65
+ ## 0.1.3
66
+
67
+ * New taskWrap API for `activity` 0.3.2.
68
+
69
+ ## 0.1.2
70
+
71
+ * Add @mensfeld's "Macaroni" step style for a keyword-only signature for steps.
72
+
73
+ ## 0.1.0
74
+
75
+ inspect: failure is << and success is >>
76
+
77
+ call vs __call__: it's now designed to be run in a composition where the skills stuff is done only once, and the reslt object is not necessary
78
+
79
+ FastTrack optional
80
+ Wrapped optional
81
+
82
+ * Add `pass` and `fail` as four-character aliases for `success` and `failure`.
83
+ * Remove `Uber::Callable` requirement and treat all non-`:symbol` steps as callable objects.
84
+ * Remove non-kw options for steps. All steps receive keyword args now:
85
+
86
+ ```ruby
87
+ def model(options)
88
+ ```
89
+
90
+ now must have a minimal signature as follows.
91
+
92
+ ```ruby
93
+ def model(options, **)
94
+ ```
95
+ * Remove `Operation#[]` and `Operation#[]=`. Please only change state in `options`.
96
+ * API change for `step Macro()`: the macro's return value is now called with the low-level "Task API" signature `(direction, options, flow_options)`. You need to return `[direction, options, flow_options]`. There's a soft-deprecation warning.
97
+ * Remove support for Ruby 1.9.3 for now. This can be re-introduced on demand.
98
+ * Remove `pipetree` in favor of [`trailblazer-circuit`](https://github.com/trailblazer/trailblazer-circuit). This allows rich workflows and state machines in an operation.
99
+ * Remove `uber` dependency.
100
+
1
101
  ## 0.0.13
2
102
 
3
103
  * Rename `Operation::New` to `:Instantiate` to avoid name clashes with `New` operations in applications.
data/Gemfile CHANGED
@@ -9,5 +9,7 @@ gem "dry-auto_inject"
9
9
 
10
10
  gem "minitest-line"
11
11
  gem "benchmark-ips"
12
- # gem "pipetree", path: "../pipetree"
13
- # gem "pipetree", github: "apotonick/pipetree"
12
+
13
+ # gem "trailblazer-developer", path: "../developer"
14
+ # gem "trailblazer-developer", git: "https://github.com/trailblazer/trailblazer-developer"
15
+ gem "trailblazer-activity", path: "../trailblazer-activity"
data/Rakefile CHANGED
@@ -4,15 +4,17 @@ require "rake/testtask"
4
4
  task :default => [:test]
5
5
 
6
6
  Rake::TestTask.new(:test) do |test|
7
- test.libs << 'test'
7
+ test.libs << "test"
8
8
  test.verbose = true
9
9
 
10
- test_files = FileList['test/*_test.rb']
10
+ test_files = FileList["test/**/*_test.rb"]
11
11
 
12
- if RUBY_VERSION == "1.9.3"
13
- test_files = test_files - %w{test/dry_container_test.rb test/2.1.0-pipetree_test.rb test/2.0.0-pipetree_test.rb}
14
- elsif RUBY_VERSION == "2.0.0"
15
- test_files = test_files - %w{test/dry_container_test.rb test/2.1.0-pipetree_test.rb}
12
+ if RUBY_VERSION == "2.0.0"
13
+ # test_files = test_files - %w{test/dry_container_test.rb test/2.1.0-pipetree_test.rb}
14
+ test_files = test_files - %w{test/step_test.rb} + %w{test/ruby-2.0.0/step_test.rb}
15
+ test_files = test_files - %w{test/operation_test.rb} + %w{test/ruby-2.0.0/operation_test.rb}
16
+ else
17
+ test_files -= FileList["test/ruby-2.0.0/*"]
16
18
  end
17
19
 
18
20
  test.test_files = test_files
@@ -1,24 +1,91 @@
1
1
  require "forwardable"
2
- require "declarative"
3
- require "trailblazer/operation/skill"
4
- require "trailblazer/operation/pipetree"
5
- require "trailblazer/operation/generic"
2
+
3
+ # trailblazer-context
4
+ require "trailblazer/option"
5
+ require "trailblazer/context"
6
+ require "trailblazer/container_chain"
7
+
8
+ require "trailblazer/activity"
9
+ require "trailblazer/activity/dsl/magnetic"
10
+
11
+
12
+ require "trailblazer/operation/callable"
13
+
14
+ require "trailblazer/operation/heritage"
15
+ require "trailblazer/operation/public_call" # TODO: Remove in 3.0.
16
+ require "trailblazer/operation/class_dependencies"
17
+ require "trailblazer/operation/deprecated_macro" # TODO: remove in 2.2.
18
+ require "trailblazer/operation/result"
19
+ require "trailblazer/operation/railway"
20
+
21
+ require "trailblazer/operation/railway/fast_track"
22
+ require "trailblazer/operation/railway/normalizer"
23
+ require "trailblazer/operation/trace"
24
+
25
+ require "trailblazer/operation/railway/macaroni"
6
26
 
7
27
  module Trailblazer
8
28
  # The Trailblazer-style operation.
9
29
  # Note that you don't have to use our "opinionated" version with result object, skills, etc.
10
30
  class Operation
11
- extend Declarative::Heritage::Inherited
12
- extend Declarative::Heritage::DSL
13
31
 
14
- extend Skill::Accessors # ::[] and ::[]=
32
+ module FastTrackActivity
33
+ builder_options = {
34
+ track_end: Railway::End::Success.new(semantic: :success),
35
+ failure_end: Railway::End::Failure.new(semantic: :failure),
36
+ pass_fast_end: Railway::End::PassFast.new(semantic: :pass_fast),
37
+ fail_fast_end: Railway::End::FailFast.new(semantic: :fail_fast),
38
+ }
39
+
40
+ extend Activity::FastTrack( pipeline: Railway::Normalizer::Pipeline, builder_options: builder_options )
41
+ end
42
+
43
+ extend Skill::Accessors # ::[] and ::[]= # TODO: fade out this usage.
44
+
45
+ def self.inherited(subclass)
46
+ super
47
+ subclass.initialize!
48
+ heritage.(subclass)
49
+ end
15
50
 
16
- include Pipetree # ::call, ::step, ...
17
- # we want the skill dependency-mechanism.
18
- extend Skill::Call # ::call(params: {}, current_user: ..)
19
- extend Skill::Call::Positional # ::call(params, options)
51
+ def self.initialize!
52
+ @activity = FastTrackActivity.clone
53
+ end
20
54
 
21
- # we want the initializer and the ::call method.
22
- include Generic # #initialize, #call, #process.
55
+
56
+ extend Activity::Interface
57
+
58
+ module Process
59
+ def to_h
60
+ @activity.to_h.merge( activity: @activity )
61
+ end
62
+ end
63
+
64
+ extend Process # make ::call etc. class methods on Operation.
65
+
66
+ extend Heritage::Accessor
67
+
68
+ class << self
69
+ extend Forwardable # TODO: test those helpers
70
+ def_delegators :@activity, :Path, :Output, :End, :Track
71
+ def_delegators :@activity, :outputs
72
+
73
+ def step(task, options={}, &block); add_task!(:step, task, options, &block) end
74
+ def pass(task, options={}, &block); add_task!(:pass, task, options, &block) end
75
+ def fail(task, options={}, &block); add_task!(:fail, task, options, &block) end
76
+
77
+ alias_method :success, :pass
78
+ alias_method :failure, :fail
79
+
80
+ def add_task!(name, task, options, &block)
81
+ heritage.record(name, task, options, &block)
82
+ @activity.send(name, task, options, &block)
83
+ end
84
+ end
85
+
86
+ extend PublicCall # ::call(params, { current_user: .. })
87
+ extend Trace # ::trace
23
88
  end
24
89
  end
90
+
91
+ require "trailblazer/operation/inspect"
@@ -0,0 +1,42 @@
1
+ module Trailblazer
2
+ class Operation
3
+ # Use {Callable} if you have an operation or any other callable object that does
4
+ # _not_ expose an {Activity interface}. For example, {Operation.call} isn't compatible
5
+ # with activities, hence you need to decorate it using {Callable}. The returned object
6
+ # exposes an {Activity interface}.
7
+ #
8
+ # @param :call [Symbol] Method name to call
9
+ # @param options [Hash] Hash to merge into {circuit_options}, e.g. {:start_task}.
10
+ #
11
+ # @example Create and use a Callable instance.
12
+ # callable = Trailblazer::Operation::Callable( Memo::Create, call: :__call__ )
13
+ # callable.( [ctx, {}] ) #=> Activity interface, ::call will invoke Memo::Create.__call__.
14
+ def self.Callable(*args)
15
+ Callable.new(*args)
16
+ end
17
+
18
+ # Subprocess allows to have tasks with a different call interface and start event.
19
+ # @param activity any object with an {Activity interface}
20
+ class Callable
21
+ include Activity::Interface
22
+
23
+ def initialize(activity, call: :call, **options)
24
+ @activity = activity
25
+ @options = options
26
+ @call = call
27
+ end
28
+
29
+ def call(args, **circuit_options)
30
+ @activity.public_send(@call, args, circuit_options.merge(@options))
31
+ end
32
+
33
+ extend Forwardable
34
+ # @private
35
+ def_delegators :@activity, :to_h, :debug
36
+
37
+ def to_s
38
+ %{#<Trailblazer::Activity::Callable activity=#{@activity}>}
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ # Dependencies can be defined on the operation. class level
2
+ class Trailblazer::Operation
3
+ module Skill
4
+ # The class-level skill container: Operation::[], ::[]=.
5
+ module Accessors
6
+ # :private:
7
+ def skills
8
+ @skills ||= {}
9
+ end
10
+
11
+ extend Forwardable
12
+ def_delegators :skills, :[], :[]=
13
+ end
14
+ end
15
+
16
+ # The use of this module is not encouraged and it is only here for backward-compatibility.
17
+ # Instead, please pass dependencies via containers, locals, or macros into the respective steps.
18
+ module ClassDependencies
19
+ def call_with_circuit_interface( (ctx, flow_options), **circuit_options )
20
+ @skills.each { |name, value| ctx[name] ||= value } # this resembles the behavior in 2.0. we didn't say we liked it.
21
+
22
+ super
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # TODO: REMOVE IN 2.2.
2
+ module Trailblazer
3
+ module Operation::DeprecatedMacro
4
+ # Allows old macros with the `(input, options)` signature.
5
+ def self.call(proc, options)
6
+ warn %{[Trailblazer] Macros with API (input, options) are deprecated. Please use the "Task API" signature (options, flow_options) or use a simpler Callable. (#{proc})}
7
+
8
+ wrapped_proc = ->( (options, flow_options), **circuit_options ) do
9
+ result = proc.(circuit_options[:exec_context], options) # run the macro, with the deprecated signature.
10
+
11
+ direction = Activity::TaskBuilder.binary_signal_for(result, Activity::Right, Activity::Left)
12
+
13
+ return direction, [options, flow_options]
14
+ end
15
+
16
+ options.merge( task: wrapped_proc )
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module Trailblazer
2
+ # This is copied from the Declarative gem. This might get removed in favor of a real heritage gem.
3
+ class Operation
4
+ class Heritage < Array
5
+ # Record inheritable assignments for replay in an inheriting class.
6
+ def record(method, *args, &block)
7
+ self << { method: method, args: args, block: block }
8
+ end
9
+
10
+ # Replay the recorded assignments on inheritor.
11
+ # Accepts a block that will allow processing the arguments for every recorded statement.
12
+ def call(inheritor, &block)
13
+ each { |cfg| call!(inheritor, cfg, &block) }
14
+ end
15
+
16
+ private
17
+ def call!(inheritor, cfg)
18
+ yield cfg if block_given? # allow messing around with recorded arguments.
19
+
20
+ inheritor.send(cfg[:method], *cfg[:args], &cfg[:block])
21
+ end
22
+
23
+ module Accessor
24
+ def heritage
25
+ @heritage ||= Heritage.new
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ module Trailblazer
2
+ module Operation::Wrap
3
+ module Inject
4
+ # Returns an Alteration wirings that, when applied, inserts the {ReverseMergeDefaults} task
5
+ # before the {Wrap::Call} task. This is meant for macros and steps that accept a dependency
6
+ # injection but need a default parameter to be set if not injected.
7
+ # @returns ADDS
8
+ def self.Defaults(default_dependencies)
9
+ Module.new do
10
+ extend Activity::Path::Plan()
11
+
12
+ task ReverseMergeDefaults.new( default_dependencies ),
13
+ id: "ReverseMergeDefaults#{default_dependencies}",
14
+ before: "task_wrap.call_task"
15
+ end
16
+ end
17
+
18
+ # @api private
19
+ # @returns Task
20
+ # @param Hash list of key/value that should be set if not already assigned/set before (or injected from the outside).
21
+ class ReverseMergeDefaults
22
+ def initialize(defaults)
23
+ @defaults = defaults
24
+ end
25
+
26
+ def call((wrap_ctx, original_args), **circuit_options)
27
+ ctx = original_args[0][0]
28
+
29
+ @defaults.each { |k, v| ctx[k] ||= v }
30
+
31
+ return Activity::Right, [ wrap_ctx, original_args ]
32
+ end
33
+ end
34
+ end # Inject
35
+ end
36
+ end
@@ -0,0 +1,79 @@
1
+ module Trailblazer
2
+ # Operation-specific circuit rendering. This is optimized for a linear railway circuit.
3
+ #
4
+ # @private
5
+ #
6
+ # NOTE: this is absolutely to be considered as prototyping and acts more like a test helper ATM as
7
+ # Inspect is not a mission-critical part.
8
+ class Operation
9
+ def self.introspect(*args)
10
+ Operation::Inspect.(*args)
11
+ end
12
+ end
13
+
14
+ module Operation::Inspect
15
+ module_function
16
+
17
+ def call(operation, options={ style: :line })
18
+ # TODO: better introspection API.
19
+
20
+ alterations = Activity::Magnetic::Builder::Finalizer.adds_to_alterations(operation.to_h[:adds])
21
+ # DISCUSS: any other way to retrieve the Alterations?
22
+
23
+ # pp alterations
24
+ railway = alterations.instance_variable_get(:@groups).instance_variable_get(:@groups)[:main]
25
+
26
+ rows = railway.each_with_index.collect do |element, i|
27
+ magnetic_to, task, plus_poles = element.configuration
28
+
29
+ created_by =
30
+ if magnetic_to == [:failure]
31
+ :fail
32
+ elsif plus_poles.size > 1
33
+ plus_poles[0].color == plus_poles[1].color ? :pass : :step
34
+ else
35
+ :pass # this is wrong for Nested, sometimes
36
+ end
37
+
38
+ [ i, [ created_by, element.id ] ]
39
+ end
40
+
41
+ return inspect_line(rows) if options[:style] == :line
42
+ return inspect_rows(rows)
43
+ end
44
+
45
+ def inspect_func(step)
46
+ @inspect[step]
47
+ end
48
+
49
+ Operator = { :fail => "<<", :pass => ">>", :step => ">"}
50
+
51
+ def inspect_line(names)
52
+ string = names.collect { |i, (end_of_edge, name)| "#{Operator[end_of_edge]}#{name}" }.join(",")
53
+ "[#{string}]"
54
+ end
55
+
56
+ def inspect_rows(names)
57
+ string = names.collect do |i, (end_of_edge, name)|
58
+ operator = Operator[end_of_edge]
59
+
60
+ op = "#{operator}#{name}"
61
+ padding = 38
62
+
63
+ proc = if operator == "<<"
64
+ sprintf("%- #{padding}s", op)
65
+ elsif [">", ">>", "&"].include?(operator.to_s)
66
+ sprintf("% #{padding}s", op)
67
+ else
68
+ pad = " " * ((padding - op.length) / 2)
69
+ "#{pad}#{op}#{pad}"
70
+ end
71
+
72
+ proc = proc.gsub(" ", "=")
73
+
74
+ sprintf("%2d %s", i, proc)
75
+ end.join("\n")
76
+ "\n#{string}"
77
+ end
78
+ end
79
+ end