terrestrial 0.1.0 → 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 +4 -4
- data/.gitignore +1 -9
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +28 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/MissingFeatures.md +64 -0
- data/README.md +161 -16
- data/Rakefile +30 -0
- data/TODO.md +41 -0
- data/features/env.rb +60 -0
- data/features/example.feature +120 -0
- data/features/step_definitions/example_steps.rb +46 -0
- data/lib/terrestrial/abstract_record.rb +99 -0
- data/lib/terrestrial/association_loaders.rb +52 -0
- data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
- data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
- data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
- data/lib/terrestrial/dataset.rb +49 -0
- data/lib/terrestrial/deleted_record.rb +20 -0
- data/lib/terrestrial/dirty_map.rb +42 -0
- data/lib/terrestrial/graph_loader.rb +63 -0
- data/lib/terrestrial/graph_serializer.rb +91 -0
- data/lib/terrestrial/identity_map.rb +22 -0
- data/lib/terrestrial/lazy_collection.rb +74 -0
- data/lib/terrestrial/lazy_object_proxy.rb +55 -0
- data/lib/terrestrial/many_to_many_association.rb +138 -0
- data/lib/terrestrial/many_to_one_association.rb +66 -0
- data/lib/terrestrial/mapper_facade.rb +137 -0
- data/lib/terrestrial/one_to_many_association.rb +66 -0
- data/lib/terrestrial/public_conveniencies.rb +139 -0
- data/lib/terrestrial/query_order.rb +32 -0
- data/lib/terrestrial/relation_mapping.rb +50 -0
- data/lib/terrestrial/serializer.rb +18 -0
- data/lib/terrestrial/short_inspection_string.rb +18 -0
- data/lib/terrestrial/struct_factory.rb +17 -0
- data/lib/terrestrial/subset_queries_proxy.rb +11 -0
- data/lib/terrestrial/upserted_record.rb +15 -0
- data/lib/terrestrial/version.rb +1 -1
- data/lib/terrestrial.rb +5 -2
- data/sequel_mapper.gemspec +31 -0
- data/spec/config_override_spec.rb +193 -0
- data/spec/custom_serializers_spec.rb +49 -0
- data/spec/deletion_spec.rb +101 -0
- data/spec/graph_persistence_spec.rb +313 -0
- data/spec/graph_traversal_spec.rb +121 -0
- data/spec/new_graph_persistence_spec.rb +71 -0
- data/spec/object_identity_spec.rb +70 -0
- data/spec/ordered_association_spec.rb +51 -0
- data/spec/persistence_efficiency_spec.rb +224 -0
- data/spec/predefined_queries_spec.rb +62 -0
- data/spec/proxying_spec.rb +88 -0
- data/spec/querying_spec.rb +48 -0
- data/spec/readme_examples_spec.rb +35 -0
- data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
- data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
- data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
- data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
- data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
- data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
- data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
- data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/blog_schema.rb +38 -0
- data/spec/support/have_persisted_matcher.rb +19 -0
- data/spec/support/mapper_setup.rb +221 -0
- data/spec/support/mock_sequel.rb +193 -0
- data/spec/support/object_graph_setup.rb +139 -0
- data/spec/support/seed_data_setup.rb +165 -0
- data/spec/support/sequel_persistence_setup.rb +19 -0
- data/spec/support/sequel_test_support.rb +166 -0
- metadata +207 -13
- data/.travis.yml +0 -4
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/terrestrial.gemspec +0 -23
@@ -0,0 +1,135 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/collection_mutability_proxy"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::CollectionMutabilityProxy do
|
6
|
+
let(:proxy) {
|
7
|
+
Terrestrial::CollectionMutabilityProxy.new(lazy_enum)
|
8
|
+
}
|
9
|
+
|
10
|
+
let(:lazy_enum) { data_set.each.lazy }
|
11
|
+
let(:data_set) { (0..9) }
|
12
|
+
|
13
|
+
def id
|
14
|
+
->(x) { x }
|
15
|
+
end
|
16
|
+
|
17
|
+
it "is Enumerable" do
|
18
|
+
expect(proxy).to be_a(Enumerable)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#to_a" do
|
22
|
+
it "is equivalent to the original enumeration" do
|
23
|
+
expect(proxy.map(&id)).to eq(data_set.to_a)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#to_ary" do
|
28
|
+
it "is equivalent to the original enumeration" do
|
29
|
+
expect(proxy.to_ary).to eq(data_set.to_a)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "implicitly coerces to Array" do
|
33
|
+
expect([-1].concat(proxy)).to eq([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#each" do
|
38
|
+
context "when called with a block" do
|
39
|
+
it "returns self" do
|
40
|
+
expect(proxy.each(&id)).to eq(proxy)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "yields each element to the block" do
|
44
|
+
yielded = []
|
45
|
+
|
46
|
+
proxy.each do |element|
|
47
|
+
yielded.push(element)
|
48
|
+
end
|
49
|
+
|
50
|
+
expect(yielded).to eq(data_set.to_a)
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when calling each more than once" do
|
54
|
+
before do
|
55
|
+
proxy.each { |x| nil }
|
56
|
+
proxy.each { |x| nil }
|
57
|
+
end
|
58
|
+
|
59
|
+
it "rewinds the enumeration on each call" do
|
60
|
+
expect(proxy.map(&id)).to eq(data_set.to_a)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "when a new element is pushed into the collection" do
|
66
|
+
let(:new_element) { double(:new_element) }
|
67
|
+
|
68
|
+
before do
|
69
|
+
proxy.push(new_element)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "adds the new element to the enumeration" do
|
73
|
+
expect(proxy.to_a.last).to eq(new_element)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#each_loaded" do
|
79
|
+
context "when called with a block" do
|
80
|
+
it "returns self" do
|
81
|
+
expect(proxy.each(&id)).to eq(proxy)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "yields each element to the block" do
|
85
|
+
yielded = []
|
86
|
+
|
87
|
+
proxy.each do |element|
|
88
|
+
yielded.push(element)
|
89
|
+
end
|
90
|
+
|
91
|
+
expect(yielded).to eq(data_set.to_a)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when called without a block" do
|
96
|
+
it "returns an enumerator" do
|
97
|
+
expect(proxy.each).to be_a(Enumerator)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "#delete" do
|
103
|
+
it "returns self" do
|
104
|
+
expect(proxy.delete(3)).to be(proxy)
|
105
|
+
end
|
106
|
+
|
107
|
+
context "after removing a element from the enumeration" do
|
108
|
+
before do
|
109
|
+
proxy.delete(3)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "skips that element in the enumeration" do
|
113
|
+
expect(proxy.map(&id)).to eq([0,1,2,4,5,6,7,8,9])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#push" do
|
119
|
+
context "after pushing another element into the enumeration" do
|
120
|
+
before do
|
121
|
+
proxy.push(new_value)
|
122
|
+
end
|
123
|
+
|
124
|
+
let(:new_value) { double(:new_value) }
|
125
|
+
|
126
|
+
it "does not alter the other elements" do
|
127
|
+
expect(proxy.map(&id)[0..-2]).to eq([0,1,2,3,4,5,6,7,8,9])
|
128
|
+
end
|
129
|
+
|
130
|
+
it "appends the element to the enumeration" do
|
131
|
+
expect(proxy.map(&id).last).to eq(new_value)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/deleted_record"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::DeletedRecord do
|
6
|
+
subject(:record) {
|
7
|
+
Terrestrial::DeletedRecord.new(namespace, identity, raw_data)
|
8
|
+
}
|
9
|
+
|
10
|
+
let(:namespace) { double(:namespace) }
|
11
|
+
|
12
|
+
let(:identity) {
|
13
|
+
{ id: id }
|
14
|
+
}
|
15
|
+
|
16
|
+
let(:raw_data) {
|
17
|
+
{
|
18
|
+
name: name,
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let(:id) { double(:id) }
|
23
|
+
let(:name) { double(:name) }
|
24
|
+
|
25
|
+
describe "#if_delete" do
|
26
|
+
it "invokes the callback" do
|
27
|
+
expect { |callback|
|
28
|
+
record.if_delete(&callback)
|
29
|
+
}.to yield_with_args(record)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#==" do
|
34
|
+
context "with another record that deletes" do
|
35
|
+
let(:comparitor) {
|
36
|
+
record.merge({})
|
37
|
+
}
|
38
|
+
|
39
|
+
it "is equal" do
|
40
|
+
expect(record.==(comparitor)).to be(true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "with another record that does not delete" do
|
45
|
+
let(:comparitor) {
|
46
|
+
Class.new(Terrestrial::AbstractRecord) do
|
47
|
+
protected
|
48
|
+
def operation
|
49
|
+
:something_else
|
50
|
+
end
|
51
|
+
end
|
52
|
+
}
|
53
|
+
|
54
|
+
it "is not equal" do
|
55
|
+
expect(record.==(comparitor)).to be(false)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/dirty_map"
|
4
|
+
require "terrestrial/upserted_record"
|
5
|
+
require "terrestrial/deleted_record"
|
6
|
+
|
7
|
+
RSpec.describe Terrestrial::DirtyMap do
|
8
|
+
subject(:dirty_map) {
|
9
|
+
Terrestrial::DirtyMap.new(storage)
|
10
|
+
}
|
11
|
+
|
12
|
+
let(:storage) { {} }
|
13
|
+
|
14
|
+
let(:loaded_record) {
|
15
|
+
create_record( namespace, identity_fields, attributes, depth)
|
16
|
+
}
|
17
|
+
|
18
|
+
let(:namespace) { :table_name }
|
19
|
+
let(:identity_fields) { [:id] }
|
20
|
+
let(:depth) { 0 }
|
21
|
+
|
22
|
+
let(:attributes) {
|
23
|
+
{
|
24
|
+
id: "record/id",
|
25
|
+
name: "record/name",
|
26
|
+
email: "record/email",
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
describe "#load" do
|
31
|
+
it "adds the record to its storage" do
|
32
|
+
dirty_map.load(loaded_record)
|
33
|
+
|
34
|
+
expect(storage.values).to include(loaded_record)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns the loaded record" do
|
38
|
+
expect(dirty_map.load(loaded_record)).to eq(loaded_record)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#dirty" do
|
43
|
+
let(:clean_record) {
|
44
|
+
create_record(namespace, identity_fields, attributes, depth)
|
45
|
+
}
|
46
|
+
|
47
|
+
let(:dirty_record) {
|
48
|
+
create_record(
|
49
|
+
namespace,
|
50
|
+
identity_fields,
|
51
|
+
attributes.merge(name: "record/dirty_name"),
|
52
|
+
depth,
|
53
|
+
)
|
54
|
+
}
|
55
|
+
|
56
|
+
context "when the record has not been loaded (new record)" do
|
57
|
+
it "return true" do
|
58
|
+
expect(dirty_map.dirty?(clean_record)).to be(true)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when a record with same identity has been loaded (existing record)" do
|
63
|
+
before do
|
64
|
+
dirty_map.load(loaded_record)
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when the record is unchanged" do
|
68
|
+
it "returns false" do
|
69
|
+
expect(dirty_map.dirty?(clean_record)).to be(false)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when the record's attributes are changed" do
|
74
|
+
it "returns true" do
|
75
|
+
expect(dirty_map.dirty?(dirty_record)).to be(true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "when the record is deleted" do
|
80
|
+
let(:deleted_record) {
|
81
|
+
Terrestrial::DeletedRecord.new(
|
82
|
+
namespace,
|
83
|
+
identity_fields,
|
84
|
+
attributes,
|
85
|
+
depth,
|
86
|
+
)
|
87
|
+
}
|
88
|
+
|
89
|
+
it "is always dirty" do
|
90
|
+
expect(dirty_map.dirty?(deleted_record)).to be(true)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "when the record's attributes hash is mutated" do
|
95
|
+
before do
|
96
|
+
attributes.merge!(name: "new_value")
|
97
|
+
end
|
98
|
+
|
99
|
+
it "returns true" do
|
100
|
+
expect(dirty_map.dirty?(clean_record)).to be(true)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "when a record's string value is mutated" do
|
105
|
+
before do
|
106
|
+
attributes.fetch(:name) << "MUTANT"
|
107
|
+
end
|
108
|
+
|
109
|
+
it "returns true" do
|
110
|
+
expect(dirty_map.dirty?(clean_record)).to be(true)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "when record contains an unchanged subset of the fields loaded" do
|
115
|
+
let(:partial_record) {
|
116
|
+
create_record(
|
117
|
+
namespace,
|
118
|
+
identity_fields,
|
119
|
+
partial_clean_attrbiutes,
|
120
|
+
depth,
|
121
|
+
)
|
122
|
+
}
|
123
|
+
|
124
|
+
let(:partial_clean_attrbiutes) {
|
125
|
+
attributes.reject { |k, _v| k == :email }
|
126
|
+
}
|
127
|
+
|
128
|
+
it "return false" do
|
129
|
+
expect(dirty_map.dirty?(partial_record)).to be(false)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when record contains a changed subset of the fields loaded" do
|
134
|
+
let(:partial_record) {
|
135
|
+
create_record(
|
136
|
+
namespace,
|
137
|
+
identity_fields,
|
138
|
+
partial_dirty_attrbiutes,
|
139
|
+
depth,
|
140
|
+
)
|
141
|
+
}
|
142
|
+
|
143
|
+
let(:partial_dirty_attrbiutes) {
|
144
|
+
attributes
|
145
|
+
.reject { |k, _v| k == :email }
|
146
|
+
.merge(name: "record/changed_name")
|
147
|
+
}
|
148
|
+
|
149
|
+
it "return false" do
|
150
|
+
expect(dirty_map.dirty?(partial_record)).to be(true)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "when record contains an unchanged superset of the fields loaded" do
|
155
|
+
let(:super_record) {
|
156
|
+
create_record(
|
157
|
+
namespace,
|
158
|
+
identity_fields,
|
159
|
+
super_clean_attributes,
|
160
|
+
depth,
|
161
|
+
)
|
162
|
+
}
|
163
|
+
|
164
|
+
let(:super_clean_attributes) {
|
165
|
+
attributes.merge(unknown_key: "record/unknown_value")
|
166
|
+
}
|
167
|
+
|
168
|
+
it "return true" do
|
169
|
+
expect(dirty_map.dirty?(super_record)).to be(true)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context "#reject_unchanged_fields" do
|
175
|
+
context "when the record has not been loaded (new record)" do
|
176
|
+
it "returns an eqiuivalent record" do
|
177
|
+
expect(dirty_map.reject_unchanged_fields(dirty_record))
|
178
|
+
.to eq(dirty_record)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context "when a record with same identity has been loaded (existing record)" do
|
183
|
+
before do
|
184
|
+
dirty_map.load(loaded_record)
|
185
|
+
end
|
186
|
+
|
187
|
+
context "with a equivalent record" do
|
188
|
+
it "returns an empty record" do
|
189
|
+
expect(dirty_map.reject_unchanged_fields(clean_record)).to be_empty
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "a record with a changed field" do
|
194
|
+
it "returns a record containing just that field" do
|
195
|
+
expect(
|
196
|
+
dirty_map
|
197
|
+
.reject_unchanged_fields(dirty_record)
|
198
|
+
.non_identity_attributes
|
199
|
+
).to eq( name: "record/dirty_name" )
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def create_record(namespace, identity_fields, attributes, depth)
|
207
|
+
Terrestrial::UpsertedRecord.new(
|
208
|
+
namespace,
|
209
|
+
identity_fields,
|
210
|
+
attributes,
|
211
|
+
depth,
|
212
|
+
)
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/lazy_collection"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::LazyCollection do
|
6
|
+
let(:proxy) {
|
7
|
+
Terrestrial::LazyCollection.new(
|
8
|
+
database_enum,
|
9
|
+
loader,
|
10
|
+
mapper,
|
11
|
+
)
|
12
|
+
}
|
13
|
+
|
14
|
+
let(:row1) { double(:row1) }
|
15
|
+
let(:row2) { double(:row2) }
|
16
|
+
let(:object1) { double(:object1) }
|
17
|
+
let(:object2) { double(:object2) }
|
18
|
+
let(:row_object_map) {
|
19
|
+
{
|
20
|
+
row1 => object1,
|
21
|
+
row2 => object2,
|
22
|
+
}
|
23
|
+
}
|
24
|
+
let(:collection_size) { row_object_map.size }
|
25
|
+
|
26
|
+
let(:database_enum) { [row1, row2].each.lazy }
|
27
|
+
|
28
|
+
let(:mapper) { double(:mapper) }
|
29
|
+
|
30
|
+
let(:loader_count) { @loader_count }
|
31
|
+
let(:loader) {
|
32
|
+
@loader_count = 0
|
33
|
+
|
34
|
+
->(row) {
|
35
|
+
@loader_count = @loader_count + 1
|
36
|
+
row_object_map.fetch(row)
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
it "is Enumerable" do
|
41
|
+
expect(proxy).to be_a(Enumerable)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#to_ary" do
|
45
|
+
it "is equivalent to the original enumeration" do
|
46
|
+
expect(proxy.to_ary).to eq([object1, object2])
|
47
|
+
end
|
48
|
+
|
49
|
+
it "implicitly coerces to Array" do
|
50
|
+
new_object = Object.new
|
51
|
+
|
52
|
+
expect([new_object].concat(proxy)).to eq([new_object, object1, object2])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#each" do
|
57
|
+
it "iterates over all elements of the database_enum" do
|
58
|
+
elements = []
|
59
|
+
proxy.each { |x| elements.push(x) }
|
60
|
+
|
61
|
+
expect(elements).to eq([object1, object2])
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when the collection is not loaded" do
|
65
|
+
it "loads the collection on first call" do
|
66
|
+
proxy.each { |x| x }
|
67
|
+
|
68
|
+
expect(loader_count).to eq(collection_size)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when the collection has already loaded (second call to #each)" do
|
73
|
+
before do
|
74
|
+
proxy.each { |x| x }
|
75
|
+
end
|
76
|
+
|
77
|
+
it "does not load a second time" do
|
78
|
+
expect {
|
79
|
+
proxy.each { |x| x }
|
80
|
+
}.not_to change { loader_count }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "when #first has been called beforehand" do
|
85
|
+
before do
|
86
|
+
proxy.first
|
87
|
+
end
|
88
|
+
|
89
|
+
it "does not reload the first element of the collection" do
|
90
|
+
proxy.each { |x| x }
|
91
|
+
|
92
|
+
expect(loader_count).to eq(collection_size)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "iterates over all elements" do
|
96
|
+
elements = []
|
97
|
+
proxy.each { |x| elements.push(x) }
|
98
|
+
|
99
|
+
expect(elements).to eq([object1, object2])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context "when drop has been called beforehand" do
|
104
|
+
it "loads each object just once" do
|
105
|
+
proxy.drop(1).each { |x| x }
|
106
|
+
|
107
|
+
expect(loader_count).to eq(collection_size)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "#first" do
|
113
|
+
it "loads only the first object" do
|
114
|
+
proxy.first
|
115
|
+
|
116
|
+
expect(loader_count).to eq(1)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/lazy_object_proxy"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::LazyObjectProxy do
|
6
|
+
subject(:proxy) {
|
7
|
+
Terrestrial::LazyObjectProxy.new(
|
8
|
+
object_loader,
|
9
|
+
key_fields,
|
10
|
+
)
|
11
|
+
}
|
12
|
+
|
13
|
+
let(:id) { double(:id) }
|
14
|
+
let(:key_fields) { { id: id } }
|
15
|
+
let(:object_loader) { double(:object_loader, call: proxied_object) }
|
16
|
+
let(:proxied_object) { double(:proxied_object, name: name) }
|
17
|
+
let(:name) { double(:name) }
|
18
|
+
|
19
|
+
describe "#__getobj__" do
|
20
|
+
it "loads the object" do
|
21
|
+
proxy.__getobj__
|
22
|
+
|
23
|
+
expect(object_loader).to have_received(:call)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns the proxied object" do
|
27
|
+
expect(proxy.__getobj__).to be(proxied_object)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when no method is called on it" do
|
32
|
+
it "does not call the loader" do
|
33
|
+
proxy
|
34
|
+
|
35
|
+
expect(object_loader).not_to have_received(:call)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when a missing method is called on the proxy" do
|
40
|
+
it "is a true decorator" do
|
41
|
+
expect(proxied_object).to receive(:arbitrary_message)
|
42
|
+
|
43
|
+
proxy.arbitrary_message
|
44
|
+
end
|
45
|
+
|
46
|
+
it "loads the object" do
|
47
|
+
proxy.name
|
48
|
+
|
49
|
+
expect(object_loader).to have_received(:call)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "returns delegates the message to the object" do
|
53
|
+
args = [ double, double ]
|
54
|
+
proxy.name(*args)
|
55
|
+
|
56
|
+
expect(proxied_object).to have_received(:name).with(*args)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns the objects return value" do
|
60
|
+
expect(proxy.name).to eq(name)
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when calling a method twice" do
|
64
|
+
it "loads the object once" do
|
65
|
+
proxy.name
|
66
|
+
proxy.name
|
67
|
+
|
68
|
+
expect(object_loader).to have_received(:call)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#loaded?" do
|
74
|
+
context "before the object is loaded" do
|
75
|
+
it "returns false" do
|
76
|
+
expect(proxy).not_to be_loaded
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "after the object is loaded" do
|
81
|
+
def force_object_load(object)
|
82
|
+
object.__getobj__
|
83
|
+
end
|
84
|
+
|
85
|
+
before { force_object_load(proxy) }
|
86
|
+
|
87
|
+
it "returns true" do
|
88
|
+
expect(proxy).to be_loaded
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "key fields" do
|
94
|
+
context "when key fields are provided before load (such as from foreign key)" do
|
95
|
+
it "does not load the object when that field is accessed" do
|
96
|
+
proxy.id
|
97
|
+
|
98
|
+
expect(proxy).not_to be_loaded
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns the given value" do
|
102
|
+
expect(proxy.id).to be(id)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "#respond_to?" do
|
108
|
+
context "when method corresponds to a key field" do
|
109
|
+
it "does not the load the object" do
|
110
|
+
proxy.respond_to?(:id)
|
111
|
+
|
112
|
+
expect(proxy).not_to be_loaded
|
113
|
+
end
|
114
|
+
|
115
|
+
it "repsonds to the method" do
|
116
|
+
expect(proxy).to respond_to(:id)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "when the method is not a key field" do
|
121
|
+
it "loads the object" do
|
122
|
+
proxy.respond_to?(:something_arbitrary)
|
123
|
+
|
124
|
+
expect(proxy).to be_loaded
|
125
|
+
end
|
126
|
+
|
127
|
+
context "when lazy proxied object does respond to the method" do
|
128
|
+
it "responds to the method" do
|
129
|
+
expect(proxy).to respond_to(:name)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when lazy proxied object does not respond to the method" do
|
134
|
+
it "does not respond to the method" do
|
135
|
+
expect(proxy).not_to respond_to(:something_arbitrary)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|