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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -9
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +28 -0
  6. data/Gemfile.lock +73 -0
  7. data/LICENSE.txt +22 -0
  8. data/MissingFeatures.md +64 -0
  9. data/README.md +161 -16
  10. data/Rakefile +30 -0
  11. data/TODO.md +41 -0
  12. data/features/env.rb +60 -0
  13. data/features/example.feature +120 -0
  14. data/features/step_definitions/example_steps.rb +46 -0
  15. data/lib/terrestrial/abstract_record.rb +99 -0
  16. data/lib/terrestrial/association_loaders.rb +52 -0
  17. data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
  18. data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
  19. data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
  20. data/lib/terrestrial/dataset.rb +49 -0
  21. data/lib/terrestrial/deleted_record.rb +20 -0
  22. data/lib/terrestrial/dirty_map.rb +42 -0
  23. data/lib/terrestrial/graph_loader.rb +63 -0
  24. data/lib/terrestrial/graph_serializer.rb +91 -0
  25. data/lib/terrestrial/identity_map.rb +22 -0
  26. data/lib/terrestrial/lazy_collection.rb +74 -0
  27. data/lib/terrestrial/lazy_object_proxy.rb +55 -0
  28. data/lib/terrestrial/many_to_many_association.rb +138 -0
  29. data/lib/terrestrial/many_to_one_association.rb +66 -0
  30. data/lib/terrestrial/mapper_facade.rb +137 -0
  31. data/lib/terrestrial/one_to_many_association.rb +66 -0
  32. data/lib/terrestrial/public_conveniencies.rb +139 -0
  33. data/lib/terrestrial/query_order.rb +32 -0
  34. data/lib/terrestrial/relation_mapping.rb +50 -0
  35. data/lib/terrestrial/serializer.rb +18 -0
  36. data/lib/terrestrial/short_inspection_string.rb +18 -0
  37. data/lib/terrestrial/struct_factory.rb +17 -0
  38. data/lib/terrestrial/subset_queries_proxy.rb +11 -0
  39. data/lib/terrestrial/upserted_record.rb +15 -0
  40. data/lib/terrestrial/version.rb +1 -1
  41. data/lib/terrestrial.rb +5 -2
  42. data/sequel_mapper.gemspec +31 -0
  43. data/spec/config_override_spec.rb +193 -0
  44. data/spec/custom_serializers_spec.rb +49 -0
  45. data/spec/deletion_spec.rb +101 -0
  46. data/spec/graph_persistence_spec.rb +313 -0
  47. data/spec/graph_traversal_spec.rb +121 -0
  48. data/spec/new_graph_persistence_spec.rb +71 -0
  49. data/spec/object_identity_spec.rb +70 -0
  50. data/spec/ordered_association_spec.rb +51 -0
  51. data/spec/persistence_efficiency_spec.rb +224 -0
  52. data/spec/predefined_queries_spec.rb +62 -0
  53. data/spec/proxying_spec.rb +88 -0
  54. data/spec/querying_spec.rb +48 -0
  55. data/spec/readme_examples_spec.rb +35 -0
  56. data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
  57. data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
  58. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  59. data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
  60. data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
  61. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  62. data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
  63. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  64. data/spec/spec_helper.rb +36 -0
  65. data/spec/support/blog_schema.rb +38 -0
  66. data/spec/support/have_persisted_matcher.rb +19 -0
  67. data/spec/support/mapper_setup.rb +221 -0
  68. data/spec/support/mock_sequel.rb +193 -0
  69. data/spec/support/object_graph_setup.rb +139 -0
  70. data/spec/support/seed_data_setup.rb +165 -0
  71. data/spec/support/sequel_persistence_setup.rb +19 -0
  72. data/spec/support/sequel_test_support.rb +166 -0
  73. metadata +207 -13
  74. data/.travis.yml +0 -4
  75. data/bin/console +0 -14
  76. data/bin/setup +0 -7
  77. 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