module ActiveFedora
module Delegating
extend ActiveSupport::Concern
extend Deprecation
included do
after_save :clear_changed_attributes
def clear_changed_attributes
@previously_changed = changes
@changed_attributes.clear
end
end
# Calling inspect may trigger a bunch of loads, but it's mainly for debugging, so no worries.
def inspect
values = self.class.delegates.keys.map {|r| "#{r}:#{send(r).inspect}"}
"#<#{self.class} pid:\"#{pretty_pid}\", #{values.join(', ')}>"
end
def [](key)
array_reader(key)
end
def []=(key, value)
array_setter(key, value)
end
private
def array_reader(field, *args)
raise UnknownAttributeError, "#{self.class} does not have an attribute `#{field}'" unless self.class.delegates.key?(field)
if args.present?
instance_exec(*args, &self.class.delegates[field][:reader])
else
instance_exec &self.class.delegates[field][:reader]
end
end
def array_setter(field, args)
raise UnknownAttributeError, "#{self.class} does not have an attribute `#{field}'" unless self.class.delegates.key?(field)
instance_exec(args, &self.class.delegates[field][:setter])
end
module ClassMethods
def delegates
@local_delegates ||= {}.with_indifferent_access
return @local_delegates unless superclass.respond_to?(:delegates) and value = superclass.delegates
@local_delegates = value.dup if @local_delegates.empty?
@local_delegates
end
def delegates= val
@local_delegates = val
end
# Provides a delegate class method to expose methods in metadata streams
# as member of the base object. Pass the target datastream via the
# :to argument. If you want to return a multivalue result, (e.g. array
# instead of a string) set the :multiple argument to true.
#
# The optional :at argument provides a terminology that the delegate will point to.
#
# class Foo < ActiveFedora::Base
# has_metadata :name => "descMetadata", :type => MyDatastream
#
# delegate :field1, :to=>"descMetadata", multiple: false
# delegate :field2, :to=>"descMetadata", :at=>[:term1, :term2], multiple: true
# end
#
# foo = Foo.new
# foo.field1 = "My Value"
# foo.field1 # => "My Value"
# foo.field2 # => [""]
# foo.field3 # => NoMethodError: undefined method `field3' for #
def delegate(*methods)
fields = methods.dup
options = fields.pop
unless options.is_a?(Hash) && to = options[:to]
raise ArgumentError, "Target is required"
end
if ds_specs.has_key? to.to_s
define_attribute_method fields.first
create_delegate_reader(fields.first, options)
create_delegate_setter(fields.first, options)
else
super(*methods)
end
end
# Allows you to delegate multiple terminologies to the same datastream, instead
# having to call the method each time for each term. The target datastream is the
# first argument, followed by an array of the terms that will point to that
# datastream. Terms must be a single value, ie. :field and not [:term1, :term2].
# This is best accomplished by refining your OM terminology using :ref or :proxy
# methods to reduce nested terms down to one.
#
# class Foo < ActiveFedora::Base
# has_metadata :name => "descMetadata", :type => MyDatastream
#
# delegate_to :descMetadata, [:field1, :field2]
# end
#
# foo = Foo.new
# foo.field1 = "My Value"
# foo.field1 # => "My Value"
# foo.field2 # => [""]
# foo.field3 # => NoMethodError: undefined method `field3' for #
def delegate_to(datastream,fields,args={})
define_attribute_methods fields
fields.each do |f|
args.merge!({:to=>datastream})
create_delegate_reader(f, args)
create_delegate_setter(f, args)
end
end
# Reveal if the delegated field is unique or not
# @param [Symbol] field the field to query
# @return [Boolean]
def unique?(field)
!multiple?(field)
end
# Reveal if the delegated field is multivalued or not
# @param [Symbol] field the field to query
# @return [Boolean]
def multiple?(field)
delegates[field][:multiple]
end
private
def create_delegate_reader(field, args)
self.delegates[field] ||= {}
self.delegates[field][:reader] = lambda do |*opts|
ds = self.send(args[:to])
if ds.kind_of?(ActiveFedora::RDFDatastream)
ds.send(field)
else
terminology = args[:at] || [field]
if terminology.length == 1 && opts.present?
ds.send(terminology.first, *opts)
else
ds.send(:term_values, *terminology)
end
end
end
if !args[:multiple].nil?
self.delegates[field][:multiple] = args[:multiple]
elsif !args[:unique].nil?
i = 0
begin
match = /in `(delegate.*)'/.match(caller[i])
i+=1
end while match.nil?
prev_method = match.captures.first
Deprecation.warn Delegating, "The :unique option for `#{prev_method}' is deprecated. Use :multiple instead. :unique will be removed in ActiveFedora 7", caller(i+1)
self.delegates[field][:multiple] = !args[:unique]
else
i = 0
begin
match = /in `(delegate.*)'/.match(caller[i])
i+=1
end while match.nil?
prev_method = match.captures.first
Deprecation.warn Delegating, "You have not explicitly set the :multiple option on `#{prev_method}'. The default value will switch from true to false in ActiveFedora 7, so if you want to future-proof this application set `multiple: true'", caller(i+ 1)
self.delegates[field][:multiple] = true # this should be false for ActiveFedora 7
end
define_method field do |*opts|
val = array_reader(field, *opts)
self.class.multiple?(field) ? val : val.first
end
end
def create_delegate_setter(field, args)
self.delegates[field] ||= {}
self.delegates[field][:setter] = lambda do |v|
ds = self.send(args[:to])
self.send("#{field}_will_change!") unless v == array_reader(field)
if ds.kind_of?(ActiveFedora::RDFDatastream)
ds.send("#{field}=", v)
else
terminology = args[:at] || [field]
ds.send(:update_indexed_attributes, {terminology => v})
end
end
define_method "#{field}=".to_sym do |v|
self[field]=v
end
end
end
end
end