store_attribute 0.4.1 → 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: 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