terrestrial 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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