speedy-af 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ed5f4d42ff93fc2fb41bbc8096025cd873767f3f01e9182ea87f309dbb7f6b0
4
- data.tar.gz: def94b1cb7b8f0dd8e8241d4e8dba0b1f80e849cb10ebe0346d788f18bf8b3b2
3
+ metadata.gz: 0fd6d0ac71c3da9c7807fe63bf98774a75abf4796130be797e19a9eba4ac6292
4
+ data.tar.gz: 549a1835b24011c7ebebef142654afb7c8ab0afdd0b70e39a713e7e1c9055dca
5
5
  SHA512:
6
- metadata.gz: 2607efe588e3b14c24712c27cfa847be6eec358abc77adbe0e47d0f68ac93e7a551184c4ac9b5486e3ba75b41989b85cf2d3a865fe00c8a0aed1460abb1b1839
7
- data.tar.gz: e79f5bb789b2527dbd728eae084ba42600d637a8284818eedbed2a7c2af2d1a7068b50830c35b6092d9f911c25df31dc49e2da2a60e0a934851200f050306b69
6
+ metadata.gz: 50e4f754a2f59937b4d08a3ccd9d460e64ecbba50d1e5eed821a28fcb77aab99f77a62307c9185269d27ac1f0baab58e72f8e56cc9debbab71bd99c2c54700e5
7
+ data.tar.gz: f0f1e0fd4e04f475085ada08769cf9617a95f6ddcd2b0955cea44adc91284844862d0c3c6729c9bc17c32ba456067e448d83c81be9899520003b5dea3b142ed1
data/.circleci/config.yml CHANGED
@@ -10,14 +10,14 @@ jobs:
10
10
  type: string
11
11
  bundler_version:
12
12
  type: string
13
- default: 2.0.2
13
+ default: 2.5.18
14
14
  ffmpeg_version:
15
15
  type: string
16
16
  default: 4.1.4
17
17
  executor:
18
18
  name: 'samvera/ruby_fcrepo_solr'
19
19
  ruby_version: << parameters.ruby_version >>
20
- solr_version: 7-alpine
20
+ solr_version: 8-slim
21
21
  environment:
22
22
  RAILS_VERSION: << parameters.rails_version >>
23
23
  working_directory: ~/project
@@ -38,10 +38,22 @@ workflows:
38
38
  ci:
39
39
  jobs:
40
40
  - bundle_and_test:
41
- name: "ruby2-7_rails6-0"
42
- ruby_version: "2.7.5"
43
- rails_version: "6.0.4.4"
41
+ name: "ruby3-3_rails7-2"
42
+ ruby_version: "3.3.4"
43
+ rails_version: "7.2.1"
44
44
  - bundle_and_test:
45
- name: "ruby2-7_rails5-2"
46
- ruby_version: "2.7.5"
47
- rails_version: "5.2.6"
45
+ name: "ruby3-2_rails7-1"
46
+ ruby_version: "3.2.5"
47
+ rails_version: "7.1.4"
48
+ - bundle_and_test:
49
+ name: "ruby3-1_rails7-1"
50
+ ruby_version: "3.1.6"
51
+ rails_version: "7.1.4"
52
+ - bundle_and_test:
53
+ name: "ruby3-2_rails7-0"
54
+ ruby_version: "3.2.5"
55
+ rails_version: "7.0.8.4"
56
+ - bundle_and_test:
57
+ name: "ruby3-1_rails7-0"
58
+ ruby_version: "3.1.6"
59
+ rails_version: "7.0.8.4"
data/.rubocop_todo.yml CHANGED
@@ -11,21 +11,34 @@
11
11
  # ExcludedMethods: refine
12
12
  Metrics/BlockLength:
13
13
  Max: 143
14
+ Exclude:
15
+ - 'spec/**/*_spec.rb'
14
16
 
15
17
  # Offense count: 1
16
18
  # Configuration parameters: CountComments.
17
19
  Metrics/ClassLength:
18
20
  Max: 165
21
+ Exclude:
22
+ - 'lib/speedy_af/base.rb'
19
23
 
20
24
  # Offense count: 1
21
25
  # Configuration parameters: IgnoredMethods.
22
26
  Metrics/CyclomaticComplexity:
23
- Max: 7
27
+ Max: 11
28
+
29
+ # Offense count: 1
30
+ # Configuration parameters: IgnoredMethods.
31
+ Metrics/PerceivedComplexity:
32
+ Max: 11
33
+
34
+ # Offense count: 1
35
+ Metrics/AbcSize:
36
+ Max: 40
24
37
 
25
38
  # Offense count: 1
26
39
  # Configuration parameters: CountComments, ExcludedMethods.
27
40
  Metrics/MethodLength:
28
- Max: 15
41
+ Max: 20
29
42
 
30
43
  # Offense count: 1
31
44
  # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, Regex, IgnoreExecutableScripts, AllowedAcronyms.
data/Rakefile CHANGED
@@ -5,6 +5,15 @@ require 'active_fedora/rake_support'
5
5
  require 'bundler'
6
6
  Bundler::GemHelper.install_tasks
7
7
 
8
+ # This is to fix a breaking change with Ruby 3.2 where some methods
9
+ # in fcrepo_wrapper use the removed `File.exists?` alias, preventing
10
+ # tests from being set up.
11
+ class File
12
+ class << self
13
+ alias exists? exist?
14
+ end
15
+ end
16
+
8
17
  require 'rspec/core/rake_task'
9
18
  desc 'Run tests only'
10
19
  RSpec::Core::RakeTask.new(:rspec) do |spec|
@@ -7,55 +7,154 @@ module SpeedyAF
7
7
 
8
8
  SOLR_ALL = 10_000_000
9
9
 
10
+ class_attribute :model_reflections, :reflection_predicates
11
+ SpeedyAF::Base.model_reflections = { belongs_to: {}, has_many: {}, subresource: {} }
12
+ SpeedyAF::Base.reflection_predicates = {}
13
+
10
14
  attr_reader :attrs, :model
11
15
 
12
- def self.defaults
13
- @defaults ||= {}
14
- end
16
+ class << self
17
+ def defaults
18
+ @defaults ||= {}
19
+ end
15
20
 
16
- def self.defaults=(value)
17
- raise ArgumentError unless value.respond_to?(:merge)
18
- @defaults = value
19
- end
21
+ def defaults=(value)
22
+ raise ArgumentError unless value.respond_to?(:merge)
23
+ @defaults = value
24
+ end
20
25
 
21
- def self.proxy_class_for(model)
22
- klass = "::SpeedyAF::Proxy::#{model.name}".safe_constantize
23
- if klass.nil?
24
- namespace = model.name.deconstantize
25
- name = model.name.demodulize
26
- klass_module = namespace.split(/::/).inject(::SpeedyAF::Proxy) do |mod, ns|
27
- mod.const_defined?(ns, false) ? mod.const_get(ns, false) : mod.const_set(ns, Module.new)
26
+ def proxy_class_for(model)
27
+ klass = "::SpeedyAF::Proxy::#{model.name}".safe_constantize
28
+ if klass.nil?
29
+ namespace = model.name.deconstantize
30
+ name = model.name.demodulize
31
+ klass_module = namespace.split(/::/).inject(::SpeedyAF::Proxy) do |mod, ns|
32
+ mod.const_defined?(ns, false) ? mod.const_get(ns, false) : mod.const_set(ns, Module.new)
33
+ end
34
+ klass = klass_module.const_set(name, Class.new(self))
28
35
  end
29
- klass = klass_module.const_set(name, Class.new(self))
36
+ klass
30
37
  end
31
- klass
32
- end
33
38
 
34
- def self.config(model, &block)
35
- proxy_class = proxy_class_for(model) { Class.new(self) }
36
- proxy_class.class_eval(&block) if block_given?
37
- end
39
+ def config(model, &block)
40
+ proxy_class = proxy_class_for(model) { Class.new(self) }
41
+ proxy_class.class_eval(&block) if block_given?
42
+ end
38
43
 
39
- def self.find(id, opts = {})
40
- where(%(id:"#{id}"), opts).first
41
- end
44
+ def find(id, opts = {})
45
+ where(%(id:"#{id}"), opts.merge(rows: 1)).first
46
+ end
42
47
 
43
- def self.where(query, opts = {})
44
- docs = ActiveFedora::SolrService.query(query, rows: SOLR_ALL)
45
- from(docs, opts)
46
- end
48
+ def where(query, opts = {})
49
+ docs = ActiveFedora::SolrService.query(query, rows: opts[:rows] || SOLR_ALL)
50
+ from(docs, opts)
51
+ end
47
52
 
48
- def self.from(docs, opts = {})
49
- hash = docs.each_with_object({}) do |doc, h|
53
+ def for(doc, opts = {})
50
54
  proxy = proxy_class_for(model_for(doc))
51
- h[doc['id']] = proxy.new(doc, opts[:defaults])
55
+ proxy.new(doc, opts[:defaults])
52
56
  end
53
- return hash.values if opts[:order].nil?
54
- opts[:order].call.collect { |id| hash[id] }.to_a
55
- end
56
57
 
57
- def self.model_for(solr_document)
58
- solr_document[:has_model_ssim].first.safe_constantize
58
+ def from(docs, opts = {})
59
+ hash = docs.each_with_object({}) do |doc, h|
60
+ h[doc['id']] = self.for(doc, opts)
61
+ end
62
+
63
+ if opts[:load_reflections]
64
+ reflections_hash = gather_reflections(hash, opts)
65
+ reflections_hash.each { |parent_id, reflections| hash[parent_id].attrs.merge!(reflections) }
66
+ end
67
+
68
+ return hash.values if opts[:order].nil?
69
+ opts[:order].call.collect { |id| hash[id] }.to_a
70
+ end
71
+
72
+ def model_for(solr_document)
73
+ solr_document[:has_model_ssim].first.safe_constantize
74
+ end
75
+
76
+ protected
77
+
78
+ def predicate_for_reflection(reflection)
79
+ SpeedyAF::Base.reflection_predicates[reflection.name] ||= reflection.predicate_for_solr
80
+ end
81
+
82
+ def gather_reflections(proxy_hash, opts)
83
+ query = [query_for_belongs_to(proxy_hash, opts), query_for_has_many(proxy_hash, opts), query_for_subresources(proxy_hash, opts)].reject(&:blank?).join(" OR ")
84
+ docs = ActiveFedora::SolrService.query query, rows: SOLR_ALL
85
+
86
+ reflections = {}
87
+ reflections.deep_merge!(gather_belongs_to(docs, proxy_hash, opts))
88
+ reflections.deep_merge!(gather_has_many(docs, proxy_hash, opts))
89
+ reflections.deep_merge!(gather_subresources(docs, proxy_hash, opts))
90
+ reflections
91
+ end
92
+
93
+ def query_for_belongs_to(proxy_hash, _opts)
94
+ proxy_hash.collect do |_id, proxy|
95
+ proxy.belongs_to_reflections.collect do |_name, reflection|
96
+ id = proxy.attrs[predicate_for_reflection(reflection).to_sym]
97
+ id.blank? ? nil : "id:#{id}"
98
+ end.compact
99
+ end.flatten.join(" OR ")
100
+ end
101
+
102
+ def gather_belongs_to(docs, proxy_hash, _opts)
103
+ hash = {}
104
+ proxy_hash.each do |proxy_id, proxy|
105
+ proxy.belongs_to_reflections.each do |name, reflection|
106
+ doc = docs.find { |d| d.id == proxy.attrs[predicate_for_reflection(reflection).to_sym] }
107
+ next unless doc
108
+ hash[proxy_id] ||= {}
109
+ hash[proxy_id][name] = doc
110
+ hash[proxy_id]["#{name}_id".to_sym] = doc.id
111
+ end
112
+ end
113
+ hash
114
+ end
115
+
116
+ def query_for_has_many(proxy_hash, _opts)
117
+ proxy_hash.collect do |id, proxy|
118
+ proxy.has_many_reflections.collect { |_name, reflection| "#{predicate_for_reflection(reflection)}_ssim:#{id}" }
119
+ end.flatten.join(" OR ")
120
+ end
121
+
122
+ def gather_has_many(docs, proxy_hash, _opts)
123
+ hash = {}
124
+ has_many_reflections = SpeedyAF::Base.model_reflections[:has_many].values.reduce(:merge)
125
+ docs.each do |doc|
126
+ has_many_reflections.each do |name, reflection|
127
+ Array(doc["#{predicate_for_reflection(reflection)}_ssim"]).each do |proxy_id|
128
+ next unless proxy_hash.key?(proxy_id)
129
+ hash[proxy_id] ||= {}
130
+ hash[proxy_id][name] ||= []
131
+ hash[proxy_id][name] << doc
132
+ hash[proxy_id]["#{name.to_s.singularize}_ids".to_sym] ||= []
133
+ hash[proxy_id]["#{name.to_s.singularize}_ids".to_sym] << doc.id
134
+ end
135
+ end
136
+ end
137
+ hash
138
+ end
139
+
140
+ def query_for_subresources(proxy_hash, _opts)
141
+ proxy_hash.collect do |id, proxy|
142
+ proxy.subresource_reflections.collect { |name, _reflection| "id:#{id}/#{name}" }
143
+ end.flatten.join(" OR ")
144
+ end
145
+
146
+ def gather_subresources(docs, proxy_hash, _opts)
147
+ docs.each_with_object({}) do |doc, hash|
148
+ doc_id = doc.id
149
+ parent_id = proxy_hash.keys.find { |id| doc_id.start_with? id }
150
+ next unless parent_id
151
+ subresource_id = proxy_hash[parent_id].subresource_reflections.keys.find { |name| doc_id == "#{parent_id}/#{name}" }
152
+ next unless subresource_id
153
+ hash[parent_id] ||= {}
154
+ hash[parent_id][subresource_id.to_sym] = doc
155
+ hash[parent_id]["#{subresource_id}_id".to_sym] = doc_id
156
+ end
157
+ end
59
158
  end
60
159
 
61
160
  def initialize(solr_document, instance_defaults = {})
@@ -101,7 +200,12 @@ module SpeedyAF
101
200
  def method_missing(sym, *args)
102
201
  return real_object.send(sym, *args) if real?
103
202
 
104
- return @attrs[sym] if @attrs.key?(sym)
203
+ if @attrs.key?(sym)
204
+ # Lazy convert the solr document into a speedy_af proxy object
205
+ @attrs[sym] = SpeedyAF::Base.for(@attrs[sym]) if @attrs[sym].is_a?(ActiveFedora::SolrHit)
206
+ @attrs[sym] = @attrs[sym].map { |doc| SpeedyAF::Base.for(doc) } if @attrs[sym].is_a?(Array) && @attrs[sym].all? { |d| d.is_a?(ActiveFedora::SolrHit) }
207
+ return @attrs[sym]
208
+ end
105
209
 
106
210
  reflection = reflection_for(sym)
107
211
  unless reflection.nil?
@@ -119,6 +223,20 @@ module SpeedyAF
119
223
  super
120
224
  end
121
225
 
226
+ def subresource_reflections
227
+ SpeedyAF::Base.model_reflections[:subresource][model] ||= model.reflections.select { |_name, reflection| reflection.is_a? ActiveFedora::Reflection::HasSubresourceReflection }
228
+ end
229
+
230
+ # rubocop:disable Naming/PredicateName
231
+ def has_many_reflections
232
+ SpeedyAF::Base.model_reflections[:has_many][model] ||= model.reflections.select { |_name, reflection| reflection.has_many? && reflection.respond_to?(:predicate_for_solr) }
233
+ end
234
+ # rubocop:enable Naming/PredicateName
235
+
236
+ def belongs_to_reflections
237
+ SpeedyAF::Base.model_reflections[:belongs_to][model] ||= model.reflections.select { |_name, reflection| reflection.belongs_to? && reflection.respond_to?(:predicate_for_solr) }
238
+ end
239
+
122
240
  protected
123
241
 
124
242
  def reflection_for(sym)
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module SpeedyAF
3
- VERSION = '0.2.0'
3
+ VERSION = '0.4.0'
4
4
  end
@@ -61,7 +61,7 @@ describe SpeedyAF::Base do
61
61
  end
62
62
 
63
63
  it '.to_query' do
64
- expect(book.to_query('book_id')).to eq("book_id=#{URI.encode(book.id, /[^\-_.!~*'()a-zA-Z\d;?:@&=+$,\[\]]/)}")
64
+ expect(book.to_query('book_id')).to eq("book_id=#{URI::Parser.new.escape(book.id, /[^\-_.!~*'()a-zA-Z\d;?:@&=+$,\[\]]/)}")
65
65
  end
66
66
 
67
67
  context 'reflections' do
@@ -104,6 +104,54 @@ describe SpeedyAF::Base do
104
104
  expect(book_presenter.library.model).to eq(library.class)
105
105
  expect(book_presenter).not_to be_real
106
106
  end
107
+
108
+ context 'missing parent id' do
109
+ let(:book) { Book.new title: 'Ordered Things', publisher: 'ActiveFedora Performance LLC', library: nil }
110
+ it 'does not cause belongs_to reflections to error' do
111
+ expect(book_presenter.library_id).to be_nil
112
+ expect(book_presenter.library).to be_nil
113
+ expect(book_presenter).not_to be_real
114
+ end
115
+ end
116
+
117
+ context 'preloaded subresources' do
118
+ let(:book_presenter) { described_class.find(book.id, load_reflections: true) }
119
+
120
+ it 'has already loaded indexed subresources' do
121
+ expect(book_presenter.attrs).to include :indexed_file
122
+ allow(ActiveFedora::SolrService).to receive(:query).and_call_original
123
+ ipsum_presenter = book_presenter.indexed_file
124
+ expect(ipsum_presenter.model).to eq(IndexedFile)
125
+ expect(ipsum_presenter.content).to eq(indexed_content)
126
+ expect(ipsum_presenter).not_to be_real
127
+ expect(ActiveFedora::SolrService).not_to have_received(:query)
128
+ end
129
+
130
+ it 'has already loaded has_many reflections' do
131
+ library.books.create(title: 'Ordered Things II')
132
+ library.save
133
+ book_ids = library.book_ids
134
+ library_presenter = described_class.find(library.id, load_reflections: true)
135
+ expect(library_presenter.attrs).to include :books
136
+ allow(ActiveFedora::SolrService).to receive(:query).and_call_original
137
+ presenter = library_presenter.books
138
+ expect(presenter.length).to eq(2)
139
+ expect(presenter.all? { |bp| bp.is_a?(described_class) }).to be_truthy
140
+ expect(library_presenter.book_ids).to match_array(book_ids)
141
+ expect(library_presenter).not_to be_real
142
+ expect(ActiveFedora::SolrService).not_to have_received(:query)
143
+ end
144
+
145
+ it 'has already loaded belongs_to reflections' do
146
+ expect(book_presenter.attrs).to include :library
147
+ allow(ActiveFedora::SolrService).to receive(:query).and_call_original
148
+ expect(book_presenter.library_id).to eq(library.id)
149
+ expect(book_presenter.library).to be_a(described_class)
150
+ expect(book_presenter.library.model).to eq(library.class)
151
+ expect(book_presenter).not_to be_real
152
+ expect(ActiveFedora::SolrService).not_to have_received(:query)
153
+ end
154
+ end
107
155
  end
108
156
 
109
157
  context 'configuration' do
data/speedy-af.gemspec CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
  s.test_files = `git ls-files -- {spec}/*`.split("\n")
25
25
 
26
26
  s.add_dependency 'active-fedora', '>= 11.0.0'
27
- s.add_dependency 'activesupport', '> 5.2', '< 6.1'
27
+ s.add_dependency 'activesupport', '> 5.2'
28
28
  s.add_development_dependency 'solr_wrapper'
29
29
  s.add_development_dependency 'fcrepo_wrapper'
30
30
  s.add_development_dependency 'simplecov'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: speedy-af
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael B. Klein
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-23 00:00:00.000000000 Z
11
+ date: 2024-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active-fedora
@@ -31,9 +31,6 @@ dependencies:
31
31
  - - ">"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '5.2'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '6.1'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
@@ -41,9 +38,6 @@ dependencies:
41
38
  - - ">"
42
39
  - !ruby/object:Gem::Version
43
40
  version: '5.2'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '6.1'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: solr_wrapper
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -232,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
232
226
  - !ruby/object:Gem::Version
233
227
  version: '0'
234
228
  requirements: []
235
- rubygems_version: 3.1.6
229
+ rubygems_version: 3.5.16
236
230
  signing_key:
237
231
  specification_version: 4
238
232
  summary: Performance enhancements for ActiveFedora