joekhoobyar/cardiac

View on GitHub
lib/cardiac/model/attributes.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Cardiac
  module Model
    
    # Cardiac::Model attribute methods.
    # Some of this has been "borrowed" from ActiveRecord.
    module Attributes
      extend ActiveSupport::Concern
      
      module ClassMethods
      
        # Overridden to support passing in aliases at the same time.
        # This could decrease the code size for attributes declarations by almost 50% for models with
        # non-friendly remote attribute names.
        def attribute(name,options={})
          aliases = Array(options[:aliases])
          super name, options.except(:aliases)
          aliases.each{|k| alias_attribute k, name } if aliases
        end

      protected
              
        # Unwraps a payload returned by the remote, requiring it to be present.
        #
        # Callers could pass <code>{allow_empty: true}</code> in the options to prevent
        # empty results from being converted to <code>nil</code>.
        def unwrap_remote_data(data,options={})
          data = data.values.first if Hash===data && data.keys.size==1 && data.keys.first.to_s!='errors'
          data = data.presence unless options[:allow_empty]
          data
        end
      end
      
      included do
        class_attribute :readonly_attributes, instance_writer: false
        class_attribute :key_attributes, instance_writer: false
        class_attribute :id_delimiter
        
        self.readonly_attributes = []
        self.key_attributes = [:id]
        self.id_delimiter = '-'
      end
    
      # Retrieves the most recently unpacked/decoded remote attributes.
      def remote_attributes
        @remote_attributes.with_indifferent_access
      end
      
      # Overridden to use this model's key_attributes to build the key.
      # Returns an array of the key values, if they are all present, otherwise, returns nil.
      def to_key
        keys = key_attributes.presence and keys.map do |key|
          return unless query_attribute(key)
          read_attribute(key)
        end
      end
      
      # This baseline implementation uses this model's id_delimiter to join what is returned by to_key.
      # If the id_delimiter is set to nil, it will simply return an array instead.
      #
      # NOTE: Defining an :id attribute on your model will override this implementation.
      def id
        delim, values = id_delimiter, to_key
        (delim && values) ? values.join(delim) : values
      end
      
      # Overridden to convert empty strings to nil before typecasting.
      def typecast_attribute(typecaster, value)
        value = nil if value.acts_like?(:string) && value.empty?
        super(typecaster, value)
      end
      
    protected

      # Stores the attributes returned by the remote, after performing any unpacking/decoding.
      def assign_remote_attributes(data,options={})
        @remote_attributes = Hash[
          decode_remote_attributes(unwrap_remote_attributes(data, options), options).map do |key,value|
            [key, value.duplicable? ? value.clone : value]
          end
        ]
      end
      
      alias remote_attributes= assign_remote_attributes
  
      # Returns a copy of this model's attributes, cloning the duplicable values.
      def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
        attribute_names.each do |name|
          attributes[name] = clone_attribute_value(reader_method, name)
        end
        attributes
      end
  
      # Reads an attribute value and returns a clone, if it is duplicable, or the original value, if not.
      def clone_attribute_value(reader_method, attribute_name) # :nodoc:
        value = send(reader_method, attribute_name)
        value.duplicable? ? value.clone : value
      rescue TypeError, NoMethodError
        value
      end

      # Unwraps attributes returned by the remote, requiring the data to be non-empty.
      #
      # Callers could pass <code>{allow_empty: true}</code> in the options to prevent
      # empty results from being converted to <code>nil</code>.
      #
      # NOTE: The baseline implementation just delegates to the class method: unwrap_remote_data
      def unwrap_remote_attributes(data,options={})
        self.class.send :unwrap_remote_data, data, options
      end
      
      # Decodes attributes returned by the remote, requiring the data to be non-nil.
      # Callers could pass <code>{only: ...}</code> or <code>{except: ...}</code> in the options
      # to filter the attributes by key.
      #
      # If the remote did not return a Hash, the data is first wrapped in a single key: <code>:data</code>
      def decode_remote_attributes(data,options={})
        unless data.nil?
          data = Hash===data ? data.with_indifferent_access : {data: data}
          data = data.slice(*options[:only]) if options[:only]
          data = data.except(*options[:except]) if options[:except]
        end
        data
      end

    private
      
      # Filters the primary keys and readonly attributes from the attribute names.
      def attributes_for_update(attribute_names)
        serializable_hash(only: attribute_names.reject{|name| readonly_attribute?(name) || key_attribute?(name) })
      end
  
      # Filters out the primary keys, from the attribute names, when the primary
      # key is to be generated (e.g. the id attribute has no value).
      def attributes_for_create(attribute_names)
        serializable_hash(only: attribute_names.reject{|name| key_attribute?(name) && ! query_attribute(name) })
      end
  
      def readonly_attribute?(name)
        self.class.readonly_attributes.include?(name.to_s)
      end
  
      def key_attribute?(name)
        self.class.key_attributes.include?(name.to_s)
      end
  
      def read_attribute_for_serialization(name)
        case value = read_attribute(name)
        when Array
          value.map{|item| item.respond_to?(:serializable_hash) ? item.serializable_hash : item }
        else
          value.respond_to?(:serializable_hash) ? value.serializable_hash : value
        end
      end
    end
  end
end