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,46 @@
1
+ Given(/^the domain objects are defined$/) do |code_sample|
2
+ Object.module_eval(code_sample)
3
+ end
4
+
5
+ Given(/^a database connection is established$/) do |code_sample|
6
+ example_eval(code_sample)
7
+ end
8
+
9
+ Given(/^the associations are defined in the mapper configuration$/) do |code_sample|
10
+ example_eval(code_sample)
11
+ end
12
+
13
+ Given(/^a mapper is instantiated$/) do |code_sample|
14
+ example_eval(code_sample)
15
+ end
16
+
17
+ Given(/^a conventionally similar database schema for table "(.*?)"$/) do |table_name, schema_table|
18
+ create_table(table_name, parse_schema_table(schema_table))
19
+ end
20
+
21
+ When(/^a new graph of objects are created$/) do |code_sample|
22
+ @objects_to_be_saved_sample = code_sample
23
+ end
24
+
25
+ When(/^the new graph is saved$/) do |save_objects_code|
26
+ example_eval(
27
+ [@objects_to_be_saved_sample, save_objects_code].join("\n")
28
+ )
29
+ end
30
+
31
+ When(/^the following query is executed$/) do |code_sample|
32
+ @query = code_sample
33
+ @result = example_eval(code_sample)
34
+ end
35
+
36
+ Then(/^the persisted user object is returned with lazy associations$/) do |expected_inspection_string|
37
+ expect(normalise_inspection_string(@result.inspect))
38
+ .to eq(normalise_inspection_string(expected_inspection_string))
39
+ end
40
+
41
+ Then(/^the user's posts will be loaded once the association proxy receives an Enumerable message$/) do |expected_inspection_string|
42
+ posts = @result.posts.to_a
43
+
44
+ expect(normalise_inspection_string(posts.inspect))
45
+ .to eq(normalise_inspection_string(expected_inspection_string))
46
+ end
@@ -0,0 +1,99 @@
1
+ require "forwardable"
2
+ require "set"
3
+
4
+ module Terrestrial
5
+ class AbstractRecord
6
+ extend Forwardable
7
+ include Comparable
8
+ include Enumerable
9
+
10
+ def initialize(namespace, identity_fields, attributes = {}, depth = NoDepth)
11
+ @namespace = namespace
12
+ @identity_fields = identity_fields
13
+ @attributes = attributes
14
+ @depth = depth
15
+ end
16
+
17
+ attr_reader :namespace, :identity_fields, :attributes, :depth
18
+ private :attributes
19
+
20
+ def_delegators :to_h, :fetch
21
+
22
+ def if_upsert(&block)
23
+ self
24
+ end
25
+
26
+ def if_delete(&block)
27
+ self
28
+ end
29
+
30
+ def each(&block)
31
+ to_h.each(&block)
32
+ end
33
+
34
+ def keys
35
+ attributes.keys
36
+ end
37
+
38
+ def identity
39
+ attributes.select { |k,_v| identity_fields.include?(k) }
40
+ end
41
+
42
+ def non_identity_attributes
43
+ attributes.reject { |k| identity.include?(k) }
44
+ end
45
+
46
+ def merge(more_data)
47
+ new_with_raw_data(attributes.merge(more_data))
48
+ end
49
+
50
+ def merge!(more_data)
51
+ attributes.merge!(more_data)
52
+ end
53
+
54
+ def reject(&block)
55
+ new_with_raw_data(non_identity_attributes.reject(&block).merge(identity))
56
+ end
57
+
58
+ def to_h
59
+ attributes.to_h
60
+ end
61
+
62
+ def empty?
63
+ non_identity_attributes.empty?
64
+ end
65
+
66
+ def ==(other)
67
+ self.class === other &&
68
+ [operation, to_h] == [other.operation, other.to_h]
69
+ end
70
+
71
+ def <=>(other)
72
+ depth <=> other.depth
73
+ end
74
+
75
+ def subset?(other_record)
76
+ namespace == other_record.namespace &&
77
+ to_set.subset?(other_record.to_set)
78
+ end
79
+
80
+ protected
81
+
82
+ def operation
83
+ NoOp
84
+ end
85
+
86
+ def to_set
87
+ Set.new(attributes.to_a)
88
+ end
89
+
90
+ private
91
+
92
+ NoOp = Module.new
93
+ NoDepth = Module.new
94
+
95
+ def new_with_raw_data(new_raw_data)
96
+ self.class.new(namespace, identity_fields, new_raw_data, depth)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,52 @@
1
+ module Terrestrial
2
+ module AssociationLoaders
3
+ class OneToMany
4
+ def initialize(type:, mapping_name:, foreign_key:, key:, proxy_factory:)
5
+ @type = type
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @proxy_factory = proxy_factory
10
+ @eager_loads = {}
11
+ end
12
+
13
+ attr_reader :type, :mapping_name, :foreign_key, :key, :proxy_factory
14
+ private :type, :mapping_name, :foreign_key, :key, :proxy_factory
15
+
16
+ def fetch(*args, &block)
17
+ {
18
+ key: key,
19
+ foreign_key: foreign_key,
20
+ type: type,
21
+ mapping_name: mapping_name,
22
+ }.fetch(*args, &block)
23
+ end
24
+
25
+ def call(mappings, record, &object_pipeline)
26
+ mapping = mappings.fetch(mapping_name)
27
+
28
+ proxy_factory.call(
29
+ query: query(mapping, record),
30
+ loader: object_pipeline.call(mapping),
31
+ association_loader: self,
32
+ )
33
+ end
34
+
35
+ def query(mapping, record)
36
+ foreign_key_value = record.fetch(key)
37
+
38
+ ->(datastore) {
39
+ @eager_loads.fetch(record) {
40
+ datastore[mapping.namespace]
41
+ .where(foreign_key => foreign_key_value)
42
+ }
43
+ }
44
+ end
45
+
46
+ def eager_load(dataset, association_name)
47
+ datastore[mapping.namespace]
48
+ .where(foreign_key => dataset.select(key))
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,81 @@
1
+ require "forwardable"
2
+ require "terrestrial/short_inspection_string"
3
+
4
+ module Terrestrial
5
+ class CollectionMutabilityProxy
6
+ extend Forwardable
7
+ include ShortInspectionString
8
+ include Enumerable
9
+
10
+ def initialize(collection)
11
+ @collection = collection
12
+ @added_nodes = []
13
+ @deleted_nodes = []
14
+ end
15
+
16
+ attr_reader :collection, :deleted_nodes, :added_nodes
17
+ private :collection, :deleted_nodes, :added_nodes
18
+
19
+ def_delegators :collection, :where, :subset
20
+
21
+ def each_loaded(&block)
22
+ loaded_enum.each(&block)
23
+ end
24
+
25
+ def each_deleted(&block)
26
+ @deleted_nodes.each(&block)
27
+ end
28
+
29
+ def to_ary
30
+ to_a
31
+ end
32
+
33
+ def each(&block)
34
+ if block
35
+ enum.each(&block)
36
+ self
37
+ else
38
+ enum
39
+ end
40
+ end
41
+
42
+ def delete(node)
43
+ @deleted_nodes.push(node)
44
+ self
45
+ end
46
+
47
+ def push(node)
48
+ @added_nodes.push(node)
49
+ end
50
+
51
+ private
52
+
53
+ def loaded_enum
54
+ Enumerator.new do |yielder|
55
+ collection.each_loaded do |element|
56
+ yielder.yield(element) unless deleted?(element)
57
+ end
58
+
59
+ added_nodes.each do |node|
60
+ yielder.yield(node)
61
+ end
62
+ end
63
+ end
64
+
65
+ def enum
66
+ Enumerator.new do |yielder|
67
+ collection.each do |element|
68
+ yielder.yield(element) unless deleted?(element)
69
+ end
70
+
71
+ added_nodes.each do |node|
72
+ yielder.yield(node)
73
+ end
74
+ end
75
+ end
76
+
77
+ def deleted?(node)
78
+ @deleted_nodes.include?(node)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,186 @@
1
+ require "terrestrial/query_order"
2
+
3
+ module Terrestrial
4
+ module Configurations
5
+ require "terrestrial/one_to_many_association"
6
+ require "terrestrial/many_to_many_association"
7
+ require "terrestrial/many_to_one_association"
8
+ require "terrestrial/collection_mutability_proxy"
9
+ require "terrestrial/lazy_collection"
10
+ require "terrestrial/lazy_object_proxy"
11
+
12
+ class ConventionalAssociationConfiguration
13
+ def initialize(mapping_name, mappings, datastore)
14
+ @local_mapping_name = mapping_name
15
+ @mappings = mappings
16
+ @local_mapping = mappings.fetch(local_mapping_name)
17
+ @datastore = datastore
18
+ end
19
+
20
+ attr_reader :local_mapping_name, :local_mapping, :mappings, :datastore
21
+ private :local_mapping_name, :local_mapping, :mappings, :datastore
22
+
23
+ DEFAULT = Module.new
24
+
25
+ def has_many(association_name, key: DEFAULT, foreign_key: DEFAULT, mapping_name: DEFAULT, order_fields: DEFAULT, order_direction: DEFAULT)
26
+ defaults = {
27
+ mapping_name: association_name,
28
+ foreign_key: [INFLECTOR.singularize(local_mapping_name), "_id"].join.to_sym,
29
+ key: :id,
30
+ order_fields: [],
31
+ order_direction: "ASC",
32
+ }
33
+
34
+ specified = {
35
+ mapping_name: mapping_name,
36
+ foreign_key: foreign_key,
37
+ key: key,
38
+ order_fields: order_fields,
39
+ order_direction: order_direction,
40
+ }.reject { |_k,v|
41
+ v == DEFAULT
42
+ }
43
+
44
+ config = defaults.merge(specified)
45
+ associated_mapping_name = config.fetch(:mapping_name)
46
+ associated_mapping = mappings.fetch(associated_mapping_name)
47
+
48
+ local_mapping.add_association(
49
+ association_name,
50
+ has_many_mapper(**config)
51
+ )
52
+ end
53
+
54
+ def belongs_to(association_name, key: DEFAULT, foreign_key: DEFAULT, mapping_name: DEFAULT)
55
+ defaults = {
56
+ key: :id,
57
+ foreign_key: [association_name, "_id"].join.to_sym,
58
+ mapping_name: INFLECTOR.pluralize(association_name).to_sym,
59
+ }
60
+
61
+ specified = {
62
+ mapping_name: mapping_name,
63
+ foreign_key: foreign_key,
64
+ key: key,
65
+ }.reject { |_k,v|
66
+ v == DEFAULT
67
+ }
68
+
69
+ config = defaults.merge(specified)
70
+
71
+ associated_mapping_name = config.fetch(:mapping_name)
72
+ associated_mapping = mappings.fetch(associated_mapping_name)
73
+
74
+ local_mapping.add_association(
75
+ association_name,
76
+ belongs_to_mapper(**config)
77
+ )
78
+ end
79
+
80
+ def has_many_through(association_name, key: DEFAULT, foreign_key: DEFAULT, mapping_name: DEFAULT, through_mapping_name: DEFAULT, association_key: DEFAULT, association_foreign_key: DEFAULT, order_fields: DEFAULT, order_direction: DEFAULT)
81
+ defaults = {
82
+ mapping_name: association_name,
83
+ key: :id,
84
+ association_key: :id,
85
+ foreign_key: [INFLECTOR.singularize(local_mapping_name), "_id"].join.to_sym,
86
+ association_foreign_key: [INFLECTOR.singularize(association_name), "_id"].join.to_sym,
87
+ order_fields: [],
88
+ order_direction: "ASC",
89
+ }
90
+
91
+ specified = {
92
+ mapping_name: mapping_name,
93
+ key: key,
94
+ association_key: association_key,
95
+ foreign_key: foreign_key,
96
+ association_foreign_key: association_foreign_key,
97
+ order_fields: order_fields,
98
+ order_direction: order_direction,
99
+ }.reject { |_k,v|
100
+ v == DEFAULT
101
+ }
102
+
103
+ config = defaults.merge(specified)
104
+ associated_mapping = mappings.fetch(config.fetch(:mapping_name))
105
+
106
+ if through_mapping_name == DEFAULT
107
+ through_mapping_name = [
108
+ associated_mapping.name,
109
+ local_mapping.name,
110
+ ].sort.join("_to_").to_sym
111
+ end
112
+
113
+ join_table_name = mappings.fetch(through_mapping_name).namespace
114
+ config = config
115
+ .merge(
116
+ through_mapping_name: through_mapping_name,
117
+ through_dataset: datastore[join_table_name.to_sym],
118
+ )
119
+
120
+ local_mapping.add_association(
121
+ association_name,
122
+ has_many_through_mapper(**config)
123
+ )
124
+ end
125
+
126
+ private
127
+
128
+ def has_many_mapper(mapping_name:, key:, foreign_key:, order_fields:, order_direction:)
129
+ OneToManyAssociation.new(
130
+ mapping_name: mapping_name,
131
+ foreign_key: foreign_key,
132
+ key: key,
133
+ order: query_order(order_fields, order_direction),
134
+ proxy_factory: collection_proxy_factory,
135
+ )
136
+ end
137
+
138
+ def belongs_to_mapper(mapping_name:, key:, foreign_key:)
139
+ ManyToOneAssociation.new(
140
+ mapping_name: mapping_name,
141
+ foreign_key: foreign_key,
142
+ key: key,
143
+ proxy_factory: single_object_proxy_factory,
144
+ )
145
+ end
146
+
147
+ def has_many_through_mapper(mapping_name:, key:, foreign_key:, association_key:, association_foreign_key:, through_mapping_name:, through_dataset:, order_fields:, order_direction:)
148
+ ManyToManyAssociation.new(
149
+ mapping_name: mapping_name,
150
+ join_mapping_name: through_mapping_name,
151
+ key: key,
152
+ foreign_key: foreign_key,
153
+ association_key: association_key,
154
+ association_foreign_key: association_foreign_key,
155
+ proxy_factory: collection_proxy_factory,
156
+ order: query_order(order_fields, order_direction),
157
+ )
158
+ end
159
+
160
+ def single_object_proxy_factory
161
+ ->(query:, loader:, preloaded_data:) {
162
+ LazyObjectProxy.new(
163
+ ->{ loader.call(query.first) },
164
+ preloaded_data,
165
+ )
166
+ }
167
+ end
168
+
169
+ def collection_proxy_factory
170
+ ->(query:, loader:, mapping_name:) {
171
+ CollectionMutabilityProxy.new(
172
+ LazyCollection.new(
173
+ query,
174
+ loader,
175
+ mappings.fetch(mapping_name).subsets,
176
+ )
177
+ )
178
+ }
179
+ end
180
+
181
+ def query_order(fields, direction)
182
+ QueryOrder.new(fields: fields, direction: direction)
183
+ end
184
+ end
185
+ end
186
+ end