speedy-af 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|