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
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