speedy-af 0.1.1
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 +4 -0
- data/.rubocop.yml +178 -0
- data/.rubocop_todo.yml +55 -0
- data/.travis.yml +15 -0
- data/CONTRIBUTING.md +113 -0
- data/Gemfile +5 -0
- data/LICENSE +12 -0
- data/README.md +100 -0
- data/Rakefile +46 -0
- data/lib/speedy-af.rb +1 -0
- data/lib/speedy_af.rb +7 -0
- data/lib/speedy_af/base.rb +214 -0
- data/lib/speedy_af/indexed_content.rb +34 -0
- data/lib/speedy_af/ordered_aggregation_index.rb +26 -0
- data/lib/speedy_af/version.rb +3 -0
- data/spec/fixture_classes.rb +48 -0
- data/spec/integration/base_spec.rb +169 -0
- data/spec/integration/ordered_aggregation_index_spec.rb +40 -0
- data/spec/spec_helper.rb +36 -0
- data/speedy-af.gemspec +39 -0
- metadata +234 -0
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'solr_wrapper'
|
2
|
+
require 'fcrepo_wrapper'
|
3
|
+
require 'active_fedora/rake_support'
|
4
|
+
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
desc 'Run tests only'
|
7
|
+
RSpec::Core::RakeTask.new(:rspec) do |spec|
|
8
|
+
spec.rspec_opts = ['--backtrace'] if ENV['CI']
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rubocop/rake_task'
|
12
|
+
desc 'Run style checker'
|
13
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
14
|
+
task.requires << 'rubocop-rspec'
|
15
|
+
task.fail_on_error = true
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
19
|
+
spec.rcov = true
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "CI build"
|
23
|
+
task :ci do
|
24
|
+
Rake::Task['rubocop'].invoke unless ENV['NO_RUBOCOP']
|
25
|
+
ENV['environment'] = "test"
|
26
|
+
with_test_server do
|
27
|
+
Rake::Task[:coverage].invoke
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Execute specs with coverage"
|
32
|
+
task :coverage do
|
33
|
+
# Put spec opts in a file named .rspec in root
|
34
|
+
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
|
35
|
+
ENV['COVERAGE'] = 'true' unless ruby_engine == 'jruby'
|
36
|
+
Rake::Task[:spec].invoke
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Execute specs with coverage"
|
40
|
+
task :spec do
|
41
|
+
with_test_server do
|
42
|
+
Rake::Task[:rspec].invoke
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
task default: :ci
|
data/lib/speedy-af.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'speedy_af'
|
data/lib/speedy_af.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module SpeedyAF
|
4
|
+
class Base
|
5
|
+
class NotAvailable < RuntimeError; end
|
6
|
+
|
7
|
+
SOLR_ALL = 10_000_000
|
8
|
+
|
9
|
+
attr_reader :attrs, :model
|
10
|
+
|
11
|
+
def self.defaults
|
12
|
+
@defaults ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.defaults=(value)
|
16
|
+
raise ArgumentError unless value.respond_to?(:merge)
|
17
|
+
@defaults = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.proxy_class_for(model)
|
21
|
+
klass = "::SpeedyAF::Proxy::#{model.name}".safe_constantize
|
22
|
+
if klass.nil?
|
23
|
+
namespace = model.name.deconstantize
|
24
|
+
name = model.name.demodulize
|
25
|
+
klass_module = namespace.split(/::/).inject(::SpeedyAF::Proxy) do |mod, ns|
|
26
|
+
mod.const_defined?(ns, false) ? mod.const_get(ns, false) : mod.const_set(ns, Module.new)
|
27
|
+
end
|
28
|
+
klass = klass_module.const_set(name, Class.new(self))
|
29
|
+
end
|
30
|
+
klass
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.config(model, &block)
|
34
|
+
proxy_class = proxy_class_for(model) { Class.new(self) }
|
35
|
+
proxy_class.class_eval(&block) if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find(id, opts = {})
|
39
|
+
where(%(id:"#{id}"), opts).first
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.where(query, opts = {})
|
43
|
+
docs = ActiveFedora::SolrService.query(query, rows: SOLR_ALL)
|
44
|
+
from(docs, opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.from(docs, opts = {})
|
48
|
+
hash = docs.each_with_object({}) do |doc, h|
|
49
|
+
proxy = proxy_class_for(model_for(doc))
|
50
|
+
h[doc['id']] = proxy.new(doc, opts[:defaults])
|
51
|
+
end
|
52
|
+
return hash.values if opts[:order].nil?
|
53
|
+
opts[:order].call.collect { |id| hash[id] }.to_a
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.model_for(solr_document)
|
57
|
+
solr_document[:has_model_ssim].first.safe_constantize
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(solr_document, instance_defaults = {})
|
61
|
+
instance_defaults ||= {}
|
62
|
+
@model = Base.model_for(solr_document)
|
63
|
+
@attrs = self.class.defaults.merge(instance_defaults)
|
64
|
+
solr_document.each_pair do |k, v|
|
65
|
+
attr_name, value = parse_solr_field(k, v)
|
66
|
+
@attrs[attr_name.to_sym] = value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def real_object
|
71
|
+
if @real_object.nil?
|
72
|
+
@real_object = model.find(id)
|
73
|
+
@attrs.clear
|
74
|
+
end
|
75
|
+
@real_object
|
76
|
+
end
|
77
|
+
|
78
|
+
def real?
|
79
|
+
!@real_object.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
def reload
|
83
|
+
dup = Base.find(id)
|
84
|
+
@attrs = dup.attrs
|
85
|
+
@model = dup.model
|
86
|
+
@real_object = nil
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_query(key)
|
91
|
+
"#{key}=#{id}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def respond_to_missing?(sym, _include_private = false)
|
95
|
+
@attrs.key?(sym) ||
|
96
|
+
model.reflections[sym].present? ||
|
97
|
+
model.instance_methods.include?(sym)
|
98
|
+
end
|
99
|
+
|
100
|
+
def method_missing(sym, *args)
|
101
|
+
return real_object.send(sym, *args) if real?
|
102
|
+
|
103
|
+
return @attrs[sym] if @attrs.key?(sym)
|
104
|
+
|
105
|
+
reflection = reflection_for(sym)
|
106
|
+
unless reflection.nil?
|
107
|
+
begin
|
108
|
+
return load_from_reflection(reflection, sym.to_s =~ /_ids?$/)
|
109
|
+
rescue NotAvailable => e
|
110
|
+
ActiveFedora::Base.logger.warn(e.message)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if model.instance_methods.include?(sym)
|
115
|
+
ActiveFedora::Base.logger.warn("Reifying #{model} because #{sym} called from #{caller.first}")
|
116
|
+
return real_object.send(sym, *args)
|
117
|
+
end
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
|
123
|
+
def reflection_for(sym)
|
124
|
+
return nil unless model.respond_to?(:reflections)
|
125
|
+
reflection_name = sym.to_s.sub(/_id(s?)$/, '\1').to_sym
|
126
|
+
model.reflections[reflection_name] || model.reflections[:"#{reflection_name.to_s.singularize}_proxies"]
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_solr_field(k, v)
|
130
|
+
# :nocov:
|
131
|
+
transforms = {
|
132
|
+
'dt' => ->(m) { Time.parse(m) },
|
133
|
+
'b' => ->(m) { m },
|
134
|
+
'db' => ->(m) { m.to_f },
|
135
|
+
'f' => ->(m) { m.to_f },
|
136
|
+
'i' => ->(m) { m.to_i },
|
137
|
+
'l' => ->(m) { m.to_i },
|
138
|
+
nil => ->(m) { m }
|
139
|
+
}
|
140
|
+
# :nocov:
|
141
|
+
attr_name, type, _stored, _indexed, _multi = k.scan(/^(.+)_(.+)(s)(i?)(m?)$/).first
|
142
|
+
return [k, v] if attr_name.nil?
|
143
|
+
value = Array(v).map { |m| transforms.fetch(type, transforms[nil]).call(m) }
|
144
|
+
value = value.first unless @model.respond_to?(:properties) && multiple?(@model.properties[attr_name])
|
145
|
+
[attr_name, value]
|
146
|
+
end
|
147
|
+
|
148
|
+
def multiple?(prop)
|
149
|
+
prop.present? && prop.respond_to?(:multiple?) && prop.multiple?
|
150
|
+
end
|
151
|
+
|
152
|
+
def load_from_reflection(reflection, ids_only = false)
|
153
|
+
if reflection.options.key?(:through)
|
154
|
+
return load_through_reflection(reflection, ids_only)
|
155
|
+
end
|
156
|
+
if reflection.belongs_to? && reflection.respond_to?(:predicate_for_solr)
|
157
|
+
return load_belongs_to_reflection(reflection.predicate_for_solr, ids_only)
|
158
|
+
end
|
159
|
+
if reflection.has_many? && reflection.respond_to?(:predicate_for_solr)
|
160
|
+
return load_has_many_reflection(reflection.predicate_for_solr, ids_only)
|
161
|
+
end
|
162
|
+
if reflection.is_a?(ActiveFedora::Reflection::HasSubresourceReflection)
|
163
|
+
return load_subresource_content(reflection)
|
164
|
+
end
|
165
|
+
# :nocov:
|
166
|
+
raise NotAvailable, "`#{reflection.name}' cannot be quick-loaded. Falling back to model."
|
167
|
+
# :nocov:
|
168
|
+
end
|
169
|
+
|
170
|
+
def load_through_reflection(reflection, ids_only = false)
|
171
|
+
ids = case reflection.options[:through]
|
172
|
+
when 'ActiveFedora::Aggregation::Proxy' then proxy_ids(reflection)
|
173
|
+
else subresource_ids(reflection)
|
174
|
+
end
|
175
|
+
return ids if ids_only
|
176
|
+
query = ActiveFedora::SolrQueryBuilder.construct_query_for_ids(ids)
|
177
|
+
Base.where(query, order: -> { ids })
|
178
|
+
end
|
179
|
+
|
180
|
+
def proxy_ids(reflection)
|
181
|
+
docs = ActiveFedora::SolrService.query %(id:#{id}/#{reflection.name}/*), rows: SOLR_ALL
|
182
|
+
docs.collect { |doc| doc['proxyFor_ssim'] }.flatten
|
183
|
+
end
|
184
|
+
|
185
|
+
def subresource_ids(reflection)
|
186
|
+
subresource = reflection.options[:through]
|
187
|
+
docs = ActiveFedora::SolrService.query %(id:"#{id}/#{subresource}"), rows: 1
|
188
|
+
return [] if docs.empty?
|
189
|
+
ids = docs.first['ordered_targets_ssim']
|
190
|
+
return [] if ids.nil?
|
191
|
+
ids
|
192
|
+
end
|
193
|
+
|
194
|
+
def load_belongs_to_reflection(predicate, ids_only = false)
|
195
|
+
id = @attrs[predicate.to_sym]
|
196
|
+
return id if ids_only
|
197
|
+
Base.find(id)
|
198
|
+
end
|
199
|
+
|
200
|
+
def load_has_many_reflection(predicate, ids_only = false)
|
201
|
+
query = %(#{predicate}_ssim:#{id})
|
202
|
+
return Base.where(query) unless ids_only
|
203
|
+
docs = ActiveFedora::SolrService.query query, rows: SOLR_ALL
|
204
|
+
docs.collect { |doc| doc['id'] }
|
205
|
+
end
|
206
|
+
|
207
|
+
def load_subresource_content(reflection)
|
208
|
+
subresource = reflection.name
|
209
|
+
docs = ActiveFedora::SolrService.query(%(id:"#{id}/#{subresource}"), rows: 1)
|
210
|
+
raise NotAvailable, "`#{subresource}' is not indexed" if docs.empty? || !docs.first.key?('has_model_ssim')
|
211
|
+
@attrs[subresource] = Base.from(docs).first
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SpeedyAF
|
2
|
+
module IndexedContent
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
MAX_CONTENT_SIZE = 8192
|
5
|
+
|
6
|
+
included do
|
7
|
+
after_save :update_external_index
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_solr(solr_doc = {}, opts = {})
|
11
|
+
return solr_doc unless opts[:external_index]
|
12
|
+
solr_doc.tap do |doc|
|
13
|
+
doc[:id] = id
|
14
|
+
doc[:has_model_ssim] = self.class.name
|
15
|
+
doc[:uri_ss] = uri.to_s
|
16
|
+
doc[:mime_type_ss] = mime_type
|
17
|
+
doc[:original_name_ss] = original_name
|
18
|
+
doc[:size_is] = content.present? ? content.size : 0
|
19
|
+
doc[:'empty?_bs'] = content.nil? || content.empty?
|
20
|
+
doc[:content_ss] = content if index_content?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_external_index
|
25
|
+
ActiveFedora::SolrService.add(to_solr({}, external_index: true), softCommit: true)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def index_content?
|
31
|
+
has_content? && mime_type =~ /(^text\/)|([\/\+]xml$)/ && size < MAX_CONTENT_SIZE && content !~ /\x00/
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SpeedyAF
|
2
|
+
module OrderedAggregationIndex
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def indexed_ordered_aggregation(name)
|
7
|
+
target_class = reflections[name].class_name
|
8
|
+
contains_key = reflections[:"ordered_#{name.to_s.singularize}_proxies"].options[:through]
|
9
|
+
mixin = generated_association_methods
|
10
|
+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
11
|
+
def indexed_#{name}
|
12
|
+
ids = self.indexed_#{name.to_s.singularize}_ids
|
13
|
+
ids.lazy.collect { |id| #{target_class}.find(id) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def indexed_#{name.to_s.singularize}_ids
|
17
|
+
return [] unless persisted?
|
18
|
+
docs = ActiveFedora::SolrService.query "id: \#{self.id}/#{contains_key}", rows: 1
|
19
|
+
return [] if docs.empty? or docs.first['ordered_targets_ssim'].nil?
|
20
|
+
docs.first['ordered_targets_ssim']
|
21
|
+
end
|
22
|
+
CODE
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class IndexedFile < ActiveFedora::File
|
2
|
+
include SpeedyAF::IndexedContent
|
3
|
+
end
|
4
|
+
|
5
|
+
class Chapter < ActiveFedora::Base
|
6
|
+
property :title, predicate: ::RDF::Vocab::DC.title, multiple: false do |index|
|
7
|
+
index.as :stored_searchable
|
8
|
+
end
|
9
|
+
property :contributor, predicate: ::RDF::Vocab::DC.contributor, multiple: true do |index|
|
10
|
+
index.as :stored_searchable
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module DowncaseBehavior
|
15
|
+
def lowercase_title
|
16
|
+
title.downcase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Book < ActiveFedora::Base
|
21
|
+
include DowncaseBehavior
|
22
|
+
include SpeedyAF::OrderedAggregationIndex
|
23
|
+
|
24
|
+
belongs_to :library, predicate: ::RDF::Vocab::DC.isPartOf
|
25
|
+
has_subresource 'indexed_file', class_name: 'IndexedFile'
|
26
|
+
has_subresource 'unindexed_file', class_name: 'ActiveFedora::File'
|
27
|
+
property :title, predicate: ::RDF::Vocab::DC.title, multiple: false do |index|
|
28
|
+
index.as :stored_searchable
|
29
|
+
end
|
30
|
+
property :publisher, predicate: ::RDF::Vocab::DC.publisher, multiple: false do |index|
|
31
|
+
index.as :stored_searchable
|
32
|
+
end
|
33
|
+
ordered_aggregation :chapters, through: :list_source
|
34
|
+
indexed_ordered_aggregation :chapters
|
35
|
+
|
36
|
+
def uppercase_title
|
37
|
+
title.upcase
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Library < ActiveFedora::Base
|
42
|
+
has_many :books, predicate: ::RDF::Vocab::DC.isPartOf
|
43
|
+
end
|
44
|
+
|
45
|
+
module SpeedySpecs
|
46
|
+
class DeepClass < ActiveFedora::Base
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rdf/vocab/dc'
|
3
|
+
|
4
|
+
describe SpeedyAF::Base do
|
5
|
+
before { load_fixture_classes! }
|
6
|
+
after { unload_fixture_classes! }
|
7
|
+
|
8
|
+
let!(:library) { Library.create }
|
9
|
+
let!(:book) { Book.new title: 'Ordered Things', publisher: 'ActiveFedora Performance LLC', library: library }
|
10
|
+
let!(:chapters) {[
|
11
|
+
Chapter.create(title: 'Chapter 3', contributor: ['Hopper', 'Lovelace', 'Johnson']),
|
12
|
+
Chapter.create(title: 'Chapter 1', contributor: ['Rogers', 'Johnson', 'Stark', 'Romanoff']),
|
13
|
+
Chapter.create(title: 'Chapter 2', contributor: ['Alice', 'Bob', 'Charlie'])
|
14
|
+
]}
|
15
|
+
let!(:indexed_content) {
|
16
|
+
<<-IPSUM
|
17
|
+
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering
|
18
|
+
animata corpora quaeritis. Summus brains sit, morbo vel maleficia? De apocalypsi gorger
|
19
|
+
omero undead survivor dictum mauris. Hi mindless mortuis soulless creaturas, imo evil
|
20
|
+
stalking monstra adventus resi dentevil vultus comedat cerebella viventium.
|
21
|
+
IPSUM
|
22
|
+
}
|
23
|
+
let!(:unindexed_content) {
|
24
|
+
<<-IPSUM
|
25
|
+
Qui animated corpse, cricket bat max brucks terribilem incessu zomby. The voodoo sacerdos
|
26
|
+
flesh eater, suscitat mortuos comedere carnem virus. Zonbi tattered for solum oculi eorum
|
27
|
+
defunctis go lum cerebro. Nescio brains an Undead zombies. Sicut malus putrid voodoo horror.
|
28
|
+
Nigh tofth eliv ingdead.
|
29
|
+
IPSUM
|
30
|
+
}
|
31
|
+
let(:book_presenter) { described_class.find(book.id) }
|
32
|
+
|
33
|
+
context 'lightweight presenter' do
|
34
|
+
before do
|
35
|
+
book.indexed_file.content = indexed_content
|
36
|
+
book.unindexed_file.content = unindexed_content
|
37
|
+
book.chapters = chapters
|
38
|
+
book.ordered_chapters = chapters.sort_by(&:title)
|
39
|
+
book.save!
|
40
|
+
end
|
41
|
+
|
42
|
+
it '#respond_to?' do
|
43
|
+
expect(book_presenter).to respond_to(:title)
|
44
|
+
expect(book_presenter).to respond_to(:chapters)
|
45
|
+
expect(book_presenter).to respond_to(:indexed_file)
|
46
|
+
expect(book_presenter).to respond_to(:unindexed_file)
|
47
|
+
expect { book_presenter.fthagn }.to raise_error(NoMethodError)
|
48
|
+
end
|
49
|
+
|
50
|
+
it '.find' do
|
51
|
+
expect(book_presenter).to be_a(described_class)
|
52
|
+
expect(book_presenter.publisher).to eq(book.publisher)
|
53
|
+
end
|
54
|
+
|
55
|
+
it '.where' do
|
56
|
+
chapter_presenter = described_class.where('contributor_tesim:"Johnson"')
|
57
|
+
expect(chapter_presenter.length).to eq(2)
|
58
|
+
end
|
59
|
+
|
60
|
+
it '.to_query' do
|
61
|
+
expect(book.to_query('book_id')).to eq("book_id=#{URI.encode(book.id, /[^\-_.!~*'()a-zA-Z\d;?:@&=+$,\[\]]/)}")
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'reflections' do
|
65
|
+
let!(:library_presenter) { described_class.find(library.id) }
|
66
|
+
|
67
|
+
it 'loads via indexed proxies' do
|
68
|
+
expect(book_presenter.chapter_ids).to match_array(book.chapter_ids)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'loads indexed targets' do
|
72
|
+
expect(book_presenter.ordered_chapter_ids).to eq(book.ordered_chapter_ids)
|
73
|
+
chapter_presenters = book_presenter.ordered_chapters
|
74
|
+
expect(chapter_presenters.length).to eq(chapters.length)
|
75
|
+
expect(chapter_presenters.all? { |cp| cp.is_a?(described_class) }).to be_truthy
|
76
|
+
expect(chapter_presenters.collect(&:title)).to eq(book.ordered_chapters.to_a.collect(&:title))
|
77
|
+
expect(book_presenter).not_to be_real
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'loads indexed subresources' do
|
81
|
+
ipsum_presenter = book_presenter.indexed_file
|
82
|
+
expect(ipsum_presenter.model).to eq(IndexedFile)
|
83
|
+
expect(ipsum_presenter.content).to eq(indexed_content)
|
84
|
+
expect(book_presenter).not_to be_real
|
85
|
+
expect(ipsum_presenter).not_to be_real
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'loads has_many reflections' do
|
89
|
+
library.books.create(title: 'Ordered Things II')
|
90
|
+
library.save
|
91
|
+
presenter = library_presenter.books
|
92
|
+
expect(presenter.length).to eq(2)
|
93
|
+
expect(presenter.all? { |bp| bp.is_a?(described_class) }).to be_truthy
|
94
|
+
expect(library_presenter.book_ids).to match_array(library.book_ids)
|
95
|
+
expect(library_presenter).not_to be_real
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'loads belongs_to reflections' do
|
99
|
+
expect(book_presenter.library_id).to eq(library.id)
|
100
|
+
expect(book_presenter.library).to be_a(described_class)
|
101
|
+
expect(book_presenter.library.model).to eq(library.class)
|
102
|
+
expect(book_presenter).not_to be_real
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'configuration' do
|
107
|
+
before do
|
108
|
+
described_class.config Book do
|
109
|
+
include DowncaseBehavior
|
110
|
+
self.defaults = { foo: 'bar!' }
|
111
|
+
end
|
112
|
+
|
113
|
+
described_class.config SpeedySpecs::DeepClass do
|
114
|
+
self.defaults = { baz: 'quux!' }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'adds default values' do
|
119
|
+
expect(book_presenter.foo).to eq('bar!')
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'mixes in the mixins' do
|
123
|
+
expect(book_presenter.lowercase_title).to eq(book.lowercase_title)
|
124
|
+
expect(book_presenter).not_to be_real
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'works with nested classes' do
|
128
|
+
expect(described_class.proxy_class_for(SpeedySpecs::DeepClass)).to eq(SpeedyAF::Proxy::SpeedySpecs::DeepClass)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'reification' do
|
133
|
+
it 'knows when it is real' do
|
134
|
+
expect(book_presenter).not_to be_real
|
135
|
+
expect(book_presenter.real_object).to be_a(Book)
|
136
|
+
expect(book_presenter).to be_real
|
137
|
+
end
|
138
|
+
|
139
|
+
it '#reload (Base)' do
|
140
|
+
expect(book_presenter.real_object).to be_a(Book)
|
141
|
+
book_presenter.reload
|
142
|
+
expect(book_presenter).not_to be_real
|
143
|
+
end
|
144
|
+
|
145
|
+
it '#reload (File)' do
|
146
|
+
ipsum_presenter = book_presenter.indexed_file
|
147
|
+
expect(ipsum_presenter.real_object).to be_a(IndexedFile)
|
148
|
+
ipsum_presenter.reload
|
149
|
+
expect(ipsum_presenter).not_to be_real
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'reifies when it has to' do
|
153
|
+
expect(book_presenter.uppercase_title).to eq(book.title.upcase)
|
154
|
+
expect(book_presenter).to be_real
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'reifies indexed subresources' do
|
158
|
+
ipsum_presenter = book_presenter.indexed_file
|
159
|
+
expect(ipsum_presenter.metadata).to be_a(ActiveFedora::WithMetadata::MetadataNode)
|
160
|
+
expect(ipsum_presenter).to be_real
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'loads unindexed subresources' do
|
164
|
+
expect(book_presenter.unindexed_file.content).to eq(unindexed_content)
|
165
|
+
expect(book_presenter).to be_real
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|