archan937/cached_record

View on GitHub
lib/cached_record/orm.rb

Summary

Maintainability
A
1 hr
Test Coverage
require "cached_record/orm/active_record"
require "cached_record/orm/data_mapper"

module CachedRecord
  module ORM

    def self.included(base)
      base.extend ClassMethods
      base.send :include, InstanceMethods
    end

    module ClassMethods

      def as_cache(*args)
        @as_cache = parse_as_cache_options args if args.any?
        @as_cache ||= {:as_json => {}}
      end

      def as_memoized_cache(*args)
        retain = args.last.delete(:retain) if args.last.is_a?(Hash)
        as_cache(*args).tap do |options|
          options[:memoize] = true
          options[:retain] = retain if retain
        end
      end

      def cache_key(id)
        "#{name.underscore.gsub("/", ".")}.#{id}"
      end

      def cache_root
        "#{name.underscore.gsub(/^.*\//, "")}".to_sym
      end

      def cached(id)
        Cache.get(self, id) do
          uncached id
        end
      end

      def uncached(id)
        raise NotImplementedError, "Cannot fetch uncached `#{self.class}` instances"
      end

      def load_cache_json(json)
        json.symbolize_keys!
        properties, variables = cache_json_to_properties_and_variables(json)
        foreign_keys, attributes = properties.partition{|k, v| k.to_s.match /_ids?$/}.collect{|x| Hash[x]}
        new_cached_instance attributes, foreign_keys, variables
      end

      def new_cached_instance(attributes, foreign_keys, variables)
        id = attributes.delete(:id) || attributes.delete("id")
        _new_cached_instance_(id, attributes).tap do |instance|
          instance.id = id if instance.respond_to?(:id=)
          foreign_keys.each do |key, value|
            set_cached_association instance, key, value
          end
          variables.each do |key, value|
            instance.instance_variable_set key, value
          end
        end
      end

    private

      def _new_cached_instance_(id, attributes)
        new attributes
      end

      def set_cached_association(instance, key, value)
        raise NotImplementedError, "Cannot set cached association for `#{self}` instances"
      end

      def parse_as_cache_options(args)
        if (symbol = args.first).is_a? Symbol
          store = symbol
        end
        if (hash = args.last).is_a? Hash
          expire = hash.delete :expire
          as_json = parse_as_cache_json_options hash
        end
        {
          :store => store,
          :expire => expire,
          :as_json => as_json || {}
        }.reject{|key, value| value.nil?}
      end

      def parse_as_cache_json_options(options)
        options.symbolize_keys!
        validate_as_cache_json_options options
        {}.tap do |opts|
          opts[:only] = symbolize_array(options[:only]) if options[:only]
          opts[:include] = symbolize_array(options[:include]) if options[:include]
          opts[:memoize] = parse_memoize_options(options[:memoize]) if options[:memoize]
          opts[:include_root] = true if options[:include_root]
        end
      end

      def validate_as_cache_json_options(options)
        options.assert_valid_keys :only, :include, :memoize, :include_root
        options.slice(:only, :include).each do |key, value|
          raise ArgumentError unless value.is_a?(Array)
        end
        if options[:memoize] && !options[:memoize].is_a?(Enumerable)
          raise ArgumentError
        end
        if options.include?(:include_root) && ![true, false].include?(options[:include_root])
          raise ArgumentError
        end
      end

      def symbolize_array(array)
        array.collect &:to_sym
      end

      def parse_memoize_options(options)
        [options].flatten.inject({}) do |memo, x|
          hash = x.is_a?(Hash) ? x : {x => :"@#{x}"}
          memo.merge hash.inject({}){|h, (k, v)| h[k.to_sym] = v.to_sym; h}
        end
      end

      def cache_json_to_properties_and_variables(json)
        if as_cache[:as_json][:include_root]
          properties = json.delete cache_root
          variables = json.inject({}){|h, (k, v)| h[:"@#{k}"] = v; h}
          [properties, variables]
        else
          json.partition{|k, v| !k.to_s.match(/^@/)}.collect{|x| Hash[x]}
        end
      end

    end

    module InstanceMethods

      def cache_attributes
        raise NotImplementedError, "Cannot return cache attributes for `#{self.class}` instances"
      end

      def cache_foreign_keys
        raise NotImplementedError, "Cannot return cache foreign keys for `#{self.class}` instances"
      end

      def as_cache_json
        attributes = {:id => id}.merge cache_attributes
        variables = (cache_json_options[:memoize] || {}).inject({}) do |hash, (method, variable)|
          hash[variable] = send method
          hash
        end
        merge_cache_json attributes, variables
      end

      def merge_cache_json(attributes, variables)
        if cache_json_options[:include_root]
          variables = variables.inject({}){|h, (k, v)| h[k.to_s.gsub(/^@/, "").to_sym] = v; h}
          {self.class.cache_root => attributes}.merge variables
        else
          attributes.merge variables
        end
      end

      def to_cache_json
        as_cache_json.to_json
      end

      def cache
        self.class.cached id
        true
      end

      def expire
        Cache.expire self
        true
      end

    private

      def cache_json_options
        self.class.as_cache[:as_json] || {}
      end

    end

  end
end