store_attribute 0.5.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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