store_attribute 0.5.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +35 -0
- data/{MIT-LICENSE → LICENSE.txt} +1 -1
- data/README.md +19 -13
- data/lib/store_attribute.rb +4 -2
- data/lib/store_attribute/active_record.rb +3 -1
- data/lib/store_attribute/active_record/store.rb +121 -16
- data/lib/store_attribute/active_record/type/typed_store.rb +41 -19
- data/lib/store_attribute/version.rb +3 -1
- metadata +32 -45
- data/.gitignore +0 -37
- data/.rspec +0 -1
- data/.rubocop.yml +0 -51
- data/.travis.yml +0 -19
- data/Gemfile +0 -11
- data/Rakefile +0 -6
- data/bench/bench.rb +0 -38
- data/bench/setup.rb +0 -67
- data/bin/console +0 -7
- data/bin/setup +0 -8
- data/gemfiles/rails5.gemfile +0 -5
- data/gemfiles/railsmaster.rb +0 -6
- data/spec/cases/store_attribute_spec.rb +0 -171
- data/spec/spec_helper.rb +0 -43
- data/spec/store_attribute/typed_store_spec.rb +0 -107
- data/spec/support/money_type.rb +0 -12
- data/spec/support/user.rb +0 -17
- data/store_attribute.gemspec +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a6d0060f9544a3e9e005c149cc189dd6647c0c83493bbb577bb67893e2b5a5fd
|
4
|
+
data.tar.gz: 0f9105dc411953cf7b3342117538e068c9ea7897220d573126bf0d4c01640726
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0521c55632478bc52bfcb03eab0fb9709ec2e63cc59c9eafa1dc3795742c88d7f2caf7019bb3eac3f3232006f49e0c9dc0cec4bcf619154b7f1873ccd7915201
|
7
|
+
data.tar.gz: e6644235407bd7481499dbb4bfc3fb9214ec0805ebf85be79914ddf8179f7b2f9837db1f33760c8af50236608820e0dc976fad74b3f9046e70f5996c86ec251c
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Change log
|
2
|
+
|
3
|
+
## master
|
4
|
+
|
5
|
+
## 0.8.0
|
6
|
+
|
7
|
+
- Add Rails 6.1 compatibility. ([@palkan][])
|
8
|
+
|
9
|
+
- Add support for `prefix` and `suffix` options. ([@palkan][])
|
10
|
+
|
11
|
+
## 0.7.1
|
12
|
+
|
13
|
+
- Fixed bug with `store` called without accessors. ([@ioki-klaus][])
|
14
|
+
|
15
|
+
See [#10](https://github.com/palkan/store_attribute/pull/10).
|
16
|
+
|
17
|
+
## 0.7.0 (2020-03-23)
|
18
|
+
|
19
|
+
- Added dirty tracking methods. ([@glaszig][])
|
20
|
+
|
21
|
+
[PR #8](https://github.com/palkan/store_attribute/pull/8).
|
22
|
+
|
23
|
+
## 0.6.0 (2019-07-24)
|
24
|
+
|
25
|
+
- Added default values support. ([@dreikanter][], [@SumLare][])
|
26
|
+
|
27
|
+
See [PR #7](https://github.com/palkan/store_attribute/pull/7).
|
28
|
+
|
29
|
+
- Start keeping changelog. ([@palkan][])
|
30
|
+
|
31
|
+
[@palkan]: https://github.com/palkan
|
32
|
+
[@dreikanter]: https://github.com/dreikanter
|
33
|
+
[@SumLare]: https://github.com/SumLare
|
34
|
+
[@glaszig]: https://github.com/glaszig
|
35
|
+
[@ioki-klaus]: https://github.com/ioki-klaus
|
data/{MIT-LICENSE → LICENSE.txt}
RENAMED
data/README.md
CHANGED
@@ -1,24 +1,24 @@
|
|
1
|
+
[](https://cultofmartians.com/tasks/store-attribute-defaults.html#task)
|
1
2
|
[](https://rubygems.org/gems/store_attribute) [](https://travis-ci.org/palkan/store_attribute)
|
2
3
|
|
3
4
|
## Store Attribute
|
4
5
|
|
5
6
|
ActiveRecord extension which adds typecasting to store accessors.
|
6
7
|
|
7
|
-
Compatible with Rails 4.2 and Rails 5
|
8
|
+
Compatible with Rails 4.2 and Rails 5+.
|
8
9
|
|
9
|
-
|
10
|
-
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
10
|
+
Extracted from not merged PR to Rails: [rails/rails#18942](https://github.com/rails/rails/pull/18942).
|
11
11
|
|
12
12
|
### Install
|
13
13
|
|
14
14
|
In your Gemfile:
|
15
15
|
|
16
16
|
```ruby
|
17
|
-
# for Rails 5
|
18
|
-
gem "store_attribute", "~>0.5.0"
|
17
|
+
# for Rails 5+ (6 is supported)
|
18
|
+
gem "store_attribute", "~> 0.5.0"
|
19
19
|
|
20
20
|
# for Rails 4.2
|
21
|
-
gem "store_attribute", "~>0.4.0"
|
21
|
+
gem "store_attribute", "~> 0.4.0"
|
22
22
|
```
|
23
23
|
|
24
24
|
### Usage
|
@@ -26,14 +26,14 @@ gem "store_attribute", "~>0.4.0"
|
|
26
26
|
You can use `store_attribute` method to add additional accessors with a type to an existing store on a model.
|
27
27
|
|
28
28
|
```ruby
|
29
|
-
|
29
|
+
store_attribute(store_name, name, type, options)
|
30
30
|
```
|
31
31
|
|
32
32
|
Where:
|
33
33
|
- `store_name` The name of the store.
|
34
34
|
- `name` The name of the accessor to the store.
|
35
35
|
- `type` A symbol such as `:string` or `:integer`, or a type object to be used for the accessor.
|
36
|
-
- `options` A hash of cast type options such as `precision`, `limit`, `scale`.
|
36
|
+
- `options` (optional) A hash of cast type options such as `precision`, `limit`, `scale`, `default`.
|
37
37
|
|
38
38
|
Type casting occurs every time you write data through accessor or update store itself
|
39
39
|
and when object is loaded from database.
|
@@ -47,30 +47,36 @@ class MegaUser < User
|
|
47
47
|
store_attribute :settings, :ratio, :integer, limit: 1
|
48
48
|
store_attribute :settings, :login_at, :datetime
|
49
49
|
store_attribute :settings, :active, :boolean
|
50
|
+
store_attribute :settings, :color, :string, default: "red"
|
51
|
+
store_attribute :settings, :data, :datetime, default: -> { Time.now }
|
50
52
|
end
|
51
53
|
|
52
|
-
u = MegaUser.new(active: false, login_at:
|
54
|
+
u = MegaUser.new(active: false, login_at: "2015-01-01 00:01", ratio: "63.4608")
|
53
55
|
|
54
56
|
u.login_at.is_a?(DateTime) # => true
|
55
|
-
u.login_at = DateTime.new(2015,1,1,11,0,0)
|
57
|
+
u.login_at = DateTime.new(2015, 1, 1, 11, 0, 0)
|
56
58
|
u.ratio # => 63
|
57
59
|
u.active # => false
|
60
|
+
# Default value is set
|
61
|
+
u.color # => red
|
62
|
+
# A dynamic default can also be provided
|
63
|
+
u.data # => Current time
|
58
64
|
# And we also have a predicate method
|
59
65
|
u.active? # => false
|
60
66
|
u.reload
|
61
67
|
|
62
68
|
# After loading record from db store contains casted data
|
63
|
-
u.settings[
|
69
|
+
u.settings["login_at"] == DateTime.new(2015, 1, 1, 11, 0, 0) # => true
|
64
70
|
|
65
71
|
# If you update store explicitly then the value returned
|
66
72
|
# by accessor isn't type casted
|
67
|
-
u.settings[
|
73
|
+
u.settings["ratio"] = "3.141592653"
|
68
74
|
u.ratio # => "3.141592653"
|
69
75
|
|
70
76
|
# On the other hand, writing through accessor set correct data within store
|
71
77
|
u.ratio = "3.141592653"
|
72
78
|
u.ratio # => 3
|
73
|
-
u.settings[
|
79
|
+
u.settings["ratio"] # => 3
|
74
80
|
```
|
75
81
|
|
76
82
|
You can also specify type using usual `store_accessor` method:
|
data/lib/store_attribute.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/store"
|
4
|
+
require "store_attribute/active_record/type/typed_store"
|
3
5
|
|
4
6
|
module ActiveRecord
|
5
7
|
module Store
|
@@ -23,9 +25,17 @@ module ActiveRecord
|
|
23
25
|
# end
|
24
26
|
def store(store_name, options = {})
|
25
27
|
accessors = options.delete(:accessors)
|
28
|
+
typed_accessors =
|
29
|
+
if accessors && accessors.last.is_a?(Hash)
|
30
|
+
accessors.pop
|
31
|
+
else
|
32
|
+
{}
|
33
|
+
end
|
34
|
+
|
26
35
|
_orig_store(store_name, options)
|
27
|
-
store_accessor(store_name, *accessors) if accessors
|
36
|
+
store_accessor(store_name, *accessors, **typed_accessors) if accessors
|
28
37
|
end
|
38
|
+
|
29
39
|
# Adds additional accessors to an existing store on this model.
|
30
40
|
#
|
31
41
|
# +store_name+ The name of the store.
|
@@ -34,21 +44,29 @@ module ActiveRecord
|
|
34
44
|
#
|
35
45
|
# +typed_keys+ The key-to-type hash of the accesors with type to the store.
|
36
46
|
#
|
47
|
+
# +prefix+ Accessor method name prefix
|
48
|
+
#
|
49
|
+
# +suffix+ Accessor method name suffix
|
50
|
+
#
|
37
51
|
# Examples:
|
38
52
|
#
|
39
53
|
# class SuperUser < User
|
40
54
|
# store_accessor :settings, :privileges, login_at: :datetime
|
41
55
|
# end
|
42
|
-
def store_accessor(store_name, *keys, **typed_keys)
|
56
|
+
def store_accessor(store_name, *keys, prefix: nil, suffix: nil, **typed_keys)
|
43
57
|
keys = keys.flatten
|
44
58
|
typed_keys = typed_keys.except(keys)
|
45
59
|
|
46
|
-
|
60
|
+
accessor_prefix, accessor_suffix = _normalize_prefix_suffix(store_name, prefix, suffix)
|
61
|
+
|
62
|
+
_define_accessors_methods(store_name, *keys, prefix: accessor_prefix, suffix: accessor_suffix)
|
63
|
+
|
64
|
+
_define_dirty_tracking_methods(store_name, keys + typed_keys.keys, prefix: accessor_prefix, suffix: accessor_suffix)
|
47
65
|
|
48
66
|
_prepare_local_stored_attributes(store_name, *keys)
|
49
67
|
|
50
68
|
typed_keys.each do |key, type|
|
51
|
-
store_attribute(store_name, key, type)
|
69
|
+
store_attribute(store_name, key, type, prefix: prefix, suffix: suffix)
|
52
70
|
end
|
53
71
|
end
|
54
72
|
|
@@ -65,6 +83,10 @@ module ActiveRecord
|
|
65
83
|
# +type+ A symbol such as +:string+ or +:integer+, or a type object
|
66
84
|
# to be used for the accessor.
|
67
85
|
#
|
86
|
+
# +prefix+ Accessor method name prefix
|
87
|
+
#
|
88
|
+
# +suffix+ Accessor method name suffix
|
89
|
+
#
|
68
90
|
# +options+ A hash of cast type options such as +precision+, +limit+, +scale+.
|
69
91
|
#
|
70
92
|
# Examples:
|
@@ -72,13 +94,16 @@ module ActiveRecord
|
|
72
94
|
# class MegaUser < User
|
73
95
|
# store_attribute :settings, :ratio, :integer, limit: 1
|
74
96
|
# store_attribute :settings, :login_at, :datetime
|
97
|
+
#
|
98
|
+
# store_attribute :extra, :version, :integer, prefix: :meta
|
75
99
|
# end
|
76
100
|
#
|
77
|
-
# u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608")
|
101
|
+
# u = MegaUser.new(active: false, login_at: '2015-01-01 00:01', ratio: "63.4608", meta_version: "1")
|
78
102
|
#
|
79
103
|
# u.login_at.is_a?(DateTime) # => true
|
80
104
|
# u.login_at = DateTime.new(2015,1,1,11,0,0)
|
81
105
|
# u.ratio # => 63
|
106
|
+
# u.meta_version #=> 1
|
82
107
|
# u.reload
|
83
108
|
#
|
84
109
|
# # After loading record from db store contains casted data
|
@@ -95,13 +120,24 @@ module ActiveRecord
|
|
95
120
|
# u.settings['ratio'] # => 3
|
96
121
|
#
|
97
122
|
# For more examples on using types, see documentation for ActiveRecord::Attributes.
|
98
|
-
def store_attribute(store_name, name, type, **options)
|
99
|
-
|
123
|
+
def store_attribute(store_name, name, type, prefix: nil, suffix: nil, **options)
|
124
|
+
prefix, suffix = _normalize_prefix_suffix(store_name, prefix, suffix)
|
125
|
+
|
126
|
+
_define_accessors_methods(store_name, name, prefix: prefix, suffix: suffix)
|
100
127
|
|
101
|
-
_define_predicate_method(name) if type == :boolean
|
128
|
+
_define_predicate_method(name, prefix: prefix, suffix: suffix) if type == :boolean
|
102
129
|
|
103
|
-
|
104
|
-
|
130
|
+
# Rails >6.0
|
131
|
+
if method(:decorate_attribute_type).parameters.count { |type, _| type == :req } == 1
|
132
|
+
attr_name = store_name.to_s
|
133
|
+
was_type = attributes_to_define_after_schema_loads[attr_name]&.first
|
134
|
+
attribute(attr_name) do |subtype|
|
135
|
+
Type::TypedStore.create_from_type(_lookup_cast_type(attr_name, was_type, {}), name, type, **options)
|
136
|
+
end
|
137
|
+
else
|
138
|
+
decorate_attribute_type(store_name, "typed_accessor_for_#{name}") do |subtype|
|
139
|
+
Type::TypedStore.create_from_type(subtype, name, type, **options)
|
140
|
+
end
|
105
141
|
end
|
106
142
|
|
107
143
|
_prepare_local_stored_attributes(store_name, name)
|
@@ -115,27 +151,96 @@ module ActiveRecord
|
|
115
151
|
self.local_stored_attributes[store_name] |= keys
|
116
152
|
end
|
117
153
|
|
118
|
-
def _define_accessors_methods(store_name, *keys) # :nodoc:
|
154
|
+
def _define_accessors_methods(store_name, *keys, prefix: nil, suffix: nil) # :nodoc:
|
119
155
|
_store_accessors_module.module_eval do
|
120
156
|
keys.each do |key|
|
121
|
-
|
157
|
+
accessor_key = "#{prefix}#{key}#{suffix}"
|
158
|
+
|
159
|
+
define_method("#{accessor_key}=") do |value|
|
122
160
|
write_store_attribute(store_name, key, value)
|
123
161
|
end
|
124
162
|
|
125
|
-
define_method(
|
163
|
+
define_method(accessor_key) do
|
126
164
|
read_store_attribute(store_name, key)
|
127
165
|
end
|
128
166
|
end
|
129
167
|
end
|
130
168
|
end
|
131
169
|
|
132
|
-
def _define_predicate_method(name)
|
170
|
+
def _define_predicate_method(name, prefix: nil, suffix: nil)
|
133
171
|
_store_accessors_module.module_eval do
|
172
|
+
name = "#{prefix}#{name}#{suffix}"
|
173
|
+
|
134
174
|
define_method("#{name}?") do
|
135
175
|
send(name) == true
|
136
176
|
end
|
137
177
|
end
|
138
178
|
end
|
179
|
+
|
180
|
+
def _define_dirty_tracking_methods(store_attribute, keys, prefix: nil, suffix: nil)
|
181
|
+
_store_accessors_module.module_eval do
|
182
|
+
keys.flatten.each do |key|
|
183
|
+
key = key.to_s
|
184
|
+
accessor_key = "#{prefix}#{key}#{suffix}"
|
185
|
+
|
186
|
+
define_method("#{accessor_key}_changed?") do
|
187
|
+
return false unless attribute_changed?(store_attribute)
|
188
|
+
prev_store, new_store = changes[store_attribute]
|
189
|
+
prev_store&.dig(key) != new_store&.dig(key)
|
190
|
+
end
|
191
|
+
|
192
|
+
define_method("#{accessor_key}_change") do
|
193
|
+
return unless attribute_changed?(store_attribute)
|
194
|
+
prev_store, new_store = changes[store_attribute]
|
195
|
+
[prev_store&.dig(key), new_store&.dig(key)]
|
196
|
+
end
|
197
|
+
|
198
|
+
define_method("#{accessor_key}_was") do
|
199
|
+
return unless attribute_changed?(store_attribute)
|
200
|
+
prev_store, _new_store = changes[store_attribute]
|
201
|
+
prev_store&.dig(key)
|
202
|
+
end
|
203
|
+
|
204
|
+
define_method("saved_change_to_#{accessor_key}?") do
|
205
|
+
return false unless saved_change_to_attribute?(store_attribute)
|
206
|
+
prev_store, new_store = saved_change_to_attribute(store_attribute)
|
207
|
+
prev_store&.dig(key) != new_store&.dig(key)
|
208
|
+
end
|
209
|
+
|
210
|
+
define_method("saved_change_to_#{accessor_key}") do
|
211
|
+
return unless saved_change_to_attribute?(store_attribute)
|
212
|
+
prev_store, new_store = saved_change_to_attribute(store_attribute)
|
213
|
+
[prev_store&.dig(key), new_store&.dig(key)]
|
214
|
+
end
|
215
|
+
|
216
|
+
define_method("#{accessor_key}_before_last_save") do
|
217
|
+
return unless saved_change_to_attribute?(store_attribute)
|
218
|
+
prev_store, _new_store = saved_change_to_attribute(store_attribute)
|
219
|
+
prev_store&.dig(key)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def _normalize_prefix_suffix(store_name, prefix, suffix)
|
226
|
+
prefix =
|
227
|
+
case prefix
|
228
|
+
when String, Symbol
|
229
|
+
"#{prefix}_"
|
230
|
+
when TrueClass
|
231
|
+
"#{store_name}_"
|
232
|
+
end
|
233
|
+
|
234
|
+
suffix =
|
235
|
+
case suffix
|
236
|
+
when String, Symbol
|
237
|
+
"_#{suffix}"
|
238
|
+
when TrueClass
|
239
|
+
"_#{store_name}"
|
240
|
+
end
|
241
|
+
|
242
|
+
[prefix, suffix]
|
243
|
+
end
|
139
244
|
end
|
140
245
|
end
|
141
246
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/type"
|
2
4
|
|
3
5
|
module ActiveRecord
|
4
6
|
module Type # :nodoc:
|
@@ -13,43 +15,57 @@ module ActiveRecord
|
|
13
15
|
|
14
16
|
def initialize(subtype)
|
15
17
|
@accessor_types = {}
|
18
|
+
@defaults = {}
|
16
19
|
@store_accessor = subtype.accessor
|
17
20
|
super(subtype)
|
18
21
|
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
UNDEFINED = Object.new
|
24
|
+
private_constant :UNDEFINED
|
25
|
+
|
26
|
+
def add_typed_key(key, type, default: UNDEFINED, **options)
|
27
|
+
type = ActiveRecord::Type.lookup(type, **options) if type.is_a?(Symbol)
|
28
|
+
safe_key = key.to_s
|
29
|
+
@accessor_types[safe_key] = type
|
30
|
+
@defaults[safe_key] = default unless default == UNDEFINED
|
23
31
|
end
|
24
32
|
|
25
33
|
def deserialize(value)
|
26
34
|
hash = super
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
return hash unless hash
|
36
|
+
accessor_types.each do |key, type|
|
37
|
+
if hash.key?(key)
|
38
|
+
hash[key] = type.deserialize(hash[key])
|
39
|
+
elsif defaults.key?(key)
|
40
|
+
hash[key] = get_default(key)
|
30
41
|
end
|
31
42
|
end
|
32
43
|
hash
|
33
44
|
end
|
34
45
|
|
35
46
|
def serialize(value)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
47
|
+
return super(value) unless value.is_a?(Hash)
|
48
|
+
typed_casted = {}
|
49
|
+
accessor_types.each do |str_key, type|
|
50
|
+
key = key_to_cast(value, str_key)
|
51
|
+
next unless key
|
52
|
+
if value.key?(key)
|
53
|
+
typed_casted[key] = type.serialize(value[key])
|
54
|
+
elsif defaults.key?(str_key)
|
55
|
+
typed_casted[key] = type.serialize(get_default(str_key))
|
41
56
|
end
|
42
|
-
super(value.merge(typed_casted))
|
43
|
-
else
|
44
|
-
super(value)
|
45
57
|
end
|
58
|
+
super(value.merge(typed_casted))
|
46
59
|
end
|
47
60
|
|
48
61
|
def cast(value)
|
49
62
|
hash = super
|
50
|
-
|
51
|
-
|
52
|
-
|
63
|
+
return hash unless hash
|
64
|
+
accessor_types.each do |key, type|
|
65
|
+
if hash.key?(key)
|
66
|
+
hash[key] = type.cast(hash[key])
|
67
|
+
elsif defaults.key?(key)
|
68
|
+
hash[key] = get_default(key)
|
53
69
|
end
|
54
70
|
end
|
55
71
|
hash
|
@@ -72,6 +88,7 @@ module ActiveRecord
|
|
72
88
|
def key_to_cast(val, key)
|
73
89
|
return key if val.key?(key)
|
74
90
|
return key.to_sym if val.key?(key.to_sym)
|
91
|
+
return key if defaults.key?(key)
|
75
92
|
end
|
76
93
|
|
77
94
|
def typed?(key)
|
@@ -82,7 +99,12 @@ module ActiveRecord
|
|
82
99
|
accessor_types.fetch(key.to_s)
|
83
100
|
end
|
84
101
|
|
85
|
-
|
102
|
+
def get_default(key)
|
103
|
+
value = defaults.fetch(key)
|
104
|
+
value.is_a?(Proc) ? value.call : value
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :accessor_types, :defaults, :store_accessor
|
86
108
|
end
|
87
109
|
end
|
88
110
|
end
|