active-fedora 9.10.0.pre1 → 9.10.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/lib/active_fedora.rb +4 -0
- data/lib/active_fedora/associations/collection_association.rb +7 -7
- data/lib/active_fedora/attribute_assignment.rb +55 -0
- data/lib/active_fedora/attribute_methods.rb +149 -47
- data/lib/active_fedora/attribute_methods/read.rb +29 -44
- data/lib/active_fedora/attribute_methods/write.rb +16 -19
- data/lib/active_fedora/attributes.rb +5 -17
- data/lib/active_fedora/base.rb +2 -0
- data/lib/active_fedora/callbacks.rb +9 -9
- data/lib/active_fedora/common.rb +67 -0
- data/lib/active_fedora/core.rb +9 -22
- data/lib/active_fedora/errors.rb +29 -0
- data/lib/active_fedora/file.rb +5 -57
- data/lib/active_fedora/file_persistence.rb +27 -0
- data/lib/active_fedora/identifiable.rb +0 -5
- data/lib/active_fedora/indexing.rb +2 -2
- data/lib/active_fedora/inheritance.rb +34 -0
- data/lib/active_fedora/persistence.rb +22 -4
- data/lib/active_fedora/scoping.rb +80 -2
- data/lib/active_fedora/scoping/default.rb +6 -2
- data/lib/active_fedora/scoping/named.rb +141 -1
- data/lib/active_fedora/version.rb +1 -1
- data/spec/integration/collection_association_spec.rb +34 -3
- data/spec/integration/file_spec.rb +1 -1
- data/spec/integration/persistence_spec.rb +1 -1
- data/spec/unit/attributes_spec.rb +13 -7
- data/spec/unit/file_spec.rb +2 -2
- data/spec/unit/validations_spec.rb +1 -1
- metadata +6 -2
@@ -0,0 +1,27 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
module FilePersistence
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
include ActiveFedora::Persistence
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def _create_record(_options = {})
|
10
|
+
return false if content.nil?
|
11
|
+
ldp_source.content = content
|
12
|
+
ldp_source.create do |req|
|
13
|
+
req.headers.merge!(ldp_headers)
|
14
|
+
end
|
15
|
+
refresh
|
16
|
+
end
|
17
|
+
|
18
|
+
def _update_record(_options = {})
|
19
|
+
return true unless content_changed?
|
20
|
+
ldp_source.content = content
|
21
|
+
ldp_source.update do |req|
|
22
|
+
req.headers.merge!(ldp_headers)
|
23
|
+
end
|
24
|
+
refresh
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -59,14 +59,14 @@ module ActiveFedora
|
|
59
59
|
private
|
60
60
|
|
61
61
|
# index the record after it has been persisted to Fedora
|
62
|
-
def
|
62
|
+
def _create_record(options = {})
|
63
63
|
super
|
64
64
|
update_index if create_needs_index? && options.fetch(:update_index, true)
|
65
65
|
true
|
66
66
|
end
|
67
67
|
|
68
68
|
# index the record after it has been updated in Fedora
|
69
|
-
def
|
69
|
+
def _update_record(options = {})
|
70
70
|
super
|
71
71
|
update_index if update_needs_index? && options.fetch(:update_index, true)
|
72
72
|
true
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
module Inheritance
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
# Returns the class descending directly from ActiveFedora::Base, or
|
7
|
+
# an abstract class, if any, in the inheritance hierarchy.
|
8
|
+
#
|
9
|
+
# If A extends ActiveFedora::Base, A.base_class will return A. If B descends from A
|
10
|
+
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
11
|
+
#
|
12
|
+
# If B < A and C < B and if A is an abstract_class then both B.base_class
|
13
|
+
# and C.base_class would return B as the answer since A is an abstract_class.
|
14
|
+
def base_class
|
15
|
+
return File if self == File || superclass == File
|
16
|
+
|
17
|
+
unless self <= Base
|
18
|
+
raise ActiveFedoraError, "#{name} doesn't belong in a hierarchy descending from ActiveFedora"
|
19
|
+
end
|
20
|
+
|
21
|
+
if self == Base || superclass == Base || superclass.abstract_class?
|
22
|
+
self
|
23
|
+
else
|
24
|
+
superclass.base_class
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Abstract classes can't have default scopes.
|
29
|
+
def abstract_class?
|
30
|
+
self == Base
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -33,7 +33,7 @@ module ActiveFedora
|
|
33
33
|
|
34
34
|
# Pushes the object and all of its new or dirty attached files into Fedora
|
35
35
|
def update(attributes)
|
36
|
-
|
36
|
+
assign_attributes(attributes)
|
37
37
|
save
|
38
38
|
end
|
39
39
|
|
@@ -70,6 +70,17 @@ module ActiveFedora
|
|
70
70
|
delete(*opts)
|
71
71
|
end
|
72
72
|
|
73
|
+
# Deletes the record in the database and freezes this instance to reflect
|
74
|
+
# that no changes should be made (since they can't be persisted).
|
75
|
+
#
|
76
|
+
# There's a series of callbacks associated with #destroy!. If the
|
77
|
+
# <tt>before_destroy</tt> callback throws +:abort+ the action is cancelled
|
78
|
+
# and #destroy! raises ActiveFedora::RecordNotDestroyed.
|
79
|
+
# See ActiveFedora::Callbacks for further details.
|
80
|
+
def destroy!
|
81
|
+
destroy || _raise_record_not_destroyed
|
82
|
+
end
|
83
|
+
|
73
84
|
def eradicate
|
74
85
|
self.class.eradicate(id)
|
75
86
|
end
|
@@ -138,12 +149,12 @@ module ActiveFedora
|
|
138
149
|
|
139
150
|
def create_or_update(*args)
|
140
151
|
raise ReadOnlyRecord if readonly?
|
141
|
-
result = new_record? ?
|
152
|
+
result = new_record? ? _create_record(*args) : _update_record(*args)
|
142
153
|
result != false
|
143
154
|
end
|
144
155
|
|
145
156
|
# Deals with preparing new object to be saved to Fedora, then pushes it and its attached files into Fedora.
|
146
|
-
def
|
157
|
+
def _create_record(_options = {})
|
147
158
|
assign_rdf_subject
|
148
159
|
serialize_attached_files
|
149
160
|
@ldp_source = @ldp_source.create
|
@@ -152,13 +163,20 @@ module ActiveFedora
|
|
152
163
|
refresh
|
153
164
|
end
|
154
165
|
|
155
|
-
def
|
166
|
+
def _update_record(_options = {})
|
156
167
|
serialize_attached_files
|
157
168
|
execute_sparql_update
|
158
169
|
save_contained_resources
|
159
170
|
refresh
|
160
171
|
end
|
161
172
|
|
173
|
+
def _raise_record_not_destroyed
|
174
|
+
@_association_destroy_exception ||= nil
|
175
|
+
raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self)
|
176
|
+
ensure
|
177
|
+
@_association_destroy_exception = nil
|
178
|
+
end
|
179
|
+
|
162
180
|
def refresh
|
163
181
|
@ldp_source = build_ldp_resource(id)
|
164
182
|
@resource = nil
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require 'active_support/per_thread_registry'
|
2
|
+
|
1
3
|
module ActiveFedora
|
2
4
|
module Scoping
|
3
5
|
extend ActiveSupport::Concern
|
6
|
+
|
4
7
|
included do
|
5
8
|
include Default
|
6
9
|
include Named
|
@@ -8,11 +11,17 @@ module ActiveFedora
|
|
8
11
|
|
9
12
|
module ClassMethods
|
10
13
|
def current_scope #:nodoc:
|
11
|
-
|
14
|
+
ScopeRegistry.value_for(:current_scope, self)
|
12
15
|
end
|
13
16
|
|
14
17
|
def current_scope=(scope) #:nodoc:
|
15
|
-
|
18
|
+
ScopeRegistry.set_value_for(:current_scope, self, scope)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Collects attributes from scopes that should be applied when creating
|
22
|
+
# an AF instance for the particular class this is called on.
|
23
|
+
def scope_attributes # :nodoc:
|
24
|
+
all.scope_for_create
|
16
25
|
end
|
17
26
|
|
18
27
|
# Are there attributes associated with this scope?
|
@@ -20,5 +29,74 @@ module ActiveFedora
|
|
20
29
|
current_scope
|
21
30
|
end
|
22
31
|
end
|
32
|
+
|
33
|
+
def populate_with_current_scope_attributes # :nodoc:
|
34
|
+
return unless self.class.scope_attributes?
|
35
|
+
|
36
|
+
self.class.scope_attributes.each do |att, value|
|
37
|
+
send("#{att}=", value) if respond_to?("#{att}=")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize_internals_callback # :nodoc:
|
42
|
+
super
|
43
|
+
populate_with_current_scope_attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
# This class stores the +:current_scope+ and +:ignore_default_scope+ values
|
47
|
+
# for different classes. The registry is stored as a thread local, which is
|
48
|
+
# accessed through +ScopeRegistry.current+.
|
49
|
+
#
|
50
|
+
# This class allows you to store and get the scope values on different
|
51
|
+
# classes and different types of scopes. For example, if you are attempting
|
52
|
+
# to get the current_scope for the +Board+ model, then you would use the
|
53
|
+
# following code:
|
54
|
+
#
|
55
|
+
# registry = ActiveFedora::Scoping::ScopeRegistry
|
56
|
+
# registry.set_value_for(:current_scope, Board, some_new_scope)
|
57
|
+
#
|
58
|
+
# Now when you run:
|
59
|
+
#
|
60
|
+
# registry.value_for(:current_scope, Board)
|
61
|
+
#
|
62
|
+
# You will obtain whatever was defined in +some_new_scope+. The #value_for
|
63
|
+
# and #set_value_for methods are delegated to the current ScopeRegistry
|
64
|
+
# object, so the above example code can also be called as:
|
65
|
+
#
|
66
|
+
# ActiveFedora::Scoping::ScopeRegistry.set_value_for(:current_scope,
|
67
|
+
# Board, some_new_scope)
|
68
|
+
class ScopeRegistry # :nodoc:
|
69
|
+
extend ActiveSupport::PerThreadRegistry
|
70
|
+
|
71
|
+
VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope].freeze
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
@registry = Hash.new { |hash, key| hash[key] = {} }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Obtains the value for a given +scope_type+ and +model+.
|
78
|
+
def value_for(scope_type, model)
|
79
|
+
raise_invalid_scope_type!(scope_type)
|
80
|
+
klass = model
|
81
|
+
base = model.base_class
|
82
|
+
while klass <= base
|
83
|
+
value = @registry[scope_type][klass.name]
|
84
|
+
return value if value
|
85
|
+
klass = klass.superclass
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sets the +value+ for a given +scope_type+ and +model+.
|
90
|
+
def set_value_for(scope_type, model, value)
|
91
|
+
raise_invalid_scope_type!(scope_type)
|
92
|
+
@registry[scope_type][model.name] = value
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def raise_invalid_scope_type!(scope_type)
|
98
|
+
raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" unless VALID_SCOPE_TYPES.include?(scope_type)
|
99
|
+
end
|
100
|
+
end
|
23
101
|
end
|
24
102
|
end
|
@@ -46,6 +46,10 @@ module ActiveFedora
|
|
46
46
|
super || default_scopes.any? || respond_to?(:default_scope)
|
47
47
|
end
|
48
48
|
|
49
|
+
def before_remove_const #:nodoc:
|
50
|
+
self.current_scope = nil
|
51
|
+
end
|
52
|
+
|
49
53
|
protected
|
50
54
|
|
51
55
|
# Use this macro in your model to set a default scope for all operations on
|
@@ -125,11 +129,11 @@ module ActiveFedora
|
|
125
129
|
end
|
126
130
|
|
127
131
|
def ignore_default_scope? # :nodoc:
|
128
|
-
|
132
|
+
ScopeRegistry.value_for(:ignore_default_scope, base_class)
|
129
133
|
end
|
130
134
|
|
131
135
|
def ignore_default_scope=(ignore) # :nodoc:
|
132
|
-
|
136
|
+
ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore)
|
133
137
|
end
|
134
138
|
|
135
139
|
# The ignore_default_scope flag is used to prevent an infinite recursion
|
@@ -16,7 +16,7 @@ module ActiveFedora
|
|
16
16
|
# fruits = fruits.limit(10) if limited?
|
17
17
|
#
|
18
18
|
# You can define a scope that applies to all finders using
|
19
|
-
# <tt>
|
19
|
+
# <tt>ActiveFedora::Base.default_scope</tt>.
|
20
20
|
def all
|
21
21
|
if current_scope
|
22
22
|
current_scope.clone
|
@@ -34,6 +34,146 @@ module ActiveFedora
|
|
34
34
|
relation
|
35
35
|
end
|
36
36
|
end
|
37
|
+
|
38
|
+
# Adds a class method for retrieving and querying objects.
|
39
|
+
# The method is intended to return an ActiveFedora::Relation
|
40
|
+
# object, which is composable with other scopes.
|
41
|
+
# If it returns nil or false, an
|
42
|
+
# {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead.
|
43
|
+
#
|
44
|
+
# A \scope represents a narrowing of a database query, such as
|
45
|
+
# <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
|
46
|
+
#
|
47
|
+
# class Shirt < ActiveFedora::Base
|
48
|
+
# scope :red, -> { where(color: 'red') }
|
49
|
+
# scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# The above calls to #scope define class methods <tt>Shirt.red</tt> and
|
53
|
+
# <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
|
54
|
+
# represents the query <tt>Shirt.where(color: 'red')</tt>.
|
55
|
+
#
|
56
|
+
# You should always pass a callable object to the scopes defined
|
57
|
+
# with #scope. This ensures that the scope is re-evaluated each
|
58
|
+
# time it is called.
|
59
|
+
#
|
60
|
+
# Note that this is simply 'syntactic sugar' for defining an actual
|
61
|
+
# class method:
|
62
|
+
#
|
63
|
+
# class Shirt < ActiveFedora::Base
|
64
|
+
# def self.red
|
65
|
+
# where(color: 'red')
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
|
70
|
+
# <tt>Shirt.red</tt> is not an Array but an ActiveFedora::Relation,
|
71
|
+
# which is composable with other scopes; it resembles the association object
|
72
|
+
# constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
|
73
|
+
# declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
|
74
|
+
# <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
|
75
|
+
# association objects, named \scopes act like an Array, implementing
|
76
|
+
# Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
|
77
|
+
# and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
|
78
|
+
# <tt>Shirt.red</tt> really was an array.
|
79
|
+
#
|
80
|
+
# These named \scopes are composable. For instance,
|
81
|
+
# <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
|
82
|
+
# both red and dry clean only. Nested finds and calculations also work
|
83
|
+
# with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
|
84
|
+
# returns the number of garments for which these criteria obtain.
|
85
|
+
# Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
86
|
+
#
|
87
|
+
# All scopes are available as class methods on the ActiveFedora::Base
|
88
|
+
# descendant upon which the \scopes were defined. But they are also
|
89
|
+
# available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
|
90
|
+
# associations. If,
|
91
|
+
#
|
92
|
+
# class Person < ActiveFedora::Base
|
93
|
+
# has_many :shirts
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
|
97
|
+
# Elton's red, dry clean only shirts.
|
98
|
+
#
|
99
|
+
# \Named scopes can also have extensions, just as with
|
100
|
+
# {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations:
|
101
|
+
#
|
102
|
+
# class Shirt < ActiveFedora::Base
|
103
|
+
# scope :red, -> { where(color: 'red') } do
|
104
|
+
# def dom_id
|
105
|
+
# 'red_shirts'
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# Scopes can also be used while creating/building a record.
|
111
|
+
#
|
112
|
+
# class Article < ActiveFedora::Base
|
113
|
+
# scope :published, -> { where(published: true) }
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# Article.published.new.published # => true
|
117
|
+
# Article.published.create.published # => true
|
118
|
+
#
|
119
|
+
# \Class methods on your model are automatically available
|
120
|
+
# on scopes. Assuming the following setup:
|
121
|
+
#
|
122
|
+
# class Article < ActiveFedora::Base
|
123
|
+
# scope :published, -> { where(published: true) }
|
124
|
+
# scope :featured, -> { where(featured: true) }
|
125
|
+
#
|
126
|
+
# def self.latest_article
|
127
|
+
# order('published_at desc').first
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# def self.titles
|
131
|
+
# pluck(:title)
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# We are able to call the methods like this:
|
136
|
+
#
|
137
|
+
# Article.published.featured.latest_article
|
138
|
+
# Article.featured.titles
|
139
|
+
def scope(name, body, &block)
|
140
|
+
unless body.respond_to?(:call)
|
141
|
+
raise ArgumentError, 'The scope body needs to be callable.'
|
142
|
+
end
|
143
|
+
|
144
|
+
if dangerous_class_method?(name)
|
145
|
+
raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
|
146
|
+
"on the model \"#{self.name}\", but Active Record already defined " \
|
147
|
+
"a class method with the same name."
|
148
|
+
end
|
149
|
+
|
150
|
+
valid_scope_name?(name)
|
151
|
+
extension = Module.new(&block) if block
|
152
|
+
|
153
|
+
if body.respond_to?(:to_proc)
|
154
|
+
singleton_class.send(:define_method, name) do |*args|
|
155
|
+
scope = all.scoping { instance_exec(*args, &body) }
|
156
|
+
scope = scope.extending(extension) if extension
|
157
|
+
|
158
|
+
scope || all
|
159
|
+
end
|
160
|
+
else
|
161
|
+
singleton_class.send(:define_method, name) do |*args|
|
162
|
+
scope = all.scoping { body.call(*args) }
|
163
|
+
scope = scope.extending(extension) if extension
|
164
|
+
|
165
|
+
scope || all
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
protected
|
171
|
+
|
172
|
+
def valid_scope_name?(name)
|
173
|
+
return unless respond_to?(name, true)
|
174
|
+
logger.warn "Creating scope :#{name}. " \
|
175
|
+
"Overwriting existing method #{self.name}.#{name}."
|
176
|
+
end
|
37
177
|
end
|
38
178
|
end
|
39
179
|
end
|
@@ -11,8 +11,6 @@ describe ActiveFedora::Base do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
let(:library) { Library.create! }
|
14
|
-
let!(:book1) { Book.create!(library: library) }
|
15
|
-
let!(:book2) { Book.create!(library: library) }
|
16
14
|
|
17
15
|
after do
|
18
16
|
Object.send(:remove_const, :Library)
|
@@ -20,6 +18,9 @@ describe ActiveFedora::Base do
|
|
20
18
|
end
|
21
19
|
|
22
20
|
describe "load_from_solr" do
|
21
|
+
let!(:book1) { Book.create!(library: library) }
|
22
|
+
let!(:book2) { Book.create!(library: library) }
|
23
|
+
|
23
24
|
it "sets rows to count, if not specified" do
|
24
25
|
expect(library.books(response_format: :solr).size).to eq 2
|
25
26
|
end
|
@@ -35,6 +36,8 @@ describe ActiveFedora::Base do
|
|
35
36
|
end
|
36
37
|
|
37
38
|
describe "#delete_all" do
|
39
|
+
let!(:book1) { Book.create!(library: library) }
|
40
|
+
let!(:book2) { Book.create!(library: library) }
|
38
41
|
it "deletes em" do
|
39
42
|
expect {
|
40
43
|
library.books.delete_all
|
@@ -59,6 +62,8 @@ describe ActiveFedora::Base do
|
|
59
62
|
end
|
60
63
|
|
61
64
|
describe "#destroy_all" do
|
65
|
+
let!(:book1) { Book.create!(library: library) }
|
66
|
+
let!(:book2) { Book.create!(library: library) }
|
62
67
|
it "deletes em" do
|
63
68
|
expect {
|
64
69
|
library.books.destroy_all
|
@@ -67,6 +72,8 @@ describe ActiveFedora::Base do
|
|
67
72
|
end
|
68
73
|
|
69
74
|
describe "#find" do
|
75
|
+
let!(:book1) { Book.create!(library: library) }
|
76
|
+
let!(:book2) { Book.create!(library: library) }
|
70
77
|
it "finds the record that matches" do
|
71
78
|
expected = library.books.find(book1.id)
|
72
79
|
expect(expected).to eq book1
|
@@ -80,8 +87,11 @@ describe ActiveFedora::Base do
|
|
80
87
|
end
|
81
88
|
|
82
89
|
describe "#select" do
|
90
|
+
let!(:book1) { Book.create!(library: library) }
|
91
|
+
let!(:book2) { Book.create!(library: library) }
|
92
|
+
|
83
93
|
# TODO: Bug described in issue #609
|
84
|
-
xit "
|
94
|
+
xit "chooses a subset of objects in the relationship" do
|
85
95
|
expect(library.books.select([:id])).to include(book1.id)
|
86
96
|
end
|
87
97
|
it "works as a block" do
|
@@ -89,6 +99,27 @@ describe ActiveFedora::Base do
|
|
89
99
|
end
|
90
100
|
end
|
91
101
|
|
102
|
+
describe "#size" do
|
103
|
+
context "with associations in memory" do
|
104
|
+
context "and the association is already loaded" do
|
105
|
+
before do
|
106
|
+
library.books.to_a # force the association to be loaded
|
107
|
+
library.books.build
|
108
|
+
end
|
109
|
+
subject { library.books.size }
|
110
|
+
it { is_expected.to eq 1 }
|
111
|
+
end
|
112
|
+
|
113
|
+
context "and the association is not loaded" do
|
114
|
+
before do
|
115
|
+
library.books.build
|
116
|
+
end
|
117
|
+
subject { library.books.size }
|
118
|
+
it { is_expected.to eq 1 }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
92
123
|
describe "finding the inverse" do
|
93
124
|
context "when no inverse exists" do
|
94
125
|
before do
|