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.
- 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
|