resque-scheduler 2.0.0 → 2.3.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of resque-scheduler might be problematic. Click here for more details.
- checksums.yaml +15 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +129 -0
- data/.travis.yml +18 -0
- data/AUTHORS.md +63 -0
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +3 -7
- data/HISTORY.md +65 -6
- data/LICENSE +3 -1
- data/{README.markdown → README.md} +218 -113
- data/Rakefile +22 -9
- data/lib/resque/scheduler/lock/base.rb +52 -0
- data/lib/resque/scheduler/lock/basic.rb +28 -0
- data/lib/resque/scheduler/lock/resilient.rb +69 -0
- data/lib/resque/scheduler/lock.rb +3 -0
- data/lib/resque/scheduler.rb +56 -19
- data/lib/resque/scheduler_locking.rb +89 -0
- data/lib/resque_scheduler/logger_builder.rb +51 -0
- data/lib/resque_scheduler/server/views/delayed.erb +2 -1
- data/lib/resque_scheduler/server/views/requeue-params.erb +23 -0
- data/lib/resque_scheduler/server/views/scheduler.erb +4 -3
- data/lib/resque_scheduler/server.rb +25 -3
- data/lib/resque_scheduler/tasks.rb +9 -3
- data/lib/resque_scheduler/util.rb +34 -0
- data/lib/resque_scheduler/version.rb +3 -1
- data/lib/resque_scheduler.rb +89 -38
- data/resque-scheduler.gemspec +26 -20
- data/script/migrate_to_timestamps_set.rb +14 -0
- data/test/delayed_queue_test.rb +67 -16
- data/test/redis-test.conf +0 -7
- data/test/resque-web_test.rb +105 -3
- data/test/scheduler_args_test.rb +1 -1
- data/test/scheduler_locking_test.rb +180 -0
- data/test/scheduler_setup_test.rb +59 -0
- data/test/scheduler_test.rb +21 -12
- data/test/support/redis_instance.rb +129 -0
- data/test/test_helper.rb +26 -14
- metadata +105 -32
data/lib/resque/scheduler.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
require 'rufus/scheduler'
|
2
|
-
require '
|
2
|
+
require 'resque/scheduler_locking'
|
3
|
+
require 'resque_scheduler/logger_builder'
|
3
4
|
|
4
5
|
module Resque
|
5
6
|
|
6
7
|
class Scheduler
|
7
8
|
|
8
|
-
extend Resque::
|
9
|
+
extend Resque::SchedulerLocking
|
9
10
|
|
10
11
|
class << self
|
11
12
|
|
@@ -15,13 +16,18 @@ module Resque
|
|
15
16
|
# If set, produces no output
|
16
17
|
attr_accessor :mute
|
17
18
|
|
18
|
-
# If set, will
|
19
|
+
# If set, will write messages to the file
|
20
|
+
attr_accessor :logfile
|
21
|
+
|
22
|
+
# If set, will try to update the schedule in the loop
|
19
23
|
attr_accessor :dynamic
|
20
24
|
|
21
25
|
# Amount of time in seconds to sleep between polls of the delayed
|
22
26
|
# queue. Defaults to 5
|
23
27
|
attr_writer :poll_sleep_amount
|
24
28
|
|
29
|
+
attr_writer :logger
|
30
|
+
|
25
31
|
# the Rufus::Scheduler jobs that are scheduled
|
26
32
|
def scheduled_jobs
|
27
33
|
@@scheduled_jobs
|
@@ -31,12 +37,23 @@ module Resque
|
|
31
37
|
@poll_sleep_amount ||= 5 # seconds
|
32
38
|
end
|
33
39
|
|
40
|
+
def logger
|
41
|
+
@logger ||= ResqueScheduler::LoggerBuilder.new(:mute => mute, :verbose => verbose, :log_dev => logfile).build
|
42
|
+
end
|
43
|
+
|
34
44
|
# Schedule all jobs and continually look for delayed jobs (never returns)
|
35
45
|
def run
|
36
46
|
$0 = "resque-scheduler: Starting"
|
47
|
+
|
37
48
|
# trap signals
|
38
49
|
register_signal_handlers
|
39
50
|
|
51
|
+
# Quote from the resque/worker.
|
52
|
+
# Fix buffering so we can `rake resque:scheduler > scheduler.log` and
|
53
|
+
# get output from the child in there.
|
54
|
+
$stdout.sync = true
|
55
|
+
$stderr.sync = true
|
56
|
+
|
40
57
|
# Load the schedule into rufus
|
41
58
|
# If dynamic is set, load that schedule otherwise use normal load
|
42
59
|
if dynamic
|
@@ -47,11 +64,13 @@ module Resque
|
|
47
64
|
|
48
65
|
# Now start the scheduling part of the loop.
|
49
66
|
loop do
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
67
|
+
if is_master?
|
68
|
+
begin
|
69
|
+
handle_delayed_items
|
70
|
+
update_schedule if dynamic
|
71
|
+
rescue Errno::EAGAIN, Errno::ECONNRESET => e
|
72
|
+
warn e.message
|
73
|
+
end
|
55
74
|
end
|
56
75
|
poll_sleep
|
57
76
|
end
|
@@ -59,6 +78,7 @@ module Resque
|
|
59
78
|
# never gets here.
|
60
79
|
end
|
61
80
|
|
81
|
+
|
62
82
|
# For all signals, set the shutdown flag and wait for current
|
63
83
|
# poll/enqueing to finish (should be almost istant). In the
|
64
84
|
# case of sleeping, exit immediately.
|
@@ -133,8 +153,10 @@ module Resque
|
|
133
153
|
if !config[interval_type].nil? && config[interval_type].length > 0
|
134
154
|
args = optionizate_interval_value(config[interval_type])
|
135
155
|
@@scheduled_jobs[name] = rufus_scheduler.send(interval_type, *args) do
|
136
|
-
|
137
|
-
|
156
|
+
if is_master?
|
157
|
+
log! "queueing #{config['class']} (#{name})"
|
158
|
+
handle_errors { enqueue_from_config(config) }
|
159
|
+
end
|
138
160
|
end
|
139
161
|
interval_defined = true
|
140
162
|
break
|
@@ -169,7 +191,8 @@ module Resque
|
|
169
191
|
item = nil
|
170
192
|
begin
|
171
193
|
handle_shutdown do
|
172
|
-
|
194
|
+
# Continually check that it is still the master
|
195
|
+
if is_master? && item = Resque.next_item_for_timestamp(timestamp)
|
173
196
|
log "queuing #{item['class']} [delayed]"
|
174
197
|
handle_errors { enqueue_from_config(item) }
|
175
198
|
end
|
@@ -197,7 +220,7 @@ module Resque
|
|
197
220
|
args = job_config['args'] || job_config[:args]
|
198
221
|
|
199
222
|
klass_name = job_config['class'] || job_config[:class]
|
200
|
-
klass = constantize(klass_name) rescue klass_name
|
223
|
+
klass = ResqueScheduler::Util.constantize(klass_name) rescue klass_name
|
201
224
|
|
202
225
|
params = args.is_a?(Hash) ? [args] : Array(args)
|
203
226
|
queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
|
@@ -207,7 +230,7 @@ module Resque
|
|
207
230
|
# job class can not be constantized (via a requeue call from the web perhaps), fall
|
208
231
|
# back to enqueing normally via Resque::Job.create.
|
209
232
|
begin
|
210
|
-
constantize(job_klass).scheduled(queue, klass_name, *params)
|
233
|
+
ResqueScheduler::Util.constantize(job_klass).scheduled(queue, klass_name, *params)
|
211
234
|
rescue NameError
|
212
235
|
# Note that the custom job class (job_config['custom_job_class']) is the one enqueued
|
213
236
|
Resque::Job.create(queue, job_klass, *params)
|
@@ -218,7 +241,14 @@ module Resque
|
|
218
241
|
# one app that schedules for another
|
219
242
|
if Class === klass
|
220
243
|
ResqueScheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
|
221
|
-
|
244
|
+
|
245
|
+
# If the class is a custom job class, call self#scheduled on it. This allows you to do things like
|
246
|
+
# Resque.enqueue_at(timestamp, CustomJobClass). Otherwise, pass off to Resque.
|
247
|
+
if klass.respond_to?(:scheduled)
|
248
|
+
klass.scheduled(queue, klass_name, *params)
|
249
|
+
else
|
250
|
+
Resque.enqueue_to(queue, klass, *params)
|
251
|
+
end
|
222
252
|
else
|
223
253
|
# This will not run the before_hooks in rescue, but will at least
|
224
254
|
# queue the job.
|
@@ -278,19 +308,26 @@ module Resque
|
|
278
308
|
true
|
279
309
|
end
|
280
310
|
|
281
|
-
# Sets the shutdown flag, exits if sleeping
|
311
|
+
# Sets the shutdown flag, clean schedules and exits if sleeping
|
282
312
|
def shutdown
|
283
313
|
@shutdown = true
|
284
|
-
|
314
|
+
|
315
|
+
if @sleeping
|
316
|
+
thread = Thread.new do
|
317
|
+
Resque.clean_schedules
|
318
|
+
release_master_lock!
|
319
|
+
end
|
320
|
+
thread.join
|
321
|
+
exit
|
322
|
+
end
|
285
323
|
end
|
286
324
|
|
287
325
|
def log!(msg)
|
288
|
-
|
326
|
+
logger.info msg
|
289
327
|
end
|
290
328
|
|
291
329
|
def log(msg)
|
292
|
-
|
293
|
-
log!(msg) if verbose
|
330
|
+
logger.debug msg
|
294
331
|
end
|
295
332
|
|
296
333
|
def procline(string)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# ### Locking the scheduler process
|
2
|
+
#
|
3
|
+
# There are two places in resque-scheduler that need to be synchonized
|
4
|
+
# in order to be able to run redundant scheduler processes while ensuring jobs don't
|
5
|
+
# get queued multiple times when the master process changes.
|
6
|
+
#
|
7
|
+
# 1) Processing the delayed queues (jobs that are created from enqueue_at/enqueue_in, etc)
|
8
|
+
# 2) Processing the scheduled (cron-like) jobs from rufus-scheduler
|
9
|
+
#
|
10
|
+
# Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
|
11
|
+
# redis would suffice. However, protecting the scheduled jobs is trickier
|
12
|
+
# because the clocks on machines could be slightly off or actual firing times
|
13
|
+
# could vary slightly due to load. If scheduler A's clock is slightly ahead
|
14
|
+
# of scheduler B's clock (since they are on different machines), when
|
15
|
+
# scheduler A dies, we need to ensure that scheduler B doesn't queue jobs
|
16
|
+
# that A already queued before it's death. (This all assumes that it is
|
17
|
+
# better to miss a few scheduled jobs than it is to run them multiple times
|
18
|
+
# for the same iteration.)
|
19
|
+
#
|
20
|
+
# To avoid queuing multiple jobs in the case of master fail-over, the master
|
21
|
+
# should remain the master as long as it can rather than a simple SETNX which
|
22
|
+
# would result in the master roll being passed around frequently.
|
23
|
+
#
|
24
|
+
# Locking Scheme:
|
25
|
+
# Each resque-scheduler process attempts to get the master lock via SETNX.
|
26
|
+
# Once obtained, it sets the expiration for 3 minutes (configurable). The
|
27
|
+
# master process continually updates the timeout on the lock key to be 3
|
28
|
+
# minutes in the future in it's loop(s) (see `run`) and when jobs come out of
|
29
|
+
# rufus-scheduler (see `load_schedule_job`). That ensures that a minimum of
|
30
|
+
# 3 minutes must pass since the last queuing operation before a new master is
|
31
|
+
# chosen. If, for whatever reason, the master fails to update the expiration
|
32
|
+
# for 3 minutes, the key expires and the lock is up for grabs. If
|
33
|
+
# miraculously the original master comes back to life, it will realize it is
|
34
|
+
# no longer the master and stop processing jobs.
|
35
|
+
#
|
36
|
+
# The clocks on the scheduler machines can then be up to 3 minutes off from
|
37
|
+
# each other without the risk of queueing the same scheduled job twice during
|
38
|
+
# a master change. The catch is, in the event of a master change, no
|
39
|
+
# scheduled jobs will be queued during those 3 minutes. So, there is a trade
|
40
|
+
# off: the higher the timeout, the less likely scheduled jobs will be fired
|
41
|
+
# twice but greater chances of missing scheduled jobs. The lower the timeout,
|
42
|
+
# less likely jobs will be missed, greater the chances of jobs firing twice. If
|
43
|
+
# you don't care about jobs firing twice or are certain your machines' clocks
|
44
|
+
# are well in sync, a lower timeout is preferable. One thing to keep in mind:
|
45
|
+
# this only effects *scheduled* jobs - delayed jobs will never be lost or
|
46
|
+
# skipped since eventually a master will come online and it will process
|
47
|
+
# everything that is ready (no matter how old it is). Scheduled jobs work
|
48
|
+
# like cron - if you stop cron, no jobs fire while it's stopped and it doesn't
|
49
|
+
# fire jobs that were missed when it starts up again.
|
50
|
+
|
51
|
+
require 'resque/scheduler/lock'
|
52
|
+
|
53
|
+
module Resque
|
54
|
+
module SchedulerLocking
|
55
|
+
def master_lock
|
56
|
+
@master_lock ||= build_master_lock
|
57
|
+
end
|
58
|
+
|
59
|
+
def supports_lua?
|
60
|
+
redis_master_version >= 2.5
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_master?
|
64
|
+
master_lock.acquire! || master_lock.locked?
|
65
|
+
end
|
66
|
+
|
67
|
+
def release_master_lock!
|
68
|
+
master_lock.release!
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def build_master_lock
|
74
|
+
if supports_lua?
|
75
|
+
Resque::Scheduler::Lock::Resilient.new(master_lock_key)
|
76
|
+
else
|
77
|
+
Resque::Scheduler::Lock::Basic.new(master_lock_key)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def master_lock_key
|
82
|
+
"#{ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''}resque_scheduler_master_lock".to_sym
|
83
|
+
end
|
84
|
+
|
85
|
+
def redis_master_version
|
86
|
+
Resque.redis.info['redis_version'].to_f
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ResqueScheduler
|
2
|
+
# Just builds a logger, with specified verbosity and destination.
|
3
|
+
# The simplest example:
|
4
|
+
#
|
5
|
+
# ResqueScheduler::LoggerBuilder.new.build
|
6
|
+
class LoggerBuilder
|
7
|
+
# Initializes new instance of the builder
|
8
|
+
#
|
9
|
+
# Pass :opts Hash with
|
10
|
+
# - :mute if logger needs to be silent for all levels. Default - false
|
11
|
+
# - :verbose if there is a need in debug messages. Default - false
|
12
|
+
# - :log_dev to output logs into a desired file. Default - STDOUT
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# LoggerBuilder.new(:mute => false, :verbose => true, :log_dev => 'log/sheduler.log')
|
17
|
+
def initialize(opts={})
|
18
|
+
@muted = !!opts[:mute]
|
19
|
+
@verbose = !!opts[:verbose]
|
20
|
+
@log_dev = opts[:log_dev] || STDOUT
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns an instance of Logger
|
24
|
+
def build
|
25
|
+
logger = Logger.new(@log_dev)
|
26
|
+
logger.level = level
|
27
|
+
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
|
28
|
+
logger.formatter = formatter
|
29
|
+
|
30
|
+
logger
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def level
|
36
|
+
if @verbose && !@muted
|
37
|
+
Logger::DEBUG
|
38
|
+
elsif !@muted
|
39
|
+
Logger::INFO
|
40
|
+
else
|
41
|
+
Logger::FATAL
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def formatter
|
46
|
+
proc do |severity, datetime, progname, msg|
|
47
|
+
"[#{severity}] #{datetime}: #{msg}\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -8,6 +8,7 @@
|
|
8
8
|
|
9
9
|
<p class='intro'>
|
10
10
|
This list below contains the timestamps for scheduled delayed jobs.
|
11
|
+
Server local time: <%= Time.now %>
|
11
12
|
</p>
|
12
13
|
|
13
14
|
<p class='sub'>
|
@@ -45,4 +46,4 @@
|
|
45
46
|
<% end %>
|
46
47
|
</table>
|
47
48
|
|
48
|
-
<%= partial :next_more, :start => start, :size => size %>
|
49
|
+
<%= partial :next_more, :start => start, :size => size %>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<h1><%= @job_name %></h1>
|
2
|
+
|
3
|
+
<p class='intro'>
|
4
|
+
This job requires parameters:
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<form style="float:left" action="<%= u "/schedule/requeue_with_params" %>" method="post">
|
8
|
+
<table>
|
9
|
+
<% @parameters.each do |key, value| %>
|
10
|
+
<% value ||= {} %>
|
11
|
+
<tr>
|
12
|
+
<td>
|
13
|
+
<%= key %>
|
14
|
+
<% if value['description'] || value[:description] %>
|
15
|
+
<span style="border-bottom:1px dotted;" title="<%=value ['description'] || value[:description] %>">(?)</span>
|
16
|
+
<% end %>:
|
17
|
+
</td>
|
18
|
+
<td><input type="text" name="<%= key %>" value="<%= value['default'] || value[:default] %>"></td>
|
19
|
+
<% end %>
|
20
|
+
</table>
|
21
|
+
<input type="hidden" name="job_name" value="<%= @job_name %>">
|
22
|
+
<input type="submit" value="Queue now">
|
23
|
+
</form>
|
@@ -3,6 +3,7 @@
|
|
3
3
|
<p class='intro'>
|
4
4
|
The list below contains all scheduled jobs. Click "Queue now" to queue
|
5
5
|
a job immediately.
|
6
|
+
Server local time: <%= Time.now %>
|
6
7
|
</p>
|
7
8
|
|
8
9
|
<table>
|
@@ -26,10 +27,10 @@
|
|
26
27
|
</td>
|
27
28
|
<td><%= h name %></td>
|
28
29
|
<td><%= h config['description'] %></td>
|
29
|
-
<td style="white-space:nowrap"><%= if !config['every'].nil?
|
30
|
-
h('every: ' + config['every'])
|
30
|
+
<td style="white-space:nowrap"><%= if !config['every'].nil?
|
31
|
+
h('every: ' + (config['every'].is_a?(Array) ? config['every'].join(", ") : config['every'].to_s ))
|
31
32
|
elsif !config['cron'].nil?
|
32
|
-
h('cron: ' + config['cron'])
|
33
|
+
h('cron: ' + config['cron'].to_s)
|
33
34
|
else
|
34
35
|
'Not currently scheduled'
|
35
36
|
end %></td>
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'resque_scheduler'
|
2
2
|
require 'resque/server'
|
3
|
-
|
4
3
|
# Extend Resque::Server to add tabs
|
5
4
|
module ResqueScheduler
|
6
5
|
|
@@ -16,7 +15,7 @@ module ResqueScheduler
|
|
16
15
|
end
|
17
16
|
|
18
17
|
def queue_from_class_name(class_name)
|
19
|
-
Resque.queue_from_class(
|
18
|
+
Resque.queue_from_class(ResqueScheduler::Util.constantize(class_name))
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
@@ -27,7 +26,30 @@ module ResqueScheduler
|
|
27
26
|
end
|
28
27
|
|
29
28
|
post "/schedule/requeue" do
|
30
|
-
|
29
|
+
@job_name = params['job_name'] || params[:job_name]
|
30
|
+
config = Resque.schedule[@job_name]
|
31
|
+
@parameters = config['parameters'] || config[:parameters]
|
32
|
+
if @parameters
|
33
|
+
erb File.read(File.join(File.dirname(__FILE__), 'server/views/requeue-params.erb'))
|
34
|
+
else
|
35
|
+
Resque::Scheduler.enqueue_from_config(config)
|
36
|
+
redirect u("/overview")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
post "/schedule/requeue_with_params" do
|
41
|
+
job_name = params['job_name'] || params[:job_name]
|
42
|
+
config = Resque.schedule[job_name]
|
43
|
+
# Build args hash from post data (removing the job name)
|
44
|
+
submitted_args = params.reject {|key, value| key == 'job_name' || key == :job_name}
|
45
|
+
|
46
|
+
# Merge constructed args hash with existing args hash for
|
47
|
+
# the job, if it exists
|
48
|
+
config_args = config['args'] || config[:args] || {}
|
49
|
+
config_args = config_args.merge(submitted_args)
|
50
|
+
|
51
|
+
# Insert the args hash into config and queue the resque job
|
52
|
+
config = config.merge('args' => config_args)
|
31
53
|
Resque::Scheduler.enqueue_from_config(config)
|
32
54
|
redirect u("/overview")
|
33
55
|
end
|
@@ -9,17 +9,23 @@ namespace :resque do
|
|
9
9
|
require 'resque'
|
10
10
|
require 'resque_scheduler'
|
11
11
|
|
12
|
+
# Need to set this here for conditional Process.daemon redirect of stderr/stdout to /dev/null
|
13
|
+
Resque::Scheduler.mute = true if ENV['MUTE']
|
14
|
+
|
12
15
|
if ENV['BACKGROUND']
|
13
16
|
unless Process.respond_to?('daemon')
|
14
17
|
abort "env var BACKGROUND is set, which requires ruby >= 1.9"
|
15
18
|
end
|
16
|
-
Process.daemon(true)
|
19
|
+
Process.daemon(true, !Resque::Scheduler.mute)
|
20
|
+
Resque.redis.client.reconnect
|
17
21
|
end
|
18
22
|
|
19
23
|
File.open(ENV['PIDFILE'], 'w') { |f| f << Process.pid.to_s } if ENV['PIDFILE']
|
20
24
|
|
21
|
-
Resque::Scheduler.dynamic
|
22
|
-
Resque::Scheduler.verbose
|
25
|
+
Resque::Scheduler.dynamic = true if ENV['DYNAMIC_SCHEDULE']
|
26
|
+
Resque::Scheduler.verbose = true if ENV['VERBOSE']
|
27
|
+
Resque::Scheduler.logfile = ENV['LOGFILE'] if ENV['LOGFILE']
|
28
|
+
Resque::Scheduler.poll_sleep_amount = Float(ENV['RESQUE_SCHEDULER_INTERVAL']) if ENV['RESQUE_SCHEDULER_INTERVAL']
|
23
29
|
Resque::Scheduler.run
|
24
30
|
end
|
25
31
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ResqueScheduler
|
2
|
+
class Util
|
3
|
+
# In order to upgrade to resque(1.25) which has deprecated following methods ,
|
4
|
+
# we just added these usefull helpers back to use in Resque Scheduler.
|
5
|
+
# refer to: https://github.com/resque/resque-scheduler/pull/273
|
6
|
+
|
7
|
+
def self.constantize(camel_cased_word)
|
8
|
+
camel_cased_word = camel_cased_word.to_s
|
9
|
+
|
10
|
+
if camel_cased_word.include?('-')
|
11
|
+
camel_cased_word = classify(camel_cased_word)
|
12
|
+
end
|
13
|
+
|
14
|
+
names = camel_cased_word.split('::')
|
15
|
+
names.shift if names.empty? || names.first.empty?
|
16
|
+
|
17
|
+
constant = Object
|
18
|
+
names.each do |name|
|
19
|
+
args = Module.method(:const_get).arity != 1 ? [false] : []
|
20
|
+
|
21
|
+
if constant.const_defined?(name, *args)
|
22
|
+
constant = constant.const_get(name)
|
23
|
+
else
|
24
|
+
constant = constant.const_missing(name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
constant
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.classify(dashed_word)
|
31
|
+
dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|