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
76
77
78
79
80
81
82
83
84
85
86
87
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
|
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 baked 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
#
# The stored attribute names can be retrieved using +stored_attributes+.
#
# User.stored_attributes[:settings] # [:color, :homepage]
module Store
extend ActiveSupport::Concern
included do
class_attribute :stored_attributes, instance_accessor: false
self.stored_attributes = {}
end
module ClassMethods
def store(store_attribute, options = {})
serialize store_attribute, IndifferentCoder.new(options[:coder])
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end
def store_accessor(store_attribute, *keys)
keys = keys.flatten
keys.each do |key|
define_method("#{key}=") do |value|
attribute = initialize_store_attribute(store_attribute)
if value != attribute[key]
send :"#{store_attribute}_will_change!"
attribute[key] = value
end
end
define_method(key) do
initialize_store_attribute(store_attribute)[key]
end
end
self.stored_attributes[store_attribute] ||= []
self.stored_attributes[store_attribute] |= keys
end
end
private
def initialize_store_attribute(store_attribute)
attribute = send(store_attribute)
unless attribute.is_a?(HashWithIndifferentAccess)
attribute = IndifferentCoder.as_indifferent_hash(attribute)
send :"#{store_attribute}=", attribute
end
attribute
end
class IndifferentCoder # :nodoc:
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 HashWithIndifferentAccess
obj
when Hash
obj.with_indifferent_access
else
HashWithIndifferentAccess.new
end
end
end
end
end
|