module Confidentiality
module ClassMethods
# Specify methods that can have a confidentiality value. Each method should have a corresponding column
# with a _confidential suffix.
#
# create_table :people do |t|
# t.string :name, :phone_number
# t.boolean :phone_number_confidential
# end
#
# class Person < ActiveRecord::Base
# confidential_methods :phone_number
# end
#
# The default read/write methods of ActiveRecord are replaced with ones that
# respect the confidentiality value of the field. Some new methods are provided:
#
# * phone_number_without_confidentiality - The original method that ignores any confidentiality value
# * phone_number_without_confidentiality= - An alias of the original write method for use on forms
#
# The original reader method changes to respect the confidentiality value:
#
# p = Person.find(:first)
# p.phone_number # 12345
# p.phone_number_confidential # false
# p.phone_number_confidential = true
# p.phone_number # (confidential)
#
# To ignore the confidentiality value and return the real value, wrap the code in a Confidentiality.ignore block.
#
# Confidentiality.ignore do
# p.phone_number # 12345
# end
#
# You can also make methods confidential based on the confidentiality value of a different method.
# For example, an address may be made up of a few different fields, but the confidentiality is not
# set for every field, but for the address as a whole.
#
# The :using option can be passed to say "use the confidentiality from this other method".
#
# class Person < ActiveRecord::Base
# def address
# [address1, address2, city, country]
# end
#
# confidential_methods :address, :address1, :address2, :city, :country, :using => :address
# end
#
# p = Person.find(:first)
# p.address # ['1 Place', 'Townsville', 'Christchurch', 'New Zealand']
# p.city # Christchurch
# p.address_confidential # false
# p.address_confidential = true
# p.address # ['(confidential)']
# p.city # (confidential)
#
# If :using is a Symbol, a confidentiality method name with a suffix of _confidentiality will be expected.
# If :using is a String, it will be used as the confidentiality method name.
def confidential_methods(*args)
options, methods = args.extract_options!, args
options.assert_valid_keys :using
# To provide confidentiality for model attributes, the generated readers must be in place
define_attribute_methods unless generated_methods?
# Store a list of the methods that have confidentiality
@confidential_methods ||= []
methods.map(&:to_s).each do |method|
confidentiality_method = determine_confidentiality_method(options, method)
@confidential_methods << method
unless instance_methods.include?(confidentiality_method)
raise "#{name} does not have instance method '#{confidentiality_method}'"
end
if instance_methods.include?("#{method}_with_confidentiality")
raise DuplicateError, "confidentiality already defined for '#{method}'"
end
define_method "#{method}_with_confidentiality" do |*args|
read_method_with_confidentiality(method, confidentiality_method, *args)
end
# Define _confidential and _confidential? methods when the :using option is present and they do not exist
if method != confidentiality_method
unless generated_methods.include?("#{method}_confidential")
alias_method "#{method}_confidential", confidentiality_method
end
unless generated_methods.include?("#{method}_confidential?")
alias_method "#{method}_confidential?", confidentiality_method
end
end
alias_method_chain method, :confidentiality
end
end
def reset_column_information_with_confidential_methods
reset_column_information_without_confidential_methods
define_attribute_methods
# Re-alias the _with_confidentiality methods dealing with attributes
if @confidential_methods
@confidential_methods.each do |method|
if column_names.include?(method)
alias_method_chain method, :confidentiality
end
end
end
nil
end
private
def determine_confidentiality_method(options, default)
if options[:using].is_a?(String)
options[:using]
else
"#{options[:using] || default}_confidential"
end
end
end
end