rom-repository 0.3.1 → 1.0.0.beta1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -13
  3. data/CHANGELOG.md +25 -0
  4. data/Gemfile +13 -3
  5. data/lib/rom/repository.rb +57 -19
  6. data/lib/rom/repository/changeset.rb +89 -26
  7. data/lib/rom/repository/changeset/create.rb +34 -0
  8. data/lib/rom/repository/changeset/delete.rb +15 -0
  9. data/lib/rom/repository/changeset/pipe.rb +11 -4
  10. data/lib/rom/repository/changeset/update.rb +11 -1
  11. data/lib/rom/repository/command_compiler.rb +51 -30
  12. data/lib/rom/repository/command_proxy.rb +3 -1
  13. data/lib/rom/repository/header_builder.rb +3 -3
  14. data/lib/rom/repository/mapper_builder.rb +2 -2
  15. data/lib/rom/repository/relation_proxy.rb +26 -35
  16. data/lib/rom/repository/relation_proxy/combine.rb +59 -27
  17. data/lib/rom/repository/root.rb +4 -6
  18. data/lib/rom/repository/session.rb +55 -0
  19. data/lib/rom/repository/struct_builder.rb +29 -17
  20. data/lib/rom/repository/version.rb +1 -1
  21. data/lib/rom/struct.rb +11 -20
  22. data/rom-repository.gemspec +4 -3
  23. data/spec/integration/command_macros_spec.rb +5 -2
  24. data/spec/integration/command_spec.rb +0 -6
  25. data/spec/integration/multi_adapter_spec.rb +8 -5
  26. data/spec/integration/repository_spec.rb +58 -2
  27. data/spec/integration/root_repository_spec.rb +9 -2
  28. data/spec/integration/typed_structs_spec.rb +31 -0
  29. data/spec/shared/database.rb +5 -1
  30. data/spec/shared/relations.rb +3 -1
  31. data/spec/shared/repo.rb +13 -1
  32. data/spec/shared/structs.rb +39 -0
  33. data/spec/spec_helper.rb +7 -5
  34. data/spec/support/mutant.rb +10 -0
  35. data/spec/unit/changeset/map_spec.rb +42 -0
  36. data/spec/unit/changeset_spec.rb +32 -6
  37. data/spec/unit/relation_proxy_spec.rb +27 -9
  38. data/spec/unit/repository/changeset_spec.rb +125 -0
  39. data/spec/unit/repository/inspect_spec.rb +18 -0
  40. data/spec/unit/repository/session_spec.rb +251 -0
  41. data/spec/unit/session_spec.rb +54 -0
  42. data/spec/unit/struct_builder_spec.rb +45 -1
  43. metadata +41 -17
  44. data/lib/rom/repository/struct_attributes.rb +0 -46
  45. data/spec/unit/header_builder_spec.rb +0 -73
  46. data/spec/unit/plugins/view_spec.rb +0 -29
  47. data/spec/unit/sql/relation_spec.rb +0 -54
  48. data/spec/unit/struct_spec.rb +0 -22
@@ -1,3 +1,5 @@
1
+ require 'dry/core/class_attributes'
2
+
1
3
  module ROM
2
4
  class Repository
3
5
  # A specialized repository type dedicated to work with a root relation
@@ -27,7 +29,7 @@ module ROM
27
29
  #
28
30
  # @api public
29
31
  class Root < Repository
30
- extend ClassMacros
32
+ extend Dry::Core::ClassAttributes
31
33
 
32
34
  defines :root
33
35
 
@@ -70,11 +72,7 @@ module ROM
70
72
  #
71
73
  # @api public
72
74
  def aggregate(*args)
73
- if args[0].is_a?(Hash) && args.size == 1
74
- root.combine_children(args[0])
75
- else
76
- root.combine(*args)
77
- end
75
+ root.combine(*args)
78
76
  end
79
77
 
80
78
  # @overload changeset(name, *args)
@@ -0,0 +1,55 @@
1
+ require 'dry/equalizer'
2
+
3
+ module ROM
4
+ class Session
5
+ include Dry::Equalizer(:queue, :status)
6
+
7
+ attr_reader :repo
8
+
9
+ attr_reader :queue
10
+
11
+ attr_reader :status
12
+
13
+ def initialize(repo)
14
+ @repo = repo
15
+ @status = :pending
16
+ initialize_queue!
17
+ end
18
+
19
+ def add(changeset)
20
+ queue << changeset
21
+ self
22
+ end
23
+
24
+ def commit!
25
+ queue.map(&:command).compact.each(&:call)
26
+
27
+ @status = :success
28
+
29
+ self
30
+ rescue => e
31
+ @status = :failure
32
+ raise e
33
+ ensure
34
+ initialize_queue!
35
+ end
36
+
37
+ def pending?
38
+ status.equal?(:pending)
39
+ end
40
+
41
+ def success?
42
+ status.equal?(:success)
43
+ end
44
+
45
+ def failure?
46
+ status.equal?(:failure)
47
+ end
48
+
49
+ private
50
+
51
+ def initialize_queue!
52
+ @queue = []
53
+ end
54
+ end
55
+ end
@@ -1,24 +1,26 @@
1
- require 'rom/struct'
2
-
3
- require 'rom/support/cache'
4
- require 'rom/support/constants'
5
- require 'rom/support/class_builder'
1
+ require 'dry/core/inflector'
2
+ require 'dry/core/cache'
3
+ require 'dry/core/class_builder'
6
4
 
7
- require 'rom/repository/struct_attributes'
5
+ require 'rom/struct'
6
+ require 'rom/schema/type'
8
7
 
9
8
  module ROM
10
9
  class Repository
11
10
  # @api private
12
11
  class StructBuilder
13
- extend Cache
12
+ extend Dry::Core::Cache
14
13
 
15
14
  def call(*args)
16
15
  fetch_or_store(*args) do
17
16
  name, header = args
17
+ attributes = visit(header).compact
18
18
 
19
- build_class(name) { |klass|
20
- klass.send(:include, StructAttributes.new(visit(header)))
21
- }
19
+ build_class(name, ROM::Struct) do |klass|
20
+ attributes.each do |(name, type)|
21
+ klass.attribute(name, type)
22
+ end
23
+ end
22
24
  end
23
25
  end
24
26
  alias_method :[], :call
@@ -35,20 +37,30 @@ module ROM
35
37
  end
36
38
 
37
39
  def visit_relation(node)
38
- relation_name, meta, * = node
39
- meta[:combine_name] || relation_name.relation
40
+ relation_name, meta, header = node
41
+ name = meta[:combine_name] || relation_name.relation
42
+
43
+ model = call(name, header)
44
+
45
+ if meta[:combine_type] == :many
46
+ [name, Types::Array.member(model)]
47
+ else
48
+ [name, model.optional]
49
+ end
40
50
  end
41
51
 
42
- def visit_attribute(node)
43
- node
52
+ def visit_attribute(attr)
53
+ unless attr.foreign_key?
54
+ [attr.aliased? && !attr.wrapped? ? attr.alias : attr.name, attr.type]
55
+ end
44
56
  end
45
57
 
46
- def build_class(name, &block)
47
- ROM::ClassBuilder.new(name: class_name(name), parent: Struct).call(&block)
58
+ def build_class(name, parent, &block)
59
+ Dry::Core::ClassBuilder.new(name: class_name(name), parent: parent).call(&block)
48
60
  end
49
61
 
50
62
  def class_name(name)
51
- "ROM::Struct[#{Inflector.classify(Inflector.singularize(name))}]"
63
+ "ROM::Struct[#{Dry::Core::Inflector.classify(Dry::Core::Inflector.singularize(name))}]"
52
64
  end
53
65
  end
54
66
  end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  class Repository
3
- VERSION = '0.3.1'.freeze
3
+ VERSION = '1.0.0.beta1'.freeze
4
4
  end
5
5
  end
data/lib/rom/struct.rb CHANGED
@@ -1,37 +1,28 @@
1
+ require 'dry/struct'
2
+
1
3
  module ROM
2
4
  # Simple data-struct
3
5
  #
4
6
  # By default mappers use this as the model
5
7
  #
6
8
  # @api public
7
- class Struct
8
- # Coerces a struct to a hash
9
- #
10
- # @return [Hash]
11
- #
12
- # @api private
13
- def to_hash
14
- to_h
15
- end
16
-
17
- # Reads an attribute value
18
- #
19
- # @param name [Symbol] The name of the attribute
9
+ class Struct < Dry::Struct
10
+ # Returns a short string representation
20
11
  #
21
- # @return [Object]
12
+ # @return [String]
22
13
  #
23
14
  # @api public
24
- def [](name)
25
- __send__(name)
15
+ def to_s
16
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
26
17
  end
27
18
 
28
- # Returns a short string representation
19
+ # Return attribute value
29
20
  #
30
- # @return [String]
21
+ # @param [Symbol] name The attribute name
31
22
  #
32
23
  # @api public
33
- def to_s
34
- "#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
24
+ def fetch(name)
25
+ __send__(name)
35
26
  end
36
27
  end
37
28
  end
@@ -15,9 +15,10 @@ Gem::Specification.new do |gem|
15
15
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
16
16
  gem.license = 'MIT'
17
17
 
18
- gem.add_runtime_dependency 'rom', '~> 2.0'
19
- gem.add_runtime_dependency 'rom-support', '~> 2.0'
20
- gem.add_runtime_dependency 'rom-mapper', '~> 0.4'
18
+ gem.add_runtime_dependency 'rom', '~> 3.0.0.beta'
19
+ gem.add_runtime_dependency 'rom-mapper', '~> 0.5.0.beta'
20
+ gem.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.1'
21
+ gem.add_runtime_dependency 'dry-struct', '~> 0.1'
21
22
 
22
23
  gem.add_development_dependency 'rake', '~> 11.2'
23
24
  gem.add_development_dependency 'rspec', '~> 3.5'
@@ -81,9 +81,12 @@ RSpec.describe ROM::Repository, '.command' do
81
81
 
82
82
  user = repo.create(name: 'Jane')
83
83
 
84
- expect(user).to be_kind_of ROM::Struct
84
+ expect(user).to be_kind_of Dry::Struct
85
+
86
+ struct_definition = [:users, [:header, [
87
+ [:attribute, repo.users.schema[:id]],
88
+ [:attribute, repo.users.schema[:name]]]]]
85
89
 
86
- struct_definition = [:users, [:header, [[:attribute, :id], [:attribute, :name]]]]
87
90
  expect(user).to be_an_instance_of ROM::Repository::StructBuilder.cache[struct_definition.hash]
88
91
  end
89
92
 
@@ -215,10 +215,4 @@ RSpec.describe ROM::Repository, '#command' do
215
215
  expect(repo.users.by_pk(3).one).to be(nil)
216
216
  end
217
217
  end
218
-
219
- it 'raises error when unsupported type is used' do
220
- expect { repo.command(:oops, repo.users) }.to raise_error(
221
- ArgumentError, /oops/
222
- )
223
- end
224
218
  end
@@ -15,21 +15,24 @@ RSpec.describe 'Repository with multi-adapters configuration' do
15
15
  before do
16
16
  module Test
17
17
  class Users < ROM::Relation[:sql]
18
- dataset :users
19
- register_as :sql_users
20
18
  gateway :default
19
+ schema(:users, infer: true)
20
+ register_as :sql_users
21
21
  end
22
22
 
23
23
  class Tasks < ROM::Relation[:memory]
24
- dataset :tasks
24
+ schema(:tasks) do
25
+ attribute :user_id, ROM::Types::Int
26
+ attribute :title, ROM::Types::String
27
+ end
28
+
25
29
  register_as :memory_tasks
26
30
  gateway :memory
27
31
 
28
- use :view
29
32
  use :key_inference
30
33
 
31
34
  view(:base, [:user_id, :title]) do
32
- project(:user_id, :title)
35
+ self
33
36
  end
34
37
 
35
38
  def for_users(users)
@@ -47,7 +47,23 @@ RSpec.describe 'ROM repository' do
47
47
  end
48
48
 
49
49
  it 'loads nested combined relations' do
50
- expect(repo.users_with_tasks_and_tags.first.to_h).to eql(user_with_task_and_tags.to_h)
50
+ user = repo.users_with_tasks_and_tags.first
51
+
52
+ expect(user.id).to be(1)
53
+ expect(user.name).to eql('Jane')
54
+ expect(user.all_tasks.size).to be(1)
55
+ expect(user.all_tasks[0].id).to be(2)
56
+ expect(user.all_tasks[0].title).to eql('Jane Task')
57
+ expect(user.all_tasks[0].tags.size).to be(1)
58
+ expect(user.all_tasks[0].tags[0].name).to eql('red')
59
+ end
60
+
61
+ it 'loads nested combined relations using configured associations' do
62
+ jane = repo.users_with_posts_and_their_labels.first
63
+
64
+ expect(jane.posts.size).to be(1)
65
+ expect(jane.posts.map(&:title)).to eql(['Hello From Jane'])
66
+ expect(jane.posts.flat_map(&:labels).flat_map(&:name)).to eql(%w(red blue))
51
67
  end
52
68
 
53
69
  it 'loads a wrapped relation' do
@@ -105,6 +121,22 @@ RSpec.describe 'ROM repository' do
105
121
  expect(post.labels.map(&:name)).to eql(%w(red blue))
106
122
  end
107
123
 
124
+ it 'loads multiple child relations' do
125
+ user = repo.users.combine_children(many: [repo.posts, repo.tasks]).where(name: 'Jane').one
126
+
127
+ expect(user.name).to eql('Jane')
128
+ expect(user.posts.size).to be(1)
129
+ expect(user.posts[0].title).to eql('Hello From Jane')
130
+ expect(user.tasks.size).to be(1)
131
+ expect(user.tasks[0].title).to eql('Jane Task')
132
+ end
133
+
134
+ it 'loads multiple parent relations' do
135
+ post_label = repo.posts_labels.combine_parents(one: [repo.posts]).first
136
+
137
+ expect(post_label.post.title).to eql('Hello From Jane')
138
+ end
139
+
108
140
  context 'not common naming conventions' do
109
141
  it 'still loads nested relations' do
110
142
  comments = comments_repo.comments_with_likes.to_a
@@ -126,11 +158,35 @@ RSpec.describe 'ROM repository' do
126
158
  end
127
159
  end
128
160
 
161
+ describe 'projecting virtual attributes' do
162
+ it 'loads auto-mapped structs' do
163
+ user = repo.users.
164
+ inner_join(:posts, author_id: :id).
165
+ select_group { [id.qualified, name.qualified] }.
166
+ select_append { int::count(:posts).as(:post_count) }.
167
+ having { count(id.qualified) >= 1 }.
168
+ first
169
+
170
+ expect(user.id).to be(1)
171
+ expect(user.name).to eql('Jane')
172
+ expect(user.post_count).to be(1)
173
+ end
174
+ end
175
+
176
+ describe 'projecting aliased attributes' do
177
+ it 'loads auto-mapped structs' do
178
+ user = repo.users.select { [id.aliased(:userId), name.aliased(:userName)] }.first
179
+
180
+ expect(user.userId).to be(1)
181
+ expect(user.userName).to eql('Jane')
182
+ end
183
+ end
184
+
129
185
  context 'with a table without columns' do
130
186
  before { conn.create_table(:dummy) unless conn.table_exists?(:dummy) }
131
187
 
132
188
  it 'does not fail with a weird error when a relation does not have attributes' do
133
- configuration.relation(:dummy)
189
+ configuration.relation(:dummy) { schema(infer: true) }
134
190
 
135
191
  repo = Class.new(ROM::Repository[:dummy]).new(rom)
136
192
  expect(repo.dummy.to_a).to eql([])
@@ -62,10 +62,18 @@ RSpec.describe ROM::Repository::Root do
62
62
 
63
63
  expect(user.name).to eql('Joe')
64
64
  expect(user.labels.size).to be(1)
65
- expect(user.labels[0].author_id).to be(user.id)
66
65
  expect(user.labels[0].name).to eql('green')
67
66
  end
68
67
 
68
+ it 'builds an aggregate with nesting level = 2' do
69
+ user = repo.aggregate(posts: [:labels, :author]).where(name: 'Joe').one
70
+
71
+ expect(user.name).to eql('Joe')
72
+ expect(user.posts.size).to be(1)
73
+ expect(user.posts[0].title).to eql('Hello From Joe')
74
+ expect(user.posts[0].labels.size).to be(1)
75
+ end
76
+
69
77
  it 'builds a command from an aggregate' do
70
78
  command = repo.command(:create, repo.aggregate(:posts))
71
79
 
@@ -73,7 +81,6 @@ RSpec.describe ROM::Repository::Root do
73
81
 
74
82
  expect(result.name).to eql('Jade')
75
83
  expect(result.posts.size).to be(1)
76
- expect(result.posts[0].author_id).to be(result.id)
77
84
  expect(result.posts[0].title).to eql('Jade post')
78
85
  end
79
86
 
@@ -0,0 +1,31 @@
1
+ RSpec.describe 'ROM repository with typed structs' do
2
+ subject(:repo) do
3
+ Class.new(ROM::Repository[:books]).new(rom)
4
+ end
5
+
6
+ include_context 'database'
7
+ include_context 'seeds'
8
+
9
+ before do
10
+ configuration.relation(:books) do
11
+ schema(infer: true)
12
+
13
+ view(:index) do
14
+ schema { project(:id, :title, :created_at) }
15
+ relation { order(:title) }
16
+ end
17
+ end
18
+
19
+ rom.relations[:books].insert(title: 'Hello World', created_at: Time.now)
20
+ end
21
+
22
+ it 'loads typed structs' do
23
+ book = repo.books.index.first
24
+
25
+ expect(book).to be_kind_of(Dry::Struct)
26
+
27
+ expect(book.id).to be_kind_of(Integer)
28
+ expect(book.title).to eql('Hello World')
29
+ expect(book.created_at).to be_kind_of(Time)
30
+ end
31
+ end
@@ -48,7 +48,7 @@ RSpec.shared_context 'database' do
48
48
  conn.create_table :posts do
49
49
  primary_key :id
50
50
  foreign_key :author_id, :users, null: false, on_delete: :cascade
51
- column :title, String
51
+ column :title, String, null: false
52
52
  column :body, String
53
53
  end
54
54
 
@@ -70,4 +70,8 @@ RSpec.shared_context 'database' do
70
70
  column :author, String
71
71
  end
72
72
  end
73
+
74
+ after do
75
+ rom.disconnect
76
+ end
73
77
  end