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
@@ -1,182 +0,0 @@
1
- require 'rom/support/options'
2
- require 'rom/relation/materializable'
3
-
4
- require 'rom/repository/loading_proxy/combine'
5
- require 'rom/repository/loading_proxy/wrap'
6
-
7
- module ROM
8
- class Repository
9
- # LoadingProxy decorates a relation and automatically generate mappers that
10
- # will map raw tuples into structs
11
- #
12
- # @api public
13
- class LoadingProxy
14
- include Options
15
- include Relation::Materializable
16
-
17
- include LoadingProxy::Combine
18
- include LoadingProxy::Wrap
19
-
20
- option :name, reader: true, type: Symbol
21
- option :mapper_builder, reader: true, default: proc { MapperBuilder.new }
22
- option :meta, reader: true, type: Hash, default: EMPTY_HASH
23
-
24
- # @attr_reader [ROM::Relation::Lazy] relation Decorated relation
25
- attr_reader :relation
26
-
27
- # @api private
28
- def initialize(relation, options = {})
29
- super
30
- @relation = relation
31
- end
32
-
33
- # Materialize wrapped relation and send it through a mapper
34
- #
35
- # For performance reasons a combined relation will skip mapping since
36
- # we only care about extracting key values for combining
37
- #
38
- # @api public
39
- def call(*args)
40
- ((combine? || composite?) ? relation : (relation >> mapper)).call(*args)
41
- end
42
-
43
- # Map this relation with other mappers too
44
- #
45
- # @api public
46
- def map_with(*names)
47
- if names.size == 1 && names[0].is_a?(Class)
48
- with(meta: meta.merge(model: names[0]))
49
- else
50
- mappers = [mapper]+names.map { |name| relation.mappers[name] }
51
- mappers.reduce(self) { |a, e| a >> e }
52
- end
53
- end
54
- alias_method :as, :map_with
55
-
56
- # Return AST for this relation
57
- #
58
- # @return [Array]
59
- #
60
- # @api private
61
- def to_ast
62
- attr_ast = attributes.map { |name| [:attribute, name] }
63
-
64
- node_ast = nodes.map(&:to_ast)
65
- wrap_ast = wraps.map(&:to_ast)
66
-
67
- wrap_attrs = wraps.flat_map { |wrap|
68
- wrap.attributes.map { |c| [:attribute, :"#{wrap.base_name}_#{c}"] }
69
- }
70
-
71
- meta = options[:meta].merge(base_name: relation.base_name)
72
- meta.delete(:wraps)
73
-
74
- [:relation, name, [:header, (attr_ast - wrap_attrs) + node_ast + wrap_ast], meta]
75
- end
76
-
77
- # Infer a mapper for the relation
78
- #
79
- # @return [ROM::Mapper]
80
- #
81
- # @api private
82
- def mapper
83
- mapper_builder[to_ast]
84
- end
85
-
86
- # @api private
87
- def respond_to_missing?(name, include_private = false)
88
- relation.respond_to?(name) || super
89
- end
90
-
91
- # Return new instance with new options
92
- #
93
- # @return [LoadingProxy]
94
- #
95
- # @api private
96
- def with(new_options)
97
- __new__(relation, new_options)
98
- end
99
-
100
- # Return if this relation is combined
101
- #
102
- # @return [Boolean]
103
- #
104
- # @api private
105
- def combine?
106
- meta[:combine_type]
107
- end
108
-
109
- # Return if this relation is a composite
110
- #
111
- # @return [Boolean]
112
- #
113
- # @api private
114
- def composite?
115
- relation.is_a?(Relation::Composite)
116
- end
117
-
118
- # Return meta info for this relation
119
- #
120
- # @return [Hash]
121
- #
122
- # @api private
123
- def meta
124
- options[:meta]
125
- end
126
-
127
- # Return all nodes that this relation combines
128
- #
129
- # @return [Array<LoadingProxy>]
130
- #
131
- # @api private
132
- def nodes
133
- relation.respond_to?(:nodes) ? relation.nodes : []
134
- end
135
-
136
- private
137
-
138
- # Return a new instance with another relation and options
139
- #
140
- # @return [LoadingProxy]
141
- #
142
- # @api private
143
- def __new__(relation, new_options = {})
144
- self.class.new(relation, options.merge(new_options))
145
- end
146
-
147
- # Return all nodes that this relation wraps
148
- #
149
- # @return [Array<LoadingProxy>]
150
- #
151
- # @api private
152
- def wraps
153
- meta.fetch(:wraps, [])
154
- end
155
-
156
- # Forward to relation and wrap it with proxy if response was a relation too
157
- #
158
- # TODO: this will be simplified once ROM::Relation has lazy-features built-in
159
- # and ROM::Lazy is gone
160
- #
161
- # @api private
162
- def method_missing(meth, *args, &block)
163
- if relation.respond_to?(meth)
164
- result = relation.__send__(meth, *args, &block)
165
-
166
- if result.kind_of?(Relation::Materializable) && !result.is_a?(Relation::Loaded)
167
- __new__(result)
168
- else
169
- result
170
- end
171
- else
172
- super
173
- end
174
- end
175
-
176
- # @api private
177
- def respond_to_missing?(meth, _include_private = false)
178
- relation.respond_to?(meth) || super
179
- end
180
- end
181
- end
182
- end
@@ -1,147 +0,0 @@
1
- RSpec.describe 'loading proxy' do
2
- include_context 'database'
3
- include_context 'relations'
4
- include_context 'repo'
5
- include_context 'structs'
6
- include_context 'seeds'
7
-
8
- let(:users) do
9
- ROM::Repository::LoadingProxy.new(rom.relation(:users), name: :users)
10
- end
11
-
12
- let(:tasks) do
13
- ROM::Repository::LoadingProxy.new(rom.relation(:tasks), name: :tasks)
14
- end
15
-
16
- let(:tags) do
17
- ROM::Repository::LoadingProxy.new(rom.relation(:tags), name: :tags)
18
- end
19
-
20
- describe '#each' do
21
- it 'yields loaded structs' do
22
- result = []
23
-
24
- users.each { |user| result << user }
25
-
26
- expect(result).to eql([jane, joe])
27
- end
28
-
29
- it 'returns an enumerator when block is not given' do
30
- expect(users.each.to_a).to eql([jane, joe])
31
- end
32
- end
33
-
34
- describe '#map_with/#as' do
35
- before do
36
- configuration.mappers do
37
- register :users, name_list: -> users { users.map(&:name) }
38
- end
39
- end
40
-
41
- it 'sends the relation through multiple mappers' do
42
- expect(users.map_with(:name_list).to_a).to eql(%w(Jane Joe))
43
- end
44
-
45
- context 'setting custom model type' do
46
- let(:user_type) { Class.new { include Anima.new(:id, :name) } }
47
- let(:custom_users) { users.as(user_type) }
48
-
49
- it 'instantiates custom model' do
50
- expect(custom_users.where(name: 'Jane').one).to be_instance_of(user_type)
51
- end
52
- end
53
- end
54
-
55
- describe 'retrieving a single struct' do
56
- describe '#first' do
57
- it 'returns exactly one struct' do
58
- expect(users.first).to eql(jane)
59
- end
60
- end
61
-
62
- describe '#one' do
63
- it 'returns exactly one struct' do
64
- expect(users.find(id: 1).one).to eql(jane)
65
-
66
- expect(users.find(id: 3).one).to be(nil)
67
-
68
- expect { users.find(id: [1,2]).one }.to raise_error(ROM::TupleCountMismatchError)
69
- end
70
- end
71
-
72
- describe '#one!' do
73
- it 'returns exactly one struct' do
74
- expect(users.find(id: 1).one!).to eql(jane)
75
-
76
- expect { users.find(id: [1, 2]).one! }.to raise_error(ROM::TupleCountMismatchError)
77
- expect { users.find(id: [3]).one! }.to raise_error(ROM::TupleCountMismatchError)
78
- end
79
- end
80
- end
81
-
82
- describe '#to_ast' do
83
- it 'returns valid ast for a single relation' do
84
- expect(users.to_ast).to eql(
85
- [
86
- :relation, :users, [
87
- :header, [[:attribute, :id], [:attribute, :name]]
88
- ],
89
- base_name: :users
90
- ]
91
- )
92
- end
93
-
94
- it 'returns valid ast for a combined relation' do
95
- relation = users.combine(many: { user_tasks: [tasks, id: :user_id] })
96
-
97
- expect(relation.to_ast).to eql(
98
- [
99
- :relation, :users, [
100
- :header, [
101
- [:attribute, :id],
102
- [:attribute, :name],
103
- [
104
- :relation, :user_tasks, [
105
- :header, [
106
- [:attribute, :id],
107
- [:attribute, :user_id],
108
- [:attribute, :title]
109
- ]
110
- ],
111
- { base_name: :tasks, keys: { id: :user_id }, combine_type: :many }
112
- ]
113
- ]
114
- ],
115
- base_name: :users
116
- ]
117
- )
118
- end
119
-
120
- it 'returns valid ast for a wrapped relation' do
121
- relation = tags.wrap_parent(task: tasks)
122
-
123
- expect(relation.to_ast).to eql(
124
- [
125
- :relation, :tags, [
126
- :header, [
127
- [:attribute, :id],
128
- [:attribute, :task_id],
129
- [:attribute, :name],
130
- [
131
- :relation, :task, [
132
- :header, [
133
- [:attribute, :id],
134
- [:attribute, :user_id],
135
- [:attribute, :title]
136
- ]
137
- ],
138
- { base_name: :tasks, keys: { id: :task_id }, wrap: true }
139
- ]
140
- ]
141
- ],
142
- base_name: :tags
143
- ]
144
- )
145
- end
146
- end
147
- end