rom-repository 0.2.0 → 0.3.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -5
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +21 -5
  6. data/README.md +6 -110
  7. data/lib/rom/repository/changeset/create.rb +26 -0
  8. data/lib/rom/repository/changeset/pipe.rb +40 -0
  9. data/lib/rom/repository/changeset/update.rb +82 -0
  10. data/lib/rom/repository/changeset.rb +99 -0
  11. data/lib/rom/repository/class_interface.rb +142 -0
  12. data/lib/rom/repository/command_compiler.rb +214 -0
  13. data/lib/rom/repository/command_proxy.rb +22 -0
  14. data/lib/rom/repository/header_builder.rb +13 -16
  15. data/lib/rom/repository/mapper_builder.rb +7 -14
  16. data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
  17. data/lib/rom/repository/relation_proxy.rb +225 -0
  18. data/lib/rom/repository/root.rb +110 -0
  19. data/lib/rom/repository/struct_attributes.rb +46 -0
  20. data/lib/rom/repository/struct_builder.rb +31 -14
  21. data/lib/rom/repository/version.rb +1 -1
  22. data/lib/rom/repository.rb +192 -31
  23. data/lib/rom/struct.rb +13 -8
  24. data/rom-repository.gemspec +9 -10
  25. data/spec/integration/changeset_spec.rb +86 -0
  26. data/spec/integration/command_macros_spec.rb +175 -0
  27. data/spec/integration/command_spec.rb +224 -0
  28. data/spec/integration/multi_adapter_spec.rb +3 -3
  29. data/spec/integration/repository_spec.rb +97 -2
  30. data/spec/integration/root_repository_spec.rb +88 -0
  31. data/spec/shared/database.rb +47 -3
  32. data/spec/shared/mappers.rb +35 -0
  33. data/spec/shared/models.rb +41 -0
  34. data/spec/shared/plugins.rb +66 -0
  35. data/spec/shared/relations.rb +76 -0
  36. data/spec/shared/repo.rb +38 -17
  37. data/spec/shared/seeds.rb +19 -0
  38. data/spec/spec_helper.rb +4 -1
  39. data/spec/support/mapper_registry.rb +1 -3
  40. data/spec/unit/changeset_spec.rb +58 -0
  41. data/spec/unit/header_builder_spec.rb +34 -35
  42. data/spec/unit/relation_proxy_spec.rb +170 -0
  43. data/spec/unit/sql/relation_spec.rb +5 -5
  44. data/spec/unit/struct_builder_spec.rb +7 -4
  45. data/spec/unit/struct_spec.rb +22 -0
  46. metadata +38 -41
  47. data/lib/rom/plugins/relation/key_inference.rb +0 -31
  48. data/lib/rom/repository/loading_proxy/combine.rb +0 -158
  49. data/lib/rom/repository/loading_proxy.rb +0 -182
  50. data/spec/unit/loading_proxy_spec.rb +0 -147
@@ -0,0 +1,35 @@
1
+ RSpec.shared_context 'mappers' do
2
+ let(:users) { rom.relation(:users).mappers[:user] }
3
+ let(:tasks) { rom.relation(:tasks).mappers[:task] }
4
+ let(:tags) { rom.relation(:tags).mappers[:tag] }
5
+
6
+ before do
7
+ configuration.mappers do
8
+ define(:users) do
9
+ model Test::Models::User
10
+ register_as :user
11
+
12
+ attribute :id
13
+ attribute :name
14
+ end
15
+
16
+ define(:tasks) do
17
+ model Test::Models::Task
18
+ register_as :task
19
+
20
+ attribute :id
21
+ attribute :user_id
22
+ attribute :title
23
+ end
24
+
25
+ define(:tags) do
26
+ model Test::Models::Tag
27
+ register_as :tag
28
+
29
+ attribute :id
30
+ attribute :task_id
31
+ attribute :name
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.shared_context 'models' do
2
+ let(:user_model) { Test::Models::User }
3
+ let(:task_model) { Test::Models::Task }
4
+ let(:tag_model) { Test::Models::Tag }
5
+
6
+ before do
7
+ module Test
8
+ module Models
9
+ class User
10
+ include Dry::Equalizer(:id, :name)
11
+
12
+ attr_reader :id, :name
13
+
14
+ def initialize(attrs)
15
+ @id, @name = attrs[:id], attrs[:name]
16
+ end
17
+ end
18
+
19
+ class Task
20
+ include Dry::Equalizer(:id, :user_id, :title)
21
+
22
+ attr_reader :id, :user_id, :title
23
+
24
+ def initialize(attrs)
25
+ @id, @name, @title = attrs[:id], attrs[:name], attrs[:title]
26
+ end
27
+ end
28
+
29
+ class Tag
30
+ include Dry::Equalizer(:id, :task_id, :name)
31
+
32
+ attr_reader :id, :task_id, :name
33
+
34
+ def initialize(attrs)
35
+ @id, @task_id, @name = attrs[:id], attrs[:task_id], attrs[:name]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,66 @@
1
+ RSpec.shared_context 'plugins' do
2
+ before do
3
+ module Test
4
+ class WrappingInput
5
+ def initialize(input)
6
+ @input = input || Hash
7
+ end
8
+ end
9
+
10
+ module Timestamps
11
+ class InputWithTimestamp < WrappingInput
12
+ def [](value)
13
+ v = @input[value]
14
+ now = Time.now
15
+
16
+ if v[:created_at]
17
+ v.merge(updated_at: now)
18
+ else
19
+ v.merge(created_at: now, updated_at: now)
20
+ end
21
+ end
22
+ end
23
+
24
+ module ClassInterface
25
+ def build(relation, options = {})
26
+ super(relation, options.merge(input: InputWithTimestamp.new(input)))
27
+ end
28
+ end
29
+
30
+ def self.included(klass)
31
+ super
32
+
33
+ klass.extend ClassInterface
34
+ end
35
+ end
36
+
37
+ module UpcaseName
38
+ class UpcaseNameInput < WrappingInput
39
+ def [](value)
40
+ v = @input[value]
41
+ v.merge(name: value.fetch(:name).upcase)
42
+ end
43
+ end
44
+
45
+ module ClassInterface
46
+ def build(relation, options = {})
47
+ super(relation, options.merge(input: UpcaseNameInput.new(options.fetch(:input))))
48
+ end
49
+ end
50
+
51
+ def self.included(klass)
52
+ super
53
+
54
+ klass.extend ClassInterface
55
+ end
56
+ end
57
+ end
58
+
59
+ ROM.plugins do
60
+ adapter :sql do
61
+ register :timestamps, Test::Timestamps, type: :command
62
+ register :upcase_name, Test::UpcaseName, type: :command
63
+ end
64
+ end
65
+ end
66
+ end
@@ -2,9 +2,31 @@ RSpec.shared_context 'relations' do
2
2
  let(:users) { rom.relation(:users) }
3
3
  let(:tasks) { rom.relation(:tasks) }
4
4
  let(:tags) { rom.relation(:tags) }
5
+ let(:posts) { rom.relation(:posts) }
6
+ let(:books) { rom.relation(:books) }
5
7
 
6
8
  before do
9
+ configuration.relation(:books) do
10
+ schema(:books) do
11
+ attribute :id, ROM::SQL::Types::Serial
12
+ attribute :title, ROM::SQL::Types::String
13
+ attribute :created_at, ROM::SQL::Types::Time
14
+ attribute :updated_at, ROM::SQL::Types::Time
15
+ end
16
+ end
17
+
7
18
  configuration.relation(:users) do
19
+ schema(infer: true) do
20
+ associations do
21
+ has_many :posts
22
+ has_many :labels, through: :posts
23
+ end
24
+ end
25
+
26
+ def by_name(name)
27
+ where(name: name)
28
+ end
29
+
8
30
  def all
9
31
  select(:id, :name).order(:name, :id)
10
32
  end
@@ -15,6 +37,12 @@ RSpec.shared_context 'relations' do
15
37
  end
16
38
 
17
39
  configuration.relation(:tasks) do
40
+ schema(infer: true) do
41
+ associations do
42
+ belongs_to :user
43
+ end
44
+ end
45
+
18
46
  def find(criteria)
19
47
  where(criteria)
20
48
  end
@@ -25,5 +53,53 @@ RSpec.shared_context 'relations' do
25
53
  end
26
54
 
27
55
  configuration.relation(:tags)
56
+
57
+ configuration.relation(:labels) do
58
+ schema(infer: true) do
59
+ associations do
60
+ has_many :posts_labels
61
+ has_many :posts, through: :posts_labels
62
+ end
63
+ end
64
+ end
65
+
66
+ configuration.relation(:posts) do
67
+ schema(:posts, infer: true) do
68
+ associations do
69
+ has_many :labels, through: :posts_labels
70
+ belongs_to :user, as: :author
71
+ end
72
+ end
73
+ end
74
+
75
+ configuration.relation(:posts_labels) do
76
+ schema(infer: true) do
77
+ associations do
78
+ belongs_to :post
79
+ belongs_to :label
80
+ end
81
+ end
82
+ end
83
+
84
+ configuration.relation(:comments) do
85
+ register_as :comments
86
+
87
+ schema(:messages, infer: true) do
88
+ associations do
89
+ has_many :reactions, relation: :likes
90
+ has_many :reactions, relation: :likes, as: :emotions
91
+ end
92
+ end
93
+ end
94
+
95
+ configuration.relation(:likes) do
96
+ register_as :likes
97
+
98
+ schema(:reactions, infer: true) do
99
+ associations do
100
+ belongs_to :message, relation: :comments
101
+ end
102
+ end
103
+ end
28
104
  end
29
105
  end
data/spec/shared/repo.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  RSpec.shared_context('repo') do
2
+ include_context 'models'
3
+ include_context 'mappers'
4
+
2
5
  let(:repo) { repo_class.new(rom) }
3
6
 
4
7
  let(:repo_class) do
5
- Class.new(ROM::Repository) do
6
- relations :users, :tasks, :tags
8
+ Class.new(ROM::Repository[:users]) do
9
+ relations :tasks, :tags, :posts, :labels
7
10
 
8
11
  def find_users(criteria)
9
12
  users.find(criteria)
@@ -13,6 +16,26 @@ RSpec.shared_context('repo') do
13
16
  users.all
14
17
  end
15
18
 
19
+ def all_users_as_users
20
+ users.as(:user).all
21
+ end
22
+
23
+ def users_with_tasks
24
+ aggregate(many: { all_tasks: tasks.for_users })
25
+ end
26
+
27
+ def users_with_tasks_and_tags
28
+ aggregate(many: { all_tasks: tasks_with_tags(tasks.for_users) })
29
+ end
30
+
31
+ def users_with_task
32
+ aggregate(one: tasks)
33
+ end
34
+
35
+ def users_with_task_by_title(title)
36
+ aggregate(one: tasks.find(title: title))
37
+ end
38
+
16
39
  def tasks_for_users(users)
17
40
  tasks.for_users(users)
18
41
  end
@@ -25,29 +48,27 @@ RSpec.shared_context('repo') do
25
48
  tasks.find(id: 2).combine_parents(one: { owner: users })
26
49
  end
27
50
 
28
- def users_with_tasks
29
- users.combine_children(many: { all_tasks: tasks.for_users })
30
- end
31
-
32
- def users_with_tasks_and_tags
33
- users.combine_children(many: { all_tasks: tasks_with_tags(tasks.for_users) })
34
- end
35
-
36
51
  def tasks_with_tags(tasks = self.tasks)
37
52
  tasks.combine_children(many: tags)
38
53
  end
39
54
 
40
- def users_with_task
41
- users.combine_children(one: tasks)
55
+ def tag_with_wrapped_task
56
+ tags.wrap_parent(task: tasks)
42
57
  end
58
+ end
59
+ end
43
60
 
44
- def users_with_task_by_title(title)
45
- users.combine_children(one: tasks.find(title: title))
61
+ let(:comments_repo) do
62
+ Class.new(ROM::Repository[:comments]) do
63
+ relations :likes
64
+
65
+ def comments_with_likes
66
+ aggregate(many: { likes: likes })
46
67
  end
47
68
 
48
- def tag_with_wrapped_task
49
- tags.wrap_parent(task: tasks)
69
+ def comments_with_emotions
70
+ root.combine(:emotions)
50
71
  end
51
- end
72
+ end.new(rom)
52
73
  end
53
74
  end
data/spec/shared/seeds.rb CHANGED
@@ -7,5 +7,24 @@ RSpec.shared_context 'seeds' do
7
7
  task_id = conn[:tasks].insert user_id: jane_id, title: 'Jane Task'
8
8
 
9
9
  conn[:tags].insert task_id: task_id, name: 'red'
10
+
11
+ jane_post_id = conn[:posts].insert author_id: jane_id, title: 'Hello From Jane', body: 'Jane Post'
12
+ joe_post_id = conn[:posts].insert author_id: joe_id, title: 'Hello From Joe', body: 'Joe Post'
13
+
14
+ red_id = conn[:labels].insert name: 'red'
15
+ green_id = conn[:labels].insert name: 'green'
16
+ blue_id = conn[:labels].insert name: 'blue'
17
+
18
+ conn[:posts_labels].insert post_id: jane_post_id, label_id: red_id
19
+ conn[:posts_labels].insert post_id: jane_post_id, label_id: blue_id
20
+
21
+ conn[:posts_labels].insert post_id: joe_post_id, label_id: green_id
22
+
23
+ conn[:messages].insert author: 'Jane', body: 'Hello folks'
24
+ conn[:messages].insert author: 'Joe', body: 'Hello Jane'
25
+
26
+ conn[:reactions].insert message_id: 1, author: 'Joe'
27
+ conn[:reactions].insert message_id: 1, author: 'Anonymous'
28
+ conn[:reactions].insert message_id: 2, author: 'Jane'
10
29
  end
11
30
  end
data/spec/spec_helper.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  require "bundler"
5
5
  Bundler.setup
6
6
 
7
- if RUBY_ENGINE == "rbx"
7
+ if RUBY_ENGINE == "ruby" && RUBY_VERSION == '2.3.1'
8
8
  require "codeclimate-test-reporter"
9
9
  CodeClimate::TestReporter.start
10
10
  end
@@ -28,6 +28,9 @@ Dir[root.join('shared/*.rb').to_s].each do |f|
28
28
  require f
29
29
  end
30
30
 
31
+ require 'rom/support/deprecations'
32
+ ROM::Deprecations.set_logger!(root.join('../log/deprecations.log'))
33
+
31
34
  # Namespace holding all objects created during specs
32
35
  module Test
33
36
  def self.remove_constants
@@ -1,8 +1,6 @@
1
1
  module MapperRegistry
2
2
  def mapper_for(relation)
3
- ROM::Repository::MapperBuilder.registry.fetch(relation.to_ast.hash) {
4
- mapper_builder[relation.to_ast]
5
- }
3
+ mapper_builder[relation.to_ast]
6
4
  end
7
5
 
8
6
  def mapper_builder
@@ -0,0 +1,58 @@
1
+ RSpec.describe ROM::Changeset do
2
+ let(:jane) { { id: 2, name: "Jane" } }
3
+ let(:relation) { double(ROM::Relation, primary_key: :id) }
4
+
5
+ describe '#diff' do
6
+ it 'returns a hash with changes' do
7
+ expect(relation).to receive(:fetch).with(2).and_return(jane)
8
+
9
+ changeset = ROM::Changeset::Update.new(relation, { name: "Jane Doe" }, primary_key: 2)
10
+
11
+ expect(changeset.diff).to eql(name: "Jane Doe")
12
+ end
13
+ end
14
+
15
+ describe '#diff?' do
16
+ it 'returns true when data differs from the original tuple' do
17
+ expect(relation).to receive(:fetch).with(2).and_return(jane)
18
+
19
+ changeset = ROM::Changeset::Update.new(relation, { name: "Jane Doe" }, primary_key: 2)
20
+
21
+ expect(changeset).to be_diff
22
+ end
23
+
24
+ it 'returns false when data are equal to the original tuple' do
25
+ expect(relation).to receive(:fetch).with(2).and_return(jane)
26
+
27
+ changeset = ROM::Changeset::Update.new(relation, { name: "Jane" }, primary_key: 2)
28
+
29
+ expect(changeset).to_not be_diff
30
+ end
31
+ end
32
+
33
+ describe 'quacks like a hash' do
34
+ subject(:changeset) { ROM::Changeset::Create.new(relation, data) }
35
+
36
+ let(:data) { instance_double(Hash) }
37
+
38
+ it 'delegates to its data hash' do
39
+ expect(data).to receive(:[]).with(:name).and_return('Jane')
40
+
41
+ expect(changeset[:name]).to eql('Jane')
42
+ end
43
+
44
+ it 'maintains its own type' do
45
+ expect(data).to receive(:merge).with(foo: 'bar').and_return(foo: 'bar')
46
+
47
+ new_changeset = changeset.merge(foo: 'bar')
48
+
49
+ expect(new_changeset).to be_instance_of(ROM::Changeset::Create)
50
+ expect(new_changeset.options).to eql(changeset.options)
51
+ expect(new_changeset.to_h).to eql(foo: 'bar')
52
+ end
53
+
54
+ it 'raises NoMethodError when an unknown message was sent' do
55
+ expect { changeset.not_here }.to raise_error(NoMethodError, /not_here/)
56
+ end
57
+ end
58
+ end
@@ -1,18 +1,25 @@
1
1
  RSpec.describe 'header builder', '#call' do
2
2
  subject(:builder) { ROM::Repository::HeaderBuilder.new }
3
3
 
4
- let(:user_struct) { builder.struct_builder[:users, [:id, :name]] }
5
- let(:task_struct) { builder.struct_builder[:tasks, [:user_id, :title]] }
6
- let(:tag_struct) { builder.struct_builder[:tags, [:user_id, :tag]] }
4
+ let(:user_struct) do
5
+ builder.struct_builder[:users, [:header, [[:attribute, :id], [:attribute, :name]]]]
6
+ end
7
+
8
+ let(:task_struct) do
9
+ builder.struct_builder[:tasks, [:header, [[:attribute, :user_id], [:attribute, :title]]]]
10
+ end
11
+
12
+ let(:tag_struct) do
13
+ builder.struct_builder[:tags, [:header, [[:attribute, :user_id], [:attribute, :tag]]]]
14
+ end
7
15
 
8
16
  describe 'with a relation' do
9
17
  let(:ast) do
10
- [
11
- :relation, :users, [
12
- :header, [[:attribute, :id], [:attribute, :name]]
13
- ],
14
- base_name: :users
15
- ]
18
+ [:relation, [
19
+ :users,
20
+ { dataset: :users, combine_name: :users },
21
+ [:header, [[:attribute, :id], [:attribute, :name]]]
22
+ ]]
16
23
  end
17
24
 
18
25
  it 'produces a valid header' do
@@ -24,32 +31,25 @@ RSpec.describe 'header builder', '#call' do
24
31
 
25
32
  describe 'with a graph' do
26
33
  let(:ast) do
27
- [
28
- :relation, :users, [
34
+ [:relation, [
35
+ :users,
36
+ { dataset: :users, combine_name: :users },
37
+ [
29
38
  :header, [
30
39
  [:attribute, :id],
31
40
  [:attribute, :name],
32
- [
33
- :relation, :tasks, [
34
- :header, [
35
- [:attribute, :user_id],
36
- [:attribute, :title]
37
- ]
38
- ],
39
- { base_name: :tasks, keys: { id: :user_id }, combine_type: :many }
40
- ],
41
- [
42
- :relation, :tags, [
43
- :header, [
44
- [:attribute, :user_id],
45
- [:attribute, :tag]
46
- ]
47
- ],
48
- { base_name: :tags, keys: { id: :user_id }, combine_type: :many }
49
- ]
50
- ]
51
- ],
52
- base_name: :users
41
+ [:relation, [
42
+ :tasks,
43
+ { dataset: :tasks, keys: { id: :user_id }, combine_type: :many, combine_name: :tasks },
44
+ [:header, [[:attribute, :user_id], [:attribute, :title]]]
45
+ ]],
46
+ [:relation, [
47
+ :tags,
48
+ { dataset: :tags, keys: { id: :user_id }, combine_type: :many, combine_name: :tags },
49
+ [:header, [[:attribute, :user_id], [:attribute, :tag]]]
50
+ ]]
51
+ ]]
52
+ ]
53
53
  ]
54
54
  end
55
55
 
@@ -63,9 +63,8 @@ RSpec.describe 'header builder', '#call' do
63
63
  header: ROM::Header.coerce([[:user_id], [:tag]], model: tag_struct)]
64
64
  ]
65
65
 
66
- header = ROM::Header.coerce(
67
- attributes,
68
- model: builder.struct_builder[:users, [:id, :name, :tasks, :tags]]
66
+ header = ROM::Header.coerce(attributes,
67
+ model: builder.struct_builder[:users, ast[1][2]]
69
68
  )
70
69
 
71
70
  expect(builder[ast]).to eql(header)