terrestrial 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile.lock +29 -24
  4. data/README.md +35 -17
  5. data/Rakefile +4 -9
  6. data/TODO.md +25 -18
  7. data/bin/test +31 -0
  8. data/docs/domain_object_contract.md +50 -0
  9. data/features/env.rb +4 -6
  10. data/features/example.feature +28 -28
  11. data/features/step_definitions/example_steps.rb +2 -2
  12. data/lib/terrestrial/adapters/memory_adapter.rb +241 -0
  13. data/lib/terrestrial/collection_mutability_proxy.rb +7 -2
  14. data/lib/terrestrial/dirty_map.rb +5 -0
  15. data/lib/terrestrial/error.rb +69 -0
  16. data/lib/terrestrial/graph_loader.rb +58 -35
  17. data/lib/terrestrial/graph_serializer.rb +37 -30
  18. data/lib/terrestrial/inspection_string.rb +19 -0
  19. data/lib/terrestrial/lazy_collection.rb +2 -2
  20. data/lib/terrestrial/lazy_object_proxy.rb +1 -1
  21. data/lib/terrestrial/many_to_one_association.rb +17 -11
  22. data/lib/terrestrial/public_conveniencies.rb +125 -95
  23. data/lib/terrestrial/relation_mapping.rb +30 -0
  24. data/lib/terrestrial/{mapper_facade.rb → relational_store.rb} +11 -1
  25. data/lib/terrestrial/version.rb +1 -1
  26. data/spec/config_override_spec.rb +10 -14
  27. data/spec/custom_serializers_spec.rb +4 -6
  28. data/spec/deletion_spec.rb +12 -14
  29. data/spec/error_handling/factory_error_handling_spec.rb +61 -0
  30. data/spec/error_handling/serialization_error_spec.rb +50 -0
  31. data/spec/error_handling/upsert_error_spec.rb +132 -0
  32. data/spec/graph_persistence_spec.rb +80 -24
  33. data/spec/graph_traversal_spec.rb +14 -6
  34. data/spec/new_graph_persistence_spec.rb +43 -9
  35. data/spec/object_identity_spec.rb +5 -7
  36. data/spec/ordered_association_spec.rb +4 -6
  37. data/spec/predefined_queries_spec.rb +4 -6
  38. data/spec/querying_spec.rb +4 -12
  39. data/spec/readme_examples_spec.rb +3 -6
  40. data/spec/{persistence_efficiency_spec.rb → sequel_query_efficiency_spec.rb} +101 -19
  41. data/spec/spec_helper.rb +24 -2
  42. data/spec/support/memory_adapter_test_support.rb +21 -0
  43. data/spec/support/{mapper_setup.rb → object_store_setup.rb} +5 -5
  44. data/spec/support/seed_data_setup.rb +3 -1
  45. data/spec/support/sequel_test_support.rb +58 -25
  46. data/spec/{sequel_mapper → terrestrial}/abstract_record_spec.rb +0 -0
  47. data/spec/{sequel_mapper → terrestrial}/collection_mutability_proxy_spec.rb +0 -0
  48. data/spec/{sequel_mapper → terrestrial}/deleted_record_spec.rb +0 -0
  49. data/spec/{sequel_mapper → terrestrial}/dirty_map_spec.rb +38 -6
  50. data/spec/{sequel_mapper → terrestrial}/lazy_collection_spec.rb +2 -3
  51. data/spec/{sequel_mapper → terrestrial}/lazy_object_proxy_spec.rb +0 -0
  52. data/spec/{sequel_mapper → terrestrial}/public_conveniencies_spec.rb +12 -7
  53. data/spec/{sequel_mapper → terrestrial}/upserted_record_spec.rb +0 -0
  54. data/{sequel_mapper.gemspec → terrestrial.gemspec} +3 -3
  55. metadata +47 -39
  56. data/lib/terrestrial/short_inspection_string.rb +0 -18
  57. data/spec/proxying_spec.rb +0 -88
  58. data/spec/support/mock_sequel.rb +0 -193
  59. data/spec/support/sequel_persistence_setup.rb +0 -19
@@ -1,28 +1,26 @@
1
1
  require "spec_helper"
2
2
 
3
3
  require "support/have_persisted_matcher"
4
- require "support/mapper_setup"
5
- require "support/sequel_persistence_setup"
4
+ require "support/object_store_setup"
6
5
  require "support/seed_data_setup"
7
6
  require "terrestrial"
8
7
 
9
8
  RSpec.describe "Graph persistence" do
10
- include_context "mapper setup"
11
- include_context "sequel persistence setup"
9
+ include_context "object store setup"
12
10
  include_context "seed data setup"
13
11
 
14
- subject(:mapper) { mappers.fetch(:users) }
12
+ subject(:user_store) { object_store.fetch(:users) }
15
13
 
16
14
  let(:user) {
17
- mapper.where(id: "users/1").first
15
+ user_store.where(id: "users/1").first
18
16
  }
19
17
 
20
18
  context "without associations" do
21
- let(:modified_email) { "bestie+modified@gmail.com" }
19
+ let(:modified_email) { "hasel+modified@gmail.com" }
22
20
 
23
21
  it "saves the root object" do
24
22
  user.email = modified_email
25
- mapper.save(user)
23
+ user_store.save(user)
26
24
 
27
25
  expect(datastore).to have_persisted(
28
26
  :users,
@@ -35,7 +33,7 @@ RSpec.describe "Graph persistence" do
35
33
 
36
34
  it "doesn't send associated objects to the database as columns" do
37
35
  user.email = modified_email
38
- mapper.save(user)
36
+ user_store.save(user)
39
37
 
40
38
  expect(datastore).not_to have_persisted(
41
39
  :users,
@@ -50,7 +48,7 @@ RSpec.describe "Graph persistence" do
50
48
  it "saves the object" do
51
49
  user.email << "MUTATED"
52
50
 
53
- mapper.save(user)
51
+ user_store.save(user)
54
52
 
55
53
  expect(datastore).to have_persisted(
56
54
  :users,
@@ -69,7 +67,7 @@ RSpec.describe "Graph persistence" do
69
67
 
70
68
  it "saves the associated object" do
71
69
  post.body = modified_post_body
72
- mapper.save(user)
70
+ user_store.save(user)
73
71
 
74
72
  expect(datastore).to have_persisted(
75
73
  :posts,
@@ -92,7 +90,7 @@ RSpec.describe "Graph persistence" do
92
90
 
93
91
  it "saves the associated object" do
94
92
  comment.body = modified_comment_body
95
- mapper.save(user)
93
+ user_store.save(user)
96
94
 
97
95
  expect(datastore).to have_persisted(
98
96
  :comments,
@@ -133,7 +131,7 @@ RSpec.describe "Graph persistence" do
133
131
  it "persists the object" do
134
132
  user.posts.push(new_post)
135
133
 
136
- mapper.save(user)
134
+ user_store.save(user)
137
135
 
138
136
  expect(datastore).to have_persisted(
139
137
  :posts,
@@ -144,6 +142,18 @@ RSpec.describe "Graph persistence" do
144
142
  )
145
143
  )
146
144
  end
145
+
146
+ context "when the collection is not loaded until the new object is persisted" do
147
+ it "is consistent with the datastore" do
148
+ user.posts.push(new_post)
149
+
150
+ user_store.save(user)
151
+
152
+ expect(user.posts.to_a.map(&:id)).to eq(
153
+ ["posts/1", "posts/2", "posts/neu"]
154
+ )
155
+ end
156
+ end
147
157
  end
148
158
 
149
159
  context "delete an object from a has many association" do
@@ -157,7 +167,7 @@ RSpec.describe "Graph persistence" do
157
167
 
158
168
  it "delete the object from the datastore on save" do
159
169
  user.posts.delete(post)
160
- mapper.save(user)
170
+ user_store.save(user)
161
171
 
162
172
  expect(datastore).not_to have_persisted(
163
173
  :posts,
@@ -182,7 +192,7 @@ RSpec.describe "Graph persistence" do
182
192
  it "deletes the 'join table' record" do
183
193
  category = post.categories.first
184
194
  post.categories.delete(category)
185
- mapper.save(user)
195
+ user_store.save(user)
186
196
 
187
197
  expect(datastore).not_to have_persisted(
188
198
  :categories_to_posts,
@@ -196,7 +206,7 @@ RSpec.describe "Graph persistence" do
196
206
  it "does not delete the object" do
197
207
  category = post.categories.first
198
208
  post.categories.delete(category)
199
- mapper.save(user)
209
+ user_store.save(user)
200
210
 
201
211
  expect(datastore).to have_persisted(
202
212
  :categories,
@@ -220,7 +230,7 @@ RSpec.describe "Graph persistence" do
220
230
 
221
231
  it "persists the change" do
222
232
  post_with_one_category.categories.push(new_category)
223
- mapper.save(user)
233
+ user_store.save(user)
224
234
 
225
235
  expect(datastore).to have_persisted(
226
236
  :categories_to_posts,
@@ -245,7 +255,7 @@ RSpec.describe "Graph persistence" do
245
255
 
246
256
  it "persists the change" do
247
257
  category.name = modified_category_name
248
- mapper.save(user)
258
+ user_store.save(user)
249
259
 
250
260
  expect(datastore).to have_persisted(
251
261
  :categories,
@@ -258,13 +268,13 @@ RSpec.describe "Graph persistence" do
258
268
  end
259
269
 
260
270
  context "node loaded as root has undefined one to many association" do
261
- let(:post_mapper) { mappers[:posts] }
262
- let(:post) { post_mapper.where(id: "posts/1").first }
271
+ let(:post_store) { object_store[:posts] }
272
+ let(:post) { post_store.where(id: "posts/1").first }
263
273
 
264
274
  it "persists the changes to the root node" do
265
275
  post.body = "modified body"
266
276
 
267
- post_mapper.save(post)
277
+ post_store.save(post)
268
278
 
269
279
  expect(datastore).to have_persisted(
270
280
  :posts,
@@ -278,7 +288,7 @@ RSpec.describe "Graph persistence" do
278
288
  it "does not overwrite unused foreign key" do
279
289
  post.body = "modified body"
280
290
 
281
- post_mapper.save(post)
291
+ post_store.save(post)
282
292
 
283
293
  expect(datastore).to have_persisted(
284
294
  :posts,
@@ -291,6 +301,52 @@ RSpec.describe "Graph persistence" do
291
301
  end
292
302
  end
293
303
 
304
+ context "when a many to one association is nil" do
305
+ context "when the object does not have a reference to its parent" do
306
+ it "populates that association with a nil" do
307
+ post = Post.new(
308
+ id: "posts/orphan",
309
+ subject: "Nils gonna getcha",
310
+ body: "",
311
+ created_at: Time.parse("2015-09-05T15:00:00+01:00"),
312
+ categories: [],
313
+ comments: [],
314
+ )
315
+
316
+ object_store[:posts].save(post)
317
+
318
+ expect(datastore).to have_persisted(
319
+ :posts,
320
+ hash_including(
321
+ id: "posts/orphan",
322
+ author_id: nil,
323
+ )
324
+ )
325
+ end
326
+ end
327
+
328
+ context "when an existing partent object reference is set to nil" do
329
+ it "populates that association with a nil" do
330
+ comment = user
331
+ .posts
332
+ .flat_map(&:comments)
333
+ .detect { |c| c.id == "comments/1" }
334
+
335
+ comment.commenter = nil
336
+
337
+ user_store.save(user)
338
+
339
+ expect(datastore).to have_persisted(
340
+ :comments,
341
+ hash_including(
342
+ id: "comments/1",
343
+ commenter_id: nil,
344
+ )
345
+ )
346
+ end
347
+ end
348
+ end
349
+
294
350
  context "when a save operation fails (some object is not persistable)" do
295
351
  let(:unpersistable_object) { ->() { } }
296
352
 
@@ -301,8 +357,8 @@ RSpec.describe "Graph persistence" do
301
357
  user.first_name = "this will be rolled back"
302
358
  user.posts.first.subject = unpersistable_object
303
359
 
304
- mapper.save(user)
305
- rescue Sequel::Error
360
+ user_store.save(user)
361
+ rescue Terrestrial::Error
306
362
  end
307
363
 
308
364
  post_change = datastore[:users].to_a.map(&:to_a).sort
@@ -1,20 +1,18 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
 
8
7
  RSpec.describe "Graph traversal" do
9
- include_context "mapper setup"
10
- include_context "sequel persistence setup"
8
+ include_context "object store setup"
11
9
  include_context "seed data setup"
12
10
 
13
11
  describe "associations" do
14
- subject(:mapper) { user_mapper }
12
+ subject(:user_store) { object_store[:users] }
15
13
 
16
14
  let(:user_query) {
17
- mapper.where(id: "users/1")
15
+ user_store.where(id: "users/1")
18
16
  }
19
17
 
20
18
  let(:user) { user_query.first }
@@ -43,6 +41,16 @@ RSpec.describe "Graph traversal" do
43
41
  ).to eq("oh noes")
44
42
  end
45
43
 
44
+ context "when a many to one association foreign key is nil" do
45
+ before do
46
+ datastore[:comments].update(commenter_id: nil)
47
+ end
48
+
49
+ it "populates that association with a nil" do
50
+ expect(user.posts.flat_map(&:comments).flat_map(&:commenter).uniq).to eq([nil])
51
+ end
52
+ end
53
+
46
54
  describe "lazy loading" do
47
55
  let(:post_factory) { double(:post_factory, call: nil) }
48
56
 
@@ -1,15 +1,13 @@
1
1
  require "spec_helper"
2
- require "support/mapper_setup"
3
- require "support/sequel_persistence_setup"
2
+ require "support/object_store_setup"
4
3
  require "support/have_persisted_matcher"
5
4
 
6
5
  RSpec.describe "Persist a new graph in empty datastore" do
7
- include_context "mapper setup"
8
- include_context "sequel persistence setup"
6
+ include_context "object store setup"
9
7
 
10
8
  context "given a graph of new objects" do
11
9
  it "persists the root node" do
12
- user_mapper.save(hansel)
10
+ object_store[:users].save(hansel)
13
11
 
14
12
  expect(datastore).to have_persisted(:users, {
15
13
  id: hansel.id,
@@ -20,7 +18,7 @@ RSpec.describe "Persist a new graph in empty datastore" do
20
18
  end
21
19
 
22
20
  it "persists one to many related nodes 1 level deep" do
23
- user_mapper.save(hansel)
21
+ object_store[:users].save(hansel)
24
22
 
25
23
  expect(datastore).to have_persisted(:posts, hash_including(
26
24
  id: "posts/1",
@@ -39,7 +37,7 @@ RSpec.describe "Persist a new graph in empty datastore" do
39
37
 
40
38
  context "deep node with two foreign keys" do
41
39
  it "persists the node with both foreign keys" do
42
- user_mapper.save(hansel)
40
+ object_store[:users].save(hansel)
43
41
 
44
42
  expect(datastore).to have_persisted(:comments, {
45
43
  id: "comments/1",
@@ -51,7 +49,7 @@ RSpec.describe "Persist a new graph in empty datastore" do
51
49
  end
52
50
 
53
51
  it "persists many to many related nodes" do
54
- user_mapper.save(hansel)
52
+ object_store[:users].save(hansel)
55
53
 
56
54
  expect(datastore).to have_persisted(:categories, {
57
55
  id: "categories/1",
@@ -60,12 +58,48 @@ RSpec.describe "Persist a new graph in empty datastore" do
60
58
  end
61
59
 
62
60
  it "persists a 'join table' to faciliate many to many" do
63
- user_mapper.save(hansel)
61
+ object_store[:users].save(hansel)
64
62
 
65
63
  expect(datastore).to have_persisted(:categories_to_posts, {
66
64
  category_id: "categories/1",
67
65
  post_id: "posts/1",
68
66
  })
69
67
  end
68
+
69
+ context "when saving an object a second time" do
70
+ context "when the first time fails" do
71
+ before do
72
+ error = attempt_unpersistable_save
73
+ unless error.is_a?(Terrestrial::UpsertError)
74
+ raise "Unpersistable save did not raise an error. Expected this save to fail."
75
+ end
76
+ hansel.email = "hansel@gmail.com"
77
+ end
78
+
79
+ let(:unpersistable) { ->() {} }
80
+
81
+ it "successfully saves all attributes the second time" do
82
+ object_store[:users].save(hansel)
83
+
84
+ expect(datastore).to have_persisted(
85
+ :users,
86
+ hash_including(
87
+ id: hansel.id,
88
+ email: hansel.email,
89
+ first_name: hansel.first_name,
90
+ last_name: hansel.last_name,
91
+ )
92
+ )
93
+ end
94
+
95
+ def attempt_unpersistable_save
96
+ email = hansel.email
97
+ hansel.email = unpersistable
98
+ object_store[:users].save(hansel)
99
+ rescue Terrestrial::UpsertError => e
100
+ e
101
+ end
102
+ end
103
+ end
70
104
  end
71
105
  end
@@ -1,18 +1,16 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
 
8
7
  RSpec.describe "Object identity" do
9
- include_context "mapper setup"
10
- include_context "sequel persistence setup"
8
+ include_context "object store setup"
11
9
  include_context "seed data setup"
12
10
 
13
- subject(:mapper) { mappers.fetch(:users) }
11
+ subject(:user_store) { object_store.fetch(:users) }
14
12
 
15
- let(:user) { mapper.where(id: "users/1").first }
13
+ let(:user) { user_store.where(id: "users/1").first }
16
14
  let(:post) { user.posts.first }
17
15
 
18
16
  context "when using arbitrary where query" do
@@ -46,7 +44,7 @@ RSpec.describe "Object identity" do
46
44
  end
47
45
 
48
46
  context "when eager loading" do
49
- let(:user_query) { mapper.where(id: "users/1") }
47
+ let(:user_query) { user_store.where(id: "users/1") }
50
48
 
51
49
  let(:eager_category) {
52
50
  user_query
@@ -1,18 +1,16 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
  require "terrestrial/configurations/conventional_configuration"
8
7
 
9
8
  RSpec.describe "Ordered associations" do
10
- include_context "mapper setup"
11
- include_context "sequel persistence setup"
9
+ include_context "object store setup"
12
10
  include_context "seed data setup"
13
11
 
14
12
  context "one to many association ordered by `created_at DESC`" do
15
- let(:posts) { user_mapper.first.posts }
13
+ let(:posts) { object_store[:users].first.posts }
16
14
 
17
15
  before do
18
16
  configs.fetch(:users).fetch(:associations).fetch(:posts).merge!(
@@ -40,7 +38,7 @@ RSpec.describe "Ordered associations" do
40
38
  )
41
39
  end
42
40
 
43
- let(:categories) { user_mapper.first.posts.first.categories }
41
+ let(:categories) { object_store[:users].first.posts.first.categories }
44
42
 
45
43
  it "enumerates the objects in order specified in the config" do
46
44
  expect(categories.map(&:id)).to eq(
@@ -1,17 +1,15 @@
1
1
  require "spec_helper"
2
2
 
3
- require "support/mapper_setup"
4
- require "support/sequel_persistence_setup"
3
+ require "support/object_store_setup"
5
4
  require "support/seed_data_setup"
6
5
  require "terrestrial"
7
6
  require "terrestrial/configurations/conventional_configuration"
8
7
 
9
8
  RSpec.describe "Predefined subset queries" do
10
- include_context "mapper setup"
11
- include_context "sequel persistence setup"
9
+ include_context "object store setup"
12
10
  include_context "seed data setup"
13
11
 
14
- subject(:users) { user_mapper }
12
+ subject(:users) { object_store[:users] }
15
13
 
16
14
  context "on the top level mapper" do
17
15
  context "subset is defined with a block" do
@@ -41,7 +39,7 @@ RSpec.describe "Predefined subset queries" do
41
39
  configs.fetch(:posts).merge!(
42
40
  subsets: {
43
41
  body_contains: ->(dataset, search_string) {
44
- dataset.where("body like '%#{search_string}%'")
42
+ dataset.where(body: /#{search_string}/)
45
43
  },
46
44
  },
47
45
  )