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,18 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module ShortInspectionString
|
3
|
+
def inspect
|
4
|
+
"\#<#{self.class.name}:#{self.object_id.<<(1).to_s(16)} " +
|
5
|
+
inspectable_properties.map { |property|
|
6
|
+
[
|
7
|
+
property,
|
8
|
+
instance_variable_get("@#{property}").inspect
|
9
|
+
].join("=")
|
10
|
+
}
|
11
|
+
.join(" ") + ">"
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspectable_properties
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
class StructFactory
|
3
|
+
def initialize(struct_class)
|
4
|
+
@constructor = struct_class.method(:new)
|
5
|
+
@members = struct_class.members
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :constructor, :members
|
9
|
+
private :constructor, :members
|
10
|
+
|
11
|
+
def call(data)
|
12
|
+
constructor.call(
|
13
|
+
*members.map { |m| data.fetch(m, nil) }
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/terrestrial/version.rb
CHANGED
data/lib/terrestrial.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'terrestrial/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "terrestrial"
|
8
|
+
spec.version = Terrestrial::VERSION
|
9
|
+
spec.authors = ["Stephen Best"]
|
10
|
+
spec.email = ["bestie@gmail.com"]
|
11
|
+
spec.summary = %q{A data mapping ORM to make your objects feel less alien}
|
12
|
+
spec.description = %q{Terrestial persists your POROs while keeping them free of database concerns.}
|
13
|
+
spec.homepage = "https://github.com/bestie/terrestrial"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "pry", "~> 0.10.1"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.1"
|
25
|
+
spec.add_development_dependency "cucumber"
|
26
|
+
spec.add_development_dependency "pg", "~> 0.17.1"
|
27
|
+
|
28
|
+
spec.add_dependency "sequel", "~> 4.16"
|
29
|
+
spec.add_dependency "activesupport", "~> 4.0"
|
30
|
+
spec.add_dependency "fetchable", "~> 1.0"
|
31
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
require "support/mapper_setup"
|
5
|
+
require "support/sequel_persistence_setup"
|
6
|
+
require "support/seed_data_setup"
|
7
|
+
require "terrestrial"
|
8
|
+
|
9
|
+
require "terrestrial/configurations/conventional_configuration"
|
10
|
+
|
11
|
+
RSpec.describe "Configuration override" do
|
12
|
+
include_context "mapper setup"
|
13
|
+
include_context "sequel persistence setup"
|
14
|
+
include_context "seed data setup"
|
15
|
+
|
16
|
+
let(:mappers) {
|
17
|
+
Terrestrial.mappers(mappings: override_config, datastore: datastore)
|
18
|
+
}
|
19
|
+
|
20
|
+
let(:override_config) {
|
21
|
+
Terrestrial::Configurations::ConventionalConfiguration.new(datastore)
|
22
|
+
.setup_mapping(:users) { |users|
|
23
|
+
users.has_many :posts, foreign_key: :author_id
|
24
|
+
users.fields([:id, :first_name, :last_name, :email])
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
let(:user) {
|
29
|
+
user_mapper.where(id: "users/1").first
|
30
|
+
}
|
31
|
+
|
32
|
+
context "override the root mapper factory" do
|
33
|
+
context "with a Struct class" do
|
34
|
+
before do
|
35
|
+
override_config.setup_mapping(:users) do |config|
|
36
|
+
config.class(user_struct)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:user_struct) { Struct.new(*User.members) }
|
41
|
+
|
42
|
+
it "uses the class from the override" do
|
43
|
+
expect(user.class).to be(user_struct)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "override an association" do
|
49
|
+
context "with a callable factory" do
|
50
|
+
before do
|
51
|
+
override_config.setup_mapping(:posts) do |config|
|
52
|
+
config.factory(override_post_factory)
|
53
|
+
config.fields([:id, :subject, :body, :created_at])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
let(:post_class) { Class.new(OpenStruct) }
|
58
|
+
|
59
|
+
let(:override_post_factory) {
|
60
|
+
post_class.method(:new)
|
61
|
+
}
|
62
|
+
|
63
|
+
let(:posts) {
|
64
|
+
user.posts
|
65
|
+
}
|
66
|
+
|
67
|
+
it "uses the specified factory" do
|
68
|
+
expect(posts.first.class).to be(post_class)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "override table names" do
|
74
|
+
context "for just the top level mapping" do
|
75
|
+
before do
|
76
|
+
datastore.rename_table(:users, unconventional_table_name)
|
77
|
+
end
|
78
|
+
|
79
|
+
after do
|
80
|
+
datastore.rename_table(unconventional_table_name, :users)
|
81
|
+
end
|
82
|
+
|
83
|
+
let(:override_config) {
|
84
|
+
Terrestrial::Configurations::ConventionalConfiguration
|
85
|
+
.new(datastore)
|
86
|
+
.setup_mapping(:users) do |config|
|
87
|
+
config.relation_name unconventional_table_name
|
88
|
+
config.class(OpenStruct)
|
89
|
+
end
|
90
|
+
}
|
91
|
+
|
92
|
+
let(:datastore) { db_connection }
|
93
|
+
|
94
|
+
let(:unconventional_table_name) {
|
95
|
+
:users_is_called_this_weird_thing_perhaps_for_legacy_reasons
|
96
|
+
}
|
97
|
+
|
98
|
+
it "maps data from the specified relation" do
|
99
|
+
expect(
|
100
|
+
user_mapper.map(&:id)
|
101
|
+
).to eq(["users/1", "users/2", "users/3"])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "for associated collections" do
|
106
|
+
before do
|
107
|
+
rename_all_the_tables
|
108
|
+
setup_the_strange_table_name_mappings
|
109
|
+
end
|
110
|
+
|
111
|
+
after do
|
112
|
+
undo_rename_all_the_tables
|
113
|
+
end
|
114
|
+
|
115
|
+
def rename_all_the_tables
|
116
|
+
strange_table_name_map.each do |name, new_name|
|
117
|
+
datastore.rename_table(name, new_name)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def undo_rename_all_the_tables
|
122
|
+
strange_table_name_map.each do |original_name, strange_name|
|
123
|
+
datastore.rename_table(strange_name, original_name)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def setup_the_strange_table_name_mappings
|
128
|
+
override_config
|
129
|
+
.setup_mapping(:users) do |config|
|
130
|
+
config.class(OpenStruct)
|
131
|
+
config.relation_name strange_table_name_map.fetch(:users)
|
132
|
+
config.has_many(:posts, foreign_key: :author_id)
|
133
|
+
end
|
134
|
+
.setup_mapping(:posts) do |config|
|
135
|
+
config.class(OpenStruct)
|
136
|
+
config.relation_name strange_table_name_map.fetch(:posts)
|
137
|
+
config.belongs_to(:author, mapping_name: :users)
|
138
|
+
config.has_many_through(:categories, through_mapping_name: strange_table_name_map.fetch(:categories_to_posts))
|
139
|
+
end
|
140
|
+
.setup_mapping(:categories) do |config|
|
141
|
+
config.class(OpenStruct)
|
142
|
+
config.relation_name strange_table_name_map.fetch(:categories)
|
143
|
+
config.has_many_through(:posts, through_mapping_name: strange_table_name_map.fetch(:categories_to_posts))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
let(:strange_table_name_map) {
|
148
|
+
{
|
149
|
+
:users => :users_table_that_has_silly_name_perhaps_for_legacy_reasons,
|
150
|
+
:posts => :thank_you_past_self_for_this_excellent_name,
|
151
|
+
:categories => :these_are_the_categories_for_real,
|
152
|
+
:categories_to_posts => :this_one_is_just_full_of_bees,
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
it "maps data from the specified relation into a has many collection" do
|
157
|
+
expect(
|
158
|
+
user.posts.map(&:id)
|
159
|
+
).to eq(["posts/1", "posts/2"])
|
160
|
+
end
|
161
|
+
|
162
|
+
it "maps data from the specified relation into a `belongs_to` field" do
|
163
|
+
expect(
|
164
|
+
user.posts.first.author.__getobj__.object_id
|
165
|
+
).to eq(user.object_id)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "multiple mappings for single table" do
|
171
|
+
TypeOneUser = Class.new(OpenStruct)
|
172
|
+
TypeTwoUser = Class.new(OpenStruct)
|
173
|
+
|
174
|
+
let(:override_config) {
|
175
|
+
Terrestrial::Configurations::ConventionalConfiguration.new(datastore)
|
176
|
+
.setup_mapping(:t1_users) { |c|
|
177
|
+
c.class(TypeOneUser)
|
178
|
+
c.table_name(:users)
|
179
|
+
}
|
180
|
+
.setup_mapping(:t2_users) { |c|
|
181
|
+
c.class(TypeTwoUser)
|
182
|
+
c.table_name(:users)
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
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)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/have_persisted_matcher"
|
4
|
+
require "support/mapper_setup"
|
5
|
+
require "support/sequel_persistence_setup"
|
6
|
+
require "support/seed_data_setup"
|
7
|
+
require "terrestrial"
|
8
|
+
|
9
|
+
require "terrestrial/configurations/conventional_configuration"
|
10
|
+
|
11
|
+
RSpec.describe "Config override" do
|
12
|
+
include_context "mapper setup"
|
13
|
+
include_context "sequel persistence setup"
|
14
|
+
include_context "seed data setup"
|
15
|
+
|
16
|
+
let(:user) { user_mapper.where(id: "users/1").first }
|
17
|
+
|
18
|
+
context "with an object that has private fields" do
|
19
|
+
let(:user_serializer) {
|
20
|
+
->(object) {
|
21
|
+
object.to_h.merge(
|
22
|
+
first_name: "I am a custom serializer",
|
23
|
+
last_name: "and i don't care about facts",
|
24
|
+
)
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
before do
|
29
|
+
mappings
|
30
|
+
.fetch(:users)
|
31
|
+
.instance_variable_set(:@serializer, user_serializer)
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when saving the object" do
|
35
|
+
it "uses the custom serializer" do
|
36
|
+
user.first_name = "This won't work"
|
37
|
+
user.last_name = "because the serialzer is weird"
|
38
|
+
|
39
|
+
user_mapper.save(user)
|
40
|
+
|
41
|
+
expect(datastore).to have_persisted(:users, hash_including(
|
42
|
+
id: user.id,
|
43
|
+
first_name: "I am a custom serializer",
|
44
|
+
last_name: "and i don't care about facts",
|
45
|
+
))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "support/mapper_setup"
|
4
|
+
require "support/sequel_persistence_setup"
|
5
|
+
require "support/seed_data_setup"
|
6
|
+
require "support/have_persisted_matcher"
|
7
|
+
require "terrestrial"
|
8
|
+
|
9
|
+
RSpec.describe "Deletion" do
|
10
|
+
include_context "mapper setup"
|
11
|
+
include_context "sequel persistence setup"
|
12
|
+
include_context "seed data setup"
|
13
|
+
|
14
|
+
subject(:mapper) { user_mapper }
|
15
|
+
|
16
|
+
let(:user) {
|
17
|
+
mapper.where(id: "users/1").first
|
18
|
+
}
|
19
|
+
|
20
|
+
let(:reloaded_user) {
|
21
|
+
mapper.where(id: "users/1").first
|
22
|
+
}
|
23
|
+
|
24
|
+
describe "Deleting the root" do
|
25
|
+
it "deletes the root object" do
|
26
|
+
mapper.delete(user, cascade: true)
|
27
|
+
|
28
|
+
expect(datastore).not_to have_persisted(
|
29
|
+
:users,
|
30
|
+
hash_including(id: "users/1")
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when much of the graph has been loaded" do
|
35
|
+
before do
|
36
|
+
user.posts.flat_map(&:comments)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "deletes the root object" do
|
40
|
+
mapper.delete(user)
|
41
|
+
|
42
|
+
expect(datastore).not_to have_persisted(
|
43
|
+
:users,
|
44
|
+
hash_including(id: "users/1")
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not delete the child objects" do
|
49
|
+
expect {
|
50
|
+
mapper.delete(user)
|
51
|
+
}.not_to change { [datastore[:posts], datastore[:comments]].map(&:count) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# context "deleting multiple" do
|
56
|
+
# it "is not currently supported"
|
57
|
+
# end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "Deleting a child object (one to many)" do
|
61
|
+
let(:post) {
|
62
|
+
user.posts.find { |post| post.id == "posts/1" }
|
63
|
+
}
|
64
|
+
|
65
|
+
it "deletes the specified node" do
|
66
|
+
user.posts.delete(post)
|
67
|
+
mapper.save(user)
|
68
|
+
|
69
|
+
expect(datastore).not_to have_persisted(
|
70
|
+
:posts,
|
71
|
+
hash_including(id: "posts/1")
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "does not delete the parent object" do
|
76
|
+
user.posts.delete(post)
|
77
|
+
mapper.save(user)
|
78
|
+
|
79
|
+
expect(datastore).to have_persisted(
|
80
|
+
:users,
|
81
|
+
hash_including(id: "users/1")
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "does not delete the sibling objects" do
|
86
|
+
user.posts.delete(post)
|
87
|
+
mapper.save(user)
|
88
|
+
|
89
|
+
expect(reloaded_user.posts.count).to be > 0
|
90
|
+
end
|
91
|
+
|
92
|
+
it "does not cascade delete" do
|
93
|
+
expect {
|
94
|
+
user.posts.delete(post)
|
95
|
+
mapper.save(user)
|
96
|
+
}.not_to change {
|
97
|
+
datastore[:comments].map { |r| r.fetch(:id) }
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|