rom-repository 1.0.2 → 1.1.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/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
|