active_model_persistence 0.3.1 → 0.4.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: be3fd5f19c5ceddcc38dbd4c7b104e0a7b26ac8945178b0e568efa787087b68c
4
- data.tar.gz: c979c5b04b91063dd8ae264cab66f4be4619c7105393b3b2545e9562899ee397
3
+ metadata.gz: d34c4046feea446188de7f1381ed024799bd8e545a50d9abf1e22f06ee0e06ee
4
+ data.tar.gz: 585c06a05af51efb5123efb0c98b618f9747955f9040852ea08835c6f962fdc6
5
5
  SHA512:
6
- metadata.gz: ece4a13179ea9317f763ffc23ffefb3109c49524fe62a75e22550659fe100bc01a07354f0d7e4450540c1dbc8c805101ca7a42ab76378f083008fcca9b7adbdc
7
- data.tar.gz: 5f7d14093744d6b27a5b4045efd4542c2b9c49e671045dd0c4aed497ca29b3486ae2925fd7bce4ec65186382c381311e166ad8ebd52f72bc98b2ec0b6878d348
6
+ metadata.gz: 846582f45f58f215e20ba67fb843898d3091c1af816d851689027d94c71c9973d4b5fc3d3b2798d4fd875a22d5a2fc7a5c2c3150f465cdd4aa29fee11e964f2c
7
+ data.tar.gz: 0ad54fb60bae3d74fceacb8aa94a860bbb399219ccd791ef2376e56b794ec52e706c01eb91bb0e1ff4fb4d4d93af6f72c187d591f657f176ba69f84f15dfa387
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@
7
7
 
8
8
  The full change log is stored on [this project's GitHub releases page](https://github.com/jcouball/active_model_persistence/releases).
9
9
 
10
+ ## v0.4.0
11
+
12
+ * e491e22 Set the version file back to the correct version (#17)
13
+ * 5774462 Improve the ObjectNotValidError message (#15)
14
+ * cadde51 Add inheritance support (#14)
15
+
16
+ See https://github.com/ruby-git/ruby-git/releases/tag/v0.4.0
17
+
10
18
  ## v0.3.1
11
19
 
12
20
  * d904b30 Fix spelling of UniqueConstraintError (#12)
data/README.md CHANGED
@@ -65,6 +65,82 @@ Employee.find_by_manager_id('boss') #=> [e1, e2]
65
65
  # etc.
66
66
  ```
67
67
 
68
+ ## Inheritance
69
+
70
+ ActiveModelPersistence supports inheritance in models. Include
71
+ `ActiveModelPersistence::Persistence` only in the base class.
72
+
73
+ Here is an example with a `User` base class and two derived clases `Employee` and `Member`.
74
+ Each derived class adds different attributes and indexes.
75
+
76
+ ```ruby
77
+ require 'active_model_persistence'
78
+
79
+ class User
80
+ include ActiveModelPersistence::Persistence
81
+
82
+ attribute :id, :integer
83
+ attribute :name, :string
84
+ end
85
+
86
+ class Employee < User
87
+ attribute :manager_id, :integer
88
+
89
+ index :manager_id, unique: false
90
+ end
91
+
92
+ class Member < User
93
+ attribute :joined_on, :date, default: Date.today
94
+
95
+ index :joined_on, unique: false
96
+ end
97
+ ```
98
+
99
+ As an example, say we have one instance of each class:
100
+
101
+ ```ruby
102
+ user = User.create!(id: 1, name: 'User James')
103
+ employee = Employee.create!(id: 2, name: 'Employee Bob', manager_id: nil)
104
+ member = Member.create!(id: 3, name: 'Member Mary', joined_on: Date.parse('2022-01-01'))
105
+ ```
106
+
107
+ The primary key is shared by all objects of these classes so it must be unique for
108
+ all objects created from these classes. For instance:
109
+
110
+ ```ruby
111
+ require 'rspec-expectations'
112
+ include RSpec::Matchers
113
+
114
+ # Creating a Member with the same primary key as a User will fail
115
+ expect { Member.create!(id: 1, name: 'Member Jason') }.to(
116
+ raise_error(ActiveModelPersistence::UniqueConstraintError)
117
+ )
118
+ ```
119
+
120
+ Calling a find method such as `find` or `find_by_*` on a class will only return
121
+ objects that are a kind of that class.
122
+
123
+ That means calling a find method on `User` will return users, employees, or members:
124
+
125
+ ```ruby
126
+ expect(User.find(1)).to eq(user)
127
+ expect(User.find(2)).to eq(employee)
128
+ expect(User.find(3)).to eq(member)
129
+ ```
130
+
131
+ While calling a find method on `Employee` will only return employees:
132
+
133
+ ```ruby
134
+ expect(Employee.find(1)).to be_nil
135
+ expect(Employee.find(2)).to eq(employee)
136
+ expect(Employee.find(3)).to be_nil
137
+ ```
138
+
139
+ `all`, `count`, `delete_all`, `destroy_all` and other methods similarly limit what
140
+ objects are acted upon based on what class they are called on.
141
+
142
+ ## API Documentation
143
+
68
144
  See [the full API documentation](https://jcouball.github.io/active_record_persistence/) for more details.
69
145
 
70
146
  ## Installation
@@ -15,6 +15,18 @@ module ActiveModelPersistence
15
15
  #
16
16
  attr_reader :name
17
17
 
18
+ # Identifies the base class this index applies to
19
+ #
20
+ # Objects that are not of this class or one of its subclasses will not be added to the index.
21
+ #
22
+ # @example
23
+ # i = Index.new(name: 'id', base_class: self, key_source: :id, unique: true)
24
+ # i.base_class == self #=> true
25
+ #
26
+ # @return [Class] the class this index applies to
27
+ #
28
+ attr_reader :base_class
29
+
18
30
  # Defines how the object's key value is calculated
19
31
  #
20
32
  # If a proc is provided, it will be called with the object as an argument to get the key value.
@@ -22,7 +34,7 @@ module ActiveModelPersistence
22
34
  # If a symbol is provided, it will identify the method to call on the object to get the key value.
23
35
  #
24
36
  # @example
25
- # i = Index.new(name: 'id', key_value_source: :id, unique: true)
37
+ # i = Index.new(name: 'id', base_class: self, key_value_source: :id, unique: true)
26
38
  # i.key_value_source # => :id
27
39
  #
28
40
  # @return [Symbol, Proc] the method name or proc used to calculate the index key
@@ -37,7 +49,7 @@ module ActiveModelPersistence
37
49
  # when trying to add the second object.
38
50
  #
39
51
  # @example
40
- # i = Index.new(name: 'id', key_value_source: :id, unique: true)
52
+ # i = Index.new(name: 'id', base_class: self, key_value_source: :id, unique: true)
41
53
  # i.unique? # => true
42
54
  #
43
55
  # @return [Boolean] true if the index is unique
@@ -74,8 +86,9 @@ module ActiveModelPersistence
74
86
  # @param key_value_source [Symbol, Proc] the attribute name or proc used to calculate the index key
75
87
  # @param unique [Boolean] when true the index will only allow one object per key
76
88
  #
77
- def initialize(name:, key_value_source: nil, unique: false)
89
+ def initialize(name:, base_class:, key_value_source: nil, unique: false)
78
90
  @name = name.to_s
91
+ @base_class = base_class
79
92
  @key_value_source = determine_key_value_source(name, key_value_source)
80
93
  @unique = unique
81
94
  @key_to_objects_map = {}
@@ -228,7 +241,7 @@ module ActiveModelPersistence
228
241
  # @api private
229
242
  #
230
243
  def remove_object_from_index(object, key)
231
- key_to_objects_map[key].delete_if { |o| o.primary_key == object.primary_key }
244
+ key_to_objects_map[key].delete_if { |o| o.primary_key_value == object.primary_key_value }
232
245
  key_to_objects_map.delete(key) if key_to_objects_map[key].empty?
233
246
  object.clear_index_key(name)
234
247
  end
@@ -60,6 +60,7 @@ module ActiveModelPersistence
60
60
  # make these class methods on that class.
61
61
  #
62
62
  module ClassMethods
63
+ # @!attribute [r] indexes
63
64
  # Returns a hash of indexes for the model keyed by name
64
65
  #
65
66
  # @example
@@ -67,9 +68,7 @@ module ActiveModelPersistence
67
68
  #
68
69
  # @return [Hash<String, ActiveModelPersistence::Index>] the indexes defined by the model
69
70
  #
70
- def indexes
71
- @indexes ||= {}
72
- end
71
+ # @api public
73
72
 
74
73
  # Adds an index to the model
75
74
  #
@@ -98,9 +97,7 @@ module ActiveModelPersistence
98
97
  indexes[index_name.to_sym] = index
99
98
 
100
99
  singleton_class.define_method("find_by_#{index_name}") do |key|
101
- index.objects(key).tap do |objects|
102
- objects.each { |o| o.instance_variable_set(:@previously_new_record, false) }
103
- end
100
+ index.objects(key).select { |object| object.is_a?(self) }
104
101
  end
105
102
  end
106
103
 
@@ -125,7 +122,9 @@ module ActiveModelPersistence
125
122
  # @return [void]
126
123
  #
127
124
  def update_indexes(object)
128
- indexes.each_value { |index| index.add_or_update(object) }
125
+ indexes.each_value do |index|
126
+ index.add_or_update(object) if object.is_a?(index.base_class)
127
+ end
129
128
  end
130
129
 
131
130
  # Removes the given object from all defined indexes
@@ -144,7 +143,9 @@ module ActiveModelPersistence
144
143
  # @return [void]
145
144
  #
146
145
  def remove_from_indexes(object)
147
- indexes.each_value { |index| index.remove(object) }
146
+ indexes.each_value do |index|
147
+ index.remove(object) if object.is_a?(index.base_class)
148
+ end
148
149
  end
149
150
 
150
151
  private
@@ -158,12 +159,15 @@ module ActiveModelPersistence
158
159
  def default_index_options(index_name)
159
160
  {
160
161
  name: index_name.to_sym,
162
+ base_class: self,
161
163
  unique: false
162
164
  }
163
165
  end
164
166
  end
165
167
 
166
168
  included do
169
+ cattr_reader :indexes, instance_accessor: false, default: {}
170
+
167
171
  # Adds the object to the indexes defined by the model
168
172
  #
169
173
  # @example
@@ -158,7 +158,7 @@ module ActiveModelPersistence
158
158
  # @return [Array<Object>] the model objects in the object store
159
159
  #
160
160
  def all
161
- object_array.each
161
+ object_array.select { |object| object.is_a?(self) }.each
162
162
  end
163
163
 
164
164
  # The number of model objects saved in the object store
@@ -174,7 +174,7 @@ module ActiveModelPersistence
174
174
  # @return [Integer] the number of model objects in the object store
175
175
  #
176
176
  def count
177
- object_array.size
177
+ object_array.select { |object| object.is_a?(self) }.size
178
178
  end
179
179
 
180
180
  alias size count
@@ -196,30 +196,11 @@ module ActiveModelPersistence
196
196
  # @return [void]
197
197
  #
198
198
  def destroy_all
199
- object_array.first.destroy while object_array.size.positive?
199
+ objects_to_destroy = object_array.select { |object| object.is_a?(self) }
200
+ objects_to_destroy.each(&:destroy)
200
201
  end
201
202
 
202
- # Removes all model objects from the object store
203
- #
204
- # Each saved model object's `#destroy` method is NOT called.
205
- #
206
- # @example
207
- # array_of_attributes = [
208
- # { id: 1, name: 'James' },
209
- # { id: 2, name: 'Frank' }
210
- # ]
211
- # ModelExample.create(array_of_attributes)
212
- # ModelExample.all.count #=> 2
213
- # ModelExample.destroy_all
214
- # ModelExample.all.count #=> 0
215
- #
216
- # @return [void]
217
- #
218
- def delete_all
219
- @object_array = []
220
- indexes.values.each(&:remove_all)
221
- nil
222
- end
203
+ alias delete_all destroy_all
223
204
 
224
205
  # private
225
206
 
@@ -229,13 +210,12 @@ module ActiveModelPersistence
229
210
  #
230
211
  # @api private
231
212
  #
232
- def object_array
233
- @object_array ||= []
234
- end
235
213
  end
236
214
 
237
215
  # rubocop:disable Metrics/BlockLength
238
216
  included do
217
+ cattr_reader :object_array, instance_accessor: false, default: []
218
+
239
219
  # Returns true if this object hasn't been saved or destroyed yet
240
220
  #
241
221
  # @example
@@ -344,7 +324,7 @@ module ActiveModelPersistence
344
324
  #
345
325
  def save!(**_options, &block)
346
326
  raise ObjectDestroyedError if destroyed?
347
- raise ObjectNotValidError unless valid?
327
+ raise ObjectNotValidError, self unless valid?
348
328
 
349
329
  new_record? ? _create(&block) : _update(&block)
350
330
  update_indexes
@@ -371,7 +351,7 @@ module ActiveModelPersistence
371
351
  def destroy
372
352
  if persisted?
373
353
  remove_from_indexes
374
- self.class.object_array.delete_if { |o| o.primary_key == primary_key }
354
+ self.class.object_array.delete_if { |o| o.primary_key_value == primary_key_value }
375
355
  end
376
356
  @new_record = false
377
357
  @destroyed = true
@@ -429,13 +409,14 @@ module ActiveModelPersistence
429
409
  # Creates a record with values matching those of the instance attributes
430
410
  # and returns its id.
431
411
  #
432
- # @return [Object] the primary_key of the created object
412
+ # @return [Object] the primary_key_value of the created object
433
413
  #
434
414
  # @api private
435
415
  #
436
416
  def _create
437
- return false unless primary_key?
438
- raise UniqueConstraintError if primary_key_index.include?(primary_key)
417
+ return false unless primary_key_value?
418
+
419
+ raise UniqueConstraintError if primary_key_index.include?(primary_key_value)
439
420
 
440
421
  self.class.object_array << self
441
422
 
@@ -443,7 +424,7 @@ module ActiveModelPersistence
443
424
 
444
425
  yield(self) if block_given?
445
426
 
446
- primary_key
427
+ primary_key_value
447
428
  end
448
429
 
449
430
  # Updates an object that is already in the object store
@@ -453,7 +434,7 @@ module ActiveModelPersistence
453
434
  # @api private
454
435
  #
455
436
  def _update
456
- raise RecordNotFound unless primary_key_index.include?(primary_key)
437
+ raise RecordNotFound unless primary_key_index.include?(primary_key_value)
457
438
 
458
439
  yield(self) if block_given?
459
440
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/module'
4
+
3
5
  module ActiveModelPersistence
4
6
  # Exposes the `primary_key` accessor to read or write the primary key attribute value
5
7
  #
@@ -42,7 +44,8 @@ module ActiveModelPersistence
42
44
  # make these class methods on that class.
43
45
  #
44
46
  module ClassMethods
45
- # Identifies the attribute that the `primary_key` accessor maps to
47
+ # @!attribute [rw] primary_key
48
+ # Identifies the attribute that the `primary_key_value` accessor maps to
46
49
  #
47
50
  # The primary key is 'id' by default.
48
51
  #
@@ -54,33 +57,28 @@ module ActiveModelPersistence
54
57
  # end
55
58
  # Employee.primary_key #=> :username
56
59
  #
57
- # @return [Symbol] the attribute that the `primary_key` accessor is an alias for
60
+ # @return [String] the attribute that the `primary_key` accessor is an alias for
58
61
  #
59
- def primary_key
60
- @primary_key ||= 'id'
61
- end
62
+ # @api public
63
+ end
62
64
 
63
- # Sets the attribute to use for the primary key
65
+ included do
66
+ # @!attribute [r] primary_key
67
+ # Identifies the attribute that the `primary_key_value` accessor maps to
68
+ #
69
+ # The primary key is 'id' by default.
64
70
  #
65
71
  # @example
66
72
  # class Employee
67
73
  # include ActiveModelPersistence::PrimaryKey
68
74
  # attribute :username, :string
69
- # primary_key = :username
75
+ # self.primary_key = :username
70
76
  # end
71
- # e = Employee.new(username: 'couballj')
72
- # e.primary_key #=> 'couballj'
73
- #
74
- # @param attribute [Symbol] the attribute to use for the primary key
75
- #
76
- # @return [void]
77
+ # Employee.primary_key #=> :username
77
78
  #
78
- def primary_key=(attribute)
79
- @primary_key = attribute.to_s
80
- end
81
- end
79
+ # @return [String] the attribute that the `primary_key` accessor is an alias for
80
+ cattr_accessor :primary_key, default: 'id', instance_writer: false
82
81
 
83
- included do
84
82
  # Returns the primary key attribute's value
85
83
  #
86
84
  # @example
@@ -94,8 +92,8 @@ module ActiveModelPersistence
94
92
  #
95
93
  # @return [Object] the primary key attribute's value
96
94
  #
97
- def primary_key
98
- __send__(self.class.primary_key)
95
+ def primary_key_value
96
+ __send__(primary_key)
99
97
  end
100
98
 
101
99
  # Sets the primary key atribute's value
@@ -114,8 +112,8 @@ module ActiveModelPersistence
114
112
  #
115
113
  # @return [void]
116
114
  #
117
- def primary_key=(value)
118
- __send__("#{self.class.primary_key}=", value)
115
+ def primary_key_value=(value)
116
+ __send__("#{primary_key}=", value)
119
117
  end
120
118
 
121
119
  # Returns true if the primary key attribute's value is not null or empty
@@ -133,8 +131,8 @@ module ActiveModelPersistence
133
131
  #
134
132
  # @return [Boolean] true if the primary key attribute's value is not null or empty
135
133
  #
136
- def primary_key?
137
- primary_key.present?
134
+ def primary_key_value?
135
+ primary_key_value.present?
138
136
  end
139
137
  end
140
138
  end
@@ -47,7 +47,7 @@ module ActiveModelPersistence
47
47
  # @api private
48
48
  #
49
49
  def self.extended(base)
50
- base.index('primary_key', key_value_source: :primary_key, unique: true)
50
+ base.index('primary_key', key_value_source: :primary_key_value, unique: true)
51
51
  end
52
52
  end
53
53
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ActiveModelPersistence
4
4
  # The version of the active_model_persistence gem
5
- VERSION = '0.3.1'
5
+ VERSION = '0.4.0'
6
6
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_support'
4
4
  require 'active_model'
5
+ require 'pp'
5
6
 
6
7
  # A set of mixins to add to ActiveModel classes to support persistence
7
8
  #
@@ -18,7 +19,48 @@ module ActiveModelPersistence
18
19
  class UniqueConstraintError < ModelError; end
19
20
 
20
21
  # Raised when trying to save an invalid object
21
- class ObjectNotValidError < ModelError; end
22
+ # @api public
23
+ class ObjectNotValidError < ModelError
24
+ # Create a new ObjectNotValidError constructing a message from the invalid object
25
+ #
26
+ # @example
27
+ # class Person
28
+ # include ActiveModelPersistence::Persistence
29
+ # attribute :id, :integer
30
+ # validates :id, presence: true
31
+ # attribute :name, :integer
32
+ # validates :name, presence: true
33
+ # attribute :age, :integer
34
+ # validates :age, numericality: { greater_than: 13, less_than: 125 }, allow_nil: true
35
+ # end
36
+ # begin
37
+ # Person.create!(id: 1)
38
+ # rescue ObjectNotValidError => e
39
+ # puts e.message
40
+ # end
41
+ #
42
+ # @param invalid_object [Object] the invalid object being reported
43
+ #
44
+ def initialize(invalid_object)
45
+ super(error_message(invalid_object))
46
+ end
47
+
48
+ private
49
+
50
+ # Create the exception message
51
+ # @return [String] the exception message
52
+ # @api private
53
+ def error_message(invalid_object)
54
+ <<~ERROR_MESSAGE
55
+ #{invalid_object.class} object is not valid
56
+
57
+ Errors:
58
+ #{invalid_object.errors.full_messages.pretty_inspect}
59
+ Attributes:
60
+ #{invalid_object.attributes.pretty_inspect}
61
+ ERROR_MESSAGE
62
+ end
63
+ end
22
64
 
23
65
  # Raised when trying to save! or update! an object that has already been destroyed
24
66
  class ObjectDestroyedError < ModelError; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_model_persistence
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-24 00:00:00.000000000 Z
11
+ date: 2022-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel