active_model_persistence 0.1.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.
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model_persistence/index'
4
+ require 'active_model_persistence/primary_key'
5
+
6
+ module ActiveModelPersistence
7
+ # Include in your model to enable index support
8
+ #
9
+ # Define an index in the model's class using the `index` method. This will create a `find_by_*`
10
+ # method for each index to find the objects by their keys.
11
+ #
12
+ # Each index has a name which must be unique for the model. The name is used to create the
13
+ # `find_by_*` method. eg. for the 'id' index, the `find_by_id` method will be created.
14
+ #
15
+ # Unique indexes are defined by passing `unique: true` to the `index` method. A unique index
16
+ # defines a `find_by_*` method that will return a single object or nil if no object is found for
17
+ # the key.
18
+ #
19
+ # A non-unique index will define a `find_by_*` method that will return an array of objects which
20
+ # may be empty.
21
+ #
22
+ # @example
23
+ # class Employee
24
+ # include ActiveModelPersistence::Indexable
25
+ #
26
+ # # Define ActiveModel attributes
27
+ # attribute :id, :integer
28
+ # attribute :name, :string
29
+ # attribute :manager_id, :integer
30
+ # attribute :birth_date, :date
31
+ #
32
+ # # Define indexes
33
+ # index :id, unique: true
34
+ # index :manager_id, key_value_source: :manager_id
35
+ # index :birth_year, key_value_source: ->(o) { o.birth_date.year }
36
+ # end
37
+ #
38
+ # e1 = Employee.new(id: 1, name: 'James', manager_id: 3, birth_date: Date.new(1967, 3, 15))
39
+ # e2 = Employee.new(id: 2, name: 'Frank', manager_id: 3, birth_date: Date.new(1968, 1, 27))
40
+ # e3 = Employee.new(id: 3, name: 'Aaron', birth_date: Date.new(1968, 6, 16))
41
+ #
42
+ # # This should be done by Employee.create or Employee#save from ActiveModelPersistence::Persistence
43
+ # [e1, e2, e3].each { |e| e.update_indexes }
44
+ #
45
+ # # Use the find_by_* methods to find objects
46
+ # #
47
+ # Employee.find_by_id(1).name # => 'James'
48
+ # Employee.find_by_manager_id(3).map(&:name) # => ['James', 'Frank']
49
+ # Employee.find_by_birth_year(1967).map(&:name) # => ['James']
50
+ # Employee.find_by_birth_year(1968).map(&:name) # => ['Frank', 'Aaron']
51
+ #
52
+ module Indexable
53
+ extend ActiveSupport::Concern
54
+
55
+ include ActiveModel::Model
56
+ include ActiveModel::Attributes
57
+ include ActiveModelPersistence::PrimaryKey
58
+
59
+ class_methods do
60
+ # Returns a hash of indexes for the model keyed by name
61
+ #
62
+ # @example
63
+ # Employee.indexes.keys # => %w[id manager_id birth_year]
64
+ #
65
+ # @return [Hash<String, ActiveModelPersistence::Index>] the indexes defined by the model
66
+ #
67
+ def indexes
68
+ @indexes ||= {}
69
+ end
70
+
71
+ # Adds an index to the model
72
+ #
73
+ # @example Add a unique index on the id attribute
74
+ # Employee.index(:id, unique: true)
75
+ #
76
+ # @example with a key_value_source when the name and the attribute are different
77
+ # Employee.index(:manager, key_value_source: :manager_id)
78
+ #
79
+ # @example with a Proc for key_value_source
80
+ # Employee.index(:birth_year, key_value_source: ->(o) { o.birth_date.year })
81
+ #
82
+ # @param index_name [String] the name of the index
83
+ # @param options [Hash] the options for the index
84
+ # @option options :unique [Boolean] whether the index is unique, default is false
85
+ # @option options :key_value_source [Symbol, Proc] the source of the key value of the object for this index
86
+ #
87
+ # If a Symbol is given, it will name a zero arg method on the object which returns
88
+ # the key's value. If a Proc is given, the key's value will be the result of calling the
89
+ # Proc with the object.
90
+ #
91
+ # @return [void]
92
+ #
93
+ def index(index_name, **options)
94
+ index = Index.new(**default_index_options(index_name).merge(options))
95
+ indexes[index_name.to_sym] = index
96
+
97
+ singleton_class.define_method("find_by_#{index_name}") do |key|
98
+ index.objects(key).tap do |objects|
99
+ objects.each { |o| o.instance_variable_set(:@previously_new_record, false) }
100
+ end
101
+ end
102
+ end
103
+
104
+ # Adds or updates all defined indexes for the given object
105
+ #
106
+ # Call this after changing the object to ensure the indexes are up to date.
107
+ #
108
+ # @example
109
+ # e1 = Employee.new(id: 1, name: 'James', manager_id: 3, birth_date: Date.new(1967, 3, 15))
110
+ # Employee.update_indexes(e1)
111
+ # Employee.find_by_id(1) # => e1
112
+ # Employee.find_by_manager_id(3) # => [e1]
113
+ # Employee.find_by_birth_year(1967) # => [e1]
114
+ #
115
+ # e1.birth_date = Date.new(1968, 1, 27)
116
+ # Employee.find_by_birth_year(1968) # => []
117
+ # Employee.update_indexes(e1)
118
+ # Employee.find_by_birth_year(1968) # => [e1]
119
+ #
120
+ # @param object [Object] the object to add to the indexes
121
+ #
122
+ # @return [void]
123
+ #
124
+ def update_indexes(object)
125
+ indexes.each_value { |index| index.add_or_update(object) }
126
+ end
127
+
128
+ # Removes the given object from all defined indexes
129
+ #
130
+ # Call this before deleting the object to ensure the indexes are up to date.
131
+ #
132
+ # @example
133
+ # e1 = Employee.new(id: 1, name: 'James', manager_id: 3, birth_date: Date.new(1967, 3, 15))
134
+ # Employee.update_indexes(e1)
135
+ # Employee.find_by_id(1) # => e1
136
+ # Employee.remove_from_indexes(e1)
137
+ # Employee.find_by_id(1) # => nil
138
+ #
139
+ # @param object [Object] the object to remove from the indexes
140
+ #
141
+ # @return [void]
142
+ #
143
+ def remove_from_indexes(object)
144
+ indexes.each_value { |index| index.remove(object) }
145
+ end
146
+
147
+ private
148
+
149
+ # Defines the default options for a new ActiveModelPersistence::Index
150
+ #
151
+ # @api private
152
+ #
153
+ def default_index_options(index_name)
154
+ {
155
+ name: index_name.to_sym,
156
+ unique: false
157
+ }
158
+ end
159
+ end
160
+
161
+ included do
162
+ # Adds the object to the indexes defined by the model
163
+ #
164
+ # @example
165
+ # e1 = Employee.new(id: 1, name: 'James', manager_id: 3, birth_date: Date.new(1967, 3, 15))
166
+ # e1.add_to_indexes
167
+ # Employee.find_by_id(1) # => e1
168
+ #
169
+ # @return [void]
170
+ #
171
+ def update_indexes
172
+ self.class.update_indexes(self)
173
+ end
174
+
175
+ # Removes the object from the indexes defined by the model
176
+ #
177
+ # @example
178
+ # e1 = Employee.new(id: 1, name: 'James', manager_id: 3, birth_date: Date.new(1967, 3, 15))
179
+ # e1.add_to_indexes
180
+ # Employee.find_by_id(1) # => e1
181
+ # e1.remove_from_indexes
182
+ # Employee.find_by_id(1) # => nil
183
+ #
184
+ # @return [void]
185
+ #
186
+ def remove_from_indexes
187
+ self.class.remove_from_indexes(self)
188
+ end
189
+
190
+ # Returns the key value for the object in the index named by index_name
191
+ # @api private
192
+ def previous_index_key(index_name)
193
+ instance_variable_get("@#{index_name}_index_key")
194
+ end
195
+
196
+ # Set the key value for the object in the index named by index_name
197
+ # @api private
198
+ def save_index_key(index_name, key)
199
+ instance_variable_set("@#{index_name}_index_key", key)
200
+ end
201
+
202
+ # Clears the key value for the object in the index named by index_name
203
+ # @api private
204
+ def clear_index_key(index_name)
205
+ instance_variable_set("@#{index_name}_index_key", nil)
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model_persistence/indexable'
4
+ require 'active_model_persistence/primary_key'
5
+ require 'active_model_persistence/primary_key_index'
6
+
7
+ module ActiveModelPersistence
8
+ # This mixin adds the ability to store and manage Ruby objects in an in-memory object store.
9
+ # These objects are commonly called 'models'.
10
+ #
11
+ # @example
12
+ # class ModelExample
13
+ # include ActiveModelPersistence::Persistence
14
+ # attribute :id, :integer
15
+ # attribute :name, :string
16
+ # index :id, unique: true
17
+ # end
18
+ #
19
+ # # Creating a model instance with `.new` does not save it to the object store
20
+ # #
21
+ # m = ModelExample.new(id: 1, name: 'James')
22
+ # m.new_record? # => true
23
+ # m.persisted? # => false
24
+ # m.destroyed? # => false
25
+ #
26
+ # # `save` will save the object to the object store
27
+ # #
28
+ # m.save
29
+ # m.new_record? # => false
30
+ # m.persisted? # => true
31
+ # m.destroyed? # => false
32
+ #
33
+ # # Once an object is persisted, it can be fetched from the object store using `.find`
34
+ # # and `.find_by_*` methods.
35
+ # m2 = ModelExample.find(1)
36
+ # m == m2 # => true
37
+ #
38
+ # m2 = ModelExample.find_by_id(1)
39
+ # m == m2 # => true
40
+ #
41
+ # # `destroy` will remove the object from the object store
42
+ # #
43
+ # m.destroy
44
+ # m.new_record? # => true
45
+ # m.persisted? # => false
46
+ # m.destroyed? # => true
47
+ #
48
+ module Persistence
49
+ extend ActiveSupport::Concern
50
+
51
+ include ActiveModelPersistence::Indexable
52
+ include ActiveModelPersistence::PrimaryKey
53
+ include ActiveModelPersistence::PrimaryKeyIndex
54
+
55
+ class_methods do
56
+ # Creates a new model object in to the object store
57
+ #
58
+ # Create a new model object passing `attributes` and `block` to `.new` and then calls `#save`.
59
+ #
60
+ # @param attributes [Hash, Array<Hash>] attributes
61
+ #
62
+ # The attributes to set on the model object. These are passed to the model's `.new` method.
63
+ #
64
+ # Multiple model objects can be created by passing an array of attribute Hashes.
65
+ #
66
+ # @param block [Proc] options
67
+ #
68
+ # The block to pass to the model's `.new` method.
69
+ #
70
+ # @example
71
+ # m = ModelExample.new(id: 1, name: 'James')
72
+ # m.id #=> 1
73
+ # m.name #=> 'James'
74
+ #
75
+ # @example Multiple model objects can be created
76
+ # array_of_attributes = [
77
+ # { id: 1, name: 'James' },
78
+ # { id: 2, name: 'Frank' }
79
+ # ]
80
+ # objects = ModelExample.create(array_of_attributes)
81
+ # objects.class #=> Array
82
+ # objects.size #=> 2
83
+ # objects.first.id #=> 1
84
+ # objects.map(&:name) #=> ['James', 'Frank']
85
+ #
86
+ # @return [Object, Array<Object>] the model object or array of model objects created
87
+ #
88
+ def create(attributes = nil, &block)
89
+ if attributes.is_a?(Array)
90
+ attributes.collect { |attr| create(attr, &block) }
91
+ else
92
+ new(attributes, &block).tap(&:save)
93
+ end
94
+ end
95
+
96
+ # Return all model objects that have been saved to the object store
97
+ #
98
+ # @example
99
+ # array_of_attributes = [
100
+ # { id: 1, name: 'James' },
101
+ # { id: 2, name: 'Frank' }
102
+ # ]
103
+ # ModelExample.create(array_of_attributes)
104
+ # ModelExample.all.count #=> 2
105
+ # ModelExample.all.map(&:id) #=> [1, 2]
106
+ # ModelExample.all.map(&:name) #=> ['James', 'Frank']
107
+ #
108
+ # @return [Array<Object>] the model objects in the object store
109
+ #
110
+ def all
111
+ object_array.each
112
+ end
113
+
114
+ # The number of model objects saved in the object store
115
+ #
116
+ # @example
117
+ # array_of_attributes = [
118
+ # { id: 1, name: 'James' },
119
+ # { id: 2, name: 'Frank' }
120
+ # ]
121
+ # ModelExample.create(array_of_attributes)
122
+ # ModelExample.all.count #=> 2
123
+ #
124
+ # @return [Integer] the number of model objects in the object store
125
+ #
126
+ def count
127
+ object_array.size
128
+ end
129
+
130
+ alias_method(:size, :count)
131
+
132
+ # Removes all model objects from the object store
133
+ #
134
+ # Each saved model object's `#destroy` method is called.
135
+ #
136
+ # @example
137
+ # array_of_attributes = [
138
+ # { id: 1, name: 'James' },
139
+ # { id: 2, name: 'Frank' }
140
+ # ]
141
+ # ModelExample.create(array_of_attributes)
142
+ # ModelExample.all.count #=> 2
143
+ # ModelExample.destroy_all
144
+ # ModelExample.all.count #=> 0
145
+ #
146
+ # @return [void]
147
+ #
148
+ def destroy_all
149
+ object_array.first.destroy while object_array.size.positive?
150
+ end
151
+
152
+ # Removes all model objects from the object store
153
+ #
154
+ # Each saved model object's `#destroy` method is NOT called.
155
+ #
156
+ # @example
157
+ # array_of_attributes = [
158
+ # { id: 1, name: 'James' },
159
+ # { id: 2, name: 'Frank' }
160
+ # ]
161
+ # ModelExample.create(array_of_attributes)
162
+ # ModelExample.all.count #=> 2
163
+ # ModelExample.destroy_all
164
+ # ModelExample.all.count #=> 0
165
+ #
166
+ # @return [void]
167
+ #
168
+ def delete_all
169
+ @object_array = []
170
+ indexes.values.each(&:remove_all)
171
+ nil
172
+ end
173
+
174
+ # private
175
+
176
+ # All saved model objects are stored in this array (this is the object store)
177
+ #
178
+ # @return [Array<Object>] the model objects in the object store
179
+ #
180
+ # @api private
181
+ #
182
+ def object_array
183
+ @object_array ||= []
184
+ end
185
+ end
186
+
187
+ # rubocop:disable Metrics/BlockLength
188
+ included do
189
+ # Returns true if this object hasn't been saved or destroyed yet
190
+ #
191
+ # @example
192
+ # object = ModelExample.new(id: 1, name: 'James')
193
+ # object.new_record? #=> true
194
+ # object.save
195
+ # object.new_record? #=> false
196
+ # object.destroy
197
+ # object.new_record? #=> false
198
+ #
199
+ # @return [Boolean] true if this object hasn't been saved yet
200
+ #
201
+ def new_record?
202
+ if instance_variable_defined?(:@new_record)
203
+ @new_record
204
+ else
205
+ @new_record = true
206
+ end
207
+ end
208
+
209
+ # Returns true if this object has been destroyed
210
+ #
211
+ # @example Destroying a saved model object
212
+ # object = ModelExample.new(id: 1, name: 'James')
213
+ # object.destroyed? #=> false
214
+ # object.save
215
+ # object.destroyed? #=> false
216
+ # object.destroy
217
+ # object.destroyed? #=> true
218
+ #
219
+ # @example Destroying a unsaved model object
220
+ # object = ModelExample.new(id: 1, name: 'James')
221
+ # object.destroyed? #=> false
222
+ # object.destroy
223
+ # object.destroyed? #=> true
224
+ #
225
+ # @return [Boolean] true if this object has been destroyed
226
+ #
227
+ def destroyed?
228
+ if instance_variable_defined?(:@destroyed)
229
+ @destroyed
230
+ else
231
+ @destroyed = false
232
+ end
233
+ end
234
+
235
+ # Returns true if the record is persisted in the object store
236
+ #
237
+ # @example
238
+ # object = ModelExample.new(id: 1, name: 'James')
239
+ # object.persisted? #=> false
240
+ # object.save
241
+ # object.persisted? #=> true
242
+ # object.destroy
243
+ # object.persisted? #=> false
244
+ #
245
+ # @return [Boolean] true if the record is persisted in the object store
246
+ #
247
+ def persisted?
248
+ !(new_record? || destroyed?)
249
+ end
250
+
251
+ # Saves the model object in the object store and updates all indexes
252
+ #
253
+ # @example
254
+ # ModelExample.all.count #=> 0
255
+ # object = ModelExample.new(id: 1, name: 'James')
256
+ # ModelExample.all.count #=> 0
257
+ # object.save
258
+ # ModelExample.all.count #=> 1
259
+ #
260
+ # @param _options [Hash] save options (currently unused)
261
+ # @param block [Proc] a block to call after the save
262
+ #
263
+ # @yield [self] a block to call after the save
264
+ # @yieldparam saved_model [self] the model object after it was saved
265
+ # @yieldreturn [void]
266
+ #
267
+ # @return [Boolean] true if the model object was saved
268
+ #
269
+ def save(**_options, &block)
270
+ return false if destroyed?
271
+
272
+ result = new_record? ? _create(&block) : _update(&block)
273
+ update_indexes
274
+ result != false
275
+ end
276
+
277
+ # Deletes the object from the object store
278
+ #
279
+ # This model object is frozen to reflect that no changes should be made
280
+ # since they can't be persisted.
281
+ #
282
+ # @example
283
+ # ModelExample.create(id: 1, name: 'James')
284
+ # object = ModelExample.create(id: 2, name: 'Frank')
285
+ # object.destroyed? #=> false
286
+ # ModelExample.all.count #=> 2
287
+ # object.destroy
288
+ # object.destroyed? #=> true
289
+ # ModelExample.all.count #=> 1
290
+ # ModelExample.all.first.name #=> 'James'
291
+ #
292
+ # @return [void]
293
+ #
294
+ def destroy
295
+ if persisted?
296
+ remove_from_indexes
297
+ self.class.object_array.delete_if { |o| o.primary_key == primary_key }
298
+ end
299
+ @new_record = false
300
+ @destroyed = true
301
+ freeze
302
+ end
303
+
304
+ def ==(other)
305
+ attributes == other.attributes
306
+ end
307
+
308
+ private
309
+
310
+ # Creates a record with values matching those of the instance attributes
311
+ # and returns its id.
312
+ def _create
313
+ return false unless primary_key?
314
+ raise UniqueContraintError if primary_key_index.include?(primary_key)
315
+
316
+ self.class.object_array << self
317
+
318
+ @new_record = false
319
+
320
+ yield(self) if block_given?
321
+
322
+ primary_key
323
+ end
324
+
325
+ def _update
326
+ raise RecordNotFound unless primary_key_index.include?(primary_key)
327
+
328
+ yield(self) if block_given?
329
+ end
330
+ end
331
+ # rubocop:enable Metrics/BlockLength
332
+ end
333
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModelPersistence
4
+ # Exposes the `primary_key` accessor to read or write the primary key attribute value
5
+ #
6
+ # The primary key should be a unique (within its model class) identifier
7
+ # for a model.
8
+ #
9
+ # By default, the `primary_key` accessors maps to the `id` attribute. You can change
10
+ # the attribute by setting the `primary_key` at the class level.
11
+ #
12
+ # @example By default, the primary key maps to the `id` attribute
13
+ # class Employee
14
+ # include ActiveModelPersistence::PrimaryKey
15
+ # attribute :id, :integer
16
+ # end
17
+ # e1 = Employee.new(id: 1)
18
+ # e1.primary_key #=> 1
19
+ #
20
+ # @example Changing the primary key attribute
21
+ # class Employee
22
+ # include ActiveModelPersistence::PrimaryKey
23
+ # attribute :short_id, :string
24
+ # # change the primary key
25
+ # self.primary_key = :short_id
26
+ # end
27
+ # e1 = Employee.new(short_id: 'couballj')
28
+ # # `primary_key` can be used as an alias for `short_id`
29
+ # e1.primary_key #=> 'couballj'
30
+ # e1.primary_key = 'fthrock'
31
+ # e1.short_id #=> 'fthrock'
32
+ #
33
+ # @api public
34
+ #
35
+ module PrimaryKey
36
+ extend ActiveSupport::Concern
37
+
38
+ include ActiveModel::Model
39
+ include ActiveModel::Attributes
40
+
41
+ class_methods do
42
+ # Identifies the attribute that the `primary_key` accessor maps to
43
+ #
44
+ # The primary key is 'id' by default.
45
+ #
46
+ # @example
47
+ # class Employee
48
+ # include ActiveModelPersistence::PrimaryKey
49
+ # attribute :username, :string
50
+ # self.primary_key = :username
51
+ # end
52
+ # Employee.primary_key #=> :username
53
+ #
54
+ # @return [Symbol] the attribute that the `primary_key` accessor is an alias for
55
+ #
56
+ def primary_key
57
+ @primary_key ||= 'id'
58
+ end
59
+
60
+ # Sets the attribute to use for the primary key
61
+ #
62
+ # @example
63
+ # class Employee
64
+ # include ActiveModelPersistence::PrimaryKey
65
+ # attribute :username, :string
66
+ # primary_key = :username
67
+ # end
68
+ # e = Employee.new(username: 'couballj')
69
+ # e.primary_key #=> 'couballj'
70
+ #
71
+ # @param attribute [Symbol] the attribute to use for the primary key
72
+ #
73
+ # @return [void]
74
+ #
75
+ def primary_key=(attribute)
76
+ @primary_key = attribute.to_s
77
+ end
78
+ end
79
+
80
+ included do
81
+ # Returns the primary key attribute's value
82
+ #
83
+ # @example
84
+ # class Employee
85
+ # include ActiveModelPersistence::PrimaryKey
86
+ # attribute :username, :string
87
+ # self.primary_key = :username
88
+ # end
89
+ # e = Employee.new(username: 'couballj')
90
+ # e.primary_key #=> 'couballj'
91
+ #
92
+ # @return [Object] the primary key attribute's value
93
+ #
94
+ def primary_key
95
+ __send__(self.class.primary_key)
96
+ end
97
+
98
+ # Sets the primary key atribute's value
99
+ #
100
+ # @example
101
+ # class Employee
102
+ # include ActiveModelPersistence::PrimaryKey
103
+ # attribute :username, :string
104
+ # primary_key = :username
105
+ # end
106
+ # e = Employee.new(username: 'couballj')
107
+ # e.primary_key = 'other'
108
+ # e.username #=> 'other'
109
+ #
110
+ # @param value [Object] the value to set the primary key attribute to
111
+ #
112
+ # @return [void]
113
+ #
114
+ def primary_key=(value)
115
+ __send__("#{self.class.primary_key}=", value)
116
+ end
117
+
118
+ # Returns true if the primary key attribute's value is not null or empty
119
+ #
120
+ # @example
121
+ # class Employee
122
+ # include ActiveModelPersistence::PrimaryKey
123
+ # attribute :id, :integer
124
+ # end
125
+ # e = Employee.new
126
+ # e.primary_key #=> nil
127
+ # e.primary_key? #=> false
128
+ # e.primary_key = 1
129
+ # e.primary_key? #=> true
130
+ #
131
+ # @return [Boolean] true if the primary key attribute's value is not null or empty
132
+ #
133
+ def primary_key?
134
+ primary_key.present?
135
+ end
136
+ end
137
+ end
138
+ end