Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
delayed (3.0.1)
delayed (3.1.0)
activerecord (>= 6.0)
concurrent-ruby

Expand Down
48 changes: 28 additions & 20 deletions lib/delayed/priority.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ def alerts
def names=(names)
raise "must include a name for priority >= 0" if names && !names.value?(0)

remove_named_priority_methods
@ranges = nil
@alerts = nil
@names_to_priority = nil
@names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }
define_named_priority_methods
end

def alerts=(alerts)
Expand Down Expand Up @@ -124,17 +126,35 @@ def midpoint(low, high)
low + ((high - low).to_d / 2).ceil
end

def respond_to_missing?(method_name, include_private = false)
names_to_priority.key?(method_name) || super
# Defines a class method (e.g. `Priority.eventual`) and an instance predicate (e.g.
# `priority.eventual?`) per name, as real, introspectable methods (visible to `methods`,
# RDoc, and Sorbet's tapioca, unlike `method_missing` dispatch). Pre-existing methods
# (e.g. a name of `superclass`) are never overridden.
def define_named_priority_methods
names.each_key do |name|
predicate = :"#{name}?"
define_singleton_method(name) { names_to_priority.fetch(name) } unless method_defined_on?(singleton_class, name)
define_method(predicate) { name.to_s == to_s } unless method_defined_on?(self, predicate)
end
end

def method_missing(method_name, *args)
if names_to_priority.key?(method_name) && args.none?
names_to_priority[method_name]
else
super
# Removes the methods previously defined for the current `names` (called before `names` is
# reassigned). The non-inherited (`false`) checks mean we only remove methods we defined
# ourselves, leaving any pre-existing method a colliding name never overrode (e.g.
# `Class#superclass`, `Numeric#zero?`) in place.
def remove_named_priority_methods
names.each_key do |name|
predicate = :"#{name}?"
singleton_class.send(:remove_method, name) if singleton_class.method_defined?(name, false)
remove_method(predicate) if method_defined?(predicate, false)
end
end

# Whether `mod` already responds to `name` (public, protected, private, or inherited), in
# which case a same-named priority must not override it.
def method_defined_on?(mod, name)
mod.method_defined?(name) || mod.private_method_defined?(name)
end
end

attr_reader :value
Expand Down Expand Up @@ -188,18 +208,6 @@ def to_d
to_i.to_d
end

private

def respond_to_missing?(method_name, include_private = false)
(method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym)) || super
end

def method_missing(method_name, *args)
if method_name.to_s.end_with?('?') && self.class.names.key?(method_name.to_s[0..-2].to_sym)
method_name.to_s[0..-2] == to_s
else
super
end
end
send(:define_named_priority_methods)
end
end
2 changes: 1 addition & 1 deletion lib/delayed/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Delayed
VERSION = '3.0.1'
VERSION = '3.1.0'
end
31 changes: 30 additions & 1 deletion spec/delayed/priority_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
ensure
described_class.alerts = nil
described_class.names = nil
described_class.assign_at_midpoint = nil
end

describe '.names, .ranges, .alerts, .names_to_priority, method_missing' do
describe '.names, .ranges, .alerts, .names_to_priority, named priority methods' do
it 'defaults to interactive, user_visible, eventual, reporting' do
expect(described_class.names).to eq(
interactive: 0,
Expand Down Expand Up @@ -68,6 +69,34 @@
end
end

it 'defines named priority methods as real, introspectable methods' do
expect(described_class.singleton_class.method_defined?(:eventual)).to be true
expect(described_class.method_defined?(:eventual?)).to be true
end

it 'removes methods for the old names when names are reassigned' do
described_class.names = { high: 0 }
expect(described_class).to respond_to(:high)
expect(described_class.new(0)).to be_high

described_class.names = nil
expect(described_class).not_to respond_to(:high)
expect(described_class.new(0)).not_to respond_to(:high?)
expect(described_class).to respond_to(:interactive)
expect(described_class.new(0)).to be_interactive
end

context 'when a name collides with an existing method' do
let(:custom_names) { { superclass: 0, zero: 10 } }

it 'does not redefine the existing method' do
expect(described_class.superclass).to eq Numeric # Class#superclass, not the priority
expect(described_class.new(0).zero?).to be true # Numeric#zero?, not the range predicate
expect(described_class.zero).to eq 10
expect(described_class.new(5).superclass?).to be true
end
end

context 'when customized to high, medium, low' do
let(:custom_names) { { high: 0, medium: 100, low: 500 } }

Expand Down
Loading