terrestrial 0.1.1 → 0.3.0

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.
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