nobrainer 0.36.0 → 0.41.1

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
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: []