store.rb 4.1 KB
Newer Older
1
require 'active_support/concern'
2
require 'active_support/core_ext/hash/indifferent_access'
3
require 'active_support/core_ext/class/attribute'
4

5 6
module ActiveRecord
  # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
C
chrismcc 已提交
7
  # It's like a simple key/value store baked into your record when you don't care about being able to
8 9 10 11 12 13 14 15 16
  # query that store outside the context of a single record.
  #
  # You can then declare accessors to this store that are then accessible just like any other attribute
  # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
  # already built around just accessing attributes on the model.
  #
  # Make sure that you declare the database column used for the serialized store as a text, so there's
  # plenty of room.
  #
17 18 19
  # You can set custom coder to encode/decode your serialized attributes to/from different formats.
  # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
  #
20 21 22
  # Examples:
  #
  #   class User < ActiveRecord::Base
23
  #     store :settings, accessors: [ :color, :homepage ], coder: JSON
24
  #   end
25
  #
26
  #   u = User.new(color: 'black', homepage: '37signals.com')
27 28 29 30 31 32
  #   u.color                          # Accessor stored attribute
  #   u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
  #
  #   # There is no difference between strings and symbols for accessing custom attributes
  #   u.settings[:country]  # => 'Denmark'
  #   u.settings['country'] # => 'Denmark'
33 34 35 36 37
  #
  #   # Add additional accessors to an existing store through store_accessor
  #   class SuperUser < User
  #     store_accessor :settings, :privileges, :servants
  #   end
38 39 40 41
  #
  # The stored attribute names can be retrieved using +stored_attributes+.
  #
  #   User.stored_attributes[:settings] # [:color, :homepage]
42 43
  module Store
    extend ActiveSupport::Concern
44

45
    included do
46
      class_attribute :stored_attributes
47 48 49
      self.stored_attributes = {}
    end

50 51
    module ClassMethods
      def store(store_attribute, options = {})
52
        serialize store_attribute, IndifferentCoder.new(options[:coder])
53 54 55 56
        store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
      end

      def store_accessor(store_attribute, *keys)
57 58
        keys = keys.flatten
        keys.each do |key|
59
          define_method("#{key}=") do |value|
60 61
            initialize_store_attribute(store_attribute)
            send(store_attribute)[key] = value
62
            send :"#{store_attribute}_will_change!"
63
          end
64

65
          define_method(key) do
66 67
            initialize_store_attribute(store_attribute)
            send(store_attribute)[key]
68 69
          end
        end
70

71
        self.stored_attributes[store_attribute] = keys
72 73
      end
    end
74 75 76 77 78 79 80 81 82 83 84 85 86 87

    private
      def initialize_store_attribute(store_attribute)
        case attribute = send(store_attribute)
        when ActiveSupport::HashWithIndifferentAccess
          # Already initialized. Do nothing.
        when Hash
          # Initialized as a Hash. Convert to indifferent access.
          send :"#{store_attribute}=", attribute.with_indifferent_access
        else
          # Uninitialized. Set to an indifferent hash.
          send :"#{store_attribute}=", ActiveSupport::HashWithIndifferentAccess.new
        end
      end
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

    class IndifferentCoder
      def initialize(coder_or_class_name)
        @coder =
          if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
            coder_or_class_name
          else
            ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
          end
      end

      def dump(obj)
        @coder.dump self.class.as_indifferent_hash(obj)
      end

      def load(yaml)
        self.class.as_indifferent_hash @coder.load(yaml)
      end

      def self.as_indifferent_hash(obj)
        case obj
        when ActiveSupport::HashWithIndifferentAccess
          obj
        when Hash
          obj.with_indifferent_access
        else
          HashWithIndifferentAccess.new
        end
      end
    end
118
  end
119
end