rom-repository 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bac31b2dc1dafd64ec8a3852d1cb9e595c49ded5
4
- data.tar.gz: 9110fc0e2f6d1b742ed89c4f7afb5c9b61ad0cdc
3
+ metadata.gz: db1338c97747a9adc2b9f00febe402d61ac1a6fd
4
+ data.tar.gz: bd6ca3140fe897a0d6b9cccc3082e0fdaf63ed9a
5
5
  SHA512:
6
- metadata.gz: 77b81f8ab9040eeaa5059dee76046f2330ebaddc75c1dad792e1b7e911d42bd4056d364d5b59194208c55d366d3d3e3358376292abfa2580f4e96a86b3ab1e83
7
- data.tar.gz: c6b9d0809edd8b5b8742ff9de4a1761c287f502706416bf698453a1153cea2f122972d0dcbfdc6d06ead0acc55100b9a1f4d230fd13d24bb536e0969b301d4ac
6
+ metadata.gz: 04f9d30b32805886118b5eb36451aeba065cc510d1a92d091c1e99a41585eb821cf69b68fa56b1f049610b390d1403316a662dacfe4ba32b7f65ba6ac60b8e84
7
+ data.tar.gz: 48a3b4136e064e00a562a584555765e8fb47a05388457b3cb31ab3b762dda4dd221299a1b621dfaf3705167412af644c8244c7ebbde22da285a8985f3afb712b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # v0.3.1 2016-07-27
2
+
3
+ Fixed gemspec so that we don't exclude all files with 'bin' in their name, geez (solnic)
4
+
5
+ [Compare v0.3.0...v0.3.1](https://github.com/rom-rb/rom-repository/compare/v0.3.0...v0.3.1)
6
+
1
7
  # v0.3.0 2016-07-27
2
8
 
3
9
  ### Added
@@ -0,0 +1,283 @@
1
+ module ROM
2
+ class Repository
3
+ class RelationProxy
4
+ # Provides convenient methods for composing relations
5
+ #
6
+ # @api public
7
+ module Combine
8
+ # Returns a combine representation of a loading-proxy relation
9
+ #
10
+ # This will carry meta info used to produce a correct AST from a relation
11
+ # so that correct mapper can be generated
12
+ #
13
+ # @return [RelationProxy]
14
+ #
15
+ # @api private
16
+ def combined(name, keys, type)
17
+ meta = { keys: keys, combine_type: type, combine_name: name }
18
+ with(name: name, meta: meta)
19
+ end
20
+
21
+ # Combine with other relations
22
+ #
23
+ # @overload combine(*associations)
24
+ # Composes relations using configured associations
25
+ # @example
26
+ # users.combine(:tasks, :posts)
27
+ # @param *associations [Array<Symbol>] A list of association names
28
+ #
29
+ # @overload combine(options)
30
+ # Composes relations based on options
31
+ #
32
+ # @example
33
+ # # users have-many tasks (name and join-keys inferred, which needs associations in schema)
34
+ # users.combine(many: tasks)
35
+ #
36
+ # # users have-many tasks with custom name (join-keys inferred, which needs associations in schema)
37
+ # users.combine(many: { priority_tasks: tasks.priority })
38
+ #
39
+ # # users have-many tasks with custom view and join keys
40
+ # users.combine(many: { tasks: [tasks.for_users, id: :task_id] })
41
+ #
42
+ # # users has-one task
43
+ # users.combine(one: { task: tasks })
44
+ #
45
+ # @param options [Hash] Options for combine
46
+ # @option :many [Hash] Sets options for "has-many" type of association
47
+ # @option :one [Hash] Sets options for "has-one/belongs-to" type of association
48
+ #
49
+ # @param [Hash] options
50
+ #
51
+ # @return [RelationProxy]
52
+ #
53
+ # @api public
54
+ def combine(*args)
55
+ options = args[0].is_a?(Hash) ? args[0] : args
56
+
57
+ combine_opts = Hash.new { |h, k| h[k] = {} }
58
+
59
+ options.each do |(type, relations)|
60
+ if relations
61
+ combine_opts[type] = combine_opts_from_relations(relations)
62
+ else
63
+ result, curried, keys = combine_opts_for_assoc(type)
64
+ combine_opts[result][type] = [curried, keys]
65
+ end
66
+ end
67
+
68
+ nodes = combine_opts.flat_map do |type, relations|
69
+ relations.map { |name, (relation, keys)|
70
+ relation.combined(name, keys, type)
71
+ }
72
+ end
73
+
74
+ __new__(relation.combine(*nodes))
75
+ end
76
+
77
+ # Shortcut for combining with parents which infers the join keys
78
+ #
79
+ # @example
80
+ # # tasks belong-to users
81
+ # tasks.combine_parents(one: users)
82
+ #
83
+ # # tasks belong-to users with custom user view
84
+ # tasks.combine_parents(one: users.task_owners)
85
+ #
86
+ # @param options [Hash] Combine options hash
87
+ #
88
+ # @return [RelationProxy]
89
+ #
90
+ # @api public
91
+ def combine_parents(options)
92
+ combine_opts = {}
93
+
94
+ options.each do |type, parents|
95
+ combine_opts[type] =
96
+ case parents
97
+ when Hash
98
+ parents.each_with_object({}) { |(name, parent), r|
99
+ keys = combine_keys(parent, relation, :parent)
100
+ r[name] = [parent, keys]
101
+ }
102
+ when Array
103
+ parents.each_with_object({}) { |parent, r|
104
+ tuple_key = parent.combine_tuple_key(type)
105
+ keys = combine_keys(parent, relation, :parent)
106
+ r[tuple_key] = [parent, keys]
107
+ }
108
+ else
109
+ tuple_key = parents.combine_tuple_key(type)
110
+ keys = combine_keys(parents, relation, :parent)
111
+ { tuple_key => [parents, keys] }
112
+ end
113
+ end
114
+
115
+ combine(combine_opts)
116
+ end
117
+
118
+ # Shortcut for combining with children which infers the join keys
119
+ #
120
+ # @example
121
+ # # users have-many tasks
122
+ # users.combine_children(many: tasks)
123
+ #
124
+ # # users have-many tasks with custom mapping (requires associations)
125
+ # users.combine_children(many: { priority_tasks: tasks.priority })
126
+ #
127
+ # @param [Hash] options
128
+ #
129
+ # @return [RelationProxy]
130
+ #
131
+ # @api public
132
+ def combine_children(options)
133
+ combine_opts = {}
134
+
135
+ options.each do |type, children|
136
+ combine_opts[type] =
137
+ case children
138
+ when Hash
139
+ children.each_with_object({}) { |(name, child), r|
140
+ keys = combine_keys(relation, child, :children)
141
+ r[name] = [child, keys]
142
+ }
143
+ when Array
144
+ parents.each_with_object({}) { |child, r|
145
+ tuple_key = parent.combine_tuple_key(type)
146
+ keys = combine_keys(relation, child, :children)
147
+ r[tuple_key] = [parent, keys]
148
+ }
149
+ else
150
+ tuple_key = children.combine_tuple_key(type)
151
+ keys = combine_keys(relation, children, :children)
152
+ { tuple_key => [children, keys] }
153
+ end
154
+ end
155
+
156
+ combine(combine_opts)
157
+ end
158
+
159
+ protected
160
+
161
+ # Infer join/combine keys for a given relation and association type
162
+ #
163
+ # When source has association corresponding to target's name, it'll be
164
+ # used to get the keys. Otherwise we fall back to using default keys based
165
+ # on naming conventions.
166
+ #
167
+ # @param [RelationProxy] relation
168
+ # @param [Symbol] type The type can be either :parent or :children
169
+ #
170
+ # @return [Hash<Symbol=>Symbol>]
171
+ #
172
+ # @api private
173
+ def combine_keys(source, target, type)
174
+ source.associations.try(target.name) { |assoc|
175
+ assoc.combine_keys(__registry__)
176
+ } or infer_combine_keys(source, target, type)
177
+ end
178
+
179
+ # Build combine options from a relation mapping hash passed to `combine`
180
+ #
181
+ # This method will infer combine keys either from defined associations
182
+ # or use the keys provided explicitly for ad-hoc combines
183
+ #
184
+ # It returns a mapping like `name => [preloadable_relation, combine_keys]`
185
+ # and this mapping is used by `combine` to build a full relation graph
186
+ #
187
+ # @api private
188
+ def combine_opts_from_relations(relations)
189
+ relations.each_with_object({}) do |(name, (other, keys)), h|
190
+ h[name] =
191
+ if other.curried?
192
+ [other, keys]
193
+ else
194
+ rel = combine_from_assoc(name, other) { other.combine_method(relation, keys) }
195
+ [rel, keys]
196
+ end
197
+ end
198
+ end
199
+
200
+ # Try to get a preloadable relation from a defined association
201
+ #
202
+ # If association doesn't exist we call the fallback block
203
+ #
204
+ # @return [RelationProxy]
205
+ #
206
+ # @api private
207
+ def combine_from_assoc(name, other, &fallback)
208
+ associations.try(name) { |assoc| other.for_combine(assoc) } or fallback.call
209
+ end
210
+
211
+ # Extract result (either :one or :many), preloadable relation and its keys
212
+ # by using given association name
213
+ #
214
+ # This is used when a flat list of association names was passed to `combine`
215
+ #
216
+ # @api private
217
+ def combine_opts_for_assoc(name)
218
+ assoc = relation.associations[name]
219
+ curried = registry[assoc.target.relation].for_combine(assoc)
220
+ keys = assoc.combine_keys(__registry__)
221
+ [assoc.result, curried, keys]
222
+ end
223
+
224
+ # Build a preloadable relation for relation graph
225
+ #
226
+ # When a given relation defines `for_other_relation` then it will be used
227
+ # to preload `other_relation`. ie `users` relation defines `for_tasks`
228
+ # then when we preload tasks for users, this custom method will be used
229
+ #
230
+ # This *defaults* to the built-in `for_combine` with explicitly provided
231
+ # keys
232
+ #
233
+ # @return [RelationProxy]
234
+ #
235
+ # @api private
236
+ def combine_method(other, keys)
237
+ custom_name = :"for_#{other.name.dataset}"
238
+
239
+ if relation.respond_to?(custom_name)
240
+ __send__(custom_name)
241
+ else
242
+ for_combine(keys)
243
+ end
244
+ end
245
+
246
+ # Infer key under which a combine relation will be loaded
247
+ #
248
+ # This is used in cases like ad-hoc combines where relation was passed
249
+ # in without specifying the key explicitly, ie:
250
+ #
251
+ # tasks.combine_parents(one: users)
252
+ #
253
+ # # ^^^ this will be expanded under-the-hood to:
254
+ # tasks.combine(one: { user: users })
255
+ #
256
+ # @return [Symbol]
257
+ #
258
+ # @api private
259
+ def combine_tuple_key(result)
260
+ if result == :one
261
+ Inflector.singularize(base_name.relation).to_sym
262
+ else
263
+ base_name.relation
264
+ end
265
+ end
266
+
267
+ # Fallback mechanism for `combine_keys` when there's no association defined
268
+ #
269
+ # @api private
270
+ def infer_combine_keys(source, target, type)
271
+ primary_key = source.primary_key
272
+ foreign_key = target.foreign_key(source)
273
+
274
+ if type == :parent
275
+ { foreign_key => primary_key }
276
+ else
277
+ { primary_key => foreign_key }
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  class Repository
3
- VERSION = '0.3.0'.freeze
3
+ VERSION = '0.3.1'.freeze
4
4
  end
5
5
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.homepage = 'http://rom-rb.org'
12
12
  gem.require_paths = ['lib']
13
13
  gem.version = ROM::Repository::VERSION.dup
14
- gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') || name.include?('examples') || name.include?('bin') }
14
+ gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') || name.include?('examples') || name.include?('bin/console') }
15
15
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
16
16
  gem.license = 'MIT'
17
17
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-repository
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
@@ -107,6 +107,7 @@ files:
107
107
  - lib/rom/repository/header_builder.rb
108
108
  - lib/rom/repository/mapper_builder.rb
109
109
  - lib/rom/repository/relation_proxy.rb
110
+ - lib/rom/repository/relation_proxy/combine.rb
110
111
  - lib/rom/repository/relation_proxy/wrap.rb
111
112
  - lib/rom/repository/root.rb
112
113
  - lib/rom/repository/struct_attributes.rb