rom-repository 0.3.1 → 1.0.0.beta1

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