store_attribute 0.4.1 → 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: 5b686f8993ef12feb2c0a1b36dcf90fbf0a7225b
4
- data.tar.gz: 5498a60d8e4ac7ce66e4316b5fe2dfb50856c662
2
+ SHA256:
3
+ metadata.gz: a6d0060f9544a3e9e005c149cc189dd6647c0c83493bbb577bb67893e2b5a5fd
4
+ data.tar.gz: 0f9105dc411953cf7b3342117538e068c9ea7897220d573126bf0d4c01640726
5
5
  SHA512:
6
- metadata.gz: b41a03c88333430a322a2b0147b445284c4335063fcfaccaae53200b8438cad9a22996b1951df936da18fbbde23bd57d06847a49042971346896e181a9729e86
7
- data.tar.gz: eb62f09e1f44a05d3d29540ac9b7f4751d226c0b4f37e6c92cc7b1c285c7cf993e16585e3f33edae18d2170f6544ac30f810590d8ac3a0fd23f1df124fed1ec6
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
@@ -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,18 +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.
8
+ Compatible with Rails 4.2 and Rails 5+.
8
9
 
10
+ Extracted from not merged PR to Rails: [rails/rails#18942](https://github.com/rails/rails/pull/18942).
9
11
 
10
12
  ### Install
11
13
 
12
14
  In your Gemfile:
13
15
 
14
16
  ```ruby
15
- gem "store_attribute", "~>0.4.0" # version 0.4.x is for Rails 4.2.x and 0.5.x is for Rails 5
17
+ # for Rails 5+ (6 is supported)
18
+ gem "store_attribute", "~> 0.5.0"
19
+
20
+ # for Rails 4.2
21
+ gem "store_attribute", "~> 0.4.0"
16
22
  ```
17
23
 
18
24
  ### Usage
@@ -20,14 +26,14 @@ gem "store_attribute", "~>0.4.0" # version 0.4.x is for Rails 4.2.x and 0.5.x is
20
26
  You can use `store_attribute` method to add additional accessors with a type to an existing store on a model.
21
27
 
22
28
  ```ruby
23
- .store_attribute(store_name, name, type, options = {})
29
+ store_attribute(store_name, name, type, options)
24
30
  ```
25
31
 
26
32
  Where:
27
33
  - `store_name` The name of the store.
28
34
  - `name` The name of the accessor to the store.
29
35
  - `type` A symbol such as `:string` or `:integer`, or a type object to be used for the accessor.
30
- - `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`.
31
37
 
32
38
  Type casting occurs every time you write data through accessor or update store itself
33
39
  and when object is loaded from database.
@@ -41,30 +47,36 @@ class MegaUser < User
41
47
  store_attribute :settings, :ratio, :integer, limit: 1
42
48
  store_attribute :settings, :login_at, :datetime
43
49
  store_attribute :settings, :active, :boolean
50
+ store_attribute :settings, :color, :string, default: "red"
51
+ store_attribute :settings, :data, :datetime, default: -> { Time.now }
44
52
  end
45
53
 
46
- 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")
47
55
 
48
56
  u.login_at.is_a?(DateTime) # => true
49
- u.login_at = DateTime.new(2015,1,1,11,0,0)
57
+ u.login_at = DateTime.new(2015, 1, 1, 11, 0, 0)
50
58
  u.ratio # => 63
51
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
52
64
  # And we also have a predicate method
53
65
  u.active? # => false
54
66
  u.reload
55
67
 
56
68
  # After loading record from db store contains casted data
57
- 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
58
70
 
59
71
  # If you update store explicitly then the value returned
60
72
  # by accessor isn't type casted
61
- u.settings['ration'] = "3.141592653"
73
+ u.settings["ratio"] = "3.141592653"
62
74
  u.ratio # => "3.141592653"
63
75
 
64
76
  # On the other hand, writing through accessor set correct data within store
65
- u.ratio = "3.14.1592653"
77
+ u.ratio = "3.141592653"
66
78
  u.ratio # => 3
67
- u.settings['ratio'] # => 3
79
+ u.settings["ratio"] # => 3
68
80
  ```
69
81
 
70
82
  You can also specify type using usual `store_accessor` method:
@@ -81,4 +93,4 @@ Or through `store`:
81
93
  class User < ActiveRecord::Base
82
94
  store :settings, accessors: [:color, :homepage, login_at: :datetime], coder: JSON
83
95
  end
84
- ```
96
+ ```
@@ -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,9 +1,12 @@
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
6
8
  module ClassMethods # :nodoc:
9
+ alias _orig_store store
7
10
  # Defines store on this model.
8
11
  #
9
12
  # +store_name+ The name of the store.
@@ -21,9 +24,18 @@ module ActiveRecord
21
24
  # store :settings, accessors: [:color, :homepage, login_at: :datetime], coder: JSON
22
25
  # end
23
26
  def store(store_name, options = {})
24
- serialize store_name, IndifferentCoder.new(options[:coder])
25
- store_accessor(store_name, *options[:accessors]) if options.key?(:accessors)
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
+
35
+ _orig_store(store_name, options)
36
+ store_accessor(store_name, *accessors, **typed_accessors) if accessors
26
37
  end
38
+
27
39
  # Adds additional accessors to an existing store on this model.
28
40
  #
29
41
  # +store_name+ The name of the store.
@@ -32,21 +44,29 @@ module ActiveRecord
32
44
  #
33
45
  # +typed_keys+ The key-to-type hash of the accesors with type to the store.
34
46
  #
47
+ # +prefix+ Accessor method name prefix
48
+ #
49
+ # +suffix+ Accessor method name suffix
50
+ #
35
51
  # Examples:
36
52
  #
37
53
  # class SuperUser < User
38
54
  # store_accessor :settings, :privileges, login_at: :datetime
39
55
  # end
40
- def store_accessor(store_name, *keys, **typed_keys)
56
+ def store_accessor(store_name, *keys, prefix: nil, suffix: nil, **typed_keys)
41
57
  keys = keys.flatten
42
58
  typed_keys = typed_keys.except(keys)
43
59
 
44
- _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)
45
65
 
46
66
  _prepare_local_stored_attributes(store_name, *keys)
47
67
 
48
68
  typed_keys.each do |key, type|
49
- store_attribute(store_name, key, type)
69
+ store_attribute(store_name, key, type, prefix: prefix, suffix: suffix)
50
70
  end
51
71
  end
52
72
 
@@ -63,6 +83,10 @@ module ActiveRecord
63
83
  # +type+ A symbol such as +:string+ or +:integer+, or a type object
64
84
  # to be used for the accessor.
65
85
  #
86
+ # +prefix+ Accessor method name prefix
87
+ #
88
+ # +suffix+ Accessor method name suffix
89
+ #
66
90
  # +options+ A hash of cast type options such as +precision+, +limit+, +scale+.
67
91
  #
68
92
  # Examples:
@@ -70,13 +94,16 @@ module ActiveRecord
70
94
  # class MegaUser < User
71
95
  # store_attribute :settings, :ratio, :integer, limit: 1
72
96
  # store_attribute :settings, :login_at, :datetime
97
+ #
98
+ # store_attribute :extra, :version, :integer, prefix: :meta
73
99
  # end
74
100
  #
75
- # 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")
76
102
  #
77
103
  # u.login_at.is_a?(DateTime) # => true
78
104
  # u.login_at = DateTime.new(2015,1,1,11,0,0)
79
105
  # u.ratio # => 63
106
+ # u.meta_version #=> 1
80
107
  # u.reload
81
108
  #
82
109
  # # After loading record from db store contains casted data
@@ -93,13 +120,24 @@ module ActiveRecord
93
120
  # u.settings['ratio'] # => 3
94
121
  #
95
122
  # For more examples on using types, see documentation for ActiveRecord::Attributes.
96
- def store_attribute(store_name, name, type, **options)
97
- _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)
98
127
 
99
- _define_predicate_method(name) if type == :boolean
128
+ _define_predicate_method(name, prefix: prefix, suffix: suffix) if type == :boolean
100
129
 
101
- decorate_attribute_type(store_name, "typed_accessor_for_#{name}") do |subtype|
102
- 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
103
141
  end
104
142
 
105
143
  _prepare_local_stored_attributes(store_name, name)
@@ -113,27 +151,96 @@ module ActiveRecord
113
151
  self.local_stored_attributes[store_name] |= keys
114
152
  end
115
153
 
116
- def _define_accessors_methods(store_name, *keys) # :nodoc:
154
+ def _define_accessors_methods(store_name, *keys, prefix: nil, suffix: nil) # :nodoc:
117
155
  _store_accessors_module.module_eval do
118
156
  keys.each do |key|
119
- define_method("#{key}=") do |value|
157
+ accessor_key = "#{prefix}#{key}#{suffix}"
158
+
159
+ define_method("#{accessor_key}=") do |value|
120
160
  write_store_attribute(store_name, key, value)
121
161
  end
122
162
 
123
- define_method(key) do
163
+ define_method(accessor_key) do
124
164
  read_store_attribute(store_name, key)
125
165
  end
126
166
  end
127
167
  end
128
168
  end
129
169
 
130
- def _define_predicate_method(name)
170
+ def _define_predicate_method(name, prefix: nil, suffix: nil)
131
171
  _store_accessors_module.module_eval do
172
+ name = "#{prefix}#{name}#{suffix}"
173
+
132
174
  define_method("#{name}?") do
133
175
  send(name) == true
134
176
  end
135
177
  end
136
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
137
244
  end
138
245
  end
139
246
  end
@@ -1,22 +1,9 @@
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:
5
- BASE_TYPES = {
6
- boolean: ::ActiveRecord::Type::Boolean,
7
- integer: ::ActiveRecord::Type::Integer,
8
- string: ::ActiveRecord::Type::String,
9
- float: ::ActiveRecord::Type::Float,
10
- date: ::ActiveRecord::Type::Date,
11
- datetime: ::ActiveRecord::Type::DateTime,
12
- decimal: ::ActiveRecord::Type::Decimal
13
- }.freeze
14
-
15
- def self.lookup_type(type, options)
16
- BASE_TYPES[type.to_sym].try(:new, options) ||
17
- ActiveRecord::Base.connection.type_map.lookup(type.to_s, options)
18
- end
19
-
20
7
  class TypedStore < DelegateClass(ActiveRecord::Type::Value) # :nodoc:
21
8
  # Creates +TypedStore+ type instance and specifies type caster
22
9
  # for key.
@@ -28,43 +15,57 @@ module ActiveRecord
28
15
 
29
16
  def initialize(subtype)
30
17
  @accessor_types = {}
18
+ @defaults = {}
31
19
  @store_accessor = subtype.accessor
32
20
  super(subtype)
33
21
  end
34
22
 
35
- def add_typed_key(key, type, **options)
36
- type = Type.lookup_type(type, options) if type.is_a?(Symbol)
37
- @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
38
31
  end
39
32
 
40
- def type_cast_from_database(value)
33
+ def deserialize(value)
41
34
  hash = super
42
- if hash
43
- accessor_types.each do |key, type|
44
- hash[key] = type.type_cast_from_database(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)
45
41
  end
46
42
  end
47
43
  hash
48
44
  end
49
45
 
50
- def type_cast_for_database(value)
51
- if value.is_a?(Hash)
52
- typed_casted = {}
53
- accessor_types.each do |key, type|
54
- k = key_to_cast(value, key)
55
- typed_casted[k] = type.type_cast_for_database(value[k]) unless k.nil?
46
+ def serialize(value)
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))
56
56
  end
57
- super(value.merge(typed_casted))
58
- else
59
- super(value)
60
57
  end
58
+ super(value.merge(typed_casted))
61
59
  end
62
60
 
63
- def type_cast_from_user(value)
61
+ def cast(value)
64
62
  hash = super
65
- if hash
66
- accessor_types.each do |key, type|
67
- hash[key] = type.type_cast_from_user(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)
68
69
  end
69
70
  end
70
71
  hash
@@ -75,7 +76,7 @@ module ActiveRecord
75
76
  end
76
77
 
77
78
  def write(object, attribute, key, value)
78
- value = type_for(key).type_cast_from_user(value) if typed?(key)
79
+ value = type_for(key).cast(value) if typed?(key)
79
80
  store_accessor.write(object, attribute, key, value)
80
81
  end
81
82
 
@@ -87,6 +88,7 @@ module ActiveRecord
87
88
  def key_to_cast(val, key)
88
89
  return key if val.key?(key)
89
90
  return key.to_sym if val.key?(key.to_sym)
91
+ return key if defaults.key?(key)
90
92
  end
91
93
 
92
94
  def typed?(key)
@@ -97,7 +99,12 @@ module ActiveRecord
97
99
  accessor_types.fetch(key.to_s)
98
100
  end
99
101
 
100
- 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
101
108
  end
102
109
  end
103
110
  end