right_scraper 3.2.6 → 5.0.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.
- checksums.yaml +7 -0
- data/lib/right_scraper.rb +16 -34
- data/lib/right_scraper/builders.rb +32 -0
- data/lib/right_scraper/builders/base.rb +19 -20
- data/lib/right_scraper/builders/filesystem.rb +8 -6
- data/lib/right_scraper/builders/union.rb +4 -1
- data/lib/right_scraper/loggers.rb +31 -0
- data/lib/right_scraper/loggers/base.rb +113 -0
- data/lib/right_scraper/loggers/default.rb +98 -0
- data/lib/right_scraper/{scraper.rb → main.rb} +53 -9
- data/lib/right_scraper/processes.rb +33 -0
- data/lib/right_scraper/processes/shell.rb +227 -0
- data/lib/right_scraper/processes/{ssh.rb → ssh_agent.rb} +4 -0
- data/lib/right_scraper/processes/svn_client.rb +117 -0
- data/lib/right_scraper/processes/warden.rb +358 -0
- data/lib/right_scraper/registered_base.rb +154 -0
- data/lib/right_scraper/repositories.rb +33 -0
- data/lib/right_scraper/repositories/base.rb +271 -232
- data/lib/right_scraper/repositories/download.rb +8 -6
- data/lib/right_scraper/repositories/git.rb +8 -9
- data/lib/right_scraper/repositories/svn.rb +8 -8
- data/lib/right_scraper/resources.rb +32 -0
- data/lib/right_scraper/resources/base.rb +5 -1
- data/lib/right_scraper/resources/cookbook.rb +34 -27
- data/lib/right_scraper/resources/workflow.rb +27 -28
- data/lib/right_scraper/retrievers.rb +34 -0
- data/lib/right_scraper/retrievers/base.rb +80 -84
- data/lib/right_scraper/retrievers/checkout_base.rb +178 -0
- data/lib/right_scraper/retrievers/download.rb +125 -117
- data/lib/right_scraper/retrievers/git.rb +377 -223
- data/lib/right_scraper/retrievers/svn.rb +102 -62
- data/lib/right_scraper/scanners.rb +37 -0
- data/lib/right_scraper/scanners/base.rb +77 -80
- data/lib/right_scraper/scanners/cookbook_manifest.rb +31 -30
- data/lib/right_scraper/scanners/cookbook_metadata.rb +380 -35
- data/lib/right_scraper/scanners/cookbook_s3_upload.rb +56 -53
- data/lib/right_scraper/scanners/union.rb +61 -58
- data/lib/right_scraper/scanners/workflow_manifest.rb +55 -54
- data/lib/right_scraper/scanners/workflow_metadata.rb +41 -39
- data/lib/right_scraper/scanners/workflow_s3_upload.rb +59 -55
- data/lib/right_scraper/scrapers.rb +32 -0
- data/lib/right_scraper/scrapers/base.rb +217 -205
- data/lib/right_scraper/scrapers/cookbook.rb +42 -40
- data/lib/right_scraper/scrapers/workflow.rb +57 -58
- data/lib/right_scraper/version.rb +3 -0
- data/right_scraper.gemspec +12 -16
- metadata +57 -163
- data/Gemfile +0 -15
- data/Rakefile +0 -89
- data/lib/right_scraper/logger.rb +0 -107
- data/lib/right_scraper/loggers/noisy.rb +0 -85
- data/lib/right_scraper/repositories/mock.rb +0 -70
- data/lib/right_scraper/retrievers/checkout.rb +0 -79
- data/lib/right_scraper/scraper_logger.rb +0 -66
- data/lib/right_scraper/svn_client.rb +0 -164
- data/right_scraper.rconf +0 -13
- data/spec/builder_spec.rb +0 -50
- data/spec/cookbook_helper.rb +0 -73
- data/spec/cookbook_manifest_spec.rb +0 -93
- data/spec/cookbook_s3_upload_spec.rb +0 -159
- data/spec/download/download_retriever_spec.rb +0 -118
- data/spec/download/download_retriever_spec_helper.rb +0 -72
- data/spec/download/download_spec.rb +0 -128
- data/spec/download/multi_dir_spec.rb +0 -106
- data/spec/download/multi_dir_spec_helper.rb +0 -40
- data/spec/git/cookbook_spec.rb +0 -165
- data/spec/git/demokey +0 -27
- data/spec/git/demokey.pub +0 -1
- data/spec/git/password_key +0 -30
- data/spec/git/password_key.pub +0 -1
- data/spec/git/repository_spec.rb +0 -110
- data/spec/git/retriever_spec.rb +0 -553
- data/spec/git/retriever_spec_helper.rb +0 -112
- data/spec/git/scraper_spec.rb +0 -151
- data/spec/git/ssh_spec.rb +0 -174
- data/spec/git/url_spec.rb +0 -103
- data/spec/logger_spec.rb +0 -185
- data/spec/repository_spec.rb +0 -111
- data/spec/retriever_spec_helper.rb +0 -146
- data/spec/scanner_spec.rb +0 -61
- data/spec/scraper_helper.rb +0 -88
- data/spec/scraper_spec.rb +0 -147
- data/spec/spec_helper.rb +0 -185
- data/spec/svn/cookbook_spec.rb +0 -96
- data/spec/svn/multi_svn_spec.rb +0 -64
- data/spec/svn/multi_svn_spec_helper.rb +0 -40
- data/spec/svn/repository_spec.rb +0 -72
- data/spec/svn/retriever_spec.rb +0 -266
- data/spec/svn/scraper_spec.rb +0 -90
- data/spec/svn/svn_retriever_spec_helper.rb +0 -90
- data/spec/svn/url_spec.rb +0 -47
- data/spec/url_spec.rb +0 -164
@@ -0,0 +1,358 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2013 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
# ancestor
|
25
|
+
require 'right_scraper/processes'
|
26
|
+
|
27
|
+
require 'fileutils'
|
28
|
+
require 'right_popen'
|
29
|
+
require 'right_popen/safe_output_buffer'
|
30
|
+
require 'tmpdir'
|
31
|
+
|
32
|
+
module RightScraper
|
33
|
+
module Processes
|
34
|
+
class Warden
|
35
|
+
|
36
|
+
DEFAULT_RVM_HOME = '/usr/local/rvm'
|
37
|
+
DEFAULT_WARDEN_HOME = '/opt/warden'
|
38
|
+
|
39
|
+
RELATIVE_SCRIPTS_RVM_PATH = 'scripts/rvm'
|
40
|
+
|
41
|
+
# TEAL FIX: dynamically discover highest rvm-installed ruby 1.9 build?
|
42
|
+
DEFAULT_RVM_RUBY_VERSION = 'ruby-1.9.3-p448'
|
43
|
+
|
44
|
+
WARDEN_SERVICE_SUBDIR_NAME = 'warden'
|
45
|
+
RELATIVE_WARDEN_SCRIPT_PATH = 'bin/warden'
|
46
|
+
|
47
|
+
WARDEN_COMMAND_TIMEOUT = 60 # max seconds to spawn, link, etc.
|
48
|
+
|
49
|
+
DEFAULT_OPTIONS = {
|
50
|
+
:warden_home => DEFAULT_WARDEN_HOME,
|
51
|
+
:rvm_home => DEFAULT_RVM_HOME,
|
52
|
+
:rvm_ruby_version => DEFAULT_RVM_RUBY_VERSION
|
53
|
+
}
|
54
|
+
|
55
|
+
# marshalling
|
56
|
+
class LinkResult
|
57
|
+
attr_reader :exit_status, :stdout, :stderr
|
58
|
+
|
59
|
+
def initialize(link_result)
|
60
|
+
@exit_status = link_result['exit_status'].to_i rescue 1
|
61
|
+
@stdout = link_result['stdout'].to_s
|
62
|
+
@stderr = link_result['stderr'].to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def succeeded?
|
66
|
+
0 == exit_status
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# exceptions
|
71
|
+
class StateError < Exception; end
|
72
|
+
class WardenError < Exception; end
|
73
|
+
|
74
|
+
class LinkError < Exception
|
75
|
+
attr_reader :link_result
|
76
|
+
|
77
|
+
def initialize(message, link_result)
|
78
|
+
super(message)
|
79
|
+
@link_result = link_result
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(options = {})
|
84
|
+
options = DEFAULT_OPTIONS.merge(options)
|
85
|
+
@warden_home = options[:warden_home]
|
86
|
+
@rvm_home = options[:rvm_home]
|
87
|
+
unless @rvm_ruby_version = options[:rvm_ruby_version]
|
88
|
+
raise ArgumentError.new('options[:rvm_ruby_version] is required')
|
89
|
+
end
|
90
|
+
|
91
|
+
# warden paths
|
92
|
+
unless @warden_home && ::File.directory?(@warden_home)
|
93
|
+
raise ArgumentError.new('options[:warden_home] is required')
|
94
|
+
end
|
95
|
+
unless @rvm_home && ::File.directory?(@rvm_home)
|
96
|
+
raise ArgumentError.new('options[:rvm_home] is required')
|
97
|
+
end
|
98
|
+
@warden_server_dir = ::File.join(@warden_home, WARDEN_SERVICE_SUBDIR_NAME)
|
99
|
+
@bin_warden_path = ::File.join(@warden_server_dir, RELATIVE_WARDEN_SCRIPT_PATH)
|
100
|
+
unless File.file?(@bin_warden_path)
|
101
|
+
raise StateError, "Warden CLI script cannot be found at #{@bin_warden_path.inspect}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# rvm paths
|
105
|
+
@scripts_rvm_path = ::File.join(@rvm_home, RELATIVE_SCRIPTS_RVM_PATH)
|
106
|
+
unless File.file?(@scripts_rvm_path)
|
107
|
+
raise StateError, "RVM setup script cannot be found at #{@scripts_rvm_path.inspect}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# build the jail.
|
111
|
+
@handle = send('create')['handle']
|
112
|
+
raise StateError, 'handle is invalid' unless @handle
|
113
|
+
end
|
114
|
+
|
115
|
+
# Runs the script given by container-relative path. Optionally copies
|
116
|
+
# files in/out before/after script execution.
|
117
|
+
#
|
118
|
+
# === Parameters
|
119
|
+
# @param [String|Array] cmds to execute
|
120
|
+
# @param [String|Array] copy_in file(s) to copy into jail (using same path on both sides) or nil or empty
|
121
|
+
# @param [Hash] copy_out files as map of jail source path to host destination path or empty or nil
|
122
|
+
#
|
123
|
+
# === Return
|
124
|
+
# @return [String] stdout text
|
125
|
+
#
|
126
|
+
# === Raise
|
127
|
+
# @raise [StateError] for invalid state
|
128
|
+
# @raise [LinkError] for link (to script output) failure
|
129
|
+
# @raise [WardenError] for warden failure
|
130
|
+
def run_command_in_jail(cmds, copy_in = nil, copy_out = nil)
|
131
|
+
cmds = Array(cmds)
|
132
|
+
raise ArgumentError, 'cmds is required' if cmds.empty?
|
133
|
+
raise StateError, 'handle is invalid' unless @handle
|
134
|
+
|
135
|
+
# copy any files in before running commands.
|
136
|
+
copy_in = Array(copy_in)
|
137
|
+
send_copy_in_cmds(copy_in) if !copy_in.empty?
|
138
|
+
|
139
|
+
# note that appending --privileged will run script as root, but we have
|
140
|
+
# no use case for running scripts as root at this time.
|
141
|
+
output = []
|
142
|
+
cmds.each do |cmd|
|
143
|
+
job_id = send("spawn --handle #{@handle} --script #{cmd.inspect}")['job_id']
|
144
|
+
link_result = LinkResult.new(send("link --handle #{@handle} --job_id #{job_id}"))
|
145
|
+
if link_result.succeeded?
|
146
|
+
output << link_result.stdout
|
147
|
+
else
|
148
|
+
raise LinkError.new('Script failed running in isolation.', link_result)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# copy any files out after command(s) succeeded.
|
153
|
+
if copy_out && !copy_out.empty?
|
154
|
+
copy_out_cmds = copy_out.inject([]) do |result, (src_path, dst_path)|
|
155
|
+
# create output directories because warden will only copy files.
|
156
|
+
parent_dir = ::File.dirname(dst_path)
|
157
|
+
::FileUtils.mkdir_p(parent_dir)
|
158
|
+
result << "copy_out --handle #{@handle} --src_path #{src_path.inspect} --dst_path #{dst_path.inspect}"
|
159
|
+
result
|
160
|
+
end
|
161
|
+
send(copy_out_cmds)
|
162
|
+
end
|
163
|
+
|
164
|
+
return output.join("\n")
|
165
|
+
end
|
166
|
+
|
167
|
+
def cleanup
|
168
|
+
raise StateError, 'handle is invalid' unless @handle
|
169
|
+
lay_to_rest
|
170
|
+
send("destroy --handle #{@handle}")
|
171
|
+
ensure
|
172
|
+
@handle = nil
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def create_uuid
|
178
|
+
(0..15).to_a.map{|a| rand(16).to_s(16)}.join
|
179
|
+
end
|
180
|
+
|
181
|
+
# warden doesn't create directories on copy_in (or _out) so we need to
|
182
|
+
# generate a script and execute it before invoking copy_in.
|
183
|
+
#
|
184
|
+
# @param [Array] copy_in as array of files to copy into jail
|
185
|
+
def send_copy_in_cmds(copy_in)
|
186
|
+
mkdir_cmds = copy_in.
|
187
|
+
map { |dst_path| ::File.dirname(dst_path) }.uniq.sort.
|
188
|
+
map { |parent_dir| "mkdir -p #{parent_dir}" }
|
189
|
+
shell_script = <<EOS
|
190
|
+
#!/bin/bash
|
191
|
+
rm $0 # this script will self-destruct
|
192
|
+
#{mkdir_cmds.join(" &&\n")}
|
193
|
+
EOS
|
194
|
+
mkdir_script_name = "mkdir_script_#{create_uuid}.sh"
|
195
|
+
|
196
|
+
job_id = nil
|
197
|
+
::Dir.mktmpdir do |tmpdir|
|
198
|
+
mkdir_script_path = ::File.join(tmpdir, mkdir_script_name)
|
199
|
+
::File.open(mkdir_script_path, 'w') { |f| f.puts shell_script }
|
200
|
+
create_parent_dir_cmds = [
|
201
|
+
"copy_in --handle #{@handle} --src_path #{mkdir_script_path} --dst_path /tmp/mkdirs.sh",
|
202
|
+
"spawn --handle #{@handle} --script '/bin/bash /tmp/mkdirs.sh'",
|
203
|
+
]
|
204
|
+
job_id = send(create_parent_dir_cmds)['job_id']
|
205
|
+
end
|
206
|
+
|
207
|
+
link_result = LinkResult.new(send("link --handle #{@handle} --job_id #{job_id}"))
|
208
|
+
if link_result.succeeded?
|
209
|
+
copy_in_cmds = copy_in.inject([]) do |result, src_path|
|
210
|
+
result << "copy_in --handle #{@handle} --src_path #{src_path.inspect} --dst_path #{src_path.inspect}"
|
211
|
+
result
|
212
|
+
end
|
213
|
+
send(copy_in_cmds)
|
214
|
+
else
|
215
|
+
raise LinkError.new('Failed to create parent directories for files to be copied.', link_result)
|
216
|
+
end
|
217
|
+
true
|
218
|
+
end
|
219
|
+
|
220
|
+
# Sends one or more commands to warden and accumulates the stdout and
|
221
|
+
# stderr from those commands.
|
222
|
+
def send(warden_cmd)
|
223
|
+
# warden runs in a ruby 1.9.3 environment, for which we need rvm and a
|
224
|
+
# slew of fancy setup on the assumption that the current environemnt is
|
225
|
+
# not that. ideally this code would run in a standalone service where
|
226
|
+
# the warden-client gem could be used to simplify some of this.
|
227
|
+
warden_cmds = Array(warden_cmd).map do |line|
|
228
|
+
# execute bin/warden (Geronimo)
|
229
|
+
"bundle exec #{RELATIVE_WARDEN_SCRIPT_PATH} -- #{line}"
|
230
|
+
end
|
231
|
+
|
232
|
+
shell_script = <<EOS
|
233
|
+
#!/bin/bash
|
234
|
+
source #{@scripts_rvm_path} 1>/dev/null &&
|
235
|
+
rvm use #{@rvm_ruby_version}@global 1>/dev/null &&
|
236
|
+
cd #{@warden_server_dir} 1>/dev/null &&
|
237
|
+
#{warden_cmds.join(" &&\n")}
|
238
|
+
EOS
|
239
|
+
|
240
|
+
# ensure bundler env vars for current process don't interfere.
|
241
|
+
::Bundler.with_clean_env do
|
242
|
+
::Dir.mktmpdir do |tmpdir|
|
243
|
+
@process = nil
|
244
|
+
@interupted_to_close = false
|
245
|
+
@stdout_buffer = []
|
246
|
+
@stderr_buffer = ::RightScale::RightPopen::SafeOutputBuffer.new
|
247
|
+
warden_script_path = ::File.join(tmpdir, "run_warden_#{create_uuid}.sh")
|
248
|
+
::File.open(warden_script_path, 'w') { |f| f.puts shell_script }
|
249
|
+
cmd = "/bin/bash #{warden_script_path}"
|
250
|
+
::RightScale::RightPopen.popen3_sync(
|
251
|
+
cmd,
|
252
|
+
:target => self,
|
253
|
+
:inherit_io => true, # avoid killing any rails connection
|
254
|
+
:watch_handler => :watch_warden,
|
255
|
+
:stderr_handler => :stderr_warden,
|
256
|
+
:stdout_handler => :stdout_warden,
|
257
|
+
:timeout_handler => :timeout_warden,
|
258
|
+
:exit_handler => :exit_warden,
|
259
|
+
:timeout_seconds => WARDEN_COMMAND_TIMEOUT)
|
260
|
+
if @process
|
261
|
+
@process = nil
|
262
|
+
warden_output = @stdout_buffer.join
|
263
|
+
if warden_output.empty?
|
264
|
+
result = {}
|
265
|
+
else
|
266
|
+
result = parse_warden_output(warden_output)
|
267
|
+
end
|
268
|
+
return result
|
269
|
+
else
|
270
|
+
raise WardenError, 'Unable to execute warden.'
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Warden outputs something that looks like YAML but also somewhat like a
|
277
|
+
# Java configuration file. in any case, the output is ambiguous because it
|
278
|
+
# does not escape characters and it is possible to spawn a process that
|
279
|
+
# prints output text that appears to be the start of a new key. *sigh*
|
280
|
+
#
|
281
|
+
# all we can do here is attempt to parse the output by some simple rules
|
282
|
+
# and hope for the best.
|
283
|
+
#
|
284
|
+
# example:
|
285
|
+
# exit_status : 0
|
286
|
+
# stdout : a
|
287
|
+
# b
|
288
|
+
# c
|
289
|
+
#
|
290
|
+
# stderr :
|
291
|
+
# info.state : active
|
292
|
+
# ...
|
293
|
+
def parse_warden_output(warden_output)
|
294
|
+
parsed_lines = {}
|
295
|
+
current_key = nil
|
296
|
+
regex = /^([a-z._]+) \: (.*)$/
|
297
|
+
warden_output.lines.each do |line|
|
298
|
+
if parts = regex.match(line)
|
299
|
+
current_key = parts[1]
|
300
|
+
parsed_lines[current_key] = [parts[2]]
|
301
|
+
elsif current_key
|
302
|
+
parsed_lines[current_key] << line.chomp
|
303
|
+
else
|
304
|
+
raise WardenError, "Unable to parse warden output:\n#{warden_output.inspect}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
parsed_lines.inject({}) do |result, (key, value)|
|
308
|
+
result[key] = value.join("\n")
|
309
|
+
result
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def lay_to_rest
|
314
|
+
if @process
|
315
|
+
if @process.interrupt
|
316
|
+
@interupted_to_close = true
|
317
|
+
@process.sync_exit_with_target
|
318
|
+
else
|
319
|
+
@process.safe_close_io
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def stdout_warden(data)
|
325
|
+
@stdout_buffer << data
|
326
|
+
end
|
327
|
+
|
328
|
+
def stderr_warden(data)
|
329
|
+
@stderr_buffer.safe_buffer_data(data)
|
330
|
+
end
|
331
|
+
|
332
|
+
def watch_warden(process)
|
333
|
+
if @interupted_to_close
|
334
|
+
true
|
335
|
+
else
|
336
|
+
@process = process
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def timeout_warden
|
341
|
+
unless @interupted_to_close
|
342
|
+
raise WardenError, 'Timed out waiting for warden to respond'
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def exit_warden(status)
|
347
|
+
unless @interupted_to_close || status.success?
|
348
|
+
raise WardenError,
|
349
|
+
"Warden failed exit_status = #{status.exitstatus}:\n" +
|
350
|
+
"stdout = #{@stdout_buffer.join}\n" +
|
351
|
+
"stderr = #{@stderr_buffer.display_text}"
|
352
|
+
end
|
353
|
+
true
|
354
|
+
end
|
355
|
+
|
356
|
+
end # Warden
|
357
|
+
end # Processes
|
358
|
+
end # RightScraper
|
@@ -0,0 +1,154 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2013 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
# ancestor
|
25
|
+
require 'right_scraper'
|
26
|
+
|
27
|
+
module RightScraper
|
28
|
+
|
29
|
+
# Abstract base class for a registered type.
|
30
|
+
#
|
31
|
+
# Example:
|
32
|
+
#
|
33
|
+
# class Foo < RegisteredBase
|
34
|
+
# ...
|
35
|
+
#
|
36
|
+
# register_self(:foo)
|
37
|
+
# end
|
38
|
+
class RegisteredBase
|
39
|
+
|
40
|
+
# exceptions
|
41
|
+
class RegisteredTypeError < ::StandardError; end
|
42
|
+
|
43
|
+
# Provides a module from which a specific set of registered types is derived
|
44
|
+
# (for registration, autoloading, etc.). It is not necessary for all types
|
45
|
+
# of the set to be declared within the scope of that module, but doing so
|
46
|
+
# will simplify registration and query.
|
47
|
+
#
|
48
|
+
# @return [Module] module or base class in common
|
49
|
+
def self.registration_module
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Hash] mapping of registered types to classes or empty
|
54
|
+
def self.registered_types
|
55
|
+
unless types = registration_module.instance_variable_get(:@registered_types)
|
56
|
+
types = {}
|
57
|
+
registration_module.instance_variable_set(:@registered_types, types)
|
58
|
+
end
|
59
|
+
types
|
60
|
+
end
|
61
|
+
|
62
|
+
# Registers self.
|
63
|
+
#
|
64
|
+
# @param [Symbol] type to register or nil
|
65
|
+
#
|
66
|
+
# @return [TrueClass] always true
|
67
|
+
def self.register_self(type = nil)
|
68
|
+
# automatically determine registered type from self, if necessary.
|
69
|
+
unless type
|
70
|
+
class_name = self.name
|
71
|
+
default_module_name = registration_module.name + '::'
|
72
|
+
if class_name.start_with?(default_module_name)
|
73
|
+
subname = class_name[default_module_name.length..-1]
|
74
|
+
class_name = subname unless subname.index('::')
|
75
|
+
end
|
76
|
+
type = class_name.
|
77
|
+
gsub(/::/, '/').
|
78
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
79
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
80
|
+
downcase
|
81
|
+
end
|
82
|
+
self.register_class(type, self)
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
# Registers given class.
|
87
|
+
#
|
88
|
+
# @param [Symbol|String] type to register
|
89
|
+
# @param [Class] clazz to register
|
90
|
+
#
|
91
|
+
# @return [TrueClass] always true
|
92
|
+
def self.register_class(type, clazz)
|
93
|
+
raise ::ArgumentError, 'clazz is required' unless clazz
|
94
|
+
raise ::ArgumentError, 'type is required' unless type
|
95
|
+
registered_types[type.to_s] = clazz
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
# Queries the implementation class for a registered type.
|
100
|
+
#
|
101
|
+
# @param [Symbol|String] type for query
|
102
|
+
#
|
103
|
+
# @return [RightScraper::Repositories::Base] repository created
|
104
|
+
def self.query_registered_type(type)
|
105
|
+
raise ::ArgumentError, 'type is required' unless type
|
106
|
+
|
107
|
+
# a quick-out when given a known registerd type. autoloading types makes
|
108
|
+
# things more interesting for unknown types.
|
109
|
+
type = type.to_s
|
110
|
+
unless clazz = registered_types[type]
|
111
|
+
# default module implementations may be auto-loading so try default
|
112
|
+
# namespace before giving up (assumes snake-case types). types
|
113
|
+
# declared in a different namespace can also be autoloaded if fully
|
114
|
+
# qualified using forward slashes (require-style).
|
115
|
+
class_path = type.split('/').map do |snake_case|
|
116
|
+
camel_case = snake_case.split('_').map{ |e| e.capitalize }.join
|
117
|
+
end
|
118
|
+
|
119
|
+
# assume no registered types at global scope and insert registration
|
120
|
+
# module before any simple name.
|
121
|
+
if class_path.size == 1
|
122
|
+
class_path = registration_module.name.split('::') + class_path
|
123
|
+
end
|
124
|
+
|
125
|
+
# walk class path from global scope because const_get doesn't understand
|
126
|
+
# the '::' notation. autoloading is usually setup to support walking
|
127
|
+
# from the base module.
|
128
|
+
last_item = nil
|
129
|
+
begin
|
130
|
+
parent_item = ::Object
|
131
|
+
class_path.each do |item|
|
132
|
+
last_item = parent_item.const_get(item)
|
133
|
+
parent_item = last_item
|
134
|
+
end
|
135
|
+
rescue ::NameError => e
|
136
|
+
if e.message =~ /uninitialized constant/
|
137
|
+
last_item = nil
|
138
|
+
else
|
139
|
+
raise
|
140
|
+
end
|
141
|
+
end
|
142
|
+
if last_item
|
143
|
+
# type still needs to successfully self-register upon definition.
|
144
|
+
unless clazz = registered_types[type]
|
145
|
+
raise RegisteredTypeError, "Discovered type did not register itself properly: #{type.inspect} => #{last_item.inspect}"
|
146
|
+
end
|
147
|
+
else
|
148
|
+
raise RegisteredTypeError, "Unknown registered type: #{type.inspect}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
clazz
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|