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