lib/consul/power.rb
module Consul
module Power
include Consul::Power::DynamicAccess::InstanceMethods
def self.included(base)
base.extend ClassMethods
base.send :include, Memoized
end
private
def default_include_power?(power_name, *context)
result = send(power_name, *context)
# Everything that is not nil is considered as included.
# We are short-circuiting for #scoped first since sometimes
# has_many associations (which behave scopish) trigger their query
# when you try to negate them, compare them or even retrieve their
# class. Unfortunately we can only reproduce this in live Rails
# apps, not in Consul tests. Might be some standard gem that is not
# loaded in Consul tests.
result.respond_to?(:load_target, true) || !!result
end
def default_include_object?(power_name, *args)
check_number_of_arguments_in_include_object(power_name, args.length)
object = args.pop
context = args
power_value = send(power_name, *context)
if power_value.nil?
false
elsif Util.scope?(power_value)
if Util.scope_selects_all_records?(power_value)
true
else
power_ids_name = self.class.power_ids_name(power_name)
send(power_ids_name, *context).include?(object.id)
end
elsif Util.collection?(power_value)
power_value.include?(object)
else
raise Consul::NoCollection, "can only call #include_object? on a collection, but power was of type #{power_value.class.name}"
end
end
def default_power_ids(power_name, *args)
scope = send(power_name, *args)
database_touched
scope.collect_ids
end
def powerless!(*args)
raise Consul::Powerless.new("No power to #{[*args].inspect}")
end
def boolean_or_nil?(value)
[TrueClass, FalseClass, NilClass].include?(value.class)
end
def database_touched
# spy for tests
end
def singularize_power_name(name)
self.class.singularize_power_name(name)
end
def check_number_of_arguments_in_include_object(power_name, given_arguments)
# check unmemoized methods as Memoizer wraps methods and masks the arity.
unmemoized_power_name = respond_to?("_unmemoized_#{power_name}") ? "_unmemoized_#{power_name}" : power_name
power_arity = method(unmemoized_power_name).arity
expected_arity = power_arity + 1 # one additional argument for the context
if power_arity >= 0 && expected_arity != given_arguments
raise ArgumentError.new("wrong number of arguments (given #{given_arguments}, expected #{expected_arity})")
end
end
module ClassMethods
include Consul::Power::DynamicAccess::ClassMethods
def power(*names, &block)
names.each do |name|
define_power(name, &block)
end
end
def power_ids_name(name)
"#{name.to_s.singularize}_ids"
end
def self.thread_key(klass)
"consul|#{klass.to_s}.current"
end
def current
Thread.current[ClassMethods.thread_key(self)]
end
def current=(power)
Thread.current[ClassMethods.thread_key(self)] = power
end
def with_power(*args, **kwargs , &block)
inner_power = if args.first.is_a?(self)
args.first
elsif args.length == 1 && args.first.nil?
nil
elsif kwargs.empty?
new(*args)
else
new(*args, **kwargs)
end
old_power = current
self.current = inner_power
block.call
ensure
self.current = old_power
end
def without_power(&block)
with_power(nil, &block)
end
def define_query_and_bang_methods(name, options, &query)
is_plural = options.fetch(:is_plural)
query_method = "#{name}?"
bang_method = "#{name}!"
define_method(query_method, &query)
memoize query_method
define_method(bang_method) do |*args|
if is_plural
if send(query_method, *args)
send(name, *args)
else
powerless!(name, *args)
end
else
send(query_method, *args) or powerless!(name, *args)
end
end
# We don't memoize the bang method since memoizer can't memoize a thrown exception
end
def define_ids_method(name)
ids_method = power_ids_name(name)
define_method(ids_method) { |*args| default_power_ids(name, *args) }
# Memoize `ids_method` in addition to the collection method itself, since
# #default_include_object? directly accesses `ids_method`.
memoize ids_method
end
def define_main_method(name, &block)
define_method(name, &block)
memoize name
end
def define_power(name, &block)
name = name.to_s
if name.ends_with?('?')
# The developer is trying to register an optimized query method
# for singular object queries.
name_without_suffix = name.chop
define_query_and_bang_methods(name_without_suffix, :is_plural => false, &block)
else
define_main_method(name, &block)
define_ids_method(name)
define_query_and_bang_methods(name, :is_plural => true) { |*args| default_include_power?(name, *args) }
begin
singular = singularize_power_name(name)
define_query_and_bang_methods(singular, :is_plural => false) { |*args| default_include_object?(name, *args) }
rescue Consul::PowerNotSingularizable
# We do not define singularized power methods if it would
# override the collection method
end
end
name
end
def singularize_power_name(name)
name = name.to_s
singularized = name.singularize
if singularized == name
raise Consul::PowerNotSingularizable, "Power name can not have a singular form: #{name}"
else
singularized
end
end
end
end
end