riboseinc/attr_masker

View on GitHub
lib/attr_masker/attribute.rb

Summary

Maintainability
A
0 mins
Test Coverage
# (c) 2017 Ribose Inc.
#

module AttrMasker
  # Holds the definition of maskable attribute.
  class Attribute
    attr_reader :name, :model, :options

    def initialize(name, model, options)
      @name = name.to_sym
      @model = model
      @options = options
    end

    # Evaluates the +:if+ and +:unless+ attribute options on given instance.
    # Returns +true+ or +false+, depending on whether the attribute should be
    # masked for this object or not.
    def should_mask?(model_instance)
      not (
        options.key?(:if) && !evaluate_option(:if, model_instance) ||
        options.key?(:unless) && evaluate_option(:unless, model_instance)
      )
    end

    # Mask the attribute on given model.  Masking will be performed regardless
    # of +:if+ and +:unless+ options.  A +should_mask?+ method should be called
    # separately to ensure that given object is eligible for masking.
    #
    # The method returns the masked value but does not modify the object's
    # attribute.
    #
    # If +marshal+ attribute's option is +true+, the attribute value will be
    # loaded before masking, and dumped to proper storage format prior
    # returning.
    def mask(model_instance)
      value = unmarshal_data(model_instance.send(name))
      masker = options[:masker]
      masker_value = masker.call(value: value, model: model_instance,
                                 attribute_name: name, masking_options: options)
      model_instance.send("#{name}=", marshal_data(masker_value))
    end

    # Returns a hash of maskable attribute names, and respective attribute
    # values.  Unchanged attributes are skipped.
    def masked_attributes_new_values(model_instance)
      model_instance.changes.slice(*column_names).transform_values(&:second)
    end

    # Evaluates option (typically +:if+ or +:unless+) on given model instance.
    # That option can be either a proc (a model is passed as an only argument),
    # or a symbol (a method of that name is called on model instance).
    def evaluate_option(option_name, model_instance)
      option = options[option_name]

      if option.is_a?(Symbol)
        model_instance.send(option)
      elsif option.respond_to?(:call)
        option.call(model_instance)
      else
        option
      end
    end

    def marshal_data(data)
      return data unless options[:marshal]

      options[:marshaler].send(options[:dump_method], data)
    end

    def unmarshal_data(data)
      return data unless options[:marshal]

      options[:marshaler].send(options[:load_method], data)
    end

    def column_names
      options[:column_names] || [name]
    end
  end
end