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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -5
- data/CHANGELOG.md +24 -0
- data/Gemfile +21 -5
- data/README.md +6 -110
- data/lib/rom/repository/changeset/create.rb +26 -0
- data/lib/rom/repository/changeset/pipe.rb +40 -0
- data/lib/rom/repository/changeset/update.rb +82 -0
- data/lib/rom/repository/changeset.rb +99 -0
- data/lib/rom/repository/class_interface.rb +142 -0
- data/lib/rom/repository/command_compiler.rb +214 -0
- data/lib/rom/repository/command_proxy.rb +22 -0
- data/lib/rom/repository/header_builder.rb +13 -16
- data/lib/rom/repository/mapper_builder.rb +7 -14
- data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
- data/lib/rom/repository/relation_proxy.rb +225 -0
- data/lib/rom/repository/root.rb +110 -0
- data/lib/rom/repository/struct_attributes.rb +46 -0
- data/lib/rom/repository/struct_builder.rb +31 -14
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/repository.rb +192 -31
- data/lib/rom/struct.rb +13 -8
- data/rom-repository.gemspec +9 -10
- data/spec/integration/changeset_spec.rb +86 -0
- data/spec/integration/command_macros_spec.rb +175 -0
- data/spec/integration/command_spec.rb +224 -0
- data/spec/integration/multi_adapter_spec.rb +3 -3
- data/spec/integration/repository_spec.rb +97 -2
- data/spec/integration/root_repository_spec.rb +88 -0
- data/spec/shared/database.rb +47 -3
- data/spec/shared/mappers.rb +35 -0
- data/spec/shared/models.rb +41 -0
- data/spec/shared/plugins.rb +66 -0
- data/spec/shared/relations.rb +76 -0
- data/spec/shared/repo.rb +38 -17
- data/spec/shared/seeds.rb +19 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/support/mapper_registry.rb +1 -3
- data/spec/unit/changeset_spec.rb +58 -0
- data/spec/unit/header_builder_spec.rb +34 -35
- data/spec/unit/relation_proxy_spec.rb +170 -0
- data/spec/unit/sql/relation_spec.rb +5 -5
- data/spec/unit/struct_builder_spec.rb +7 -4
- data/spec/unit/struct_spec.rb +22 -0
- metadata +38 -41
- data/lib/rom/plugins/relation/key_inference.rb +0 -31
- data/lib/rom/repository/loading_proxy/combine.rb +0 -158
- data/lib/rom/repository/loading_proxy.rb +0 -182
- 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
|