nobrainer 0.36.0 → 0.41.1

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: 61c15de4e6f0d6ede87a2db309ff25a79d36d61befdfcd280815925d0f66bb68
4
- data.tar.gz: f1516356fe4f53577a744dfacca6a8143d98844aee540e8c0290950113bedd88
3
+ metadata.gz: 5864cbae71a2eaa5c5f7efeb27d46c3a05f083618904641d793bef097b0b66d5
4
+ data.tar.gz: 1a905f02f1623ade41cc4fe2913dbfbdbcbb539a494a1df37d457b0b695d15df
5
5
  SHA512:
6
- metadata.gz: 3fabd4ff40209300d3630cee18bd27115d77f6c157b6d9f910ac93896ef11487f1cf1cb116fb7a98ef356e3c78033412c05ddf06d60ed473b753a26a9aab4ab2
7
- data.tar.gz: 9d3299f45f30001d6856ea015b15c19a9f5b7ec5642337c3416da160468deb5f00b5714f54eb3837b0af716c1a61fc817963280830bffa6b09bf15775624590f
6
+ metadata.gz: 8f3effc2a43c6f71f1eb0a42cd19d4ba6652599839998e746f9978a96aa8e3f9bc0b779e0524ac3315bc918cc06cd21399c8006d0f28f88c58d8a62008ab83c0
7
+ data.tar.gz: 2801dbe94a1268d752dd9844ecd79c56a34e5029797cf5414cabc9ec85189d8703ee1e3ab40d7645ffa260f6f09ba5cc58542d3fa769def2cfb25424312a3b6b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  ## [Unreleased]
8
8
 
9
9
 
10
+ ## [0.41.1] - 2022-03-21
11
+ ### Fixed
12
+ - Removing table_config duplicates after a runtime exception (caspiano)
13
+
14
+ ## [0.41.0] - 2021-10-17
15
+ ### Added
16
+ - ActiveRecord `store_accessor` helper method
17
+
18
+ ### Fixed
19
+ - gemspec dependencies on activemodel and activesupport
20
+
21
+ ## [0.40.0] - 2021-10-16
22
+ ### Fixed
23
+ - Ruby 3 compatibility
24
+ - Test Ruby 3 + Rails 7 alpha2 on Travis CI
25
+
10
26
  ## [0.36.0] - 2021-08-08
11
27
  ### Added
12
28
  - Array and TypedArray types for validation and serialization
@@ -104,7 +120,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
104
120
  - Locks: bug fix: allow small timeouts in lock()
105
121
  - Fix reentrant lock counter on steals
106
122
 
107
- [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...HEAD
123
+ [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...HEAD
124
+ [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...v0.41.1
125
+ [0.41.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...v0.41.0
126
+ [0.40.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...v0.40.0
108
127
  [0.36.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.35.0...v0.36.0
109
128
  [0.35.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.34.1...v0.35.0
110
129
  [0.34.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.34.0...v0.34.1
@@ -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
@@ -9,7 +9,7 @@ module NoBrainer::Document::Types
9
9
  def add_type_errors
10
10
  return unless @pending_type_errors
11
11
  @pending_type_errors.each do |name, error|
12
- errors.add(name, :invalid_type, error.error)
12
+ errors.add(name, :invalid_type, **error.error)
13
13
  end
14
14
  end
15
15
 
@@ -9,7 +9,7 @@ module NoBrainer::Document::Validation::NotNull
9
9
 
10
10
  class NotNullValidator < ActiveModel::EachValidator
11
11
  def validate_each(doc, attr, value)
12
- doc.errors.add(attr, :undefined, options) if value.nil?
12
+ doc.errors.add(attr, :undefined, **options) if value.nil?
13
13
  end
14
14
  end
15
15
  end
@@ -74,7 +74,7 @@ module NoBrainer::Document::Validation::Uniqueness
74
74
  criteria = self.model.unscoped.where(attr => value)
75
75
  criteria = apply_scopes(criteria, doc)
76
76
  criteria = exclude_doc(criteria, doc) if doc.persisted?
77
- doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless criteria.empty?
77
+ doc.errors.add(attr, :taken, **options.except(:scope).merge(:value => value)) unless criteria.empty?
78
78
  rescue NoBrainer::Error::InvalidType
79
79
  # We can't run the uniqueness validator: where() won't accept bad types
80
80
  # and we have some values that don't have the right type.
@@ -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
 
@@ -51,7 +51,7 @@ module NoBrainer::Error
51
51
  value = self.value
52
52
  mock = model.allocate
53
53
  mock.singleton_class.send(:define_method, :read_attribute_for_validation) { |_| value }
54
- mock.errors.add(attr_name, :invalid_type, error)
54
+ mock.errors.add(attr_name, :invalid_type, **error)
55
55
  mock.errors.full_messages.first
56
56
  end
57
57
  end
@@ -29,9 +29,13 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
29
29
  env[:last_auto_create_table] = [db_name, table_name]
30
30
 
31
31
  create_options = model.table_create_options
32
-
33
- NoBrainer.run(:db => db_name) do |r|
34
- r.table_create(table_name, create_options.reject { |k,_| k.in? [:name, :write_acks] })
32
+ begin
33
+ NoBrainer.run(:db => db_name) do |r|
34
+ r.table_create(table_name, create_options.reject { |k,_| k.in? [:name, :write_acks] })
35
+ end
36
+ rescue RuntimeError => e
37
+ # We might have raced with another table create
38
+ raise unless e.message =~ /Table `#{db_name}\.#{table_name}` already exists/
35
39
  end
36
40
 
37
41
  # Prevent duplicate table errors on a cluster.
@@ -49,8 +53,5 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
49
53
  r.table(table_name).config().update(:write_acks => create_options[:write_acks])
50
54
  end
51
55
  end
52
- rescue RuntimeError => e
53
- # We might have raced with another table create
54
- raise unless e.message =~ /Table `#{db_name}\.#{table_name}` already exists/
55
56
  end
56
57
  end
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.36.0
4
+ version: 0.41.1
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-09-09 00:00:00.000000000 Z
11
+ date: 2022-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 4.1.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,9 @@ dependencies:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 4.1.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: activesupport
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -31,6 +37,9 @@ dependencies:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
39
  version: 4.1.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '8'
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +47,9 @@ dependencies:
38
47
  - - ">="
39
48
  - !ruby/object:Gem::Version
40
49
  version: 4.1.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '8'
41
53
  - !ruby/object:Gem::Dependency
42
54
  name: middleware
43
55
  requirement: !ruby/object:Gem::Requirement
@@ -59,6 +71,9 @@ dependencies:
59
71
  - - ">="
60
72
  - !ruby/object:Gem::Version
61
73
  version: 2.3.0
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '2.5'
62
77
  type: :runtime
63
78
  prerelease: false
64
79
  version_requirements: !ruby/object:Gem::Requirement
@@ -66,6 +81,9 @@ dependencies:
66
81
  - - ">="
67
82
  - !ruby/object:Gem::Version
68
83
  version: 2.3.0
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '2.5'
69
87
  - !ruby/object:Gem::Dependency
70
88
  name: symbol_decoration
71
89
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +168,7 @@ files:
150
168
  - lib/no_brainer/document/primary_key/generator.rb
151
169
  - lib/no_brainer/document/readonly.rb
152
170
  - lib/no_brainer/document/serialization.rb
171
+ - lib/no_brainer/document/store.rb
153
172
  - lib/no_brainer/document/table_config.rb
154
173
  - lib/no_brainer/document/table_config/synchronizer.rb
155
174
  - lib/no_brainer/document/timestamps.rb
@@ -222,7 +241,7 @@ files:
222
241
  - lib/rails/generators/templates/nobrainer.rb
223
242
  homepage: http://nobrainer.io
224
243
  licenses:
225
- - LGPLv3
244
+ - LGPL-3.0-only
226
245
  metadata: {}
227
246
  post_install_message:
228
247
  rdoc_options: []