rom-repository 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/Gemfile +1 -1
- data/lib/rom/repository.rb +32 -5
- data/lib/rom/repository/header_builder.rb +9 -9
- data/lib/rom/repository/relation_proxy.rb +78 -3
- data/lib/rom/repository/relation_proxy/wrap.rb +26 -9
- data/lib/rom/repository/root.rb +1 -3
- data/lib/rom/repository/struct_builder.rb +10 -3
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/struct.rb +13 -0
- data/spec/integration/repository/aggregate_spec.rb +49 -0
- data/spec/integration/repository_spec.rb +94 -1
- data/spec/unit/relation_proxy_spec.rb +5 -3
- data/spec/unit/struct_builder_spec.rb +26 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14c56e0b7faccfcb4ed7d1b7b8506e3f5adc6384
|
4
|
+
data.tar.gz: 2c9e669ae2e2e863a033c9bc9169589d98200d87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac27d7fd92fe6385817f18063573da18f98f710d391d2dcf369cfc0fd07b4540582d6190aadd2d7cfd39b89a073541c7aa2defcb2d5c03e896238a405c6f8d27
|
7
|
+
data.tar.gz: 6e6af0a5a553888023c7d1164ab9e864572a9c3ac659f76b9220e484e8e9db06dae704c7fda3e244c8cef87fe050722891307fcc6379f26fecd0ac1936b2c105
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
# v1.1.0 2017-02-16
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Mapping to `ROM::Struct` can be disabled via `auto_struct` option (solnic)
|
6
|
+
* You can now use custom mappers along with auto-mapping via `map_with(:my_mapper, auto_map: true)` (solnic)
|
7
|
+
* `wrap` can be used along with association names ie `users.wrap(:address)` (solnic)
|
8
|
+
* `#node` relation method that can be used to adjust graph nodes produced by `aggregate` or `combine` (solnic)
|
9
|
+
|
10
|
+
## Fixed
|
11
|
+
|
12
|
+
* Structs raise nicer `NoMethodError` (flash-gordon)
|
13
|
+
* Custom model set on a node relation is respected when loading aggregates (solnic)
|
14
|
+
|
15
|
+
[Compare v1.0.2...v1.1.0](https://github.com/rom-rb/rom-repository/compare/v1.0.2...v1.1.0)
|
16
|
+
|
1
17
|
# v1.0.2 2017-02-13
|
2
18
|
|
3
19
|
### Fixed
|
data/Gemfile
CHANGED
data/lib/rom/repository.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require 'dry/core/deprecations'
|
2
2
|
|
3
|
+
require 'rom/initializer'
|
3
4
|
require 'rom/repository/class_interface'
|
4
5
|
require 'rom/repository/mapper_builder'
|
5
6
|
require 'rom/repository/relation_proxy'
|
6
7
|
require 'rom/repository/command_compiler'
|
7
8
|
|
8
|
-
require 'rom/repository/root'
|
9
9
|
require 'rom/repository/changeset'
|
10
10
|
require 'rom/repository/session'
|
11
11
|
|
@@ -63,10 +63,32 @@ module ROM
|
|
63
63
|
}.freeze
|
64
64
|
|
65
65
|
extend ClassInterface
|
66
|
+
extend Initializer
|
67
|
+
extend Dry::Core::ClassAttributes
|
68
|
+
|
69
|
+
# @!method self.auto_struct
|
70
|
+
# Get or set auto_struct setting
|
71
|
+
#
|
72
|
+
# When disabled, rom structs won't be created
|
73
|
+
#
|
74
|
+
# @overload auto_struct
|
75
|
+
# Return auto_struct setting value
|
76
|
+
# @return [TrueClass,FalseClass]
|
77
|
+
#
|
78
|
+
# @overload auto_struct(value)
|
79
|
+
# Set auto_struct value
|
80
|
+
# @return [Class]
|
81
|
+
defines :auto_struct
|
82
|
+
|
83
|
+
auto_struct true
|
84
|
+
|
85
|
+
# @!attribute [r] container
|
86
|
+
# @return [ROM::Container] The container used to set up a repo
|
87
|
+
param :container, allow: ROM::Container
|
66
88
|
|
67
89
|
# @!attribute [r] container
|
68
90
|
# @return [ROM::Container] The container used to set up a repo
|
69
|
-
|
91
|
+
option :auto_struct, optional: true, default: -> repo { repo.class.auto_struct }
|
70
92
|
|
71
93
|
# @!attribute [r] relations
|
72
94
|
# @return [RelationRegistry] The relation proxy registry used by a repo
|
@@ -86,15 +108,17 @@ module ROM
|
|
86
108
|
# @param container [ROM::Container] The rom container with relations and optional commands
|
87
109
|
#
|
88
110
|
# @api public
|
89
|
-
def initialize(container)
|
90
|
-
|
111
|
+
def initialize(container, opts = EMPTY_HASH)
|
112
|
+
super
|
113
|
+
|
91
114
|
@mappers = MapperBuilder.new
|
115
|
+
|
92
116
|
@relations = RelationRegistry.new do |registry, relations|
|
93
117
|
self.class.relations.each do |name|
|
94
118
|
relation = container.relation(name)
|
95
119
|
|
96
120
|
proxy = RelationProxy.new(
|
97
|
-
relation, name: name, mappers: mappers, registry: registry
|
121
|
+
relation, name: name, mappers: mappers, registry: registry, auto_struct: auto_struct
|
98
122
|
)
|
99
123
|
|
100
124
|
instance_variable_set("@#{name}", proxy)
|
@@ -102,6 +126,7 @@ module ROM
|
|
102
126
|
relations[name] = proxy
|
103
127
|
end
|
104
128
|
end
|
129
|
+
|
105
130
|
@command_compiler = method(:command)
|
106
131
|
end
|
107
132
|
|
@@ -327,3 +352,5 @@ module ROM
|
|
327
352
|
end
|
328
353
|
end
|
329
354
|
end
|
355
|
+
|
356
|
+
require 'rom/repository/root'
|
@@ -7,7 +7,7 @@ module ROM
|
|
7
7
|
class HeaderBuilder
|
8
8
|
attr_reader :struct_builder
|
9
9
|
|
10
|
-
def initialize
|
10
|
+
def initialize(options = EMPTY_HASH)
|
11
11
|
@struct_builder = StructBuilder.new
|
12
12
|
end
|
13
13
|
|
@@ -18,12 +18,12 @@ module ROM
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
def visit(node
|
21
|
+
def visit(node)
|
22
22
|
name, node = node
|
23
|
-
__send__("visit_#{name}", node
|
23
|
+
__send__("visit_#{name}", node)
|
24
24
|
end
|
25
25
|
|
26
|
-
def visit_relation(node
|
26
|
+
def visit_relation(node)
|
27
27
|
relation_name, meta, header = node
|
28
28
|
name = meta[:combine_name] || relation_name
|
29
29
|
|
@@ -31,7 +31,7 @@ module ROM
|
|
31
31
|
struct_builder[meta.fetch(:dataset), header]
|
32
32
|
end
|
33
33
|
|
34
|
-
options = [visit(header
|
34
|
+
options = [visit(header), model: model]
|
35
35
|
|
36
36
|
if meta[:combine_type]
|
37
37
|
type = meta[:combine_type] == :many ? :array : :hash
|
@@ -45,12 +45,12 @@ module ROM
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
def visit_header(node
|
49
|
-
node.map { |attribute| visit(attribute
|
48
|
+
def visit_header(node)
|
49
|
+
node.map { |attribute| visit(attribute) }
|
50
50
|
end
|
51
51
|
|
52
|
-
def visit_attribute(attr
|
53
|
-
if
|
52
|
+
def visit_attribute(attr)
|
53
|
+
if attr.wrapped?
|
54
54
|
[attr.name, from: attr.alias]
|
55
55
|
else
|
56
56
|
[attr.name]
|
@@ -30,6 +30,7 @@ module ROM
|
|
30
30
|
option :mappers, reader: true, default: proc { MapperBuilder.new }
|
31
31
|
option :meta, reader: true, default: proc { EMPTY_HASH }
|
32
32
|
option :registry, type: RelationRegistryType, default: proc { RelationRegistry.new }, reader: true
|
33
|
+
option :auto_struct, optional: true
|
33
34
|
|
34
35
|
# Relation name
|
35
36
|
#
|
@@ -68,20 +69,78 @@ module ROM
|
|
68
69
|
#
|
69
70
|
# @param [Array<Symbol>] mappers A list of mapper identifiers
|
70
71
|
#
|
72
|
+
# @overload map_with(*mappers, auto_map: true)
|
73
|
+
# Map tuples using auto-mapping and custom registered mappers
|
74
|
+
#
|
75
|
+
# If `auto_map` is enabled, your mappers will be applied after performing
|
76
|
+
# default auto-mapping. This means that you can compose complex relations
|
77
|
+
# and have them auto-mapped, and use much simpler custom mappers to adjust
|
78
|
+
# resulting data according to your requirements.
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# users.map_with(:my_mapper, :my_other_mapper, auto_map: true)
|
82
|
+
#
|
83
|
+
# @param [Array<Symbol>] mappers A list of mapper identifiers
|
84
|
+
#
|
71
85
|
# @return [RelationProxy] A new relation proxy with pipelined relation
|
72
86
|
#
|
73
87
|
# @api public
|
74
|
-
def map_with(*names)
|
88
|
+
def map_with(*names, **opts)
|
75
89
|
if names.size == 1 && names[0].is_a?(Class)
|
76
90
|
with(meta: meta.merge(model: names[0]))
|
77
91
|
elsif names.size > 1 && names.any? { |name| name.is_a?(Class) }
|
78
92
|
raise ArgumentError, 'using custom mappers and a model is not supported'
|
79
93
|
else
|
80
|
-
|
94
|
+
if opts[:auto_map]
|
95
|
+
mappers = [mapper, *names.map { |name| relation.mappers[name] }]
|
96
|
+
mappers.reduce(self) { |a, e| a >> e }
|
97
|
+
else
|
98
|
+
names.reduce(self) { |a, e| a >> relation.mappers[e] }
|
99
|
+
end
|
81
100
|
end
|
82
101
|
end
|
83
102
|
alias_method :as, :map_with
|
84
103
|
|
104
|
+
# Return a new graph with adjusted node returned from a block
|
105
|
+
#
|
106
|
+
# @example with a node identifier
|
107
|
+
# aggregate(:tasks).node(:tasks) { |tasks| tasks.prioritized }
|
108
|
+
#
|
109
|
+
# @example with a nested path
|
110
|
+
# aggregate(tasks: :tags).node(tasks: :tags) { |tags| tags.where(name: 'red') }
|
111
|
+
#
|
112
|
+
# @param [Symbol] name The node relation name
|
113
|
+
#
|
114
|
+
# @yieldparam [RelationProxy] The relation node
|
115
|
+
# @yieldreturn [RelationProxy] The new relation node
|
116
|
+
#
|
117
|
+
# @return [RelationProxy]
|
118
|
+
#
|
119
|
+
# @api public
|
120
|
+
def node(name, &block)
|
121
|
+
if name.is_a?(Symbol) && !nodes.map { |n| n.name.relation }.include?(name)
|
122
|
+
raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
|
123
|
+
end
|
124
|
+
|
125
|
+
new_nodes = nodes.map { |node|
|
126
|
+
case name
|
127
|
+
when Symbol
|
128
|
+
name == node.name.relation ? yield(node) : node
|
129
|
+
when Hash
|
130
|
+
other, *rest = name.flatten(1)
|
131
|
+
if other == node.name.relation
|
132
|
+
nodes.detect { |n| n.name.relation == other }.node(*rest, &block)
|
133
|
+
else
|
134
|
+
node
|
135
|
+
end
|
136
|
+
else
|
137
|
+
node
|
138
|
+
end
|
139
|
+
}
|
140
|
+
|
141
|
+
with_nodes(new_nodes)
|
142
|
+
end
|
143
|
+
|
85
144
|
# Return a string representation of this relation proxy
|
86
145
|
#
|
87
146
|
# @return [String]
|
@@ -147,6 +206,7 @@ module ROM
|
|
147
206
|
attr_ast = schema.map { |attr| [:attribute, attr] }
|
148
207
|
|
149
208
|
meta = self.meta.merge(dataset: base_name.dataset)
|
209
|
+
meta.update(model: false) unless meta[:model] || auto_struct
|
150
210
|
meta.delete(:wraps)
|
151
211
|
|
152
212
|
header = attr_ast + nodes_ast + wraps_ast
|
@@ -155,6 +215,17 @@ module ROM
|
|
155
215
|
end
|
156
216
|
end
|
157
217
|
|
218
|
+
# TODO: add a proper interface to `Relation::Graph` and `Relation::Curried` to rom core
|
219
|
+
#
|
220
|
+
# @api private
|
221
|
+
def with_nodes(nodes)
|
222
|
+
if relation.curried?
|
223
|
+
__new__(relation.send(:__new__, relation.relation.class.new(relation.root, nodes)))
|
224
|
+
else
|
225
|
+
__new__(relation.class.new(relation.root, nodes))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
158
229
|
# @api private
|
159
230
|
def respond_to_missing?(meth, _include_private = false)
|
160
231
|
relation.respond_to?(meth) || super
|
@@ -164,7 +235,11 @@ module ROM
|
|
164
235
|
|
165
236
|
# @api private
|
166
237
|
def schema
|
167
|
-
meta[:wrap]
|
238
|
+
if meta[:wrap]
|
239
|
+
relation.schema.wrap
|
240
|
+
else
|
241
|
+
relation.schema.reject(&:wrapped?)
|
242
|
+
end
|
168
243
|
end
|
169
244
|
|
170
245
|
# @api private
|
@@ -15,16 +15,15 @@ module ROM
|
|
15
15
|
# @return [RelationProxy]
|
16
16
|
#
|
17
17
|
# @api public
|
18
|
-
def wrap(options)
|
19
|
-
|
20
|
-
relation.wrapped(name, keys)
|
21
|
-
}
|
18
|
+
def wrap(*names, **options)
|
19
|
+
new_wraps = wraps_from_names(names) + wraps_from_options(options)
|
22
20
|
|
23
|
-
relation =
|
24
|
-
|
21
|
+
relation = new_wraps.reduce(self) { |a, e|
|
22
|
+
name = e.meta[:wrap_from_assoc] ? e.meta[:combine_name] : e.base_name.relation
|
23
|
+
a.relation.for_wrap(e.meta.fetch(:keys), name)
|
25
24
|
}
|
26
25
|
|
27
|
-
__new__(relation, meta: { wraps: wraps })
|
26
|
+
__new__(relation, meta: { wraps: wraps + new_wraps })
|
28
27
|
end
|
29
28
|
|
30
29
|
# Shortcut to wrap parents
|
@@ -52,8 +51,26 @@ module ROM
|
|
52
51
|
# @return [RelationProxy]
|
53
52
|
#
|
54
53
|
# @api private
|
55
|
-
def wrapped(name, keys)
|
56
|
-
with(
|
54
|
+
def wrapped(name, keys, wrap_from_assoc = false)
|
55
|
+
with(
|
56
|
+
name: name,
|
57
|
+
meta: {
|
58
|
+
keys: keys, wrap_from_assoc: wrap_from_assoc, wrap: true, combine_name: name
|
59
|
+
}
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @api private
|
64
|
+
def wraps_from_options(options)
|
65
|
+
options.map { |(name, (relation, keys))| relation.wrapped(name, keys) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def wraps_from_names(names)
|
70
|
+
names.map { |name|
|
71
|
+
assoc = associations[name]
|
72
|
+
registry[assoc.target].wrapped(name, assoc.combine_keys(__registry__), true)
|
73
|
+
}
|
57
74
|
end
|
58
75
|
end
|
59
76
|
end
|
data/lib/rom/repository/root.rb
CHANGED
@@ -29,8 +29,6 @@ module ROM
|
|
29
29
|
#
|
30
30
|
# @api public
|
31
31
|
class Root < Repository
|
32
|
-
extend Dry::Core::ClassAttributes
|
33
|
-
|
34
32
|
# @!method self.root
|
35
33
|
# Get or set repository root relation identifier
|
36
34
|
#
|
@@ -59,7 +57,7 @@ module ROM
|
|
59
57
|
end
|
60
58
|
|
61
59
|
# @see Repository#initialize
|
62
|
-
def initialize(container)
|
60
|
+
def initialize(container, opts = EMPTY_HASH)
|
63
61
|
super
|
64
62
|
@root = relations[self.class.root]
|
65
63
|
end
|
@@ -40,12 +40,19 @@ module ROM
|
|
40
40
|
relation_name, meta, header = node
|
41
41
|
name = meta[:combine_name] || relation_name.relation
|
42
42
|
|
43
|
-
model = call(name, header)
|
43
|
+
model = meta.fetch(:model) { call(name, header) }
|
44
|
+
|
45
|
+
member =
|
46
|
+
if model < Dry::Struct
|
47
|
+
model
|
48
|
+
else
|
49
|
+
Dry::Types::Definition.new(model).constructor(&model.method(:new))
|
50
|
+
end
|
44
51
|
|
45
52
|
if meta[:combine_type] == :many
|
46
|
-
[name, Types::Array.member(
|
53
|
+
[name, Types::Array.member(member)]
|
47
54
|
else
|
48
|
-
[name,
|
55
|
+
[name, member.optional]
|
49
56
|
end
|
50
57
|
end
|
51
58
|
|
data/lib/rom/struct.rb
CHANGED
@@ -62,5 +62,18 @@ module ROM
|
|
62
62
|
def fetch(name)
|
63
63
|
__send__(name)
|
64
64
|
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def method_missing(m, *args)
|
69
|
+
inspected = inspect
|
70
|
+
trace = caller
|
71
|
+
|
72
|
+
# This is how MRI currently works
|
73
|
+
# see func name_err_mesg_to_str in error.c
|
74
|
+
name = inspected.size > 65 ? to_s : inspected
|
75
|
+
|
76
|
+
raise NoMethodError.new("undefined method `#{ m }' for #{ name }", m, args).tap { |e| e.set_backtrace(trace) }
|
77
|
+
end
|
65
78
|
end
|
66
79
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
RSpec.describe ROM::Repository::Root, '#aggregate' do
|
2
|
+
subject(:repo) do
|
3
|
+
Class.new(ROM::Repository[:users]) do
|
4
|
+
relations :tasks, :posts, :labels
|
5
|
+
end.new(rom)
|
6
|
+
end
|
7
|
+
|
8
|
+
include_context 'database'
|
9
|
+
include_context 'relations'
|
10
|
+
include_context 'seeds'
|
11
|
+
|
12
|
+
it 'exposes nodes via `node` method' do
|
13
|
+
jane = repo.
|
14
|
+
aggregate(:posts).
|
15
|
+
node(:posts) { |posts| posts.where(title: 'Another one') }.
|
16
|
+
where(name: 'Jane').one
|
17
|
+
|
18
|
+
expect(jane.name).to eql('Jane')
|
19
|
+
expect(jane.posts).to be_empty
|
20
|
+
|
21
|
+
repo.posts.insert author_id: 1, title: 'Another one'
|
22
|
+
|
23
|
+
jane = repo.
|
24
|
+
aggregate(:posts).
|
25
|
+
node(:posts) { |posts| posts.where(title: 'Another one') }.
|
26
|
+
where(name: 'Jane').one
|
27
|
+
|
28
|
+
expect(jane.name).to eql('Jane')
|
29
|
+
expect(jane.posts.size).to be(1)
|
30
|
+
expect(jane.posts[0].title).to eql('Another one')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'exposes nested nodes via `node` method' do
|
34
|
+
jane = repo.
|
35
|
+
aggregate(posts: :labels).
|
36
|
+
node(posts: :labels) { |labels| labels.where(name: 'red') }.
|
37
|
+
where(name: 'Jane').one
|
38
|
+
|
39
|
+
expect(jane.name).to eql('Jane')
|
40
|
+
expect(jane.posts.size).to be(1)
|
41
|
+
expect(jane.posts[0].labels.size).to be(1)
|
42
|
+
expect(jane.posts[0].labels[0].name).to eql('red')
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'raises arg error when invalid relation name was passed to `node` method' do
|
46
|
+
expect { repo.aggregate(:posts).node(:poztz) {} }.
|
47
|
+
to raise_error(ArgumentError, ':poztz is not a valid aggregate node name')
|
48
|
+
end
|
49
|
+
end
|
@@ -70,6 +70,13 @@ RSpec.describe 'ROM repository' do
|
|
70
70
|
expect(repo.tag_with_wrapped_task.first).to eql(tag_with_task)
|
71
71
|
end
|
72
72
|
|
73
|
+
it 'loads multiple wraps' do
|
74
|
+
post_label = repo.posts_labels.wrap(:post).wrap(:label).to_a.first
|
75
|
+
|
76
|
+
expect(post_label.label_id).to be(post_label.label.id)
|
77
|
+
expect(post_label.post_id).to be(post_label.post.id)
|
78
|
+
end
|
79
|
+
|
73
80
|
it 'loads an aggregate via custom fks' do
|
74
81
|
jane = repo.aggregate(many: repo.posts).where(name: 'Jane').one
|
75
82
|
|
@@ -135,7 +142,7 @@ RSpec.describe 'ROM repository' do
|
|
135
142
|
expect(jane.labels[1].name).to eql('blue')
|
136
143
|
end
|
137
144
|
|
138
|
-
it 'loads children and its parents via
|
145
|
+
it 'loads children and its parents via wrap_parent' do
|
139
146
|
posts = repo.posts.wrap_parent(author: repo.users)
|
140
147
|
|
141
148
|
label = repo.labels.combine(many: { posts: posts }).first
|
@@ -146,6 +153,15 @@ RSpec.describe 'ROM repository' do
|
|
146
153
|
expect(label.posts[0].author.name).to eql('Jane')
|
147
154
|
end
|
148
155
|
|
156
|
+
it 'loads children and its parents via wrap and association name' do
|
157
|
+
label = repo.labels.combine(many: { posts: repo.posts.wrap(:author) }).first
|
158
|
+
|
159
|
+
expect(label.name).to eql('red')
|
160
|
+
expect(label.posts.size).to be(1)
|
161
|
+
expect(label.posts[0].title).to eql('Hello From Jane')
|
162
|
+
expect(label.posts[0].author.name).to eql('Jane')
|
163
|
+
end
|
164
|
+
|
149
165
|
it 'loads a parent via custom fks' do
|
150
166
|
post = repo.posts.combine(:author).where(title: 'Hello From Jane').one
|
151
167
|
|
@@ -243,4 +259,81 @@ RSpec.describe 'ROM repository' do
|
|
243
259
|
expect(repo.dummy.to_a).to eql([])
|
244
260
|
end
|
245
261
|
end
|
262
|
+
|
263
|
+
describe 'mapping without structs' do
|
264
|
+
shared_context 'plain hash mapping' do
|
265
|
+
describe '#one' do
|
266
|
+
it 'returns a hash' do
|
267
|
+
expect(repo.users.limit(1).one).to eql(id: 1, name: 'Jane')
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'returns a nested hash for an aggregate' do
|
271
|
+
expect(repo.aggregate(:posts).limit(1).one).
|
272
|
+
to eql(id: 1, name: 'Jane', posts: [{ id: 1, author_id: 1, title: 'Hello From Jane', body: 'Jane Post'}])
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
context 'with auto_struct disabled upon initialization' do
|
278
|
+
subject(:repo) do
|
279
|
+
repo_class.new(rom, auto_struct: false)
|
280
|
+
end
|
281
|
+
|
282
|
+
include_context 'plain hash mapping'
|
283
|
+
end
|
284
|
+
|
285
|
+
context 'with auto_struct disabled at the class level' do
|
286
|
+
before do
|
287
|
+
repo_class.auto_struct(false)
|
288
|
+
end
|
289
|
+
|
290
|
+
include_context 'plain hash mapping'
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
describe 'using custom mappers along with auto-mapping' do
|
295
|
+
before do
|
296
|
+
configuration.mappers do
|
297
|
+
define(:users) do
|
298
|
+
register_as :embed_address
|
299
|
+
|
300
|
+
def call(rel)
|
301
|
+
rel.map { |tuple| Hash(tuple).merge(mapped: true) }
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'auto-maps and applies a custom mapper' do
|
308
|
+
jane = repo.users.combine(:posts).map_with(:embed_address, auto_map: true).to_a.first
|
309
|
+
|
310
|
+
expect(jane).
|
311
|
+
to eql(id:1, name: 'Jane', mapped: true, posts: [{ id: 1, author_id: 1, title: 'Hello From Jane', body: 'Jane Post' }])
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
describe 'using a custom model for a node' do
|
316
|
+
before do
|
317
|
+
class Test::Post < OpenStruct; end
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'uses provided model for the member type' do
|
321
|
+
jane = repo.users.combine(many: repo.posts.as(Test::Post)).where(name: 'Jane').one
|
322
|
+
|
323
|
+
expect(jane.name).to eql('Jane')
|
324
|
+
expect(jane.posts.size).to be(1)
|
325
|
+
expect(jane.posts[0]).to be_instance_of(Test::Post)
|
326
|
+
expect(jane.posts[0].title).to eql('Hello From Jane')
|
327
|
+
expect(jane.posts[0].body).to eql('Jane Post')
|
328
|
+
end
|
329
|
+
|
330
|
+
it 'uses provided model for the attribute type' do
|
331
|
+
jane = repo.users.combine(one: repo.posts.as(Test::Post)).where(name: 'Jane').one
|
332
|
+
|
333
|
+
expect(jane.name).to eql('Jane')
|
334
|
+
expect(jane.post).to be_instance_of(Test::Post)
|
335
|
+
expect(jane.post.title).to eql('Hello From Jane')
|
336
|
+
expect(jane.post.body).to eql('Jane Post')
|
337
|
+
end
|
338
|
+
end
|
246
339
|
end
|
@@ -134,7 +134,8 @@ RSpec.describe 'loading proxy' do
|
|
134
134
|
[:attribute, users.schema[:name]],
|
135
135
|
[:relation, [
|
136
136
|
:tasks,
|
137
|
-
{ dataset: :tasks, keys: { id: :user_id },
|
137
|
+
{ dataset: :tasks, keys: { id: :user_id },
|
138
|
+
combine_type: :many, combine_name: :user_tasks },
|
138
139
|
[:header, [
|
139
140
|
[:attribute, tasks.schema[:id]],
|
140
141
|
[:attribute, tasks.schema[:user_id]],
|
@@ -149,7 +150,7 @@ RSpec.describe 'loading proxy' do
|
|
149
150
|
relation = tags.wrap_parent(task: tasks)
|
150
151
|
|
151
152
|
tags_schema = tags.schema.qualified
|
152
|
-
tasks_schema = tasks.schema.wrap
|
153
|
+
tasks_schema = tasks.schema.wrap
|
153
154
|
|
154
155
|
expect(relation.to_ast).to eql(
|
155
156
|
[:relation, [
|
@@ -161,7 +162,8 @@ RSpec.describe 'loading proxy' do
|
|
161
162
|
[:attribute, tags_schema[:name]],
|
162
163
|
[:relation, [
|
163
164
|
:tasks,
|
164
|
-
{ dataset: :tasks, keys: { id: :task_id },
|
165
|
+
{ dataset: :tasks, keys: { id: :task_id },
|
166
|
+
wrap_from_assoc: false, wrap: true, combine_name: :task },
|
165
167
|
[:header, [
|
166
168
|
[:attribute, tasks_schema[:id]],
|
167
169
|
[:attribute, tasks_schema[:user_id]],
|
@@ -69,4 +69,30 @@ RSpec.describe 'struct builder', '#call' do
|
|
69
69
|
Dry::Struct::Error, /:name is missing/
|
70
70
|
)
|
71
71
|
end
|
72
|
+
|
73
|
+
context 'name errors' do
|
74
|
+
let(:struct) { builder.class.cache[input.hash] }
|
75
|
+
|
76
|
+
context 'missing method on instance' do
|
77
|
+
it 'uses inspect and class name for small structs' do
|
78
|
+
user = struct.new(id: 1, name: 'Jane')
|
79
|
+
|
80
|
+
expect { user.missing }.
|
81
|
+
to raise_error(
|
82
|
+
NoMethodError,
|
83
|
+
%r{undefined method `missing' for #<ROM::Struct\[User\] id=1 name="Jane">}
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'uses class name in name errors' do
|
88
|
+
user = struct.new(id: 1, name: 'J' * 50)
|
89
|
+
|
90
|
+
expect { user.missing }.
|
91
|
+
to raise_error(
|
92
|
+
NoMethodError,
|
93
|
+
%r{undefined method `missing' for #<ROM::Struct\[User\]:0x\h+>}
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
72
98
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rom-repository
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rom
|
@@ -145,6 +145,7 @@ files:
|
|
145
145
|
- spec/integration/command_macros_spec.rb
|
146
146
|
- spec/integration/command_spec.rb
|
147
147
|
- spec/integration/multi_adapter_spec.rb
|
148
|
+
- spec/integration/repository/aggregate_spec.rb
|
148
149
|
- spec/integration/repository_spec.rb
|
149
150
|
- spec/integration/root_repository_spec.rb
|
150
151
|
- spec/integration/typed_structs_spec.rb
|
@@ -188,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
188
189
|
version: '0'
|
189
190
|
requirements: []
|
190
191
|
rubyforge_project:
|
191
|
-
rubygems_version: 2.6.
|
192
|
+
rubygems_version: 2.6.9
|
192
193
|
signing_key:
|
193
194
|
specification_version: 4
|
194
195
|
summary: Repository abstraction for rom-rb
|