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,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
@@ -0,0 +1,11 @@
1
+ module Terrestrial
2
+ class SubsetQueriesProxy
3
+ def initialize(query_map)
4
+ @query_map = query_map
5
+ end
6
+
7
+ def execute(superset, name, *params)
8
+ @query_map.fetch(name).call(superset, *params)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ require "terrestrial/abstract_record"
2
+
3
+ module Terrestrial
4
+ class UpsertedRecord < AbstractRecord
5
+ def if_upsert(&block)
6
+ block.call(self)
7
+ self
8
+ end
9
+
10
+ protected
11
+ def operation
12
+ :upsert
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Terrestrial
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/terrestrial.rb CHANGED
@@ -1,5 +1,8 @@
1
- require "terrestrial/version"
1
+ require "logger"
2
+ require "terrestrial/public_conveniencies"
2
3
 
3
4
  module Terrestrial
4
- # Your code goes here...
5
+ extend PublicConveniencies
6
+
7
+ LOGGER = Logger.new(STDERR)
5
8
  end
@@ -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