right_scraper 3.2.6 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/lib/right_scraper.rb +16 -34
  3. data/lib/right_scraper/builders.rb +32 -0
  4. data/lib/right_scraper/builders/base.rb +19 -20
  5. data/lib/right_scraper/builders/filesystem.rb +8 -6
  6. data/lib/right_scraper/builders/union.rb +4 -1
  7. data/lib/right_scraper/loggers.rb +31 -0
  8. data/lib/right_scraper/loggers/base.rb +113 -0
  9. data/lib/right_scraper/loggers/default.rb +98 -0
  10. data/lib/right_scraper/{scraper.rb → main.rb} +53 -9
  11. data/lib/right_scraper/processes.rb +33 -0
  12. data/lib/right_scraper/processes/shell.rb +227 -0
  13. data/lib/right_scraper/processes/{ssh.rb → ssh_agent.rb} +4 -0
  14. data/lib/right_scraper/processes/svn_client.rb +117 -0
  15. data/lib/right_scraper/processes/warden.rb +358 -0
  16. data/lib/right_scraper/registered_base.rb +154 -0
  17. data/lib/right_scraper/repositories.rb +33 -0
  18. data/lib/right_scraper/repositories/base.rb +271 -232
  19. data/lib/right_scraper/repositories/download.rb +8 -6
  20. data/lib/right_scraper/repositories/git.rb +8 -9
  21. data/lib/right_scraper/repositories/svn.rb +8 -8
  22. data/lib/right_scraper/resources.rb +32 -0
  23. data/lib/right_scraper/resources/base.rb +5 -1
  24. data/lib/right_scraper/resources/cookbook.rb +34 -27
  25. data/lib/right_scraper/resources/workflow.rb +27 -28
  26. data/lib/right_scraper/retrievers.rb +34 -0
  27. data/lib/right_scraper/retrievers/base.rb +80 -84
  28. data/lib/right_scraper/retrievers/checkout_base.rb +178 -0
  29. data/lib/right_scraper/retrievers/download.rb +125 -117
  30. data/lib/right_scraper/retrievers/git.rb +377 -223
  31. data/lib/right_scraper/retrievers/svn.rb +102 -62
  32. data/lib/right_scraper/scanners.rb +37 -0
  33. data/lib/right_scraper/scanners/base.rb +77 -80
  34. data/lib/right_scraper/scanners/cookbook_manifest.rb +31 -30
  35. data/lib/right_scraper/scanners/cookbook_metadata.rb +380 -35
  36. data/lib/right_scraper/scanners/cookbook_s3_upload.rb +56 -53
  37. data/lib/right_scraper/scanners/union.rb +61 -58
  38. data/lib/right_scraper/scanners/workflow_manifest.rb +55 -54
  39. data/lib/right_scraper/scanners/workflow_metadata.rb +41 -39
  40. data/lib/right_scraper/scanners/workflow_s3_upload.rb +59 -55
  41. data/lib/right_scraper/scrapers.rb +32 -0
  42. data/lib/right_scraper/scrapers/base.rb +217 -205
  43. data/lib/right_scraper/scrapers/cookbook.rb +42 -40
  44. data/lib/right_scraper/scrapers/workflow.rb +57 -58
  45. data/lib/right_scraper/version.rb +3 -0
  46. data/right_scraper.gemspec +12 -16
  47. metadata +57 -163
  48. data/Gemfile +0 -15
  49. data/Rakefile +0 -89
  50. data/lib/right_scraper/logger.rb +0 -107
  51. data/lib/right_scraper/loggers/noisy.rb +0 -85
  52. data/lib/right_scraper/repositories/mock.rb +0 -70
  53. data/lib/right_scraper/retrievers/checkout.rb +0 -79
  54. data/lib/right_scraper/scraper_logger.rb +0 -66
  55. data/lib/right_scraper/svn_client.rb +0 -164
  56. data/right_scraper.rconf +0 -13
  57. data/spec/builder_spec.rb +0 -50
  58. data/spec/cookbook_helper.rb +0 -73
  59. data/spec/cookbook_manifest_spec.rb +0 -93
  60. data/spec/cookbook_s3_upload_spec.rb +0 -159
  61. data/spec/download/download_retriever_spec.rb +0 -118
  62. data/spec/download/download_retriever_spec_helper.rb +0 -72
  63. data/spec/download/download_spec.rb +0 -128
  64. data/spec/download/multi_dir_spec.rb +0 -106
  65. data/spec/download/multi_dir_spec_helper.rb +0 -40
  66. data/spec/git/cookbook_spec.rb +0 -165
  67. data/spec/git/demokey +0 -27
  68. data/spec/git/demokey.pub +0 -1
  69. data/spec/git/password_key +0 -30
  70. data/spec/git/password_key.pub +0 -1
  71. data/spec/git/repository_spec.rb +0 -110
  72. data/spec/git/retriever_spec.rb +0 -553
  73. data/spec/git/retriever_spec_helper.rb +0 -112
  74. data/spec/git/scraper_spec.rb +0 -151
  75. data/spec/git/ssh_spec.rb +0 -174
  76. data/spec/git/url_spec.rb +0 -103
  77. data/spec/logger_spec.rb +0 -185
  78. data/spec/repository_spec.rb +0 -111
  79. data/spec/retriever_spec_helper.rb +0 -146
  80. data/spec/scanner_spec.rb +0 -61
  81. data/spec/scraper_helper.rb +0 -88
  82. data/spec/scraper_spec.rb +0 -147
  83. data/spec/spec_helper.rb +0 -185
  84. data/spec/svn/cookbook_spec.rb +0 -96
  85. data/spec/svn/multi_svn_spec.rb +0 -64
  86. data/spec/svn/multi_svn_spec_helper.rb +0 -40
  87. data/spec/svn/repository_spec.rb +0 -72
  88. data/spec/svn/retriever_spec.rb +0 -266
  89. data/spec/svn/scraper_spec.rb +0 -90
  90. data/spec/svn/svn_retriever_spec_helper.rb +0 -90
  91. data/spec/svn/url_spec.rb +0 -47
  92. 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