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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 191063cf9091064962abf3548153b75656a55665
4
- data.tar.gz: cbc5f2bfc1b33f186296fd9b6c0cc1b8d08fb1df
2
+ SHA256:
3
+ metadata.gz: a6d0060f9544a3e9e005c149cc189dd6647c0c83493bbb577bb67893e2b5a5fd
4
+ data.tar.gz: 0f9105dc411953cf7b3342117538e068c9ea7897220d573126bf0d4c01640726
5
5
  SHA512:
6
- metadata.gz: ae7b28363deb33a59141481eba79eecebd530c7810e9b29a79e9b2a618ca4e5dd8fa6e0211d5ae3d167fe29bff286a175aac0094a8b353e8ae59f484e1caadeb
7
- data.tar.gz: bd3858caaa4d37e67bc586a18e1607a2d5c88f2385d9760fc59c9b15ad8fb403694f7a354248299928644acd4cec19bfbc4771f091fbdfd8c75348c6930b9ac0
6
+ metadata.gz: 0521c55632478bc52bfcb03eab0fb9709ec2e63cc59c9eafa1dc3795742c88d7f2caf7019bb3eac3f3232006f49e0c9dc0cec4bcf619154b7f1873ccd7915201
7
+ data.tar.gz: e6644235407bd7481499dbb4bfc3fb9214ec0805ebf85be79914ddf8179f7b2f9837db1f33760c8af50236608820e0dc976fad74b3f9046e70f5996c86ec251c
@@ -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
@@ -1,4 +1,4 @@
1
- Copyright 2016 palkan
1
+ Copyright 2016-2020 palkan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,24 +1,24 @@
1
+ [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/store-attribute-defaults.html#task)
1
2
  [![Gem Version](https://badge.fury.io/rb/store_attribute.svg)](https://rubygems.org/gems/store_attribute) [![Build Status](https://travis-ci.org/palkan/store_attribute.svg?branch=master)](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
- <a href="https://evilmartians.com/">
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
- store_attribute(store_name, name, type, options = {})
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: '2015-01-01 00:01', ratio: "63.4608")
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['login_at'] == DateTime.new(2015,1,1,11,0,0) # => true
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['ration'] = "3.141592653"
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['ratio'] # => 3
79
+ u.settings["ratio"] # => 3
74
80
  ```
75
81
 
76
82
  You can also specify type using usual `store_accessor` method:
@@ -1,2 +1,4 @@
1
- require 'store_attribute/version'
2
- require 'store_attribute/active_record'
1
+ # frozen_string_literal: true
2
+
3
+ require "store_attribute/version"
4
+ require "store_attribute/active_record"
@@ -1 +1,3 @@
1
- require 'store_attribute/active_record/store'
1
+ # frozen_string_literal: true
2
+
3
+ require "store_attribute/active_record/store"
@@ -1,5 +1,7 @@
1
- require 'active_record/store'
2
- require 'store_attribute/active_record/type/typed_store'
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
- _define_accessors_methods(store_name, *keys)
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
- _define_accessors_methods(store_name, name)
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
- decorate_attribute_type(store_name, "typed_accessor_for_#{name}") do |subtype|
104
- Type::TypedStore.create_from_type(subtype, name, type, **options)
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
- define_method("#{key}=") do |value|
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(key) do
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
- require 'active_record/type'
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
- def add_typed_key(key, type, **options)
21
- type = ActiveRecord::Type.lookup(type, options) if type.is_a?(Symbol)
22
- @accessor_types[key.to_s] = type
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
- if hash
28
- accessor_types.each do |key, type|
29
- hash[key] = type.deserialize(hash[key]) if hash.key?(key)
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
- if value.is_a?(Hash)
37
- typed_casted = {}
38
- accessor_types.each do |key, type|
39
- k = key_to_cast(value, key)
40
- typed_casted[k] = type.serialize(value[k]) unless k.nil?
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
- if hash
51
- accessor_types.each do |key, type|
52
- hash[key] = type.cast(hash[key]) if hash.key?(key)
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
- attr_reader :accessor_types, :store_accessor
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