active_model_persistence 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/Rakefile +89 -0
- data/active_model_persistence.gemspec +52 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/active_model_persistence/index.rb +290 -0
- data/lib/active_model_persistence/indexable.rb +209 -0
- data/lib/active_model_persistence/persistence.rb +333 -0
- data/lib/active_model_persistence/primary_key.rb +138 -0
- data/lib/active_model_persistence/primary_key_index.rb +57 -0
- data/lib/active_model_persistence/version.rb +6 -0
- data/lib/active_model_persistence.rb +24 -0
- metadata +234 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4358309182de30b6d706336fdebb8d8e4b803d35cfc75c86074cb2fe96230a92
|
4
|
+
data.tar.gz: 3003146bc077bacff57e3d62ee55fb4ced6a3cec443a6250bd27e1b59cb17285
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ec9030ea8eff12cf3d05bcaee715b846069cc4c7358c473c2688d198c73db949b79364089c10860a28b9a9ee83fe1a22f3d2bc049c4078026cfd2204d3b61082
|
7
|
+
data.tar.gz: 74a2bcd9bf79fbd3747f365cf8c7c7059fec276d61e2880d36870daba6c2c12cb9e85d359d6216edff526c5f3a39da5f7e655272e06c8dbe89eb3de6c6781193
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
AllCops:
|
2
|
+
SuggestExtensions: false
|
3
|
+
NewCops: enable
|
4
|
+
# Output extra information for each offense to make it easier to diagnose:
|
5
|
+
DisplayCopNames: true
|
6
|
+
DisplayStyleGuide: true
|
7
|
+
ExtraDetails: true
|
8
|
+
# RuboCop enforces rules depending on the oldest version of Ruby which
|
9
|
+
# your project supports:
|
10
|
+
TargetRubyVersion: 2.7
|
11
|
+
|
12
|
+
# The default max line length is 80 characters
|
13
|
+
Layout/LineLength:
|
14
|
+
Max: 120
|
15
|
+
|
16
|
+
# The DSL for RSpec and the gemspec file make it very hard to limit block length:
|
17
|
+
Metrics/BlockLength:
|
18
|
+
Exclude:
|
19
|
+
- "spec/**/*_spec.rb"
|
20
|
+
- "*.gemspec"
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 James Couball
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# active_model_persistence
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/active_model_persistence.svg)](https://badge.fury.io/rb/active_model_persistence)
|
4
|
+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
5
|
+
[![Documentation](https://img.shields.io/badge/Documentation-OK-green.svg)](https://jcouball.github.io/github_pages_rake_tasks/)
|
6
|
+
|
7
|
+
A gem to add in-memory persistence to Models built with ActiveModel.
|
8
|
+
|
9
|
+
The goals of this gem are:
|
10
|
+
|
11
|
+
* Add ActiveRecord-like meta-programming to configure Models used for ETI (Extract,
|
12
|
+
Transform, Load) work where a lot of data is loaded from desparate sources, transformed
|
13
|
+
in memory, and then written to other sources.
|
14
|
+
* Make creation of these model objects consistent across several different teams,
|
15
|
+
* Make it so easy to create model objects that teams will use these models instead
|
16
|
+
of using hashs
|
17
|
+
* Encourage a separation from the models from the business logic of reading, transforming,
|
18
|
+
and writing the data
|
19
|
+
* Make it easy to use FactoryBot to generate test data instead of having to maintain a
|
20
|
+
bunch of fixture files.
|
21
|
+
|
22
|
+
An example model built with this gem might look like this:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'active_model_persistence'
|
26
|
+
|
27
|
+
class Employee
|
28
|
+
include ActiveModelPersistence::Persistence
|
29
|
+
|
30
|
+
# Use ActiveModel attributes and validations to define the model's state
|
31
|
+
|
32
|
+
attribute :id, :string
|
33
|
+
attribute :name, :string
|
34
|
+
attribute :manager_id, :string
|
35
|
+
|
36
|
+
validates :id, presence: true
|
37
|
+
validates :name, presence: true
|
38
|
+
|
39
|
+
# A unique index is automatically created on the primary key attribute (which is :id by default)
|
40
|
+
# index :id, unique: true
|
41
|
+
|
42
|
+
# You can set the primary key attribute if you are using a different attribute for
|
43
|
+
# the primary key using the statement `self.primary_key = attribute_name`
|
44
|
+
|
45
|
+
# Indexes are non-unique by default and create a `find_by_{index_name}` method on
|
46
|
+
# the model class.
|
47
|
+
index :manager_id
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Use the employee model like you would an ActiveRecord model:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
e1 = Enmployee.create(id: 'jdoe1', name: 'John Doe', manager_id: 'boss')
|
55
|
+
e2 = Enmployee.create(id: 'jdoe2', name: 'Bob Doe', manager_id: 'boss')
|
56
|
+
e3 = Enmployee.create(id: 'boss', name: 'Boss Person')
|
57
|
+
|
58
|
+
# The `find` method looks up objects by the primary key and returns a single object or nil
|
59
|
+
Employee.find('jdoe1') #=> [e1]
|
60
|
+
|
61
|
+
# The `find_by_*` methods return a (possibly empty) object array based on the indexes
|
62
|
+
# declared in the model class.
|
63
|
+
Employee.find_by_manager_id('boss') #=> [e1, e2]
|
64
|
+
|
65
|
+
# etc.
|
66
|
+
```
|
67
|
+
|
68
|
+
See [the full API documentation](https://jcouball.github.io/active_record_persistence/) for more details.
|
69
|
+
|
70
|
+
## Installation
|
71
|
+
|
72
|
+
Add this line to your application's Gemfile (or equivalent comamnd in the project's gemspec):
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
gem 'active_model_persistence'
|
76
|
+
```
|
77
|
+
|
78
|
+
And then execute:
|
79
|
+
|
80
|
+
```shell
|
81
|
+
bundle install
|
82
|
+
```
|
83
|
+
|
84
|
+
Or install it manually using the `gem` command:
|
85
|
+
|
86
|
+
```shell
|
87
|
+
gem install active_model_persistence
|
88
|
+
```
|
89
|
+
|
90
|
+
## Development
|
91
|
+
|
92
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
93
|
+
|
94
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_model_persistence.
|
99
|
+
|
100
|
+
## License
|
101
|
+
|
102
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The default task
|
4
|
+
|
5
|
+
desc 'Run the same tasks that the CI build will run'
|
6
|
+
task default: %w[spec rubocop yard yard:coverage yard:audit bundle:audit build]
|
7
|
+
|
8
|
+
# Bundler Audit
|
9
|
+
|
10
|
+
require 'bundler/audit/task'
|
11
|
+
Bundler::Audit::Task.new
|
12
|
+
|
13
|
+
# Bundler Gem Build
|
14
|
+
|
15
|
+
require 'bundler'
|
16
|
+
require 'bundler/gem_tasks'
|
17
|
+
|
18
|
+
begin
|
19
|
+
Bundler.setup(:default, :development)
|
20
|
+
rescue Bundler::BundlerError => e
|
21
|
+
warn e.message
|
22
|
+
warn 'Run `bundle install` to install missing gems'
|
23
|
+
exit e.status_code
|
24
|
+
end
|
25
|
+
|
26
|
+
CLEAN << 'pkg'
|
27
|
+
CLOBBER << 'Gemfile.lock'
|
28
|
+
|
29
|
+
# Bump
|
30
|
+
|
31
|
+
require 'bump/tasks'
|
32
|
+
|
33
|
+
# RSpec
|
34
|
+
|
35
|
+
require 'rspec/core/rake_task'
|
36
|
+
|
37
|
+
RSpec::Core::RakeTask.new
|
38
|
+
|
39
|
+
CLEAN << 'coverage'
|
40
|
+
CLEAN << '.rspec_status'
|
41
|
+
CLEAN << 'rspec-report.xml'
|
42
|
+
|
43
|
+
# Rubocop
|
44
|
+
|
45
|
+
require 'rubocop/rake_task'
|
46
|
+
|
47
|
+
RuboCop::RakeTask.new do |t|
|
48
|
+
t.options = %w[
|
49
|
+
--format progress
|
50
|
+
--format json --out rubocop-report.json
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
CLEAN << 'rubocop-report.json'
|
55
|
+
|
56
|
+
# YARD
|
57
|
+
|
58
|
+
require 'yard'
|
59
|
+
|
60
|
+
YARD::Rake::YardocTask.new do |t|
|
61
|
+
t.files = %w[lib/**/*.rb examples/**/*]
|
62
|
+
end
|
63
|
+
|
64
|
+
CLEAN << '.yardoc'
|
65
|
+
CLEAN << 'doc'
|
66
|
+
|
67
|
+
# Yardstick
|
68
|
+
|
69
|
+
desc 'Run yardstick to show missing YARD doc elements'
|
70
|
+
task :'yard:audit' do
|
71
|
+
sh "yardstick 'lib/**/*.rb'"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Yardstick coverage
|
75
|
+
|
76
|
+
require 'yardstick/rake/verify'
|
77
|
+
|
78
|
+
Yardstick::Rake::Verify.new(:'yard:coverage') do |verify|
|
79
|
+
verify.threshold = 100
|
80
|
+
end
|
81
|
+
|
82
|
+
# Publish YARD documentation to GitHub
|
83
|
+
|
84
|
+
require 'github_pages_rake_tasks'
|
85
|
+
|
86
|
+
GithubPagesRakeTasks::PublishTask.new do |task|
|
87
|
+
# task.doc_dir = 'documentation'
|
88
|
+
task.verbose = true
|
89
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/active_model_persistence/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'active_model_persistence'
|
7
|
+
spec.version = ActiveModelPersistence::VERSION
|
8
|
+
spec.authors = ['James Couball']
|
9
|
+
spec.email = ['couballj@verizonmedia.com']
|
10
|
+
|
11
|
+
spec.summary = 'Adds in-memory persistence to ActiveModel models'
|
12
|
+
spec.description = <<~DESCRIPTION
|
13
|
+
Adds in-memory persistence to ActiveModel models
|
14
|
+
DESCRIPTION
|
15
|
+
spec.homepage = 'https://github.com/jcouball/active_model_persistence'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
spec.required_ruby_version = '>= 2.7.0'
|
18
|
+
|
19
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
21
|
+
|
22
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
23
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
24
|
+
spec.metadata['changelog_uri'] = "#{spec.metadata['source_code_uri']}/blob/main/CHANGELOG.md"
|
25
|
+
|
26
|
+
# Specify which files should be added to the gem when it is released.
|
27
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
28
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
29
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
30
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
31
|
+
end
|
32
|
+
end
|
33
|
+
spec.bindir = 'exe'
|
34
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
35
|
+
spec.require_paths = ['lib']
|
36
|
+
|
37
|
+
# Runtime dependencies
|
38
|
+
spec.add_dependency 'activemodel', '~> 7.0'
|
39
|
+
spec.add_dependency 'activesupport', '~> 7.0'
|
40
|
+
|
41
|
+
# Development dependencies
|
42
|
+
spec.add_development_dependency 'bump', '~> 0.10'
|
43
|
+
spec.add_development_dependency 'bundler-audit', '~> 0.9'
|
44
|
+
spec.add_development_dependency 'github_pages_rake_tasks', '~> 0.1'
|
45
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
46
|
+
spec.add_development_dependency 'redcarpet', '~> 3.5'
|
47
|
+
spec.add_development_dependency 'rspec', '~> 3.10'
|
48
|
+
spec.add_development_dependency 'rubocop', '~> 1.24'
|
49
|
+
spec.add_development_dependency 'simplecov', '~> 0.21'
|
50
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
51
|
+
spec.add_development_dependency 'yardstick', '~> 0.9'
|
52
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'active_model_persistence'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,290 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModelPersistence
|
4
|
+
# An Index keeps a map from a key to zero of more objects
|
5
|
+
# @api public
|
6
|
+
#
|
7
|
+
class Index
|
8
|
+
# The name of the index
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
12
|
+
# i.name # => 'id'
|
13
|
+
#
|
14
|
+
# @return [String] The name of the index
|
15
|
+
#
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
# Defines how the object's key value is calculated
|
19
|
+
#
|
20
|
+
# If a proc is provided, it will be called with the object as an argument to get the key value.
|
21
|
+
#
|
22
|
+
# If a symbol is provided, it will identify the method to call on the object to get the key value.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# i = Index.new(name: 'id', key_value_source: :id, unique: true)
|
26
|
+
# i.key_value_source # => :id
|
27
|
+
#
|
28
|
+
# @return [Symbol, Proc] the method name or proc used to calculate the index key
|
29
|
+
#
|
30
|
+
attr_reader :key_value_source
|
31
|
+
|
32
|
+
# Determines if a key value can index more than one object
|
33
|
+
#
|
34
|
+
# The default value is false.
|
35
|
+
#
|
36
|
+
# If true, if two objects have the same key, a UniqueContraintError will be raised
|
37
|
+
# when trying to add the second object.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# i = Index.new(name: 'id', key_value_source: :id, unique: true)
|
41
|
+
# i.unique? # => true
|
42
|
+
#
|
43
|
+
# @return [Boolean] true if the index is unique
|
44
|
+
#
|
45
|
+
attr_reader :unique
|
46
|
+
|
47
|
+
alias unique? unique
|
48
|
+
|
49
|
+
# Create an Index
|
50
|
+
#
|
51
|
+
# @example An object that can be indexed must include Indexable (which includes PrimaryKey)
|
52
|
+
# Employee = Struct.new(:id, :name, keyword_init: true)
|
53
|
+
# include ActiveModelPersistence::Indexable
|
54
|
+
# end
|
55
|
+
# e1 = Employee.new(id: 1, name: 'James')
|
56
|
+
# e2 = Employee.new(id: 2, name: 'Frank')
|
57
|
+
# e3 = Employee.new(id: 1, name: 'Margaret') # Note e1.id == e3.id
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
61
|
+
# i.name # => 'id'
|
62
|
+
# i.key_source # => :id -- get the key value by calling the 'id' method on object
|
63
|
+
# i.unique # => true -- each key can only have one object associated with it
|
64
|
+
#
|
65
|
+
# i.objects(e1.id) # => []
|
66
|
+
# i.add(e1)
|
67
|
+
# i.add(e2)
|
68
|
+
# i.objects(e1.id) # => [e1]
|
69
|
+
# i.objects(e2.id) # => [e2]
|
70
|
+
# i.add(e3) # => raises a UniqueContraintError since e1.id == e3.id
|
71
|
+
# i.add(e1) # => raises an ObjectAlreadyInIndexError
|
72
|
+
#
|
73
|
+
# @param name [String] the name of the index
|
74
|
+
# @param key_value_source [Symbol, Proc] the attribute name or proc used to calculate the index key
|
75
|
+
# @param unique [Boolean] when true the index will only allow one object per key
|
76
|
+
#
|
77
|
+
def initialize(name:, key_value_source: nil, unique: false)
|
78
|
+
@name = name.to_s
|
79
|
+
@key_value_source = determine_key_value_source(name, key_value_source)
|
80
|
+
@unique = unique
|
81
|
+
@key_to_objects_map = {}
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the objects that match the key
|
85
|
+
#
|
86
|
+
# A unique index will return an Array containing zero or one objects. A non-unique
|
87
|
+
# index will return an array containing zero or more objects.
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
91
|
+
# e = Employee.new(id: 1, name: 'James')
|
92
|
+
# i.object(e.id) # => []
|
93
|
+
# i.add(e.id, e)
|
94
|
+
# i.object(e.id) # => [e]
|
95
|
+
#
|
96
|
+
# @param key [Object] the key to search for
|
97
|
+
#
|
98
|
+
# @return [Array<Object>] the objects that match the key
|
99
|
+
#
|
100
|
+
def objects(key)
|
101
|
+
key_to_objects_map[key] || []
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if the index contains an object with the given key
|
105
|
+
#
|
106
|
+
# @example
|
107
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
108
|
+
# e = Employee.new(id: 1, name: 'James')
|
109
|
+
# i.include?(e.id) # => false
|
110
|
+
# i << e
|
111
|
+
# i.include?(e.id) # => true
|
112
|
+
#
|
113
|
+
# @param key [Object] the key to search for
|
114
|
+
#
|
115
|
+
# @return [Boolean] true if the index contains an object with the given key
|
116
|
+
#
|
117
|
+
def include?(key)
|
118
|
+
key_to_objects_map.key?(key)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Adds an object to the index
|
122
|
+
#
|
123
|
+
# If the object was already in the index using a different key, remote the object
|
124
|
+
# from the index using the previous key before adding it again.
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
128
|
+
# e = Employee.new(id: 1, name: 'James')
|
129
|
+
# i << e
|
130
|
+
# i.objects(1) # => [e]
|
131
|
+
# e.id = 2
|
132
|
+
# i << e
|
133
|
+
# i.objects(1) # => []
|
134
|
+
# i.objects(2) # => [e]
|
135
|
+
#
|
136
|
+
# @param object [Object] the object to add to the index
|
137
|
+
#
|
138
|
+
# @return [Index] self so calls can be chained
|
139
|
+
#
|
140
|
+
# @raise [UniqueConstraintError] if the index is unique and there is already an index
|
141
|
+
# entry for the same key
|
142
|
+
#
|
143
|
+
def add_or_update(object)
|
144
|
+
previous_key = object.previous_index_key(name)
|
145
|
+
key = key_value_for(object)
|
146
|
+
|
147
|
+
return if previous_key == key
|
148
|
+
|
149
|
+
remove(object, previous_key) unless previous_key.nil?
|
150
|
+
|
151
|
+
add(object, key) unless key.nil?
|
152
|
+
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
alias << add_or_update
|
157
|
+
|
158
|
+
# Removes an object from the index
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
162
|
+
# e = Employee.new(id: 1, name: 'James')
|
163
|
+
# i << e
|
164
|
+
# i.objects(1) # => [e]
|
165
|
+
# i.remove(e)
|
166
|
+
# i.objects(1) # => []
|
167
|
+
#
|
168
|
+
# @param object [Object] the object to remove from the index
|
169
|
+
# @param key [Object] the object's key in the index
|
170
|
+
# If nil (the default), call the object.previous_index_key to get the key.
|
171
|
+
# `previous_index_key` is implemented by the Indexable concern.
|
172
|
+
#
|
173
|
+
# @return [void]
|
174
|
+
#
|
175
|
+
# @raise [ObjectNotInIndexError] if the object is not in the index
|
176
|
+
#
|
177
|
+
def remove(object, key = nil)
|
178
|
+
key ||= object.previous_index_key(name)
|
179
|
+
|
180
|
+
raise ActiveModelPersistence::ObjectNotInIndexError if key.nil?
|
181
|
+
|
182
|
+
remove_object_from_index(object, key)
|
183
|
+
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
|
187
|
+
# Removes all objects from the index
|
188
|
+
#
|
189
|
+
# @example
|
190
|
+
# i = Index.new(name: 'id', key_source: :id, unique: true)
|
191
|
+
# e1 = Employee.new(id: 1, name: 'James')
|
192
|
+
# e2 = Employee.new(id: 2, name: 'Frank')
|
193
|
+
# i << e1 << e2
|
194
|
+
# i.objects(1) # => [e1]
|
195
|
+
# i.objects(2) # => [e2]
|
196
|
+
# i.remove_all
|
197
|
+
# i.objects(1) # => []
|
198
|
+
# i.objects(2) # => []
|
199
|
+
#
|
200
|
+
# @return [void]
|
201
|
+
#
|
202
|
+
def remove_all
|
203
|
+
@key_to_objects_map.each_pair do |_key, objects|
|
204
|
+
objects.each do |object|
|
205
|
+
object.clear_index_key(name)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
@key_to_objects_map = {}
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
|
212
|
+
protected
|
213
|
+
|
214
|
+
# The map of keys to objects
|
215
|
+
#
|
216
|
+
# @return [Hash<Object, Array<Object>>] the map from key to an array objects added for that key
|
217
|
+
#
|
218
|
+
# @api private
|
219
|
+
#
|
220
|
+
attr_reader :key_to_objects_map
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# Remove an object from the index with no additional checks
|
225
|
+
#
|
226
|
+
# @return [void]
|
227
|
+
#
|
228
|
+
# @api private
|
229
|
+
#
|
230
|
+
def remove_object_from_index(object, key)
|
231
|
+
key_to_objects_map[key].delete_if { |o| o.primary_key == object.primary_key }
|
232
|
+
key_to_objects_map.delete(key) if key_to_objects_map[key].empty?
|
233
|
+
object.clear_index_key(name)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Adds an object to the index
|
237
|
+
#
|
238
|
+
# @param object [Object] the object to add to the index
|
239
|
+
# @param key [Object] the object's key in the index
|
240
|
+
#
|
241
|
+
# @return [void]
|
242
|
+
#
|
243
|
+
# @raise [UniqueConstraintError] if the index is unique and there is already an index
|
244
|
+
# entry for the same key
|
245
|
+
#
|
246
|
+
# @api private
|
247
|
+
#
|
248
|
+
def add(object, key)
|
249
|
+
raise UniqueConstraintError if unique? && include?(key)
|
250
|
+
|
251
|
+
key_to_objects_map[key] = [] unless key_to_objects_map.include?(key)
|
252
|
+
key_to_objects_map[key] << object
|
253
|
+
object.save_index_key(name, key)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Uses the `key_value_source` to calculate the key value for the given object
|
257
|
+
#
|
258
|
+
# @param object [Object] the object to calculate the key value for
|
259
|
+
#
|
260
|
+
# @return [Object] the key value
|
261
|
+
#
|
262
|
+
# @api private
|
263
|
+
#
|
264
|
+
def key_value_for(object)
|
265
|
+
if key_value_source.is_a?(Proc)
|
266
|
+
key_value_source.call(object)
|
267
|
+
else
|
268
|
+
object.send(key_value_source)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Determine the value for key_value_source
|
273
|
+
#
|
274
|
+
# @return [Symbol, Proc] the value for key_value_source
|
275
|
+
#
|
276
|
+
# @api private
|
277
|
+
#
|
278
|
+
def determine_key_value_source(name, key_value_source)
|
279
|
+
@key_value_source =
|
280
|
+
case key_value_source
|
281
|
+
when nil
|
282
|
+
name.to_sym
|
283
|
+
when Proc
|
284
|
+
key_value_source
|
285
|
+
else
|
286
|
+
key_value_source.to_sym
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|