active_model_persistence 0.3.1 → 0.4.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
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