samvera-nesting_indexer 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|