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