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,3 +1,7 @@
1
+ require "terrestrial/error"
2
+ require "terrestrial/upserted_record"
3
+ require "terrestrial/deleted_record"
4
+
1
5
  module Terrestrial
2
6
  class RelationMapping
3
7
  def initialize(name:, namespace:, fields:, primary_key:, factory:, serializer:, associations:, subsets:)
@@ -12,11 +16,18 @@ module Terrestrial
12
16
  end
13
17
 
14
18
  attr_reader :name, :namespace, :fields, :primary_key, :factory, :serializer, :associations, :subsets
19
+ private :factory, :serializer
15
20
 
16
21
  def add_association(name, new_association)
17
22
  @associations = associations.merge(name => new_association)
18
23
  end
19
24
 
25
+ def load(record)
26
+ factory.call(record)
27
+ rescue => e
28
+ raise LoadError.new(namespace, factory, record, e)
29
+ end
30
+
20
31
  def serialize(object, depth, foreign_keys = {})
21
32
  object_attributes = serializer.call(object)
22
33
 
@@ -24,8 +35,18 @@ module Terrestrial
24
35
  record(object_attributes, depth, foreign_keys),
25
36
  extract_associations(object_attributes)
26
37
  ]
38
+ rescue => e
39
+ raise SerializationError.new(name, serializer, object, e)
40
+ end
41
+
42
+ def delete(object, depth)
43
+ object_attributes = serializer.call(object)
44
+
45
+ [deleted_record(object_attributes, depth)]
27
46
  end
28
47
 
48
+ private
49
+
29
50
  def record(attributes, depth, foreign_keys)
30
51
  UpsertedRecord.new(
31
52
  namespace,
@@ -35,6 +56,15 @@ module Terrestrial
35
56
  )
36
57
  end
37
58
 
59
+ def deleted_record(attributes, depth)
60
+ DeletedRecord.new(
61
+ namespace,
62
+ primary_key,
63
+ attributes,
64
+ depth,
65
+ )
66
+ end
67
+
38
68
  def extract_associations(attributes)
39
69
  Hash[
40
70
  associations.map { |name, _association|
@@ -1,9 +1,11 @@
1
1
  require "terrestrial/graph_serializer"
2
2
  require "terrestrial/graph_loader"
3
+ require "terrestrial/inspection_string"
3
4
 
4
5
  module Terrestrial
5
- class MapperFacade
6
+ class RelationalStore
6
7
  include Enumerable
8
+ include InspectionString
7
9
 
8
10
  def initialize(mappings:, mapping_name:, datastore:, dataset:, load_pipeline:, dump_pipeline:)
9
11
  @mappings = mappings
@@ -133,5 +135,13 @@ module Terrestrial
133
135
  object_load_pipeline: load_pipeline,
134
136
  )
135
137
  end
138
+
139
+ def inspectable_properties
140
+ [
141
+ :mapping_name,
142
+ :dataset,
143
+ :eager_load,
144
+ ]
145
+ end
136
146
  end
137
147
  end
@@ -1,3 +1,3 @@
1
1
  module Terrestrial
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,20 +1,18 @@
1
1
  require "spec_helper"
2
2
  require "ostruct"
3
3
 
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
  require "terrestrial/configurations/conventional_configuration"
10
9
 
11
10
  RSpec.describe "Configuration override" do
12
- include_context "mapper setup"
13
- include_context "sequel persistence setup"
11
+ include_context "object store setup"
14
12
  include_context "seed data setup"
15
13
 
16
- let(:mappers) {
17
- Terrestrial.mappers(mappings: override_config, datastore: datastore)
14
+ let(:object_store) {
15
+ Terrestrial.object_store(mappings: override_config, datastore: datastore)
18
16
  }
19
17
 
20
18
  let(:override_config) {
@@ -26,7 +24,7 @@ RSpec.describe "Configuration override" do
26
24
  }
27
25
 
28
26
  let(:user) {
29
- user_mapper.where(id: "users/1").first
27
+ object_store[:users].where(id: "users/1").first
30
28
  }
31
29
 
32
30
  context "override the root mapper factory" do
@@ -89,15 +87,13 @@ RSpec.describe "Configuration override" do
89
87
  end
90
88
  }
91
89
 
92
- let(:datastore) { db_connection }
93
-
94
90
  let(:unconventional_table_name) {
95
91
  :users_is_called_this_weird_thing_perhaps_for_legacy_reasons
96
92
  }
97
93
 
98
94
  it "maps data from the specified relation" do
99
95
  expect(
100
- user_mapper.map(&:id)
96
+ object_store[:users].map(&:id)
101
97
  ).to eq(["users/1", "users/2", "users/3"])
102
98
  end
103
99
  end
@@ -184,10 +180,10 @@ RSpec.describe "Configuration override" do
184
180
  }
185
181
 
186
182
  it "provides access to the same data via the different configs" do
187
- expect(mappers[:t1_users].first.id).to eq("users/1")
188
- expect(mappers[:t1_users].first).to be_a(TypeOneUser)
189
- expect(mappers[:t2_users].first.id).to eq("users/1")
190
- expect(mappers[:t2_users].first).to be_a(TypeTwoUser)
183
+ expect(object_store[:t1_users].first.id).to eq("users/1")
184
+ expect(object_store[:t1_users].first).to be_a(TypeOneUser)
185
+ expect(object_store[:t2_users].first.id).to eq("users/1")
186
+ expect(object_store[:t2_users].first).to be_a(TypeTwoUser)
191
187
  end
192
188
  end
193
189
  end
@@ -1,19 +1,17 @@
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
  require "terrestrial/configurations/conventional_configuration"
10
9
 
11
10
  RSpec.describe "Config override" do
12
- include_context "mapper setup"
13
- include_context "sequel persistence setup"
11
+ include_context "object store setup"
14
12
  include_context "seed data setup"
15
13
 
16
- let(:user) { user_mapper.where(id: "users/1").first }
14
+ let(:user) { object_store[:users].where(id: "users/1").first }
17
15
 
18
16
  context "with an object that has private fields" do
19
17
  let(:user_serializer) {
@@ -36,7 +34,7 @@ RSpec.describe "Config override" do
36
34
  user.first_name = "This won't work"
37
35
  user.last_name = "because the serialzer is weird"
38
36
 
39
- user_mapper.save(user)
37
+ object_store[:users].save(user)
40
38
 
41
39
  expect(datastore).to have_persisted(:users, hash_including(
42
40
  id: user.id,
@@ -1,29 +1,27 @@
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 "support/have_persisted_matcher"
7
6
  require "terrestrial"
8
7
 
9
8
  RSpec.describe "Deletion" 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) { user_mapper }
12
+ subject(:user_store) { object_store[: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
  let(:reloaded_user) {
21
- mapper.where(id: "users/1").first
19
+ user_store.where(id: "users/1").first
22
20
  }
23
21
 
24
22
  describe "Deleting the root" do
25
23
  it "deletes the root object" do
26
- mapper.delete(user, cascade: true)
24
+ user_store.delete(user, cascade: true)
27
25
 
28
26
  expect(datastore).not_to have_persisted(
29
27
  :users,
@@ -37,7 +35,7 @@ RSpec.describe "Deletion" do
37
35
  end
38
36
 
39
37
  it "deletes the root object" do
40
- mapper.delete(user)
38
+ user_store.delete(user)
41
39
 
42
40
  expect(datastore).not_to have_persisted(
43
41
  :users,
@@ -47,7 +45,7 @@ RSpec.describe "Deletion" do
47
45
 
48
46
  it "does not delete the child objects" do
49
47
  expect {
50
- mapper.delete(user)
48
+ user_store.delete(user)
51
49
  }.not_to change { [datastore[:posts], datastore[:comments]].map(&:count) }
52
50
  end
53
51
  end
@@ -64,7 +62,7 @@ RSpec.describe "Deletion" do
64
62
 
65
63
  it "deletes the specified node" do
66
64
  user.posts.delete(post)
67
- mapper.save(user)
65
+ user_store.save(user)
68
66
 
69
67
  expect(datastore).not_to have_persisted(
70
68
  :posts,
@@ -74,7 +72,7 @@ RSpec.describe "Deletion" do
74
72
 
75
73
  it "does not delete the parent object" do
76
74
  user.posts.delete(post)
77
- mapper.save(user)
75
+ user_store.save(user)
78
76
 
79
77
  expect(datastore).to have_persisted(
80
78
  :users,
@@ -84,7 +82,7 @@ RSpec.describe "Deletion" do
84
82
 
85
83
  it "does not delete the sibling objects" do
86
84
  user.posts.delete(post)
87
- mapper.save(user)
85
+ user_store.save(user)
88
86
 
89
87
  expect(reloaded_user.posts.count).to be > 0
90
88
  end
@@ -92,7 +90,7 @@ RSpec.describe "Deletion" do
92
90
  it "does not cascade delete" do
93
91
  expect {
94
92
  user.posts.delete(post)
95
- mapper.save(user)
93
+ user_store.save(user)
96
94
  }.not_to change {
97
95
  datastore[:comments].map { |r| r.fetch(:id) }
98
96
  }
@@ -0,0 +1,61 @@
1
+ require "terrestrial"
2
+ require "terrestrial/configurations/conventional_configuration"
3
+
4
+ RSpec.describe "factory error handling" do
5
+ context "factory with too few parameters" do
6
+ before do
7
+ seed_user(record)
8
+ override_user_factory_with(no_parameters_factory)
9
+ end
10
+
11
+ let(:record) {
12
+ {
13
+ id: "users/999",
14
+ first_name: "Badger",
15
+ last_name: "Smith",
16
+ email: "b@smith.biz",
17
+ }
18
+ }
19
+
20
+ let(:no_parameters_factory) {
21
+ ->() { }
22
+ }
23
+
24
+ it "raises a helpful error message" do
25
+ error = nil
26
+ begin
27
+ load_first_user
28
+ rescue Terrestrial::Error => error
29
+ end
30
+
31
+ expect(error.message).to eq(
32
+ [
33
+ "Error loading record from `users` relation `#{record.inspect}`.",
34
+ "Using: `#{no_parameters_factory.inspect}`.",
35
+ "Check that the factory is compatible.",
36
+ "Got Error: ArgumentError wrong number of arguments (given 1, expected 0)",
37
+ ].join("\n")
38
+ )
39
+ end
40
+ end
41
+
42
+ def load_first_user
43
+ @object_store[:users].first
44
+ end
45
+
46
+ def override_user_factory_with(factory)
47
+ config = Terrestrial.config(datastore)
48
+ .setup_mapping(:users) { |users|
49
+ users.factory(factory)
50
+ }
51
+
52
+ @object_store = Terrestrial.object_store(
53
+ mappings: config,
54
+ datastore: datastore,
55
+ )
56
+ end
57
+
58
+ def seed_user(record)
59
+ datastore[:users].insert(record)
60
+ end
61
+ end
@@ -0,0 +1,50 @@
1
+ require "support/object_store_setup"
2
+
3
+ RSpec.describe "Serialization error handling" do
4
+ include_context "object store setup"
5
+ context "when a domain object is incompatible with its serializer" do
6
+ before do
7
+ override_user_serializer_with(incompatible_custom_serializer)
8
+ end
9
+
10
+ let(:user) { Object.new }
11
+
12
+ let(:incompatible_custom_serializer) {
13
+ ->(x) { raise "I am incompatible" }
14
+ }
15
+
16
+ it "rescues and re-raises a more detailed error" do
17
+ error = nil
18
+ begin
19
+ save_user
20
+ rescue Terrestrial::SerializationError => error
21
+ end
22
+
23
+ expect(error.message).to eq(
24
+ [
25
+ "Error serializing object with mapping `users` `#{user.inspect}`.",
26
+ "Using serializer: `#{incompatible_custom_serializer.inspect}`.",
27
+ "Check the specified serializer can transform objects into a Hash.",
28
+ "Got Error: RuntimeError I am incompatible",
29
+ ].join("\n")
30
+ )
31
+ end
32
+
33
+ # TODO: make configuration easier override
34
+ def override_user_serializer_with(serializer)
35
+ config = Terrestrial.config(datastore)
36
+ .setup_mapping(:users) { |users|
37
+ users.serializer(serializer)
38
+ }
39
+
40
+ @object_store = Terrestrial.object_store(
41
+ mappings: config,
42
+ datastore: datastore,
43
+ )
44
+ end
45
+
46
+ def save_user
47
+ @object_store[:users].save(user)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,132 @@
1
+ require "support/object_store_setup"
2
+ require "terrestrial/inspection_string"
3
+
4
+ RSpec.describe "Upsert error handling" do
5
+ include_context "object store setup"
6
+ let(:user) { double(:user) }
7
+
8
+ context "with an record that raises error on persistence" do
9
+ before do
10
+ use_custom_serializer( ->(_) { unpersistable_record } )
11
+ end
12
+ let(:unpersistable_record) { UnpersistableRecord.new(original_error) }
13
+ let(:original_error) { RuntimeError.new("Cannot upsert") }
14
+
15
+ it "raises an UpsertError with detail of the original error" do
16
+ error = nil
17
+ begin
18
+ save_user
19
+ rescue Terrestrial::UpsertError => error
20
+ end
21
+
22
+ expect(error.message).to eq(
23
+ [
24
+ "Error upserting record into `users` with data `#{unpersistable_record}`.",
25
+ "Got Error: #{original_error.class} #{original_error.message}",
26
+ ].join("\n")
27
+ )
28
+ end
29
+ end
30
+
31
+ class UnpersistableRecord
32
+ include Terrestrial::InspectionString
33
+
34
+ def initialize(error)
35
+ @error = error
36
+ end
37
+
38
+ def to_h
39
+ # This is used in error reporting
40
+ self
41
+ end
42
+
43
+ def method_missing(*_)
44
+ # Raising on anything else ensures a problem while persisting
45
+ raise_if_upserting
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ def raise_if_upserting
52
+ raise(@error) if caller.any? { |line| /if_upsert/ === line }
53
+ end
54
+ end
55
+
56
+ context "serializer returns an extra field not in the schema" do
57
+ before do
58
+ use_custom_serializer(extra_field_serializer)
59
+ end
60
+
61
+ let(:extra_field_serializer) {
62
+ ->(_x) {
63
+ {
64
+ extra_field_that_does_not_match_column: "some value",
65
+ id: "users/999",
66
+ first_name: "Hansel",
67
+ last_name: "Trickett",
68
+ email: "hansel@tricketts.org",
69
+ }
70
+ }
71
+ }
72
+
73
+ it "filters the serialization result and raises no error" do
74
+ expect { save_user }.not_to raise_error
75
+ end
76
+ end
77
+
78
+ context "serialization result omits required fields" do
79
+ before do
80
+ use_custom_serializer(missing_id_serializer)
81
+ end
82
+
83
+ let(:missing_id_serializer) {
84
+ ->(_x) { serialization_result }
85
+ }
86
+
87
+ let(:serialization_result) {
88
+ object_attributes.reject { |k,v| k == :id }
89
+ }
90
+
91
+ let(:object_attributes) {
92
+ {
93
+ id: "users/999",
94
+ first_name: "Hansel",
95
+ last_name: "Trickett",
96
+ email: "hansel@tricketts.org",
97
+ }
98
+ }
99
+
100
+ it "raises an UpsertError with detail of the original error", backend: "sequel" do
101
+ error = nil
102
+ begin
103
+ save_user
104
+ rescue Terrestrial::UpsertError => error
105
+ end
106
+
107
+ expect(error.message).to eq(
108
+ [
109
+ "Error upserting record into `users` with data `#{serialization_result}`.",
110
+ "Got Error: Sequel::NotNullConstraintViolation PG::NotNullViolation: ERROR: null value in column \"id\" violates not-null constraint",
111
+ "DETAIL: Failing row contains (null, Hansel, Trickett, hansel@tricketts.org).\n",
112
+ ].join("\n")
113
+ )
114
+ end
115
+ end
116
+
117
+ def save_user
118
+ @object_store[:users].save(user)
119
+ end
120
+
121
+ def use_custom_serializer(serializer)
122
+ config = Terrestrial.config(datastore)
123
+ .setup_mapping(:users) { |users|
124
+ users.serializer(serializer)
125
+ }
126
+
127
+ @object_store = Terrestrial.object_store(
128
+ mappings: config,
129
+ datastore: datastore,
130
+ )
131
+ end
132
+ end