ripe 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -5
- data/Guardfile +2 -4
- data/README.md +1 -0
- data/bin/ripe +1 -59
- data/lib/ripe.rb +8 -1
- data/lib/ripe/blocks.rb +13 -0
- data/lib/ripe/blocks/block.rb +142 -0
- data/lib/ripe/blocks/liquid_block.rb +48 -0
- data/lib/ripe/blocks/multi_block.rb +71 -0
- data/lib/ripe/blocks/parallel_block.rb +29 -0
- data/lib/ripe/blocks/serial_block.rb +61 -0
- data/lib/ripe/blocks/working_block.rb +101 -0
- data/lib/ripe/cli.rb +121 -0
- data/lib/ripe/cli/helper.rb +31 -0
- data/lib/ripe/db.rb +7 -0
- data/lib/ripe/db/task.rb +42 -0
- data/lib/ripe/db/task_migration.rb +33 -0
- data/lib/ripe/db/worker.rb +64 -0
- data/lib/ripe/db/worker_migration.rb +41 -0
- data/lib/ripe/dsl.rb +2 -4
- data/lib/ripe/dsl/task_dsl.rb +4 -2
- data/lib/ripe/dsl/workflow_dsl.rb +5 -0
- data/lib/ripe/library.rb +34 -45
- data/lib/ripe/repo.rb +24 -23
- data/lib/ripe/version.rb +1 -1
- data/lib/ripe/worker_controller.rb +72 -144
- data/lib/ripe/worker_controller/preparer.rb +172 -0
- data/lib/ripe/worker_controller/syncer.rb +118 -0
- data/spec/cli_spec.rb +14 -0
- data/spec/library_spec.rb +18 -18
- data/spec/spec_helper.rb +2 -0
- data/spec/testpack.rb +16 -5
- data/spec/testpack/.ripe/meta.db +0 -0
- data/spec/testpack/.ripe/tasks/bar.sh +3 -0
- data/spec/testpack/{ripe → .ripe}/tasks/foo.sh +0 -0
- data/spec/testpack/.ripe/workers/1/1.sh +16 -0
- data/spec/testpack/.ripe/workers/1/2.sh +16 -0
- data/spec/testpack/.ripe/workers/1/job.sh +54 -0
- data/spec/testpack/.ripe/workers/2/3.sh +16 -0
- data/spec/testpack/.ripe/workers/2/4.sh +16 -0
- data/spec/testpack/.ripe/workers/2/job.sh +54 -0
- data/spec/testpack/.ripe/workers/3/5.sh +16 -0
- data/spec/testpack/.ripe/workers/3/6.sh +16 -0
- data/spec/testpack/.ripe/workers/3/job.sh +54 -0
- data/spec/testpack/.ripe/workflows/foobar.rb +23 -0
- data/spec/testpack/{case/Sample1 → Sample1}/bar_output.txt +0 -0
- data/spec/testpack/{case/Sample1 → Sample1}/foo_input.txt +0 -0
- data/spec/testpack/{case/Sample1 → Sample1}/foo_output.txt +0 -0
- data/spec/testpack/{case/Sample2 → Sample2}/bar_output.txt +0 -0
- data/spec/testpack/{case/Sample2 → Sample2}/foo_input.txt +0 -0
- data/spec/testpack/{case/Sample2 → Sample2}/foo_output.txt +0 -0
- data/spec/testpack/{case/Sample3 → Sample3}/bar_output.txt +0 -0
- data/spec/testpack/{case/Sample3 → Sample3}/foo_input.txt +0 -0
- data/spec/testpack/{case/Sample3 → Sample3}/foo_output.txt +0 -0
- data/spec/worker_controller_spec.rb +143 -0
- metadata +66 -40
- data/lib/ripe/block.rb +0 -41
- data/lib/ripe/liquid_block.rb +0 -17
- data/lib/ripe/multi_block.rb +0 -35
- data/lib/ripe/parallel_block.rb +0 -13
- data/lib/ripe/serial_block.rb +0 -37
- data/lib/ripe/task.rb +0 -21
- data/lib/ripe/task_migration.rb +0 -18
- data/lib/ripe/worker.rb +0 -44
- data/lib/ripe/worker_migration.rb +0 -26
- data/lib/ripe/working_block.rb +0 -41
- data/spec/block_spec.rb +0 -7
- data/spec/ripe_spec.rb +0 -7
- data/spec/testpack/ripe/tasks/bar.sh +0 -3
- data/spec/testpack/ripe/workflows/foobar.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28ae834d0a84d2072400169910c39825c1c37aa3
|
4
|
+
data.tar.gz: 2b1be6510c88ad3679dc69e4b198eea81d050734
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cac0941044a07fe4b97a8fda9f68e52881bc6be0943efafb6d104e0fa6e0c45e4f219ad25431b6099aa30e6fac3847fa79cc28d472b13f3cce59f852fe52fb60
|
7
|
+
data.tar.gz: c43fcd8a697350737a8b02fef384d40bcc6666954f0ce2e8204efb1a88f9a4661099c4c2598cbe654d1ca86dcc9e89d8c28ab456b1ea34c4a4dd5b59e8780603
|
data/.travis.yml
CHANGED
data/Guardfile
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
guard :rspec, cmd: 'rspec' do
|
2
|
-
watch(%r{^spec
|
3
|
-
watch(%r{^lib/ripe/(.+)\.rb$}) { |m| "spec
|
4
|
-
watch('spec/spec_helper.rb') { "spec" }
|
5
|
-
watch('spec/testpack.rb') { "spec" }
|
2
|
+
watch(%r{^spec/.+\.rb$}) { 'spec' }
|
3
|
+
watch(%r{^lib/ripe/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
6
4
|
end
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Ripe
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/ripe.svg)](http://badge.fury.io/rb/ripe)
|
2
3
|
[![Build Status](https://travis-ci.org/ndejay/ripe.svg)](https://travis-ci.org/ndejay/ripe)
|
3
4
|
[![Code Climate](https://codeclimate.com/github/ndejay/ripe/badges/gpa.svg)](https://codeclimate.com/github/ndejay/ripe)
|
4
5
|
[![Test Coverage](https://codeclimate.com/github/ndejay/ripe/badges/coverage.svg)](https://codeclimate.com/github/ndejay/ripe)
|
data/bin/ripe
CHANGED
@@ -1,63 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require_relative '../lib/ripe'
|
4
|
-
require_relative '../lib/ripe/dsl'
|
5
|
-
require 'ripl' # REPL
|
6
|
-
require 'hirb' # Pretty output for +ActiveRecord+ objects
|
7
|
-
require 'thor'
|
8
4
|
|
9
|
-
|
10
|
-
include Ripe::DSL
|
11
|
-
|
12
|
-
class CLI < Thor
|
13
|
-
desc 'console', 'Enter ripe console'
|
14
|
-
def console
|
15
|
-
repo = Repo.new
|
16
|
-
repo.attach
|
17
|
-
|
18
|
-
# Do not send arguments to the REPL
|
19
|
-
ARGV.clear
|
20
|
-
|
21
|
-
Ripl.config[:prompt] = proc do
|
22
|
-
# This is the only place I could think of placing +Hirb#enable+.
|
23
|
-
Hirb.enable unless Hirb::View.enabled?
|
24
|
-
'ripe> '
|
25
|
-
end
|
26
|
-
|
27
|
-
# Launch the REPL session in the context of +WorkerController+.
|
28
|
-
Ripl.start :binding => repo.controller.instance_eval { binding }
|
29
|
-
end
|
30
|
-
|
31
|
-
desc 'prepare SAMPLES', 'Prepare jobs from template workflow'
|
32
|
-
option :workflow, :aliases => '-w', :type => :string, :required => true,
|
33
|
-
:desc => 'Workflow to be applied'
|
34
|
-
option :options, :aliases => '-o', :type => :string, :required => false,
|
35
|
-
:desc => 'Options', :default => ''
|
36
|
-
def prepare(*samples)
|
37
|
-
abort "No samples specified." if (samples.length == 0)
|
38
|
-
|
39
|
-
additional_vars = options[:options].split(/,/).map do |pair|
|
40
|
-
key, value = pair.split(/=/)
|
41
|
-
{ key.to_sym => value }
|
42
|
-
end
|
43
|
-
additional_vars = additional_vars.inject(&:merge) || {}
|
44
|
-
|
45
|
-
repo = Repo.new
|
46
|
-
|
47
|
-
filename = repo.library.find_workflow(options[:workflow])
|
48
|
-
abort "Could not find workflow #{@handle}." if filename == nil
|
49
|
-
require_relative filename # Imports +$workflow+ from the workflow component
|
50
|
-
|
51
|
-
repo.attach_or_create # Create .ripe if it doesn't exist
|
52
|
-
repo.controller.prepare(samples,
|
53
|
-
$workflow.callback,
|
54
|
-
$workflow.params.merge(additional_vars))
|
55
|
-
end
|
56
|
-
|
57
|
-
desc 'version', 'Retrieve ripe version'
|
58
|
-
def version
|
59
|
-
puts "ripe version #{Ripe::VERSION}"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
CLI.start(ARGV)
|
5
|
+
Ripe::CLI.start(ARGV)
|
data/lib/ripe.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
-
require_relative 'ripe/
|
1
|
+
require_relative 'ripe/blocks'
|
2
|
+
require_relative 'ripe/db'
|
3
|
+
require_relative 'ripe/dsl'
|
4
|
+
require_relative 'ripe/library'
|
2
5
|
require_relative 'ripe/repo'
|
6
|
+
require_relative 'ripe/worker_controller'
|
7
|
+
|
8
|
+
require_relative 'ripe/cli'
|
9
|
+
require_relative 'ripe/version'
|
3
10
|
|
4
11
|
module Ripe
|
5
12
|
PATH = File.expand_path('..', File.dirname(__FILE__))
|
data/lib/ripe/blocks.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Ripe
|
2
|
+
module Blocks
|
3
|
+
# Forward declaration to prevent cyclic dependencies
|
4
|
+
class Block; end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative 'blocks/multi_block'
|
9
|
+
require_relative 'blocks/block'
|
10
|
+
require_relative 'blocks/parallel_block'
|
11
|
+
require_relative 'blocks/serial_block'
|
12
|
+
require_relative 'blocks/working_block'
|
13
|
+
require_relative 'blocks/liquid_block'
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Ripe
|
2
|
+
|
3
|
+
module Blocks
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class represents the fundamental building block of ripe.
|
7
|
+
#
|
8
|
+
# @abstract
|
9
|
+
#
|
10
|
+
# @attr_reader id [String] a mandatory, but optionally unique identifier
|
11
|
+
# for the block
|
12
|
+
# @attr_reader blocks [Array<Block>] list of children blocks
|
13
|
+
# @attr vars [Hash<Symbol, String>] key-value pairs
|
14
|
+
#
|
15
|
+
# @see Ripe::WorkerController::Preparer
|
16
|
+
|
17
|
+
class Block
|
18
|
+
|
19
|
+
attr_reader :id, :blocks
|
20
|
+
|
21
|
+
attr_accessor :vars
|
22
|
+
|
23
|
+
##
|
24
|
+
# @param id [String] a mandatory, but optionally unique identifier
|
25
|
+
# for the block
|
26
|
+
# @param blocks [Array<Block>] list of children blocks
|
27
|
+
# @param vars [Hash<Symbol, String>] key-value pairs
|
28
|
+
|
29
|
+
def initialize(id, blocks = [], vars = {})
|
30
|
+
@id, @blocks, @vars = id, blocks, vars
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Return the string command of the subtree starting at the current block.
|
35
|
+
#
|
36
|
+
# @return [String] subtree command
|
37
|
+
|
38
|
+
def command
|
39
|
+
''
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Prune the subtree starting at the current block.
|
44
|
+
#
|
45
|
+
# @param protect [Boolean] if the current block (and recursively, its
|
46
|
+
# children) should be protected from pruning -- setting this parameter
|
47
|
+
# to +true+ guarantees that the block will not be pruned
|
48
|
+
# @param depend [Boolean] if the current block is unprotected because
|
49
|
+
# its parent (serially) needs to be executed
|
50
|
+
# @return [Block, nil] a +Block+ representing the subtree that has not
|
51
|
+
# been pruned, and +nil+ otherwise
|
52
|
+
|
53
|
+
def prune(protect, depend)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Test whether all targets for the current block exist.
|
59
|
+
#
|
60
|
+
# @return [Boolean] whether all targets exist
|
61
|
+
|
62
|
+
def targets_exist?
|
63
|
+
# {Block} is an abstract class. By default, assume that no targets
|
64
|
+
# exist.
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Return the topology of the subtree starting at the current block in a
|
70
|
+
# nested list format. The first element of any nested list is the
|
71
|
+
# identifier of the corresponding block in the subtree, and the
|
72
|
+
# subsequent elements are each a subtree corresponding to the children
|
73
|
+
# blocks of the current subtree.
|
74
|
+
#
|
75
|
+
# @return [Array<Symbol, Array>] topology nested list
|
76
|
+
|
77
|
+
def topology
|
78
|
+
[]
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Compose a new parallel block from two blocks. This method provides
|
83
|
+
# syntactic sugar in the form:
|
84
|
+
#
|
85
|
+
# Block1 | Block2 | Block3
|
86
|
+
#
|
87
|
+
# @param block [Block] a block
|
88
|
+
# @return [Block] parallel block composition of the current block with
|
89
|
+
# the block passed in the argument list
|
90
|
+
|
91
|
+
def |(block)
|
92
|
+
ParallelBlock.new(self, block)
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Compose a new serial block from two blocks. This method provides
|
97
|
+
# syntactic sugar in the form:
|
98
|
+
#
|
99
|
+
# Block1 + Block2 + Block3
|
100
|
+
#
|
101
|
+
# @param (see #|)
|
102
|
+
# @return [Block] serial block composition of the current block with
|
103
|
+
# the block passed in the argument list
|
104
|
+
|
105
|
+
def +(block)
|
106
|
+
SerialBlock.new(self, block)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# +NilClass+ is monkey-patched+ to provide syntactic sugar for +Block#|+ and
|
117
|
+
# +Block#\++ by treating +nil+ like an empty block.
|
118
|
+
#
|
119
|
+
# @see Ripe::Blocks::Block
|
120
|
+
|
121
|
+
class NilClass
|
122
|
+
|
123
|
+
##
|
124
|
+
# If attempting to compose a new block with a +nil+ element, ignore the +nil+
|
125
|
+
# element.
|
126
|
+
#
|
127
|
+
# @param (see Ripe::Blocks::Block#|)
|
128
|
+
# @return [Block] the +block+ parameter
|
129
|
+
|
130
|
+
def |(block)
|
131
|
+
raise NoMethodError unless Ripe::Blocks::Block > block.class
|
132
|
+
block
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# (see #|)
|
137
|
+
|
138
|
+
def +(block)
|
139
|
+
self.|(block)
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'liquid'
|
2
|
+
|
3
|
+
module Ripe
|
4
|
+
|
5
|
+
module Blocks
|
6
|
+
|
7
|
+
##
|
8
|
+
# This class represents a working block that should be processed using the
|
9
|
+
# Liquid templating engine, rather than the simple +bash+ engine defined in
|
10
|
+
# {WorkingBlock}.
|
11
|
+
#
|
12
|
+
# @see Ripe::Blocks::WorkingBlock
|
13
|
+
|
14
|
+
class LiquidBlock < WorkingBlock
|
15
|
+
|
16
|
+
##
|
17
|
+
# @param filename [String] filename of the template file
|
18
|
+
# @param vars [Hash<Symbol, String>] key-value pairs
|
19
|
+
|
20
|
+
def initialize(filename, vars = {})
|
21
|
+
super(filename, vars)
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Return liquid block +parameters+ as a +Hash<Symbol, Object>+.
|
26
|
+
#
|
27
|
+
# @return [Hash<Symbol, Object>] liquid block +parameters+
|
28
|
+
|
29
|
+
def declarations
|
30
|
+
@vars.inject({}) { |memo, (k, v)| memo[k.to_s] = v; memo }
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# (see Block#command)
|
35
|
+
#
|
36
|
+
# The resulting string contains the render result of the liquid template
|
37
|
+
# based on the parameters specified in +vars+.
|
38
|
+
|
39
|
+
def command
|
40
|
+
template = Liquid::Template.parse(File.new(@filename).read)
|
41
|
+
template.render(declarations)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Ripe
|
2
|
+
|
3
|
+
module Blocks
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class represents a block composition, that is, a placeholder block
|
7
|
+
# that joins multiple blocks together.
|
8
|
+
#
|
9
|
+
# This class only exists to provide a superclass for {ParallelBlock} and
|
10
|
+
# {SerialBlock}.
|
11
|
+
#
|
12
|
+
# @abstract
|
13
|
+
#
|
14
|
+
# @see Ripe::Blocks::ParallelBlock
|
15
|
+
# @see Ripe::Blocks::SerialBlock
|
16
|
+
|
17
|
+
class MultiBlock < Block
|
18
|
+
|
19
|
+
##
|
20
|
+
# @param id [String] a mandatory, but optionally unique identifier
|
21
|
+
# for the block
|
22
|
+
# @param blocks [Array<Block>] list of children blocks
|
23
|
+
|
24
|
+
def initialize(id, *blocks)
|
25
|
+
# Ignore nil objects
|
26
|
+
super(id, blocks.compact, {})
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# (see Block#prune)
|
31
|
+
#
|
32
|
+
# Unless the block is protected, attempt to prune all children blocks.
|
33
|
+
# If all blocks are pruned, return nothing. If a single block remains,
|
34
|
+
# return that block. If more than one block remains, return the current
|
35
|
+
# {MultiBlock}.
|
36
|
+
|
37
|
+
def prune(protect, depend)
|
38
|
+
if !protect
|
39
|
+
@blocks = @blocks.map { |block| block.prune(protect, depend) }.compact
|
40
|
+
case @blocks.length
|
41
|
+
when 0; nil
|
42
|
+
when 1; @blocks.first
|
43
|
+
else; self
|
44
|
+
end
|
45
|
+
else
|
46
|
+
self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# (see Block#targets_exist?)
|
52
|
+
#
|
53
|
+
# A {MultiBlock}'s targets exist if the targets of all its # children
|
54
|
+
# exist.
|
55
|
+
|
56
|
+
def targets_exist?
|
57
|
+
@blocks.map(&:targets_exist?).inject(:&)
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# (see Block#topology)
|
62
|
+
|
63
|
+
def topology
|
64
|
+
[@id] + @blocks.map(&:topology)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ripe
|
2
|
+
|
3
|
+
module Blocks
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class represents a parallel composition of blocks, in that the
|
7
|
+
# children blocks of an instance of this class are to be run in parallel.
|
8
|
+
|
9
|
+
class ParallelBlock < MultiBlock
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param blocks [Array<Block>] list of children blocks
|
13
|
+
|
14
|
+
def initialize(*blocks)
|
15
|
+
super(:|, *blocks)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# (see Block#command)
|
20
|
+
|
21
|
+
def command
|
22
|
+
@blocks.map { |block| "(\n%s\n) & " % block.command }.join('') + 'wait'
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Ripe
|
2
|
+
|
3
|
+
module Blocks
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class represents a parallel composition of blocks, in that the
|
7
|
+
# children blocks of an instance of this class are to be run in serial.
|
8
|
+
|
9
|
+
class SerialBlock < MultiBlock
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param blocks [Array<Block>] list of children blocks
|
13
|
+
|
14
|
+
def initialize(*blocks)
|
15
|
+
super(:+, *blocks)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# (see Block#command)
|
20
|
+
|
21
|
+
def command
|
22
|
+
@blocks.map { |block| "(\n%s\n)" % block.command }.join(' ; ')
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :super_prune :prune
|
26
|
+
|
27
|
+
##
|
28
|
+
# (see MultiBlock#prune)
|
29
|
+
#
|
30
|
+
# A {SerialBlock} differs from a {MultiBlock} or {ParallelBlock} in that
|
31
|
+
# there is a linear dependency for its children blocks as they are to be
|
32
|
+
# run in serial. If a given block must be run, then all subsequent
|
33
|
+
# blocks that depend on it must be run as well.
|
34
|
+
|
35
|
+
def prune(protect, depend)
|
36
|
+
return super_prune(protect, depend) if !depend
|
37
|
+
return self if protect
|
38
|
+
|
39
|
+
@blocks = @blocks.map do |block|
|
40
|
+
new_protect = !block.targets_exist?
|
41
|
+
new_block = block.prune(protect, depend)
|
42
|
+
protect = new_protect
|
43
|
+
new_block
|
44
|
+
end
|
45
|
+
@blocks = @blocks.compact
|
46
|
+
|
47
|
+
case @blocks.length
|
48
|
+
when 0
|
49
|
+
nil
|
50
|
+
when 1
|
51
|
+
@blocks.first
|
52
|
+
else
|
53
|
+
self
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|