mortar 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/mortar/auth.rb +4 -0
- data/lib/mortar/command/base.rb +16 -0
- data/lib/mortar/command/describe.rb +7 -1
- data/lib/mortar/command/help.rb +1 -22
- data/lib/mortar/command/illustrate.rb +8 -3
- data/lib/mortar/command/jobs.rb +41 -14
- data/lib/mortar/command/local.rb +87 -0
- data/lib/mortar/command/projects.rb +12 -11
- data/lib/mortar/command/validate.rb +6 -1
- data/lib/mortar/helpers.rb +12 -0
- data/lib/mortar/local/controller.rb +108 -0
- data/lib/mortar/local/installutil.rb +94 -0
- data/lib/mortar/local/java.rb +48 -0
- data/lib/mortar/local/pig.rb +309 -0
- data/lib/mortar/local/python.rb +184 -0
- data/lib/mortar/project.rb +35 -9
- data/lib/mortar/templates/project/gitignore +3 -1
- data/lib/mortar/templates/report/illustrate-report.html +96 -0
- data/lib/mortar/templates/script/runpig.sh +23 -0
- data/lib/mortar/version.rb +1 -1
- data/spec/mortar/auth_spec.rb +8 -0
- data/spec/mortar/command/describe_spec.rb +14 -1
- data/spec/mortar/command/illustrate_spec.rb +14 -1
- data/spec/mortar/command/jobs_spec.rb +125 -7
- data/spec/mortar/command/local_spec.rb +144 -0
- data/spec/mortar/command/validate_spec.rb +14 -1
- data/spec/mortar/local/controller_spec.rb +102 -0
- data/spec/mortar/local/installutil_spec.rb +70 -0
- data/spec/mortar/local/java_spec.rb +62 -0
- data/spec/mortar/local/pig_spec.rb +157 -0
- data/spec/mortar/project_spec.rb +11 -0
- data/spec/spec_helper.rb +1 -0
- metadata +152 -130
@@ -0,0 +1,94 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2012 Mortar Data Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'zlib'
|
18
|
+
require 'excon'
|
19
|
+
require 'rbconfig'
|
20
|
+
require 'rubygems/package'
|
21
|
+
|
22
|
+
require 'mortar/helpers'
|
23
|
+
|
24
|
+
module Mortar
|
25
|
+
module Local
|
26
|
+
module InstallUtil
|
27
|
+
|
28
|
+
include Mortar::Helpers
|
29
|
+
|
30
|
+
def local_install_directory
|
31
|
+
# note: assumes that CWD is the project root, is
|
32
|
+
# this a safe assumption?
|
33
|
+
File.join(Dir.getwd, ".mortar-local")
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# Drops a marker file for an installed package, used
|
38
|
+
# to help determine if updates should be performed
|
39
|
+
def note_install(subdirectory)
|
40
|
+
install_file = install_file_for(subdirectory)
|
41
|
+
File.open(install_file, "w") do |install_file|
|
42
|
+
# Write out the current epoch so we know when this
|
43
|
+
# dependency was installed
|
44
|
+
install_file.write("#{Time.now.to_i}\n")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def install_date(subsection)
|
49
|
+
install_file = install_file_for(subsection)
|
50
|
+
if File.exists?(install_file)
|
51
|
+
File.open(install_file, "r") do |f|
|
52
|
+
file_contents = f.read()
|
53
|
+
file_contents.strip.to_i
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def install_file_for(subdirectory)
|
59
|
+
File.join(local_install_directory, subdirectory, "install-date.txt")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Given a path to a foo.tgz or foo.tar.gz file, extracts its
|
63
|
+
# contents to the specified output directory
|
64
|
+
def extract_tgz(tgz_path, dest_dir)
|
65
|
+
FileUtils.mkdir_p(dest_dir)
|
66
|
+
Gem::Package::TarReader.new(Zlib::GzipReader.open(tgz_path)).each do |entry|
|
67
|
+
entry_path = File.join(dest_dir, entry.full_name)
|
68
|
+
if entry.directory?
|
69
|
+
FileUtils.mkdir_p(entry_path)
|
70
|
+
elsif entry.file?
|
71
|
+
File.open(entry_path, "wb") do |entry_file|
|
72
|
+
entry_file.write(entry.read)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Downloads the file at a specified url into the supplied director
|
79
|
+
def download_file(url, dest_dir)
|
80
|
+
dest_file_path = dest_dir + "/" + File.basename(url)
|
81
|
+
File.open(dest_file_path, "wb") do |dest_file|
|
82
|
+
contents = Excon.get(url).body
|
83
|
+
dest_file.write(contents)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def osx?
|
88
|
+
os_platform_name = RbConfig::CONFIG['target_os']
|
89
|
+
return os_platform_name.start_with?('darwin')
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2012 Mortar Data Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require "mortar/local/installutil"
|
18
|
+
|
19
|
+
class Mortar::Local::Java
|
20
|
+
include Mortar::Local::InstallUtil
|
21
|
+
|
22
|
+
@command = nil
|
23
|
+
|
24
|
+
def check_install
|
25
|
+
jbin = File.join(ENV['JAVA_HOME']||'', "bin", "java")
|
26
|
+
if ENV['JAVA_HOME'] and File.exists?(jbin)
|
27
|
+
@command = jbin
|
28
|
+
return true
|
29
|
+
elsif File.exists?("/usr/libexec/java_home")
|
30
|
+
# OSX has a nice little tool for finding this value, assuming
|
31
|
+
# that it won't give us a bad value
|
32
|
+
java_home = run_java_home
|
33
|
+
if java_home != ""
|
34
|
+
ENV['JAVA_HOME'] = java_home
|
35
|
+
@command = File.join(ENV['JAVA_HOME'], "bin", "java")
|
36
|
+
return true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Runs the java_home command line tool which on osx will
|
43
|
+
# return the appropriate JAVA_home value
|
44
|
+
def run_java_home
|
45
|
+
`/usr/libexec/java_home`.to_s.strip
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2012 Mortar Data Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require "erb"
|
18
|
+
require 'tempfile'
|
19
|
+
require "mortar/helpers"
|
20
|
+
require "mortar/local/installutil"
|
21
|
+
|
22
|
+
class Mortar::Local::Pig
|
23
|
+
include Mortar::Local::InstallUtil
|
24
|
+
|
25
|
+
PIG_LOG_FORMAT = "humanreadable"
|
26
|
+
PIG_TAR_DEFAULT_URL = "https://s3.amazonaws.com/mortar-public-artifacts/pig.tgz"
|
27
|
+
|
28
|
+
# Tempfile objects have a hook to delete the file when the object is
|
29
|
+
# destroyed by the garbage collector. In practice this means that a
|
30
|
+
# file we want sitting around could disappear out from under us. To
|
31
|
+
# prevent this behavior, we're keeping references to these objects so
|
32
|
+
# that the garbage collector will not destroy them until the program
|
33
|
+
# exits (and our files won't be deleted until we don't care about them
|
34
|
+
# any more).
|
35
|
+
@temp_file_objects
|
36
|
+
|
37
|
+
# We copy some resources to the user's illustrate-output directory
|
38
|
+
# for styling the output. This only happens if they are not already present.
|
39
|
+
@resource_locations
|
40
|
+
@resource_destinations
|
41
|
+
|
42
|
+
attr_accessor :resource_locations
|
43
|
+
attr_accessor :resource_destinations
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@temp_file_objects = []
|
47
|
+
|
48
|
+
@resource_locations = {
|
49
|
+
"illustrate_template" => File.expand_path("../../templates/report/illustrate-report.html", __FILE__),
|
50
|
+
"illustrate_css" => File.expand_path("../../../../css/illustrate.css", __FILE__),
|
51
|
+
"jquery" => File.expand_path("../../../../js/jquery-1.7.1.min.js", __FILE__),
|
52
|
+
"jquery_transit" => File.expand_path("../../../../js/jquery.transit.js", __FILE__),
|
53
|
+
"jquery_stylestack" => File.expand_path("../../../../js/jquery.stylestack.js", __FILE__),
|
54
|
+
"mortar_table" => File.expand_path("../../../../js/mortar-table.js", __FILE__),
|
55
|
+
"zeroclipboard" => File.expand_path("../../../../js/zero_clipboard.js", __FILE__),
|
56
|
+
"zeroclipboard_swf" => File.expand_path("../../../../flash/zeroclipboard.swf", __FILE__)
|
57
|
+
}
|
58
|
+
|
59
|
+
@resource_destinations = {
|
60
|
+
"illustrate_html" => "illustrate-output/illustrate-output.html",
|
61
|
+
"illustrate_css" => "illustrate-output/resources/css/illustrate-output.css",
|
62
|
+
"jquery" => "illustrate-output/resources/js/jquery-1.7.1.min.js",
|
63
|
+
"jquery_transit" => "illustrate-output/resources/js/jquery.transit.js",
|
64
|
+
"jquery_stylestack" => "illustrate-output/resources/js/jquery.stylestack.js",
|
65
|
+
"mortar_table" => "illustrate-output/resources/js/mortar-table.js",
|
66
|
+
"zeroclipboard" => "illustrate-output/resources/js/zero_clipboard.js",
|
67
|
+
"zeroclipboard_swf" => "illustrate-output/resources/flash/zeroclipboard.swf"
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def command
|
72
|
+
return File.join(pig_directory, "bin", "pig")
|
73
|
+
end
|
74
|
+
|
75
|
+
def pig_directory
|
76
|
+
return File.join(local_install_directory, "pig")
|
77
|
+
end
|
78
|
+
|
79
|
+
def pig_archive_url
|
80
|
+
ENV.fetch('PIG_DISTRO_URL', PIG_TAR_DEFAULT_URL)
|
81
|
+
end
|
82
|
+
|
83
|
+
def pig_archive_file
|
84
|
+
File.basename(pig_archive_url)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Determines if a pig install needs to occur, true if no
|
88
|
+
# pig install present or a newer version is available
|
89
|
+
def should_do_pig_install?
|
90
|
+
not (File.exists?(pig_directory))
|
91
|
+
end
|
92
|
+
|
93
|
+
# Installs pig for this project if it is not already present
|
94
|
+
def install
|
95
|
+
if should_do_pig_install?
|
96
|
+
FileUtils.mkdir_p(local_install_directory)
|
97
|
+
action "Installing pig" do
|
98
|
+
download_file(pig_archive_url, local_install_directory)
|
99
|
+
local_tgz = File.join(local_install_directory, pig_archive_file)
|
100
|
+
extract_tgz(local_tgz, local_install_directory)
|
101
|
+
|
102
|
+
# This has been seening coming out of the tgz w/o +x so we do
|
103
|
+
# here to be sure it has the necessary permissions
|
104
|
+
FileUtils.chmod(0755, command)
|
105
|
+
|
106
|
+
File.delete(local_tgz)
|
107
|
+
note_install("pig")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# run the pig script with user supplied pig parameters
|
113
|
+
def run_script(pig_script, pig_parameters)
|
114
|
+
run_pig_command(" -f #{pig_script.path}", pig_parameters)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create a temp file to be used for writing the illustrate
|
118
|
+
# json output, and return it's path. This data file will
|
119
|
+
# later be used to create the result html output. Tempfile
|
120
|
+
# will take care of cleaning up the file when we exit.
|
121
|
+
def create_illustrate_output_path
|
122
|
+
# Using Tempfile for the path generation and so that the
|
123
|
+
# file will be cleaned up on process exit
|
124
|
+
outfile = Tempfile.new("mortar-illustrate-output")
|
125
|
+
outfile.close(false)
|
126
|
+
outfile.path
|
127
|
+
end
|
128
|
+
|
129
|
+
# Given a file path, open it and decode the containing json
|
130
|
+
def decode_illustrate_input_file(illustrate_outpath)
|
131
|
+
data_raw = File.read(illustrate_outpath)
|
132
|
+
begin
|
133
|
+
data_encoded = data_raw.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '')
|
134
|
+
rescue NoMethodError
|
135
|
+
require 'iconv'
|
136
|
+
ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
|
137
|
+
data_encoded = ic.iconv(data_raw)
|
138
|
+
end
|
139
|
+
json_decode(data_encoded)
|
140
|
+
end
|
141
|
+
|
142
|
+
def show_illustrate_output(illustrate_outpath)
|
143
|
+
ensure_dir_exists("illustrate-output")
|
144
|
+
ensure_dir_exists("illustrate-output/resources")
|
145
|
+
ensure_dir_exists("illustrate-output/resources/css")
|
146
|
+
ensure_dir_exists("illustrate-output/resources/js")
|
147
|
+
ensure_dir_exists("illustrate-output/resources/flash")
|
148
|
+
|
149
|
+
["illustrate_css",
|
150
|
+
"jquery", "jquery_transit", "jquery_stylestack",
|
151
|
+
"mortar_table", "zeroclipboard", "zeroclipboard_swf"].each { |resource|
|
152
|
+
copy_if_not_present_at_dest(@resource_locations[resource], @resource_destinations[resource])
|
153
|
+
}
|
154
|
+
|
155
|
+
# Pull in the dumped json file
|
156
|
+
illustrate_data = decode_illustrate_input_file(illustrate_outpath)
|
157
|
+
|
158
|
+
# Render a template using it's values
|
159
|
+
template_params = create_illustrate_template_parameters(illustrate_data)
|
160
|
+
|
161
|
+
# template_params = {'tables' => []}
|
162
|
+
erb = ERB.new(File.read(@resource_locations["illustrate_template"]), 0, "%<>")
|
163
|
+
html = erb.result(BindingClazz.new(template_params).get_binding)
|
164
|
+
|
165
|
+
# Write the rendered template out to a file
|
166
|
+
File.open(@resource_destinations["illustrate_html"], 'w') { |f|
|
167
|
+
f.write(html)
|
168
|
+
}
|
169
|
+
|
170
|
+
# Open a browser pointing to the rendered template output file
|
171
|
+
action("Opening illustrate results from #{@resource_destinations["illustrate_html"]} ") do
|
172
|
+
require "launchy"
|
173
|
+
Launchy.open(File.expand_path(@resource_destinations["illustrate_html"]))
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
def create_illustrate_template_parameters(illustrate_data)
|
179
|
+
params = {}
|
180
|
+
params['tables'] = illustrate_data['tables']
|
181
|
+
params['udf_output'] = illustrate_data['udf_output']
|
182
|
+
return params
|
183
|
+
end
|
184
|
+
|
185
|
+
def illustrate_alias(pig_script, pig_alias, skip_pruning, pig_parameters)
|
186
|
+
cmd = "-e 'illustrate "
|
187
|
+
|
188
|
+
# Parameters have to be entered with the illustrate command (as
|
189
|
+
# apposed to as a command line argument) or it will result in an
|
190
|
+
# 'Undefined parameter' error.
|
191
|
+
param_file = make_pig_param_file(pig_parameters)
|
192
|
+
cmd += "-param_file #{param_file} "
|
193
|
+
|
194
|
+
# Now point us at the script/alias to illustrate
|
195
|
+
illustrate_outpath = create_illustrate_output_path()
|
196
|
+
cmd += "-script #{pig_script.path} -out #{illustrate_outpath} "
|
197
|
+
if skip_pruning
|
198
|
+
cmd += " -skipPruning "
|
199
|
+
end
|
200
|
+
cmd += " #{pig_alias} '"
|
201
|
+
|
202
|
+
run_pig_command(cmd, [])
|
203
|
+
show_illustrate_output(illustrate_outpath)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Run pig with the specified command ('command' is anything that
|
207
|
+
# can be appended to the command line invocation of Pig that will
|
208
|
+
# get it to do something interesting, such as '-f some-file.pig'
|
209
|
+
def run_pig_command(cmd, parameters = nil)
|
210
|
+
# Generate the script for running the command, then
|
211
|
+
# write it to a temp script which will be exectued
|
212
|
+
script_text = script_for_command(cmd, parameters)
|
213
|
+
script = Tempfile.new("mortar-")
|
214
|
+
script.write(script_text)
|
215
|
+
script.close(false)
|
216
|
+
FileUtils.chmod(0755, script.path)
|
217
|
+
system(script.path)
|
218
|
+
script.unlink
|
219
|
+
end
|
220
|
+
|
221
|
+
# Generates a bash script which sets up the necessary environment and
|
222
|
+
# then runs the pig command
|
223
|
+
def script_for_command(cmd, parameters)
|
224
|
+
template_params = pig_command_script_template_parameters(cmd, parameters)
|
225
|
+
erb = ERB.new(File.read(pig_command_script_template_path), 0, "%<>")
|
226
|
+
erb.result(BindingClazz.new(template_params).get_binding)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Path to the template which generates the bash script for running pig
|
230
|
+
def pig_command_script_template_path
|
231
|
+
File.expand_path("../../templates/script/runpig.sh", __FILE__)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Parameters necessary for rendering the bash script template
|
235
|
+
def pig_command_script_template_parameters(cmd, pig_parameters)
|
236
|
+
template_params = {}
|
237
|
+
template_params['pig_params_file'] = make_pig_param_file(pig_parameters)
|
238
|
+
template_params['pig_home'] = pig_directory
|
239
|
+
template_params['pig_classpath'] = "#{pig_directory}/lib-pig/*"
|
240
|
+
template_params['classpath'] = "#{pig_directory}/lib/*:#{pig_directory}/conf/jets3t.properties"
|
241
|
+
template_params['project_home'] = File.expand_path("..", local_install_directory)
|
242
|
+
template_params['local_install_dir'] = local_install_directory
|
243
|
+
template_params['pig_sub_command'] = cmd
|
244
|
+
template_params['pig_opts'] = pig_options
|
245
|
+
template_params
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns a hash of settings that need to be passed
|
249
|
+
# in via pig options
|
250
|
+
def pig_options
|
251
|
+
opts = {}
|
252
|
+
opts['fs.s3n.awsAccessKeyId'] = ENV['AWS_ACCESS_KEY']
|
253
|
+
opts['fs.s3n.awsSecretAccessKey'] = ENV['AWS_SECRET_KEY']
|
254
|
+
opts['pig.events.logformat'] = PIG_LOG_FORMAT
|
255
|
+
return opts
|
256
|
+
end
|
257
|
+
|
258
|
+
# Pig Paramenters that are supplied directly from Mortar when
|
259
|
+
# running on the server side. We duplicate these here.
|
260
|
+
def automatic_pig_parameters
|
261
|
+
params = {}
|
262
|
+
if ENV['MORTAR_EMAIL_S3_ESCAPED']
|
263
|
+
params['MORTAR_EMAIL_S3_ESCAPED'] = ENV['MORTAR_EMAIL_S3_ESCAPED']
|
264
|
+
else
|
265
|
+
params['MORTAR_EMAIL_S3_ESCAPED'] = Mortar::Auth.user_s3_safe
|
266
|
+
end
|
267
|
+
# Coerce into the same format as pig parameters that were
|
268
|
+
# passed in via the command line or a parameter file
|
269
|
+
param_list = []
|
270
|
+
params.each{ |k,v|
|
271
|
+
param_list.push({"name" => k, "value" => v})
|
272
|
+
}
|
273
|
+
return param_list
|
274
|
+
end
|
275
|
+
|
276
|
+
# Given a set of user specified pig parameters, combine with the
|
277
|
+
# automatic mortar parameters and write out to a tempfile, returning
|
278
|
+
# it's path so it may be referenced later in the process
|
279
|
+
def make_pig_param_file(pig_parameters)
|
280
|
+
mortar_pig_params = automatic_pig_parameters
|
281
|
+
all_parameters = mortar_pig_params.concat(pig_parameters)
|
282
|
+
param_file = Tempfile.new("mortar-pig-parameters")
|
283
|
+
all_parameters.each { |p|
|
284
|
+
param_file.write("#{p['name']}=#{p['value']}\n")
|
285
|
+
}
|
286
|
+
param_file.close(false)
|
287
|
+
|
288
|
+
# Keep track a reference the tempfile object so that the
|
289
|
+
# garbage collector does not automatically delete the file
|
290
|
+
# out from under us
|
291
|
+
@temp_file_objects.push(param_file)
|
292
|
+
|
293
|
+
param_file.path
|
294
|
+
end
|
295
|
+
|
296
|
+
# Allows us to use a hash for template variables
|
297
|
+
class BindingClazz
|
298
|
+
def initialize(attrs)
|
299
|
+
attrs.each{ |k, v|
|
300
|
+
# set an instance variable with the key name so the binding will find it in scope
|
301
|
+
self.instance_variable_set("@#{k}".to_sym, v)
|
302
|
+
}
|
303
|
+
end
|
304
|
+
def get_binding()
|
305
|
+
binding
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|