aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/store.rb
blob: fdd82b489a1f800a7b1283253275efa8b6932aaf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
require 'active_support/core_ext/hash/indifferent_access'

module ActiveRecord
  # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
  # It's like a simple key/value store backed into your record when you don't care about being able to
  # 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.
  #
  # 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+.
  #
  # Examples:
  #
  #   class User < ActiveRecord::Base
  #     store :settings, accessors: [ :color, :homepage ], coder: JSON
  #   end
  #
  #   u = User.new(color: 'black', homepage: '37signals.com')
  #   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'
  #
  #   # Add additional accessors to an existing store through store_accessor
  #   class SuperUser < User
  #     store_accessor :settings, :privileges, :servants
  #   end
  module Store
    extend ActiveSupport::Concern

    module ClassMethods
      def store(store_attribute, options = {})
        serialize store_attribute, options.fetch(:coder, ActiveSupport::HashWithIndifferentAccess)
        store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
      end

      def store_accessor(store_attribute, *keys)
        keys.flatten.each do |key|
          define_method("#{key}=") do |value|
            initialize_store_attribute(store_attribute)
            send(store_attribute)[key] = value
            send("#{store_attribute}_will_change!")
          end

          define_method(key) do
            initialize_store_attribute(store_attribute)
            send(store_attribute)[key]
          end
        end
      end
    end

    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
  end
end