rom-repository 0.2.0 → 0.3.0

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