rom 5.4.2 → 6.0.0.alpha1

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.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -71
  3. data/LICENSE +1 -1
  4. data/README.md +7 -6
  5. data/lib/rom/array_dataset.rb +46 -0
  6. data/lib/rom/associations/abstract.rb +217 -0
  7. data/lib/rom/associations/definitions/abstract.rb +150 -0
  8. data/lib/rom/associations/definitions/many_to_many.rb +29 -0
  9. data/lib/rom/associations/definitions/many_to_one.rb +14 -0
  10. data/lib/rom/associations/definitions/one_to_many.rb +14 -0
  11. data/lib/rom/associations/definitions/one_to_one.rb +14 -0
  12. data/lib/rom/associations/definitions/one_to_one_through.rb +14 -0
  13. data/lib/rom/associations/definitions.rb +7 -0
  14. data/lib/rom/associations/many_to_many.rb +128 -0
  15. data/lib/rom/associations/many_to_one.rb +65 -0
  16. data/lib/rom/associations/one_to_many.rb +65 -0
  17. data/lib/rom/associations/one_to_one.rb +13 -0
  18. data/lib/rom/associations/one_to_one_through.rb +13 -0
  19. data/lib/rom/associations/through_identifier.rb +41 -0
  20. data/lib/rom/attribute.rb +425 -0
  21. data/lib/rom/auto_curry.rb +70 -0
  22. data/lib/rom/cache.rb +87 -0
  23. data/lib/rom/changeset/associated.rb +110 -0
  24. data/lib/rom/changeset/create.rb +18 -0
  25. data/lib/rom/changeset/delete.rb +15 -0
  26. data/lib/rom/changeset/extensions/relation.rb +26 -0
  27. data/lib/rom/changeset/pipe.rb +81 -0
  28. data/lib/rom/changeset/pipe_registry.rb +27 -0
  29. data/lib/rom/changeset/stateful.rb +285 -0
  30. data/lib/rom/changeset/update.rb +81 -0
  31. data/lib/rom/changeset.rb +185 -0
  32. data/lib/rom/command.rb +351 -0
  33. data/lib/rom/command_compiler.rb +201 -0
  34. data/lib/rom/command_proxy.rb +36 -0
  35. data/lib/rom/commands/class_interface.rb +236 -0
  36. data/lib/rom/commands/composite.rb +55 -0
  37. data/lib/rom/commands/create.rb +15 -0
  38. data/lib/rom/commands/delete.rb +16 -0
  39. data/lib/rom/commands/graph/class_interface.rb +64 -0
  40. data/lib/rom/commands/graph/input_evaluator.rb +94 -0
  41. data/lib/rom/commands/graph.rb +88 -0
  42. data/lib/rom/commands/lazy/create.rb +35 -0
  43. data/lib/rom/commands/lazy/delete.rb +39 -0
  44. data/lib/rom/commands/lazy/update.rb +46 -0
  45. data/lib/rom/commands/lazy.rb +106 -0
  46. data/lib/rom/commands/update.rb +16 -0
  47. data/lib/rom/commands.rb +5 -0
  48. data/lib/rom/compat/auto_registration.rb +115 -0
  49. data/lib/rom/compat/auto_registration_strategies/base.rb +29 -0
  50. data/lib/rom/compat/auto_registration_strategies/custom_namespace.rb +84 -0
  51. data/lib/rom/compat/auto_registration_strategies/no_namespace.rb +33 -0
  52. data/lib/rom/compat/auto_registration_strategies/with_namespace.rb +29 -0
  53. data/lib/rom/compat/command.rb +74 -0
  54. data/lib/rom/compat/components/dsl/schema.rb +130 -0
  55. data/lib/rom/compat/components.rb +91 -0
  56. data/lib/rom/compat/global.rb +17 -0
  57. data/lib/rom/compat/mapper.rb +22 -0
  58. data/lib/rom/compat/registries.rb +47 -0
  59. data/lib/rom/compat/relation.rb +40 -0
  60. data/lib/rom/compat/schema/dsl.rb +260 -0
  61. data/lib/rom/compat/setting_proxy.rb +44 -0
  62. data/lib/rom/compat/setup.rb +151 -0
  63. data/lib/rom/compat/transformer.rb +49 -0
  64. data/lib/rom/compat.rb +22 -0
  65. data/lib/rom/components/association.rb +26 -0
  66. data/lib/rom/components/command.rb +24 -0
  67. data/lib/rom/components/core.rb +148 -0
  68. data/lib/rom/components/dataset.rb +60 -0
  69. data/lib/rom/components/dsl/association.rb +47 -0
  70. data/lib/rom/components/dsl/command.rb +60 -0
  71. data/lib/rom/components/dsl/core.rb +126 -0
  72. data/lib/rom/components/dsl/dataset.rb +33 -0
  73. data/lib/rom/components/dsl/gateway.rb +14 -0
  74. data/lib/rom/components/dsl/mapper.rb +70 -0
  75. data/lib/rom/components/dsl/relation.rb +49 -0
  76. data/lib/rom/components/dsl/schema.rb +150 -0
  77. data/lib/rom/components/dsl/view.rb +82 -0
  78. data/lib/rom/components/dsl.rb +255 -0
  79. data/lib/rom/components/gateway.rb +50 -0
  80. data/lib/rom/components/mapper.rb +29 -0
  81. data/lib/rom/components/provider.rb +160 -0
  82. data/lib/rom/components/registry.rb +154 -0
  83. data/lib/rom/components/relation.rb +41 -0
  84. data/lib/rom/components/schema.rb +61 -0
  85. data/lib/rom/components/view.rb +55 -0
  86. data/lib/rom/components.rb +55 -0
  87. data/lib/rom/configuration_dsl.rb +4 -0
  88. data/lib/rom/constants.rb +135 -0
  89. data/lib/rom/container.rb +182 -0
  90. data/lib/rom/core.rb +125 -0
  91. data/lib/rom/data_proxy.rb +97 -0
  92. data/lib/rom/enumerable_dataset.rb +70 -0
  93. data/lib/rom/gateway.rb +232 -0
  94. data/lib/rom/global.rb +56 -0
  95. data/lib/rom/header/attribute.rb +190 -0
  96. data/lib/rom/header.rb +198 -0
  97. data/lib/rom/inferrer.rb +55 -0
  98. data/lib/rom/initializer.rb +80 -0
  99. data/lib/rom/lint/enumerable_dataset.rb +56 -0
  100. data/lib/rom/lint/gateway.rb +120 -0
  101. data/lib/rom/lint/linter.rb +79 -0
  102. data/lib/rom/lint/spec.rb +22 -0
  103. data/lib/rom/lint/test.rb +98 -0
  104. data/lib/rom/loader.rb +161 -0
  105. data/lib/rom/mapper/attribute_dsl.rb +480 -0
  106. data/lib/rom/mapper/dsl.rb +107 -0
  107. data/lib/rom/mapper/model_dsl.rb +61 -0
  108. data/lib/rom/mapper.rb +99 -0
  109. data/lib/rom/mapper_compiler.rb +84 -0
  110. data/lib/rom/memory/associations/many_to_many.rb +12 -0
  111. data/lib/rom/memory/associations/many_to_one.rb +12 -0
  112. data/lib/rom/memory/associations/one_to_many.rb +12 -0
  113. data/lib/rom/memory/associations/one_to_one.rb +12 -0
  114. data/lib/rom/memory/associations.rb +6 -0
  115. data/lib/rom/memory/commands.rb +60 -0
  116. data/lib/rom/memory/dataset.rb +127 -0
  117. data/lib/rom/memory/gateway.rb +66 -0
  118. data/lib/rom/memory/mapper_compiler.rb +10 -0
  119. data/lib/rom/memory/relation.rb +91 -0
  120. data/lib/rom/memory/schema.rb +32 -0
  121. data/lib/rom/memory/storage.rb +61 -0
  122. data/lib/rom/memory/types.rb +11 -0
  123. data/lib/rom/memory.rb +7 -0
  124. data/lib/rom/model_builder.rb +103 -0
  125. data/lib/rom/open_struct.rb +112 -0
  126. data/lib/rom/pipeline.rb +111 -0
  127. data/lib/rom/plugin.rb +130 -0
  128. data/lib/rom/plugins/class_methods.rb +37 -0
  129. data/lib/rom/plugins/command/schema.rb +45 -0
  130. data/lib/rom/plugins/command/timestamps.rb +149 -0
  131. data/lib/rom/plugins/dsl.rb +53 -0
  132. data/lib/rom/plugins/relation/changeset.rb +97 -0
  133. data/lib/rom/plugins/relation/instrumentation.rb +66 -0
  134. data/lib/rom/plugins/relation/registry_reader.rb +36 -0
  135. data/lib/rom/plugins/schema/timestamps.rb +59 -0
  136. data/lib/rom/plugins.rb +100 -0
  137. data/lib/rom/processor/composer.rb +37 -0
  138. data/lib/rom/processor/transformer.rb +415 -0
  139. data/lib/rom/processor.rb +30 -0
  140. data/lib/rom/registries/associations.rb +26 -0
  141. data/lib/rom/registries/commands.rb +11 -0
  142. data/lib/rom/registries/container.rb +12 -0
  143. data/lib/rom/registries/datasets.rb +21 -0
  144. data/lib/rom/registries/gateways.rb +8 -0
  145. data/lib/rom/registries/mappers.rb +21 -0
  146. data/lib/rom/registries/nestable.rb +32 -0
  147. data/lib/rom/registries/relations.rb +8 -0
  148. data/lib/rom/registries/root.rb +203 -0
  149. data/lib/rom/registries/schemas.rb +44 -0
  150. data/lib/rom/registries/views.rb +11 -0
  151. data/lib/rom/relation/class_interface.rb +61 -0
  152. data/lib/rom/relation/combined.rb +160 -0
  153. data/lib/rom/relation/commands.rb +65 -0
  154. data/lib/rom/relation/composite.rb +53 -0
  155. data/lib/rom/relation/curried.rb +129 -0
  156. data/lib/rom/relation/graph.rb +107 -0
  157. data/lib/rom/relation/loaded.rb +136 -0
  158. data/lib/rom/relation/materializable.rb +62 -0
  159. data/lib/rom/relation/name.rb +122 -0
  160. data/lib/rom/relation/wrap.rb +64 -0
  161. data/lib/rom/relation.rb +625 -0
  162. data/lib/rom/repository/class_interface.rb +162 -0
  163. data/lib/rom/repository/relation_reader.rb +48 -0
  164. data/lib/rom/repository/root.rb +75 -0
  165. data/lib/rom/repository/session.rb +60 -0
  166. data/lib/rom/repository.rb +179 -0
  167. data/lib/rom/schema/associations_dsl.rb +222 -0
  168. data/lib/rom/schema/inferrer.rb +106 -0
  169. data/lib/rom/schema.rb +471 -0
  170. data/lib/rom/settings.rb +141 -0
  171. data/lib/rom/setup.rb +297 -0
  172. data/lib/rom/struct.rb +99 -0
  173. data/lib/rom/struct_compiler.rb +114 -0
  174. data/lib/rom/support/configurable.rb +213 -0
  175. data/lib/rom/support/inflector.rb +31 -0
  176. data/lib/rom/support/memoizable.rb +61 -0
  177. data/lib/rom/support/notifications.rb +238 -0
  178. data/lib/rom/transaction.rb +26 -0
  179. data/lib/rom/transformer.rb +46 -0
  180. data/lib/rom/types.rb +74 -0
  181. data/lib/rom/version.rb +1 -1
  182. data/lib/rom-changeset.rb +4 -0
  183. data/lib/rom-core.rb +3 -0
  184. data/lib/rom-repository.rb +4 -0
  185. data/lib/rom.rb +3 -3
  186. metadata +302 -23
@@ -0,0 +1,480 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/header"
4
+ require "rom/mapper/model_dsl"
5
+
6
+ module ROM
7
+ class Mapper
8
+ # Mapper attribute DSL exposed by mapper subclasses
9
+ #
10
+ # This class is private even though its methods are exposed by mappers.
11
+ # Typically it's not meant to be used directly.
12
+ #
13
+ # TODO: break this madness down into smaller pieces
14
+ #
15
+ # @api private
16
+ class AttributeDSL
17
+ include ModelDSL
18
+
19
+ attr_reader :attributes, :options, :copy_keys, :symbolize_keys, :reject_keys, :steps
20
+
21
+ # @param [Array] attributes accumulator array
22
+ # @param [Hash] options
23
+ #
24
+ # @api private
25
+ def initialize(attributes, options)
26
+ @attributes = attributes
27
+ @options = options
28
+ @copy_keys = options.fetch(:copy_keys)
29
+ @symbolize_keys = options.fetch(:symbolize_keys)
30
+ @prefix = options.fetch(:prefix)
31
+ @prefix_separator = options.fetch(:prefix_separator)
32
+ @reject_keys = options.fetch(:reject_keys)
33
+ @steps = []
34
+ end
35
+
36
+ # Redefine the prefix for the following attributes
37
+ #
38
+ # @example
39
+ #
40
+ # dsl = AttributeDSL.new([])
41
+ # dsl.attribute(:prefix, 'user')
42
+ #
43
+ # @api public
44
+ def prefix(value = Undefined)
45
+ if value.equal?(Undefined)
46
+ @prefix
47
+ else
48
+ @prefix = value
49
+ end
50
+ end
51
+
52
+ # Redefine the prefix separator for the following attributes
53
+ #
54
+ # @example
55
+ #
56
+ # dsl = AttributeDSL.new([])
57
+ # dsl.attribute(:prefix_separator, '.')
58
+ #
59
+ # @api public
60
+ def prefix_separator(value = Undefined)
61
+ if value.equal?(Undefined)
62
+ @prefix_separator
63
+ else
64
+ @prefix_separator = value
65
+ end
66
+ end
67
+
68
+ # Define a mapping attribute with its options and/or block
69
+ #
70
+ # @example
71
+ # dsl = AttributeDSL.new([])
72
+ #
73
+ # dsl.attribute(:name)
74
+ # dsl.attribute(:email, from: 'user_email')
75
+ # dsl.attribute(:name) { 'John' }
76
+ # dsl.attribute(:name) { |t| t.upcase }
77
+ #
78
+ # @api public
79
+ def attribute(name, options = EMPTY_HASH, &block)
80
+ with_attr_options(name, options) do |attr_options|
81
+ if options[:type] && block
82
+ raise ArgumentError,
83
+ "can't specify type and block at the same time"
84
+ end
85
+ attr_options[:coercer] = block if block
86
+ add_attribute(name, attr_options)
87
+ end
88
+ end
89
+
90
+ def exclude(name)
91
+ attributes << [name, {exclude: true}]
92
+ end
93
+
94
+ # Perform transformations sequentially
95
+ #
96
+ # @example
97
+ # dsl = AttributeDSL.new()
98
+ #
99
+ # dsl.step do
100
+ # attribute :name
101
+ # end
102
+ #
103
+ # @api public
104
+ def step(options = EMPTY_HASH, &block)
105
+ steps << new(options, &block)
106
+ end
107
+
108
+ # Define an embedded attribute
109
+ #
110
+ # Block exposes the attribute dsl too
111
+ #
112
+ # @example
113
+ # dsl = AttributeDSL.new([])
114
+ #
115
+ # dsl.embedded :tags, type: :array do
116
+ # attribute :name
117
+ # end
118
+ #
119
+ # dsl.embedded :address, type: :hash do
120
+ # model Address
121
+ # attribute :name
122
+ # end
123
+ #
124
+ # @param [Symbol] name attribute
125
+ #
126
+ # @param [Hash] options
127
+ # @option options [Symbol] :type Embedded type can be :hash or :array
128
+ # @option options [Symbol] :prefix Prefix that should be used for
129
+ # its attributes
130
+ #
131
+ # @api public
132
+ def embedded(name, options, &block)
133
+ with_attr_options(name) do |attr_options|
134
+ mapper = options[:mapper]
135
+
136
+ if mapper
137
+ embedded_options = {type: :array}.update(options)
138
+ attributes_from_mapper(
139
+ mapper, name, embedded_options.update(attr_options)
140
+ )
141
+ else
142
+ dsl = new(options, &block)
143
+ attr_options.update(options)
144
+ add_attribute(
145
+ name, {header: dsl.header, type: :array}.update(attr_options)
146
+ )
147
+ end
148
+ end
149
+ end
150
+
151
+ # Define an embedded hash attribute that requires "wrapping" transformation
152
+ #
153
+ # Typically this is used in sql context when relation is a join.
154
+ #
155
+ # @example
156
+ # dsl = AttributeDSL.new([])
157
+ #
158
+ # dsl.wrap(address: [:street, :zipcode, :city])
159
+ #
160
+ # dsl.wrap(:address) do
161
+ # model Address
162
+ # attribute :street
163
+ # attribute :zipcode
164
+ # attribute :city
165
+ # end
166
+ #
167
+ # @see AttributeDSL#embedded
168
+ #
169
+ # @api public
170
+ def wrap(*args, &block)
171
+ ensure_mapper_configuration("wrap", args, block_given?)
172
+
173
+ with_name_or_options(*args) do |name, options, mapper|
174
+ wrap_options = {type: :hash, wrap: true}.update(options)
175
+
176
+ if mapper
177
+ attributes_from_mapper(mapper, name, wrap_options)
178
+ else
179
+ dsl(name, wrap_options, &block)
180
+ end
181
+ end
182
+ end
183
+
184
+ # Define an embedded hash attribute that requires "unwrapping" transformation
185
+ #
186
+ # Typically this is used in no-sql context to normalize data before
187
+ # inserting to sql gateway.
188
+ #
189
+ # @example
190
+ # dsl = AttributeDSL.new([])
191
+ #
192
+ # dsl.unwrap(address: [:street, :zipcode, :city])
193
+ #
194
+ # dsl.unwrap(:address) do
195
+ # attribute :street
196
+ # attribute :zipcode
197
+ # attribute :city
198
+ # end
199
+ #
200
+ # @see AttributeDSL#embedded
201
+ #
202
+ # @api public
203
+ def unwrap(*args, &block)
204
+ with_name_or_options(*args) do |name, options, mapper|
205
+ unwrap_options = {type: :hash, unwrap: true}.update(options)
206
+
207
+ if mapper
208
+ attributes_from_mapper(mapper, name, unwrap_options)
209
+ else
210
+ dsl(name, unwrap_options, &block)
211
+ end
212
+ end
213
+ end
214
+
215
+ # Define an embedded hash attribute that requires "grouping" transformation
216
+ #
217
+ # Typically this is used in sql context when relation is a join.
218
+ #
219
+ # @example
220
+ # dsl = AttributeDSL.new([])
221
+ #
222
+ # dsl.group(tags: [:name])
223
+ #
224
+ # dsl.group(:tags) do
225
+ # model Tag
226
+ # attribute :name
227
+ # end
228
+ #
229
+ # @see AttributeDSL#embedded
230
+ #
231
+ # @api public
232
+ def group(*args, &block)
233
+ ensure_mapper_configuration("group", args, block_given?)
234
+
235
+ with_name_or_options(*args) do |name, options, mapper|
236
+ group_options = {type: :array, group: true}.update(options)
237
+
238
+ if mapper
239
+ attributes_from_mapper(mapper, name, group_options)
240
+ else
241
+ dsl(name, group_options, &block)
242
+ end
243
+ end
244
+ end
245
+
246
+ # Define an embedded array attribute that requires "ungrouping" transformation
247
+ #
248
+ # Typically this is used in non-sql context being prepared for import to sql.
249
+ #
250
+ # @example
251
+ # dsl = AttributeDSL.new([])
252
+ # dsl.ungroup(tags: [:name])
253
+ #
254
+ # @see AttributeDSL#embedded
255
+ #
256
+ # @api public
257
+ def ungroup(*args, &block)
258
+ with_name_or_options(*args) do |name, options, *|
259
+ ungroup_options = {type: :array, ungroup: true}.update(options)
260
+ dsl(name, ungroup_options, &block)
261
+ end
262
+ end
263
+
264
+ # Define an embedded hash attribute that requires "fold" transformation
265
+ #
266
+ # Typically this is used in sql context to fold single joined field
267
+ # to the array of values.
268
+ #
269
+ # @example
270
+ # dsl = AttributeDSL.new([])
271
+ #
272
+ # dsl.fold(tags: [:name])
273
+ #
274
+ # @see AttributeDSL#embedded
275
+ #
276
+ # @api public
277
+ def fold(*args, &block)
278
+ with_name_or_options(*args) do |name, *|
279
+ fold_options = {type: :array, fold: true}
280
+ dsl(name, fold_options, &block)
281
+ end
282
+ end
283
+
284
+ # Define an embedded hash attribute that requires "unfold" transformation
285
+ #
286
+ # Typically this is used in non-sql context to convert array of
287
+ # values (like in Cassandra 'SET' or 'LIST' types) to array of tuples.
288
+ #
289
+ # Source values are assigned to the first key, the other keys being left blank.
290
+ #
291
+ # @example
292
+ # dsl = AttributeDSL.new([])
293
+ #
294
+ # dsl.unfold(tags: [:name, :type], from: :tags_list)
295
+ #
296
+ # dsl.unfold :tags, from: :tags_list do
297
+ # attribute :name, from: :tag_name
298
+ # attribute :type, from: :tag_type
299
+ # end
300
+ #
301
+ # @see AttributeDSL#embedded
302
+ #
303
+ # @api public
304
+ def unfold(name, options = EMPTY_HASH)
305
+ with_attr_options(name, options) do |attr_options|
306
+ old_name = attr_options.fetch(:from, name)
307
+ dsl(old_name, type: :array, unfold: true) do
308
+ attribute name, attr_options
309
+ yield if block_given?
310
+ end
311
+ end
312
+ end
313
+
314
+ # Define an embedded combined attribute that requires "combine" transformation
315
+ #
316
+ # Typically this can be used to process results of eager-loading
317
+ #
318
+ # @example
319
+ # dsl = AttributeDSL.new([])
320
+ #
321
+ # dsl.combine(:tags, user_id: :id) do
322
+ # model Tag
323
+ #
324
+ # attribute :name
325
+ # end
326
+ #
327
+ # @param [Symbol] name
328
+ # @param [Hash] options
329
+ # @option options [Hash] :on The "join keys"
330
+ # @option options [Symbol] :type The type, either :array (default) or :hash
331
+ #
332
+ # @api public
333
+ def combine(name, options, &block)
334
+ dsl = new(options, &block)
335
+
336
+ attr_opts = {
337
+ type: options.fetch(:type, :array),
338
+ keys: options.fetch(:on),
339
+ combine: true,
340
+ header: dsl.header
341
+ }
342
+
343
+ add_attribute(name, attr_opts)
344
+ end
345
+
346
+ # Generate a header from attribute definitions
347
+ #
348
+ # @return [Header]
349
+ #
350
+ # @api private
351
+ def header
352
+ Header.coerce(attributes, copy_keys: copy_keys, model: model, reject_keys: reject_keys)
353
+ end
354
+
355
+ private
356
+
357
+ # Remove the attribute used somewhere else (in wrap, group, model etc.)
358
+ #
359
+ # @api private
360
+ def remove(*names)
361
+ attributes.delete_if { |attr| names.include?(attr.first) }
362
+ end
363
+
364
+ # Handle attribute options common for all definitions
365
+ #
366
+ # @api private
367
+ def with_attr_options(name, options = EMPTY_HASH)
368
+ attr_options = options.dup
369
+
370
+ if @prefix
371
+ attr_options[:from] ||= "#{@prefix}#{@prefix_separator}#{name}"
372
+ attr_options[:from] = attr_options[:from].to_sym if name.is_a? Symbol
373
+ end
374
+
375
+ attr_options.update(from: attr_options.fetch(:from) { name }.to_s) if symbolize_keys
376
+
377
+ yield(attr_options)
378
+ end
379
+
380
+ # Handle "name or options" syntax used by `wrap` and `group`
381
+ #
382
+ # @api private
383
+ def with_name_or_options(*args)
384
+ name, options =
385
+ if args.size > 1
386
+ args
387
+ else
388
+ [args.first, {}]
389
+ end
390
+
391
+ yield(name, options, options[:mapper])
392
+ end
393
+
394
+ # Create another instance of the dsl for nested definitions
395
+ #
396
+ # This is used by embedded, wrap and group
397
+ #
398
+ # @api private
399
+ def dsl(name_or_attrs, options, &block)
400
+ if block
401
+ attributes_from_block(name_or_attrs, options, &block)
402
+ else
403
+ attributes_from_hash(name_or_attrs, options)
404
+ end
405
+ end
406
+
407
+ # Define attributes from a nested block
408
+ #
409
+ # Used by embedded, wrap and group
410
+ #
411
+ # @api private
412
+ def attributes_from_block(name, options, &block)
413
+ dsl = new(options, &block)
414
+ header = dsl.header
415
+ add_attribute(name, options.update(header: header))
416
+ header.each { |attr| remove(attr.key) unless name == attr.key }
417
+ end
418
+
419
+ # Define attributes from the `name => attributes` hash syntax
420
+ #
421
+ # Used by wrap and group
422
+ #
423
+ # @api private
424
+ def attributes_from_hash(hash, options)
425
+ hash.each do |name, header|
426
+ with_attr_options(name, options) do |attr_options|
427
+ add_attribute(name, attr_options.update(header: header.zip))
428
+ header.each { |attr| remove(attr) unless name == attr }
429
+ end
430
+ end
431
+ end
432
+
433
+ # Infer mapper header for an embedded attribute
434
+ #
435
+ # @api private
436
+ def attributes_from_mapper(mapper, name, options)
437
+ if mapper.is_a?(Class)
438
+ add_attribute(name, {header: mapper.header}.update(options))
439
+ else
440
+ raise(
441
+ ArgumentError, ":mapper must be a class #{mapper.inspect}"
442
+ )
443
+ end
444
+ end
445
+
446
+ # Add a new attribute and make sure it overrides previous definition
447
+ #
448
+ # @api private
449
+ def add_attribute(name, options)
450
+ remove(name, name.to_s)
451
+ attributes << [name, options]
452
+ end
453
+
454
+ # Create a new dsl instance of potentially overidden options
455
+ #
456
+ # Embedded, wrap and group can override top-level options like `prefix`
457
+ #
458
+ # @api private
459
+ def new(options, &block)
460
+ dsl = self.class.new([], @options.merge(options))
461
+ dsl.instance_exec(&block) unless block.nil?
462
+ dsl
463
+ end
464
+
465
+ # Ensure the mapping configuration isn't ambiguous
466
+ #
467
+ # @api private
468
+ def ensure_mapper_configuration(method_name, args, block_present)
469
+ if args.first.is_a?(Hash) && block_present
470
+ raise MapperMisconfiguredError,
471
+ "Cannot configure `#{method_name}#` using both options and a block"
472
+ end
473
+ if args.first.is_a?(Hash) && args.first[:mapper]
474
+ raise MapperMisconfiguredError,
475
+ "Cannot configure `#{method_name}#` using both options and a mapper"
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/class_attributes"
4
+ require "rom/mapper/attribute_dsl"
5
+
6
+ module ROM
7
+ class Mapper
8
+ # Mapper class-level DSL including Attribute DSL and Model DSL
9
+ module DSL
10
+ # Extend mapper class with macros and DSL methods
11
+ #
12
+ # @api private
13
+ def self.included(klass)
14
+ klass.extend(Dry::Core::ClassAttributes)
15
+ klass.extend(ClassMethods)
16
+ end
17
+
18
+ # Class methods for all mappers
19
+ #
20
+ # @private
21
+ module ClassMethods
22
+ # Set base ivars for the mapper class
23
+ #
24
+ # @api private
25
+ def inherited(klass)
26
+ super
27
+
28
+ klass.instance_variable_set("@attributes", nil)
29
+ klass.instance_variable_set("@header", nil)
30
+ klass.instance_variable_set("@attribute_dsl", nil)
31
+ end
32
+
33
+ # Return base_relation used for creating mapper registry
34
+ #
35
+ # This is used to "gather" mappers under same root name
36
+ #
37
+ # @api private
38
+ def base_relation
39
+ superclass.relation || relation
40
+ end
41
+
42
+ # Return header of the mapper
43
+ #
44
+ # This is memoized so mutating mapper class won't have an effect wrt
45
+ # header after it was initialized for the first time.
46
+ #
47
+ # TODO: freezing mapper class here is probably a good idea
48
+ #
49
+ # @api private
50
+ def header
51
+ @header ||= attribute_dsl.header
52
+ end
53
+
54
+ # @api private
55
+ def respond_to_missing?(name, _include_private = false)
56
+ attribute_dsl.respond_to?(name) || super
57
+ end
58
+
59
+ private
60
+
61
+ # Return default Attribute DSL options based on settings of the mapper
62
+ # class
63
+ #
64
+ # @api private
65
+ def options
66
+ {copy_keys: copy_keys,
67
+ prefix: prefix,
68
+ prefix_separator: prefix_separator,
69
+ symbolize_keys: symbolize_keys,
70
+ reject_keys: reject_keys}
71
+ end
72
+
73
+ # Return default attributes that might have been inherited from the
74
+ # superclass
75
+ #
76
+ # @api private
77
+ def attributes
78
+ @attributes ||=
79
+ if superclass.respond_to?(:attributes, true) && config.inherit_header
80
+ superclass.__send__(:attributes).dup
81
+ else
82
+ []
83
+ end
84
+ end
85
+
86
+ # Create the attribute DSL instance used by the mapper class
87
+ #
88
+ # @api private
89
+ def attribute_dsl
90
+ @attribute_dsl ||= AttributeDSL.new(attributes, config.to_h)
91
+ end
92
+
93
+ # Delegate Attribute DSL method to the dsl instance
94
+ #
95
+ # @api private
96
+ def method_missing(name, *args, &block)
97
+ if attribute_dsl.respond_to?(name)
98
+ attribute_dsl.public_send(name, *args, &block)
99
+ else
100
+ super
101
+ end
102
+ end
103
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/model_builder"
4
+
5
+ module ROM
6
+ class Mapper
7
+ # Model DSL allows setting a model class
8
+ #
9
+ # @private
10
+ module ModelDSL
11
+ attr_reader :attributes, :builder, :klass
12
+
13
+ DEFAULT_TYPE = :poro
14
+
15
+ # Set or generate a model
16
+ #
17
+ # @example
18
+ # class MyDefinition
19
+ # include ROM::Mapper::ModelDSL
20
+ #
21
+ # def initialize
22
+ # @attributes = [[:name], [:title]]
23
+ # end
24
+ # end
25
+ #
26
+ # definition = MyDefinition.new
27
+ #
28
+ # # just set a model constant
29
+ # definition.model(User)
30
+ #
31
+ # # generate model class for the attributes
32
+ # definition.model(name: 'User')
33
+ #
34
+ # @api public
35
+ def model(options = nil)
36
+ if options.is_a?(Class)
37
+ @klass = options
38
+ elsif options
39
+ type = options.fetch(:type) { DEFAULT_TYPE }
40
+ @builder = ModelBuilder[type].new(options)
41
+ end
42
+
43
+ build_class unless options
44
+ end
45
+
46
+ private
47
+
48
+ # Build a model class using a specialized builder
49
+ #
50
+ # @api private
51
+ def build_class
52
+ return klass if klass
53
+
54
+ included_attrs = attributes.reject do |_name, opts|
55
+ opts && opts[:exclude]
56
+ end
57
+ builder&.call(included_attrs.map(&:first))
58
+ end
59
+ end
60
+ end
61
+ end