samvera-nesting_indexer 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ require 'rails/railtie'
2
+
3
+ module Samvera
4
+ module NestingIndexer
5
+ # Connect into the boot sequence of a Rails application
6
+ class Railtie < Rails::Railtie
7
+ config.to_prepare do
8
+ Samvera::NestingIndexer.configure!
9
+ end
10
+ end
11
+ end
12
+ end