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,224 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/mapper_setup"
|
4
|
+
require "support/sequel_persistence_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
require "terrestrial"
|
7
|
+
|
8
|
+
RSpec.describe "Graph persistence efficiency" do
|
9
|
+
include_context "mapper setup"
|
10
|
+
include_context "sequel persistence setup"
|
11
|
+
include_context "seed data setup"
|
12
|
+
|
13
|
+
let(:mapper) { user_mapper }
|
14
|
+
let(:user_query) { mapper.where(id: "users/1") }
|
15
|
+
let(:user) { user_query.first }
|
16
|
+
|
17
|
+
context "when modifying the root node" do
|
18
|
+
let(:modified_email) { "modified@example.com" }
|
19
|
+
|
20
|
+
context "and only the root node" do
|
21
|
+
before do
|
22
|
+
user.email = modified_email
|
23
|
+
end
|
24
|
+
|
25
|
+
it "performs 1 update" do
|
26
|
+
expect {
|
27
|
+
mapper.save(user)
|
28
|
+
}.to change { query_counter.update_count }.by(1)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "sends only the updated fields to the datastore" do
|
32
|
+
mapper.save(user)
|
33
|
+
update_sql = query_counter.updates.last
|
34
|
+
|
35
|
+
expect(update_sql).to eq(
|
36
|
+
%{UPDATE "users" SET "email" = '#{modified_email}' WHERE ("id" = '#{user.id}')}
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when modifying a directly associated (has many) object" do
|
43
|
+
let(:modified_post_subject) { "modified post subject" }
|
44
|
+
|
45
|
+
before do
|
46
|
+
user.posts.first.subject = modified_post_subject
|
47
|
+
end
|
48
|
+
|
49
|
+
it "performs 1 update" do
|
50
|
+
expect {
|
51
|
+
mapper.save(user)
|
52
|
+
}.to change { query_counter.update_count }.by(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "performs 0 deletes" do
|
56
|
+
expect {
|
57
|
+
mapper.save(user)
|
58
|
+
}.to change { query_counter.delete_count }.by(0)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "performs 0 additional reads" do
|
62
|
+
expect {
|
63
|
+
mapper.save(user)
|
64
|
+
}.to change { query_counter.read_count }.by(0)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when loading many nodes of the graph" do
|
69
|
+
let(:post) {
|
70
|
+
user.posts.first
|
71
|
+
}
|
72
|
+
|
73
|
+
context "and modifying an intermediate node" do
|
74
|
+
before do
|
75
|
+
post.subject = "MODIFIED"
|
76
|
+
end
|
77
|
+
|
78
|
+
it "performs 1 write" do
|
79
|
+
expect {
|
80
|
+
mapper.save(user)
|
81
|
+
}.to change { query_counter.update_count }.by(1)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "and modifying a leaf node" do
|
86
|
+
let(:comment) { post.comments.first }
|
87
|
+
|
88
|
+
before do
|
89
|
+
comment.body = "UPDATED!"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "performs 1 update" do
|
93
|
+
expect {
|
94
|
+
mapper.save(user)
|
95
|
+
}.to change { query_counter.update_count }.by(1)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "and modifying both a leaf and intermediate node" do
|
100
|
+
let(:comment) { post.comments.first }
|
101
|
+
|
102
|
+
before do
|
103
|
+
comment.body = "UPDATED!"
|
104
|
+
post.subject = "MODIFIED"
|
105
|
+
end
|
106
|
+
|
107
|
+
it "performs 2 updates" do
|
108
|
+
expect {
|
109
|
+
mapper.save(user)
|
110
|
+
}.to change { query_counter.update_count }.by(2)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "when modifying a many to many association" do
|
116
|
+
let(:post) { user.posts.first }
|
117
|
+
let(:category) { post.categories.first }
|
118
|
+
|
119
|
+
before do
|
120
|
+
category.name = "UPDATED"
|
121
|
+
end
|
122
|
+
|
123
|
+
it "performs 1 write" do
|
124
|
+
expect {
|
125
|
+
mapper.save(user)
|
126
|
+
}.to change { query_counter.update_count }.by(1)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "eager loading" do
|
131
|
+
context "on root node" do
|
132
|
+
it "performs 1 read per table rather than n + 1" do
|
133
|
+
expect {
|
134
|
+
mapper.eager_load(:posts => []).all.map { |user|
|
135
|
+
[user.id, user.posts.map(&:id)]
|
136
|
+
}
|
137
|
+
}.to change { query_counter.read_count }.by(2)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "with nested has many" do
|
142
|
+
it "performs 1 read per table rather than n + 1" do
|
143
|
+
expect {
|
144
|
+
user_query
|
145
|
+
.eager_load(:posts => { :comments => [] })
|
146
|
+
.first
|
147
|
+
.posts
|
148
|
+
.map { |post| post.comments.map(&:id) }
|
149
|
+
}.to change { query_counter.read_count }.by(3)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "with has many and belongs to" do
|
154
|
+
it "performs 1 read per table rather than n + 1" do
|
155
|
+
expect {
|
156
|
+
user_query
|
157
|
+
.eager_load(:posts => { :comments => { :commenter => [] }})
|
158
|
+
.flat_map(&:posts)
|
159
|
+
.flat_map(&:comments)
|
160
|
+
.flat_map(&:commenter)
|
161
|
+
}.to change { query_counter.read_count }.by(4)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "for has many to has many through" do
|
166
|
+
it "performs 1 read per table (including join table) rather than n + 1" do
|
167
|
+
expect {
|
168
|
+
user_query
|
169
|
+
.eager_load(:posts => { :categories => [] })
|
170
|
+
.flat_map(&:posts)
|
171
|
+
.flat_map(&:categories)
|
172
|
+
.flat_map(&:id)
|
173
|
+
}.to change { query_counter.read_count }.by(4)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "for has many through to has many" do
|
178
|
+
it "performs 1 read per table (includiing join table) rather than n + 1" do
|
179
|
+
expect {
|
180
|
+
user_query
|
181
|
+
.eager_load(:posts => { :categories => { :posts => [] }})
|
182
|
+
.flat_map(&:posts)
|
183
|
+
.flat_map(&:categories)
|
184
|
+
.flat_map(&:posts)
|
185
|
+
}.to change { query_counter.read_count }.by(6)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context "eager load multiple associations at same level" do
|
190
|
+
it "performs 1 read per table (includiing join table) rather than n + 1" do
|
191
|
+
expect {
|
192
|
+
posts = user_query
|
193
|
+
.eager_load(:posts => { :comments => {}, :categories => {} })
|
194
|
+
.flat_map(&:posts)
|
195
|
+
|
196
|
+
categories = posts.flat_map(&:categories)
|
197
|
+
comments = posts.flat_map(&:comments)
|
198
|
+
}.to change { query_counter.read_count }.by(5)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "mixed eager and lazy loading" do
|
203
|
+
it "lazy data can still be loaded while eager data remains efficient" do
|
204
|
+
eager_queries = 6
|
205
|
+
lazy_comment_queries = 3
|
206
|
+
|
207
|
+
expect {
|
208
|
+
user_query
|
209
|
+
.eager_load(:posts => { :categories => { :posts => [] }})
|
210
|
+
.flat_map(&:posts)
|
211
|
+
.flat_map(&:categories)
|
212
|
+
.flat_map(&:posts)
|
213
|
+
.flat_map(&:comments)
|
214
|
+
}.to change {
|
215
|
+
query_counter.read_count
|
216
|
+
}.by(eager_queries + lazy_comment_queries)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
after do |example|
|
222
|
+
query_counter.show_queries if example.exception
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/mapper_setup"
|
4
|
+
require "support/sequel_persistence_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
require "terrestrial"
|
7
|
+
require "terrestrial/configurations/conventional_configuration"
|
8
|
+
|
9
|
+
RSpec.describe "Predefined subset queries" do
|
10
|
+
include_context "mapper setup"
|
11
|
+
include_context "sequel persistence setup"
|
12
|
+
include_context "seed data setup"
|
13
|
+
|
14
|
+
subject(:users) { user_mapper }
|
15
|
+
|
16
|
+
context "on the top level mapper" do
|
17
|
+
context "subset is defined with a block" do
|
18
|
+
before do
|
19
|
+
configs.fetch(:users).merge!(
|
20
|
+
subsets: {
|
21
|
+
tricketts: ->(dataset) {
|
22
|
+
dataset
|
23
|
+
.where(last_name: "Trickett")
|
24
|
+
.order(:first_name)
|
25
|
+
},
|
26
|
+
},
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "maps the result of the subset" do
|
31
|
+
expect(users.subset(:tricketts).map(&:first_name)).to eq([
|
32
|
+
"Hansel",
|
33
|
+
"Jasper",
|
34
|
+
])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "on a has many association" do
|
40
|
+
before do
|
41
|
+
configs.fetch(:posts).merge!(
|
42
|
+
subsets: {
|
43
|
+
body_contains: ->(dataset, search_string) {
|
44
|
+
dataset.where("body like '%#{search_string}%'")
|
45
|
+
},
|
46
|
+
},
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
let(:user) { users.first }
|
51
|
+
|
52
|
+
it "maps the datastore subset" do
|
53
|
+
expect(user.posts.subset(:body_contains, "purrr").map(&:id))
|
54
|
+
.to eq(["posts/2"])
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns an immutable collection" do
|
58
|
+
expect(user.posts.subset(:body_contains, "purrr").public_methods)
|
59
|
+
.not_to include(:push, :<<, :delete)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/mapper_setup"
|
4
|
+
require "support/sequel_persistence_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
require "terrestrial"
|
7
|
+
|
8
|
+
RSpec.describe "Proxying associations" do
|
9
|
+
include_context "mapper setup"
|
10
|
+
include_context "sequel persistence setup"
|
11
|
+
include_context "seed data setup"
|
12
|
+
|
13
|
+
context "of type `has_many`" do
|
14
|
+
subject(:mapper) { user_mapper }
|
15
|
+
|
16
|
+
let(:user) {
|
17
|
+
mapper.where(id: "users/1").first
|
18
|
+
}
|
19
|
+
|
20
|
+
let(:posts) { user.posts }
|
21
|
+
|
22
|
+
describe "limiting datastore reads" do
|
23
|
+
context "when loading the root node" do
|
24
|
+
it "only performs one read" do
|
25
|
+
expect {
|
26
|
+
user
|
27
|
+
}.to change { query_counter.read_count }.by(1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when getting a reference to an association proxy" do
|
32
|
+
before { user }
|
33
|
+
|
34
|
+
it "does no additional reads" do
|
35
|
+
expect{
|
36
|
+
user.posts
|
37
|
+
}.to change { query_counter.read_count }.by(0)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when iteratiing over a has many association" do
|
42
|
+
before { posts }
|
43
|
+
|
44
|
+
it "does a single additional read for the association collection" do
|
45
|
+
expect {
|
46
|
+
user.posts.each { |x| x }
|
47
|
+
}.to change { query_counter.read_count }.by(1)
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when doing this more than once" do
|
51
|
+
before do
|
52
|
+
posts.each { |x| x }
|
53
|
+
end
|
54
|
+
|
55
|
+
it "performs no additional reads" do
|
56
|
+
expect {
|
57
|
+
user.posts.each { |x| x }
|
58
|
+
}.not_to change { query_counter.read_count }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when getting a reference to a many to many association" do
|
64
|
+
before { post }
|
65
|
+
|
66
|
+
let(:post) { user.posts.first }
|
67
|
+
|
68
|
+
it "does no additional reads" do
|
69
|
+
expect {
|
70
|
+
post.categories
|
71
|
+
}.to change { query_counter.read_count }.by(0)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "when iterating over a many to many association" do
|
76
|
+
let(:category_count) { 3 }
|
77
|
+
|
78
|
+
it "does 1 read" do
|
79
|
+
post = user.posts.first
|
80
|
+
|
81
|
+
expect {
|
82
|
+
post.categories.each { |x| x }
|
83
|
+
}.to change { query_counter.read_count }.by(1)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/mapper_setup"
|
4
|
+
require "support/sequel_persistence_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
require "terrestrial"
|
7
|
+
|
8
|
+
RSpec.describe "Querying" do
|
9
|
+
include_context "mapper setup"
|
10
|
+
include_context "sequel persistence setup"
|
11
|
+
include_context "seed data setup"
|
12
|
+
|
13
|
+
subject(:mapper) { user_mapper }
|
14
|
+
|
15
|
+
let(:user) {
|
16
|
+
mapper.where(id: "users/1").first
|
17
|
+
}
|
18
|
+
|
19
|
+
let(:query_criteria) {
|
20
|
+
{
|
21
|
+
body: "I do it three times purrr day",
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
let(:filtered_posts) {
|
26
|
+
user.posts.where(query_criteria)
|
27
|
+
}
|
28
|
+
|
29
|
+
describe "arbitrary where query" do
|
30
|
+
it "returns a filtered version of the association" do
|
31
|
+
expect(filtered_posts.map(&:id)).to eq(["posts/2"])
|
32
|
+
end
|
33
|
+
|
34
|
+
it "delegates the query to the datastore, performs two additiona reads" do
|
35
|
+
expect {
|
36
|
+
filtered_posts.map(&:id)
|
37
|
+
}.to change { query_counter.read_count }.by(2)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns another collection" do
|
41
|
+
expect(filtered_posts).not_to be(user.posts)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns an immutable collection" do
|
45
|
+
expect(filtered_posts.public_methods).not_to include(:push, :<<, :delete)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/mapper_setup"
|
4
|
+
require "support/sequel_persistence_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
require "terrestrial"
|
7
|
+
|
8
|
+
require "spec_helper"
|
9
|
+
|
10
|
+
require "support/mapper_setup"
|
11
|
+
require "support/sequel_persistence_setup"
|
12
|
+
require "support/seed_data_setup"
|
13
|
+
require "terrestrial"
|
14
|
+
|
15
|
+
RSpec.describe "README examples" do
|
16
|
+
include_context "sequel persistence setup"
|
17
|
+
|
18
|
+
readme_contents = File.read("README.md")
|
19
|
+
|
20
|
+
code_samples = readme_contents
|
21
|
+
.split("```ruby")
|
22
|
+
.drop(1)
|
23
|
+
.map { |s| s.split("```").first }
|
24
|
+
|
25
|
+
code_samples.each_with_index do |code_sample, i|
|
26
|
+
it "executes without error" do
|
27
|
+
begin
|
28
|
+
Module.new.module_eval(code_sample)
|
29
|
+
rescue => e
|
30
|
+
File.open("./example#{i}.rb", "w") { |f| f.puts(code_sample) }
|
31
|
+
binding.pry if ENV["DEBUG"]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "terrestrial/abstract_record"
|
4
|
+
|
5
|
+
RSpec.describe Terrestrial::AbstractRecord do
|
6
|
+
subject(:record) {
|
7
|
+
Terrestrial::AbstractRecord.new(
|
8
|
+
namespace,
|
9
|
+
primary_key_fields,
|
10
|
+
raw_data,
|
11
|
+
depth,
|
12
|
+
)
|
13
|
+
}
|
14
|
+
|
15
|
+
let(:namespace) { double(:namespace) }
|
16
|
+
let(:primary_key_fields) { [ :id1, :id2 ] }
|
17
|
+
let(:depth) { 0 }
|
18
|
+
|
19
|
+
let(:raw_data) {
|
20
|
+
{
|
21
|
+
id1: id1,
|
22
|
+
id2: id2,
|
23
|
+
name: name,
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
let(:id1) { double(:id1) }
|
28
|
+
let(:id2) { double(:id2) }
|
29
|
+
let(:name) { double(:name) }
|
30
|
+
|
31
|
+
describe "#namespace" do
|
32
|
+
it "returns the namespace" do
|
33
|
+
expect(record.namespace).to eq(namespace)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#identity" do
|
38
|
+
it "returns the primary key fields" do
|
39
|
+
expect(record.identity).to eq(
|
40
|
+
id1: id1,
|
41
|
+
id2: id2,
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#fetch" do
|
47
|
+
it "delegates to the underlying Hash representation" do
|
48
|
+
expect(record.fetch(:id1)).to eq(id1)
|
49
|
+
expect(record.fetch(:name)).to eq(name)
|
50
|
+
expect(record.fetch(:not_there, "nope")).to eq("nope")
|
51
|
+
expect(record.fetch(:not_there) { "lord no" }).to eq("lord no")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#to_h" do
|
56
|
+
it "returns a raw_data merged with identity" do
|
57
|
+
expect(record.to_h).to eq(
|
58
|
+
id1: id1,
|
59
|
+
id2: id2,
|
60
|
+
name: name,
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#if_upsert" do
|
66
|
+
it "returns self" do
|
67
|
+
expect(
|
68
|
+
record.if_upsert { |_| }
|
69
|
+
).to be(record)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "does not call the block" do
|
73
|
+
expect {
|
74
|
+
record.if_upsert { |_| raise "Does not happen" }
|
75
|
+
}.not_to raise_error
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#if_delete" do
|
80
|
+
it "returns self" do
|
81
|
+
expect(
|
82
|
+
record.if_delete { |_| }
|
83
|
+
).to be(record)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "does not call the block" do
|
87
|
+
expect {
|
88
|
+
record.if_delete { |_| raise "Does not happen" }
|
89
|
+
}.not_to raise_error
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#merge" do
|
94
|
+
let(:extra_data) {
|
95
|
+
{
|
96
|
+
location: location,
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
let(:location) { double(:location) }
|
101
|
+
|
102
|
+
it "returns a new record with same identity" do
|
103
|
+
expect(record.merge(extra_data).identity).to eq(
|
104
|
+
id1: id1,
|
105
|
+
id2: id2,
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "returns a new record with same namespace" do
|
110
|
+
expect(
|
111
|
+
record.merge(extra_data).namespace
|
112
|
+
).to eq(namespace)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "returns a new record with merged data" do
|
116
|
+
merged_record = record.merge(extra_data)
|
117
|
+
|
118
|
+
expect(merged_record.to_h).to eq(
|
119
|
+
id1: id1,
|
120
|
+
id2: id2,
|
121
|
+
name: name,
|
122
|
+
location: location,
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "does not mutate the original record" do
|
127
|
+
expect {
|
128
|
+
record.merge(extra_data)
|
129
|
+
}.not_to change { record.to_h }
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when attempting to overwrite the existing identity" do
|
133
|
+
let(:extra_data) {
|
134
|
+
{
|
135
|
+
id1: double(:new_id),
|
136
|
+
location: location,
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
it "does not change the identity" do
|
141
|
+
expect {
|
142
|
+
record.merge(extra_data)
|
143
|
+
}.not_to change { record.identity }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "#<=>" do
|
149
|
+
let(:deep_record) {
|
150
|
+
Terrestrial::AbstractRecord.new(
|
151
|
+
namespace,
|
152
|
+
primary_key_fields,
|
153
|
+
raw_data,
|
154
|
+
_depth = 5,
|
155
|
+
)
|
156
|
+
}
|
157
|
+
|
158
|
+
let(:shallow_record) {
|
159
|
+
Terrestrial::AbstractRecord.new(
|
160
|
+
namespace,
|
161
|
+
primary_key_fields,
|
162
|
+
raw_data,
|
163
|
+
_depth = 1,
|
164
|
+
)
|
165
|
+
}
|
166
|
+
|
167
|
+
context "when other record has deeper depth" do
|
168
|
+
it "is sortable by depth" do
|
169
|
+
expect([shallow_record, record, deep_record].sort).to eq(
|
170
|
+
[record, deep_record, shallow_record]
|
171
|
+
)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "#==" do
|
177
|
+
context "super class contract" do
|
178
|
+
let(:comparitor) { record.merge({}) }
|
179
|
+
|
180
|
+
it "compares" do
|
181
|
+
record == comparitor
|
182
|
+
end
|
183
|
+
|
184
|
+
context "when subclassed" do
|
185
|
+
subject(:record) {
|
186
|
+
record_subclass.new(namespace, primary_key_fields, raw_data)
|
187
|
+
}
|
188
|
+
|
189
|
+
let(:record_subclass) {
|
190
|
+
Class.new(Terrestrial::AbstractRecord) {
|
191
|
+
protected
|
192
|
+
|
193
|
+
def operation
|
194
|
+
:do_a_thing
|
195
|
+
end
|
196
|
+
}
|
197
|
+
}
|
198
|
+
|
199
|
+
context "when comparitor is of the wrong type" do
|
200
|
+
it "is not equal" do
|
201
|
+
expect(record.==(Object.new)).to be(false)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context "when the operation type is equal" do
|
206
|
+
context "when the combined `raw_data` and `identity` are equal" do
|
207
|
+
let(:comparitor) { record.merge({}) }
|
208
|
+
|
209
|
+
it "is equal" do
|
210
|
+
expect(record.==(comparitor)).to be(true)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context "when the combined `raw_data` and `identity` are not equal" do
|
215
|
+
let(:comparitor) { record.merge(something_else: "i'm different") }
|
216
|
+
|
217
|
+
it "is not equal" do
|
218
|
+
expect(record.==(comparitor)).to be(false)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
context "when the operation name differs" do
|
224
|
+
let(:comparitor) {
|
225
|
+
record_class_with_different_operation.new(namespace, primary_key_fields, raw_data)
|
226
|
+
}
|
227
|
+
|
228
|
+
let(:record_class_with_different_operation) {
|
229
|
+
Class.new(Terrestrial::AbstractRecord) {
|
230
|
+
protected
|
231
|
+
def operation
|
232
|
+
:do_a_different_thing
|
233
|
+
end
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
it "is not equal" do
|
238
|
+
expect(record.==(comparitor)).to be(false)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|