samvera-nesting_indexer 0.3.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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +53 -0
- data/.rubocop_todo.yml +12 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/Gemfile +4 -0
- data/Guardfile +49 -0
- data/LICENSE +14 -0
- data/README.md +102 -0
- data/Rakefile +34 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/samvera/nesting_indexer.rb +82 -0
- data/lib/samvera/nesting_indexer/adapters.rb +12 -0
- data/lib/samvera/nesting_indexer/adapters/abstract_adapter.rb +44 -0
- data/lib/samvera/nesting_indexer/adapters/in_memory_adapter.rb +157 -0
- data/lib/samvera/nesting_indexer/adapters/interface_behavior_spec.rb +53 -0
- data/lib/samvera/nesting_indexer/configuration.rb +51 -0
- data/lib/samvera/nesting_indexer/documents.rb +90 -0
- data/lib/samvera/nesting_indexer/exceptions.rb +35 -0
- data/lib/samvera/nesting_indexer/railtie.rb +12 -0
- data/lib/samvera/nesting_indexer/relationship_reindexer.rb +127 -0
- data/lib/samvera/nesting_indexer/repository_reindexer.rb +65 -0
- data/lib/samvera/nesting_indexer/version.rb +5 -0
- data/samvera-nesting_indexer.gemspec +39 -0
- metadata +294 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module Samvera
|
2
|
+
module NestingIndexer
|
3
|
+
module Adapters
|
4
|
+
# @api public
|
5
|
+
# A module that defines the interface of methods required to interact with Samvera::NestingIndexer operations
|
6
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
7
|
+
module AbstractAdapter
|
8
|
+
# @api public
|
9
|
+
# @param id [String]
|
10
|
+
# @return Samvera::NestingIndexer::Document::PreservationDocument
|
11
|
+
def self.find_preservation_document_by(id:)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
# @api public
|
16
|
+
# @param id [String]
|
17
|
+
# @return Samvera::NestingIndexer::Documents::IndexDocument
|
18
|
+
def self.find_index_document_by(id:)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api public
|
23
|
+
# @yield Samvera::NestingIndexer::Document::PreservationDocument
|
24
|
+
def self.each_preservation_document(&block)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
# @api public
|
29
|
+
# @param document [Samvera::NestingIndexer::Documents::IndexDocument]
|
30
|
+
# @yield Samvera::NestingIndexer::Documents::IndexDocument
|
31
|
+
def self.each_child_document_of(document:, &block)
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api public
|
36
|
+
# @return Hash - the attributes written to the indexing layer
|
37
|
+
def self.write_document_attributes_to_index_layer(attributes = {})
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'samvera/nesting_indexer/adapters/abstract_adapter'
|
2
|
+
require 'samvera/nesting_indexer/documents'
|
3
|
+
|
4
|
+
module Samvera
|
5
|
+
module NestingIndexer
|
6
|
+
module Adapters
|
7
|
+
# @api public
|
8
|
+
#
|
9
|
+
# Defines the interface for interacting with the InMemory layer. It is a reference
|
10
|
+
# implementation that is used throughout tests.
|
11
|
+
module InMemoryAdapter
|
12
|
+
extend AbstractAdapter
|
13
|
+
# @api public
|
14
|
+
# @param id [String]
|
15
|
+
# @return Samvera::NestingIndexer::Document::PreservationDocument
|
16
|
+
def self.find_preservation_document_by(id:)
|
17
|
+
Preservation.find(id)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api public
|
21
|
+
# @param id [String]
|
22
|
+
# @return Samvera::NestingIndexer::Documents::IndexDocument
|
23
|
+
def self.find_index_document_by(id:)
|
24
|
+
Index.find(id)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api public
|
28
|
+
# @yield Samvera::NestingIndexer::Document::PreservationDocument
|
29
|
+
def self.each_preservation_document(&block)
|
30
|
+
Preservation.find_each { |document| yield(document) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api public
|
34
|
+
# @param document [Samvera::NestingIndexer::Documents::IndexDocument]
|
35
|
+
# @yield Samvera::NestingIndexer::Documents::IndexDocument
|
36
|
+
def self.each_child_document_of(document:, &block)
|
37
|
+
Index.each_child_document_of(document: document, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @api public
|
41
|
+
# This is not something that I envision using in the production environment;
|
42
|
+
# It is hear to keep the Preservation system isolated and accessible only through interfaces.
|
43
|
+
# @return Samvera::NestingIndexer::Documents::PreservationDocument
|
44
|
+
def self.write_document_attributes_to_preservation_layer(attributes = {})
|
45
|
+
Preservation.write_document(attributes)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @api public
|
49
|
+
# @return Hash - the attributes written to the indexing layer
|
50
|
+
def self.write_document_attributes_to_index_layer(attributes = {})
|
51
|
+
Index.write_document(attributes)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def self.clear_cache!
|
56
|
+
Preservation.clear_cache!
|
57
|
+
Index.clear_cache!
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
#
|
62
|
+
# A module mixin to expose rudimentary read/write capabilities
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# module Foo
|
66
|
+
# extend Samvera::NestingIndexer::StorageModule
|
67
|
+
# end
|
68
|
+
module StorageModule
|
69
|
+
def write(doc)
|
70
|
+
cache[doc.id] = doc
|
71
|
+
end
|
72
|
+
|
73
|
+
def find(id)
|
74
|
+
cache.fetch(id.to_s)
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_each
|
78
|
+
cache.each { |_key, document| yield(document) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def clear_cache!
|
82
|
+
@cache = {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def cache
|
86
|
+
@cache ||= {}
|
87
|
+
end
|
88
|
+
private :cache
|
89
|
+
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
#
|
93
|
+
# A module responsible for containing the "preservation interface" logic.
|
94
|
+
# In the case of CurateND, there will need to be an adapter to get a Fedora
|
95
|
+
# object coerced into a Samvera::NestingIndexer::Preservation::Document
|
96
|
+
module Preservation
|
97
|
+
def self.find(id, *)
|
98
|
+
MemoryStorage.find(id)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.find_each(*, &block)
|
102
|
+
MemoryStorage.find_each(&block)
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.clear_cache!
|
106
|
+
MemoryStorage.clear_cache!
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.write_document(attributes = {})
|
110
|
+
Documents::PreservationDocument.new(attributes).tap do |doc|
|
111
|
+
MemoryStorage.write(doc)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# :nodoc:
|
116
|
+
module MemoryStorage
|
117
|
+
extend StorageModule
|
118
|
+
end
|
119
|
+
private_constant :MemoryStorage
|
120
|
+
end
|
121
|
+
private_constant :Preservation
|
122
|
+
|
123
|
+
# @api private
|
124
|
+
#
|
125
|
+
# An abstract representation of the underlying index service. In the case of
|
126
|
+
# CurateND this is an abstraction of Solr.
|
127
|
+
module Index
|
128
|
+
def self.clear_cache!
|
129
|
+
Storage.clear_cache!
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.find(id)
|
133
|
+
Storage.find(id)
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.each_child_document_of(document:, &block)
|
137
|
+
Storage.find_children_of_id(document.id).each(&block)
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.write_document(attributes = {})
|
141
|
+
Documents::IndexDocument.new(attributes).tap { |doc| Storage.write(doc) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# :nodoc:
|
145
|
+
module Storage
|
146
|
+
extend StorageModule
|
147
|
+
def self.find_children_of_id(id)
|
148
|
+
cache.values.select { |document| document.parent_ids.include?(id) }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
private_constant :Storage
|
152
|
+
end
|
153
|
+
private_constant :Index
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
if defined?(RSpec)
|
2
|
+
RSpec.shared_examples 'a Samvera::NestingIndexer::Adapter' do
|
3
|
+
let(:required_parameters_extractor) { ->(method) { method.parameters.select { |type, kwarg| type == :keyreq }.map(&:last) } }
|
4
|
+
let(:block_parameter_extracter) { ->(method) { method.parameters.select { |type, kwarg| type == :block }.map(&:last) } }
|
5
|
+
|
6
|
+
describe '.find_preservation_document_by' do
|
7
|
+
subject { described_class.method(:find_preservation_document_by) }
|
8
|
+
|
9
|
+
it 'requires the :id keyword (and does not require any others)' do
|
10
|
+
expect(required_parameters_extractor.call(subject)).to eq([:id])
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'does not expect a block' do
|
14
|
+
expect(block_parameter_extracter.call(subject)).to be_empty
|
15
|
+
end
|
16
|
+
end
|
17
|
+
describe '.find_index_document_by' do
|
18
|
+
subject { described_class.method(:find_index_document_by) }
|
19
|
+
|
20
|
+
it 'requires the :id keyword (and does not require any others)' do
|
21
|
+
expect(required_parameters_extractor.call(subject)).to eq([:id])
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'does not expect a block' do
|
25
|
+
expect(block_parameter_extracter.call(subject)).to be_empty
|
26
|
+
end
|
27
|
+
end
|
28
|
+
describe '.each_preservation_document' do
|
29
|
+
subject { described_class.method(:each_preservation_document) }
|
30
|
+
|
31
|
+
it 'requires no keywords' do
|
32
|
+
expect(required_parameters_extractor.call(subject)).to eq([])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'expects a block' do
|
36
|
+
expect(block_parameter_extracter.call(subject)).to be_present
|
37
|
+
end
|
38
|
+
end
|
39
|
+
describe '.each_child_document_of' do
|
40
|
+
subject { described_class.method(:each_child_document_of) }
|
41
|
+
|
42
|
+
it 'requires the :document keyword (and does not require any others)' do
|
43
|
+
expect(required_parameters_extractor.call(subject)).to eq([:document])
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'expects a block' do
|
47
|
+
expect(block_parameter_extracter.call(subject)).to be_present
|
48
|
+
end
|
49
|
+
end
|
50
|
+
describe '.write_document_attributes_to_index_layer' do
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'samvera/nesting_indexer/adapters/abstract_adapter'
|
2
|
+
require 'samvera/nesting_indexer/exceptions'
|
3
|
+
|
4
|
+
module Samvera
|
5
|
+
# :nodoc:
|
6
|
+
module NestingIndexer
|
7
|
+
# @api public
|
8
|
+
# Responsible for the configuration of the Samvera::NestingIndexer
|
9
|
+
class Configuration
|
10
|
+
DEFAULT_MAXIMUM_NESTING_DEPTH = 15
|
11
|
+
|
12
|
+
def initialize(adapter: default_adapter, maximum_nesting_depth: DEFAULT_MAXIMUM_NESTING_DEPTH)
|
13
|
+
self.adapter = adapter
|
14
|
+
self.maximum_nesting_depth = maximum_nesting_depth
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :maximum_nesting_depth
|
18
|
+
|
19
|
+
def maximum_nesting_depth=(input)
|
20
|
+
@maximum_nesting_depth = input.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api public
|
24
|
+
# @return Samvera::NestingIndexer::Adapters::AbstractAdapter
|
25
|
+
def adapter
|
26
|
+
@adapter || default_adapter
|
27
|
+
end
|
28
|
+
|
29
|
+
# @raise AdapterConfigurationError if the given adapter does not implement the correct interface
|
30
|
+
def adapter=(object)
|
31
|
+
object_methods = object.methods
|
32
|
+
adapter_methods = Adapters::AbstractAdapter.methods(false)
|
33
|
+
# Making sure that the adapter methods are all available in the object_methods
|
34
|
+
raise Exceptions::AdapterConfigurationError.new(object, adapter_methods) unless adapter_methods & object_methods == adapter_methods
|
35
|
+
@adapter = object
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
IN_MEMORY_ADAPTER_WARNING_MESSAGE =
|
41
|
+
"WARNING: You are using the default Samvera::NestingIndexer::Adapters::InMemoryAdapter for the Samvera::NestingIndexer.adapter.".freeze
|
42
|
+
|
43
|
+
def default_adapter
|
44
|
+
$stdout.puts IN_MEMORY_ADAPTER_WARNING_MESSAGE unless defined?(SUPPRESS_MEMORY_ADAPTER_WARNING)
|
45
|
+
require 'samvera/nesting_indexer/adapters/in_memory_adapter'
|
46
|
+
Adapters::InMemoryAdapter
|
47
|
+
end
|
48
|
+
end
|
49
|
+
private_constant :Configuration
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'dry-equalizer'
|
2
|
+
|
3
|
+
module Samvera
|
4
|
+
module NestingIndexer
|
5
|
+
module Documents
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
# A simplified document that reflects the necessary attributes for re-indexing
|
9
|
+
# the children of Fedora objects.
|
10
|
+
class PreservationDocument
|
11
|
+
def initialize(keywords = {})
|
12
|
+
@id = keywords.fetch(:id).to_s
|
13
|
+
@parent_ids = Array(keywords.fetch(:parent_ids))
|
14
|
+
end
|
15
|
+
|
16
|
+
# @api public
|
17
|
+
# @return String The Fedora object's PID
|
18
|
+
attr_reader :id
|
19
|
+
|
20
|
+
# @api public
|
21
|
+
#
|
22
|
+
# All of the direct parents of the Fedora document associated with the given PID.
|
23
|
+
#
|
24
|
+
# This does not include grandparents, great-grandparents, etc.
|
25
|
+
# @return Array<String>
|
26
|
+
attr_reader :parent_ids
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api public
|
30
|
+
#
|
31
|
+
# A rudimentary representation of what is needed to reindex Solr documents
|
32
|
+
class IndexDocument
|
33
|
+
# A quick and dirty means of doing comparative logic
|
34
|
+
include Dry::Equalizer(:id, :sorted_parent_ids, :sorted_pathnames, :sorted_ancestors)
|
35
|
+
|
36
|
+
def initialize(keywords = {})
|
37
|
+
@id = keywords.fetch(:id).to_s
|
38
|
+
@parent_ids = Array(keywords.fetch(:parent_ids))
|
39
|
+
@pathnames = Array(keywords.fetch(:pathnames))
|
40
|
+
@ancestors = Array(keywords.fetch(:ancestors))
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api public
|
44
|
+
# @return String The Fedora object's PID
|
45
|
+
attr_reader :id
|
46
|
+
|
47
|
+
# @api public
|
48
|
+
#
|
49
|
+
# All of the direct parents of the Fedora document associated with the given PID.
|
50
|
+
#
|
51
|
+
# This does not include grandparents, great-grandparents, etc.
|
52
|
+
# @return Array<String>
|
53
|
+
attr_reader :parent_ids
|
54
|
+
|
55
|
+
# @api public
|
56
|
+
#
|
57
|
+
# All nodes in the graph are addressable by one or more pathnames.
|
58
|
+
#
|
59
|
+
# If I have A, with parent B, and B has parents C and D, we have the
|
60
|
+
# following pathnames:
|
61
|
+
# [D/B/A, C/B/A]
|
62
|
+
#
|
63
|
+
# In the graph representation, we can get to A by going from D to B to A, or by going from C to B to A.
|
64
|
+
# @return Array<String>
|
65
|
+
attr_reader :pathnames
|
66
|
+
|
67
|
+
# @api public
|
68
|
+
#
|
69
|
+
# All of the :pathnames of each of the documents ancestors. If I have A, with parent B, and B has
|
70
|
+
# parents C and D then we have the following ancestors:
|
71
|
+
# [D/B], [C/B]
|
72
|
+
#
|
73
|
+
# @return Array<String>
|
74
|
+
attr_reader :ancestors
|
75
|
+
|
76
|
+
def sorted_parent_ids
|
77
|
+
parent_ids.sort
|
78
|
+
end
|
79
|
+
|
80
|
+
def sorted_pathnames
|
81
|
+
pathnames.sort
|
82
|
+
end
|
83
|
+
|
84
|
+
def sorted_ancestors
|
85
|
+
ancestors.sort
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Samvera
|
2
|
+
module NestingIndexer
|
3
|
+
module Exceptions
|
4
|
+
class RuntimeError < ::RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Raised when we have a misconfigured adapter
|
8
|
+
class AdapterConfigurationError < RuntimeError
|
9
|
+
attr_reader :expected_methods
|
10
|
+
def initialize(object, expected_methods)
|
11
|
+
@expected_methods = expected_methods
|
12
|
+
super "Expected #{object.inspect} to implement #{expected_methods.inspect} methods"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Raised when we may have detected a cycle within the graph
|
17
|
+
class CycleDetectionError < RuntimeError
|
18
|
+
attr_reader :id
|
19
|
+
def initialize(id)
|
20
|
+
@id = id
|
21
|
+
super "Possible graph cycle discovered related to PID=#{id}."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
# A wrapper exception that includes the original exception and the id
|
25
|
+
class ReindexingError < RuntimeError
|
26
|
+
attr_reader :id, :original_exception
|
27
|
+
def initialize(id, original_exception)
|
28
|
+
@id = id
|
29
|
+
@original_exception = original_exception
|
30
|
+
super "Error PID=#{id} - #{original_exception}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|