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.
@@ -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