terrestrial 0.1.0 → 0.1.1

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