nobrainer 0.40.0 → 0.41.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
2
  SHA256:
3
- metadata.gz: 2be88d279ca6fb8c9c8698d25efe539c635949fa5d328034f9a30430e084d396
4
- data.tar.gz: 4c5d8c072503e15d84198c9e1c33c293efdf3d4789784bbfe8420c7755cbc968
3
+ metadata.gz: 477e66e4e3380775847ce79e706f50a04134e7e87002b31aa0ca33c9e164d9d7
4
+ data.tar.gz: b303bbb3dc6a45df84f39edd2cce6d26af27000939c56be05e8f036c0ad11e76
5
5
  SHA512:
6
- metadata.gz: 552bc01e727596b8060026e28dcd825cdce2152e3fbf7aa6ec128379bea7dc38b945938bfa79d82917e186d22477fded04127d6c565e4d2bc6e569b775bafc88
7
- data.tar.gz: 9e52d7ffea00b58b0f80102e8d31b43c32d397082e30ccf3bb82e140565c0aec9ce853bf84d2ba204241076cd2e14523ac48670d9b55a8b12b918a08422f8072
6
+ metadata.gz: f1f883d15467994b72aabddbb61adcadd7095a5220f89ad1b4d8e58cf6927cfb7a99e0e1443fc24d7edf37bf4a24c24e95d0bc99c47d39281b0e56c73ee549b0
7
+ data.tar.gz: '04592d9c64732b25089dccd897ad451cd2f71b12a96a93532604768c9c40c1757e6f2b528792976f8be054e376d0a3a4541ca3a3aa7de60014a7e88d97e10453'
data/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+
10
+ ## [0.41.0] - 2021-10-17
11
+ ### Added
12
+ - ActiveRecord `store_accessor` helper method
13
+
14
+ ### Fixed
15
+ - gemspec dependencies on activemodel and activesupport
16
+
9
17
  ## [0.40.0] - 2021-10-16
10
18
  ### Fixed
11
19
  - Ruby 3 compatibility
@@ -108,7 +116,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
108
116
  - Locks: bug fix: allow small timeouts in lock()
109
117
  - Fix reentrant lock counter on steals
110
118
 
111
- [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...HEAD
119
+ [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...HEAD
120
+ [0.41.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...v0.41.0
112
121
  [0.40.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...v0.40.0
113
122
  [0.36.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.35.0...v0.36.0
114
123
  [0.35.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.34.1...v0.35.0
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module NoBrainer
6
+ module Document
7
+ # Store gives you a way for storing hashes in a single field with accessors
8
+ # to the Hash keys.
9
+ # It is a portage of the ActiveRecord::Store which make gems using it
10
+ # compatible with NoBrainer.
11
+ #
12
+ # You can then declare accessors to this store that are then accessible just
13
+ # like any other attribute of the model. This is very helpful for easily
14
+ # exposing store keys to a form or elsewhere that's already built around
15
+ # just accessing attributes on the model.
16
+ #
17
+ # Every accessor comes with dirty tracking methods (+key_changed?+,
18
+ # +key_was+ and +key_change+).
19
+ #
20
+ # You can set custom coder to encode/decode your serialized attributes
21
+ # to/from different formats.
22
+ # JSON, YAML, Marshal are supported out of the box.
23
+ # Generally it can be any wrapper that provides +load+ and +dump+.
24
+ #
25
+ # NOTE: The {.store}[rdoc-ref:rdoc-ref:ClassMethods#store] method is here
26
+ # for compatibility reason, but you should use
27
+ # {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead
28
+ # to generate the accessor methods.
29
+ # Be aware that these columns use a string keyed hash and do not allow
30
+ # access using a symbol.
31
+ #
32
+ # NOTE: The default validations with the exception of +uniqueness+ will work.
33
+ #
34
+ # Examples:
35
+ #
36
+ # class User
37
+ # include NoBrainer::Document
38
+ #
39
+ # store :settings, accessors: [ :color, :homepage ], coder: JSON
40
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
41
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
42
+ # store :settings, accessors: [ :two_factor_auth ], suffix: true
43
+ # store :settings, accessors: [ :login_retry ], suffix: :config
44
+ # end
45
+ #
46
+ # u = User.new(color: 'black', homepage: '37signals.com',
47
+ # parent_name: 'Mary', partner_name: 'Lily')
48
+ # u.color # Accessor stored attribute
49
+ # u.parent_name # Accessor stored attribute with prefix
50
+ # u.partner_name # Accessor stored attribute with custom prefix
51
+ # u.two_factor_auth_settings # Accessor stored attribute with suffix
52
+ # u.login_retry_config # Accessor stored attribute with custom suffix
53
+ # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
54
+ #
55
+ # # There is no difference between strings and symbols for accessing
56
+ # # custom attributes
57
+ # u.settings[:country] # => 'Denmark'
58
+ # u.settings['country'] # => 'Denmark'
59
+ #
60
+ # # Dirty tracking
61
+ # u.color = 'green'
62
+ # u.color_changed? # => true
63
+ # u.color_was # => 'black'
64
+ # u.color_change # => ['black', 'red']
65
+ #
66
+ # # Add additional accessors to an existing store through store_accessor
67
+ # class SuperUser < User
68
+ # store_accessor :settings, :privileges, :servants
69
+ # store_accessor :parent, :birthday, prefix: true
70
+ # store_accessor :settings, :secret_question, suffix: :config
71
+ # end
72
+ #
73
+ # The stored attribute names can be retrieved using
74
+ # {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
75
+ #
76
+ # User.stored_attributes[:settings]
77
+ # #=> [:color, :homepage, :two_factor_auth, :login_retry]
78
+ #
79
+ # == Overwriting default accessors
80
+ #
81
+ # All stored values are automatically available through accessors on
82
+ # the NoBrainer Document object, but sometimes you want to specialize
83
+ # this behavior. This can be done by overwriting the default accessors
84
+ # (using the same name as the attribute) and calling <tt>super</tt>
85
+ # to actually change things.
86
+ #
87
+ # class Song
88
+ # include NoBrainer::Document
89
+ #
90
+ # # Uses a stored integer to hold the volume adjustment of the song
91
+ # store :settings, accessors: [:volume_adjustment]
92
+ #
93
+ # def volume_adjustment=(decibels)
94
+ # super(decibels.to_i)
95
+ # end
96
+ #
97
+ # def volume_adjustment
98
+ # super.to_i
99
+ # end
100
+ # end
101
+ module Store
102
+ extend ActiveSupport::Concern
103
+
104
+ included do
105
+ class << self
106
+ attr_accessor :local_stored_attributes
107
+ end
108
+ end
109
+
110
+ module ClassMethods
111
+ def store(store_attribute, options = {})
112
+ store_accessor(store_attribute, options[:accessors], **options.slice(:prefix, :suffix)) if options.has_key? :accessors
113
+ end
114
+
115
+ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
116
+ keys = keys.flatten
117
+
118
+ accessor_prefix =
119
+ case prefix
120
+ when String, Symbol
121
+ "#{prefix}_"
122
+ when TrueClass
123
+ "#{store_attribute}_"
124
+ else
125
+ ""
126
+ end
127
+ accessor_suffix =
128
+ case suffix
129
+ when String, Symbol
130
+ "_#{suffix}"
131
+ when TrueClass
132
+ "_#{store_attribute}"
133
+ else
134
+ ""
135
+ end
136
+
137
+ field store_attribute, type: Hash, default: {} unless has_field?(store_attribute)
138
+
139
+ define_method("#{store_attribute}=") do |value|
140
+ super(value) if value.is_a?(Hash) || value.nil?
141
+ end
142
+
143
+ _store_accessors_module.module_eval do
144
+ keys.each do |key|
145
+ accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}"
146
+
147
+ define_method("#{accessor_key}=") do |value|
148
+ write_store_attribute(store_attribute, key, value)
149
+ end
150
+
151
+ define_method(accessor_key) do
152
+ read_store_attribute(store_attribute, key)
153
+ end
154
+
155
+ define_method("#{accessor_key}_changed?") do
156
+ return false unless __send__("#{store_attribute}_changed?")
157
+ prev_store, new_store = changes[store_attribute]
158
+ if NoBrainer.rails4?
159
+ (prev_store && prev_store[key.to_s]) != (new_store && new_store[key.to_s])
160
+ else
161
+ (prev_store && prev_store.dig(key)) != (new_store && new_store.dig(key))
162
+ end
163
+ end
164
+
165
+ define_method("#{accessor_key}_change") do
166
+ return unless __send__("#{store_attribute}_changed?")
167
+ prev_store, new_store = changes[store_attribute]
168
+ if NoBrainer.rails4?
169
+ [(prev_store && prev_store[key.to_s]), (new_store && new_store[key.to_s])]
170
+ else
171
+ [(prev_store && prev_store.dig(key)), (new_store && new_store.dig(key))]
172
+ end
173
+ end
174
+
175
+ define_method("#{accessor_key}_was") do
176
+ return unless __send__("#{store_attribute}_changed?")
177
+ prev_store, _new_store = changes[store_attribute]
178
+ if NoBrainer.rails4?
179
+ (prev_store && prev_store[key.to_s])
180
+ else
181
+ (prev_store && prev_store.dig(key))
182
+ end
183
+ end
184
+
185
+ # NoBrainer doesn't have `attribute_will_change!` so those methods
186
+ # can't be implemented yet.
187
+ # See https://github.com/NoBrainerORM/nobrainer/pull/190
188
+ #
189
+ # define_method("saved_change_to_#{accessor_key}?") do
190
+ # return false unless __send__("saved_change_to_#{store_attribute}?")
191
+ # prev_store, new_store = __send__("saved_change_to_#{store_attribute}")
192
+ # prev_store&.dig(key) != new_store&.dig(key)
193
+ # end
194
+
195
+ # define_method("saved_change_to_#{accessor_key}") do
196
+ # return unless __send__("saved_change_to_#{store_attribute}?")
197
+ # prev_store, new_store = __send__("saved_change_to_#{store_attribute}")
198
+ # [prev_store&.dig(key), new_store&.dig(key)]
199
+ # end
200
+
201
+ # define_method("#{accessor_key}_before_last_save") do
202
+ # return unless __send__("saved_change_to_#{store_attribute}?")
203
+ # prev_store, _new_store = __send__("saved_change_to_#{store_attribute}")
204
+ # prev_store&.dig(key)
205
+ # end
206
+ end
207
+ end
208
+
209
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
210
+ # has its own hash of stored attributes.
211
+ self.local_stored_attributes ||= {}
212
+ self.local_stored_attributes[store_attribute] ||= []
213
+ self.local_stored_attributes[store_attribute] |= keys
214
+ end
215
+
216
+ def _store_accessors_module # :nodoc:
217
+ @_store_accessors_module ||= begin
218
+ mod = Module.new
219
+ include mod
220
+ mod
221
+ end
222
+ end
223
+
224
+ def stored_attributes
225
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
226
+ if local_stored_attributes
227
+ parent.merge!(local_stored_attributes) { |k, a, b| a | b }
228
+ end
229
+ parent
230
+ end
231
+ end
232
+
233
+ private
234
+
235
+ def read_store_attribute(store_attribute, key) # :doc:
236
+ StringKeyedHashAccessor.read(self, store_attribute, key)
237
+ end
238
+
239
+ def write_store_attribute(store_attribute, key, value) # :doc:
240
+ StringKeyedHashAccessor.write(self, store_attribute, key, value)
241
+ end
242
+
243
+ class HashAccessor # :nodoc:
244
+ def self.read(object, attribute, key)
245
+ prepare(object, attribute)
246
+ object.public_send(attribute)[key]
247
+ end
248
+
249
+ def self.write(object, attribute, key, value)
250
+ prepare(object, attribute)
251
+ if value != read(object, attribute, key)
252
+ # "#{attribute}_will_change!" is not implemented in NoBrainer. See issue #190
253
+ # object.public_send :"#{attribute}_will_change!"
254
+ object.public_send(attribute)[key] = value
255
+ end
256
+ end
257
+
258
+ def self.prepare(object, attribute)
259
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
260
+ end
261
+ end
262
+
263
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
264
+ def self.read(object, attribute, key)
265
+ super object, attribute, key.to_s
266
+ end
267
+
268
+ def self.write(object, attribute, key, value)
269
+ super object, attribute, key.to_s, value
270
+ end
271
+ end
272
+
273
+ class IndifferentCoder # :nodoc:
274
+ def initialize(attr_name, coder_or_class_name)
275
+ @coder =
276
+ if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
277
+ coder_or_class_name
278
+ else
279
+ NoBrainer::Document::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object)
280
+ end
281
+ end
282
+
283
+ def dump(obj)
284
+ @coder.dump self.class.as_indifferent_hash(obj)
285
+ end
286
+
287
+ def load(yaml)
288
+ self.class.as_indifferent_hash(@coder.load(yaml || ""))
289
+ end
290
+
291
+ def self.as_indifferent_hash(obj)
292
+ case obj
293
+ when ActiveSupport::HashWithIndifferentAccess
294
+ obj
295
+ when Hash
296
+ obj.with_indifferent_access
297
+ else
298
+ ActiveSupport::HashWithIndifferentAccess.new
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -7,7 +7,7 @@ module NoBrainer::Document
7
7
  autoload_and_include :Core, :TableConfig, :InjectionLayer, :Attributes, :Readonly,
8
8
  :Persistance, :Callbacks, :Validation, :Types, :Dirty, :PrimaryKey,
9
9
  :Association, :Serialization, :Criteria, :Polymorphic, :Index, :Aliases,
10
- :MissingAttributes, :LazyFetch, :AtomicOps, :VirtualAttributes
10
+ :MissingAttributes, :LazyFetch, :AtomicOps, :VirtualAttributes, :Store
11
11
 
12
12
  autoload :DynamicAttributes, :Timestamps
13
13
 
data/lib/nobrainer.rb CHANGED
@@ -37,6 +37,10 @@ module NoBrainer
37
37
  RUBY_PLATFORM == 'java'
38
38
  end
39
39
 
40
+ def rails4?
41
+ Gem.loaded_specs['activesupport'].version >= Gem::Version.new('4.0.0')
42
+ end
43
+
40
44
  def rails5?
41
45
  Gem.loaded_specs['activesupport'].version >= Gem::Version.new('5.0.0.beta')
42
46
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nobrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-16 00:00:00.000000000 Z
11
+ date: 2021-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 4.1.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.2'
22
+ version: '8'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 4.1.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.2'
32
+ version: '8'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activesupport
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: 4.1.0
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '6.2'
42
+ version: '8'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +49,7 @@ dependencies:
49
49
  version: 4.1.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '6.2'
52
+ version: '8'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: middleware
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -168,6 +168,7 @@ files:
168
168
  - lib/no_brainer/document/primary_key/generator.rb
169
169
  - lib/no_brainer/document/readonly.rb
170
170
  - lib/no_brainer/document/serialization.rb
171
+ - lib/no_brainer/document/store.rb
171
172
  - lib/no_brainer/document/table_config.rb
172
173
  - lib/no_brainer/document/table_config/synchronizer.rb
173
174
  - lib/no_brainer/document/timestamps.rb