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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d225169d8e6601c42a19f9e8504363fb766b49b
4
- data.tar.gz: 2277ca38edb2706b648fabbd30da68cbbb9adb3c
3
+ metadata.gz: 14c56e0b7faccfcb4ed7d1b7b8506e3f5adc6384
4
+ data.tar.gz: 2c9e669ae2e2e863a033c9bc9169589d98200d87
5
5
  SHA512:
6
- metadata.gz: fb36f1ef4cd751ddb8de93a54018b6c057db268c00f66131a5911f7b3f632e1db159952fc6be2043a486ed6c31dedb6aced2933df57a3b9cb74768d2fa09f346
7
- data.tar.gz: 73ce2e1fbcedc63495fd7b6bcd959a938427d9b16774396450dd04b26a64585a4f4f56033cf7763c29f9ca0e94cdb72517e01b2aa24920f0c7894bb60a1bfa4b
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
@@ -11,7 +11,7 @@ group :development do
11
11
  end
12
12
 
13
13
  group :test do
14
- gem 'rom-sql'
14
+ gem 'rom-sql', git: 'https://github.com/rom-rb/rom-sql.git', branch: 'master'
15
15
  gem 'rspec'
16
16
  gem 'dry-struct'
17
17
  gem 'byebug', platforms: :mri
@@ -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
- attr_reader :container
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
- @container = container
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, *args)
21
+ def visit(node)
22
22
  name, node = node
23
- __send__("visit_#{name}", node, *args)
23
+ __send__("visit_#{name}", node)
24
24
  end
25
25
 
26
- def visit_relation(node, meta = {})
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, meta), model: model]
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, meta = {})
49
- node.map { |attribute| visit(attribute, meta) }
48
+ def visit_header(node)
49
+ node.map { |attribute| visit(attribute) }
50
50
  end
51
51
 
52
- def visit_attribute(attr, meta = {})
53
- if meta[:wrap]
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
- names.reduce(self) { |a, e| a >> relation.mappers[e] }
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] ? relation.schema.wrap.qualified : relation.schema.reject(&:wrapped?)
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
- wraps = options.map { |(name, (relation, keys))|
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 = wraps.reduce(self) { |a, e|
24
- a.relation.for_wrap(e.meta.fetch(:keys), e.base_name.relation)
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(name: name, meta: { keys: keys, wrap: true, combine_name: name })
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
@@ -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(model)]
53
+ [name, Types::Array.member(member)]
47
54
  else
48
- [name, model.optional]
55
+ [name, member.optional]
49
56
  end
50
57
  end
51
58
 
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  class Repository
3
- VERSION = '1.0.2'.freeze
3
+ VERSION = '1.1.0'.freeze
4
4
  end
5
5
  end
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 wrap' do
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 }, combine_type: :many, combine_name: :user_tasks },
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.qualified
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 }, wrap: true, combine_name: :task },
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.2
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-13 00:00:00.000000000 Z
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.10
192
+ rubygems_version: 2.6.9
192
193
  signing_key:
193
194
  specification_version: 4
194
195
  summary: Repository abstraction for rom-rb