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.
- 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
|
+
[](https://badge.fury.io/rb/active_model_persistence)
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
5
|
+
[](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
|