resque-scheduler 2.0.0 → 2.3.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.
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
|