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,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/support/configurable"
4
+ require "rom/types"
5
+ require "rom/initializer"
6
+ require "rom/pipeline"
7
+ require "rom/plugins/class_methods"
8
+
9
+ require "rom/commands/class_interface"
10
+ require "rom/commands/composite"
11
+ require "rom/commands/graph"
12
+ require "rom/commands/lazy"
13
+
14
+ require_relative "components/provider"
15
+
16
+ module ROM
17
+ # Abstract command class
18
+ #
19
+ # Provides a constructor accepting relation with options and basic behavior
20
+ # for calling, currying and composing commands.
21
+ #
22
+ # Typically command subclasses should inherit from specialized
23
+ # Create/Update/Delete, not this one.
24
+ #
25
+ # @abstract
26
+ #
27
+ # @api public
28
+ class Command
29
+ extend ROM::Provider(type: :command)
30
+ extend Plugins::ClassMethods
31
+ extend Initializer
32
+ extend ClassInterface
33
+
34
+ include Dry::Equalizer(:relation, :options)
35
+ include Commands
36
+ include Pipeline::Operator
37
+
38
+ setting :restrictable
39
+ setting :result
40
+ setting :input
41
+ setting :type
42
+
43
+ config.input = Hash
44
+ config.result = :many
45
+
46
+ # @!attribute [r] relation
47
+ # @return [Relation] Command's relation
48
+ param :relation
49
+
50
+ CommandType = Types::Strict::Symbol.enum(:create, :update, :delete)
51
+ Result = Types::Strict::Symbol.enum(:one, :many)
52
+
53
+ # @!attribute [r] type
54
+ # @return [Symbol] The command type, one of :create, :update or :delete
55
+ option :type, type: CommandType, optional: true
56
+
57
+ # @!attribute [r] config
58
+ # @return [Dry::Configurable::Config]
59
+ option :config, default: -> { self.class.config }
60
+
61
+ # @!attribute [r] source
62
+ # @return [Relation] The source relation
63
+ option :source, default: -> { relation }
64
+
65
+ # @!attribute [r] result
66
+ # @return [Symbol] Result type, either :one or :many
67
+ option :result, type: Result, default: -> { config.result }
68
+
69
+ # @!attribute [r] input
70
+ # @return [Proc, #call] Tuple processing function, typically uses Relation#input_schema
71
+ option :input, default: -> { config.input }
72
+
73
+ # @!attribute [r] curry_args
74
+ # @return [Array] Curried args
75
+ option :curry_args, default: -> { EMPTY_ARRAY }
76
+
77
+ # @!attribute [r] before
78
+ # @return [Array<Hash>] An array with before hooks configuration
79
+ option :before, Types::Coercible::Array, reader: false, default: -> { self.class.before }
80
+
81
+ # @!attribute [r] after
82
+ # @return [Array<Hash>] An array with after hooks configuration
83
+ option :after, Types::Coercible::Array, reader: false, default: -> { self.class.after }
84
+
85
+ # Return name of this command's relation
86
+ #
87
+ # @return [ROM::Relation::Name]
88
+ #
89
+ # @api public
90
+ def name
91
+ relation.name
92
+ end
93
+
94
+ # Return gateway of this command's relation
95
+ #
96
+ # @return [Symbol]
97
+ #
98
+ # @api public
99
+ def gateway
100
+ relation.gateway
101
+ end
102
+
103
+ # Execute the command
104
+ #
105
+ # @abstract
106
+ #
107
+ # @return [Array] an array with inserted tuples
108
+ #
109
+ # @api private
110
+ def execute(*)
111
+ raise(
112
+ NotImplementedError,
113
+ "#{self.class}##{__method__} must be implemented"
114
+ )
115
+ end
116
+
117
+ # Call the command and return one or many tuples
118
+ #
119
+ # This method will apply before/after hooks automatically
120
+ #
121
+ # @api public
122
+ def call(*args, &block)
123
+ tuples =
124
+ if hooks?
125
+ prepared =
126
+ if curried?
127
+ apply_hooks(before_hooks, *(curry_args + args))
128
+ else
129
+ apply_hooks(before_hooks, *args)
130
+ end
131
+
132
+ result = prepared ? execute(prepared, &block) : execute(&block)
133
+
134
+ if curried?
135
+ if !args.empty?
136
+ apply_hooks(after_hooks, result, *args)
137
+ elsif curry_args.size > 1
138
+ apply_hooks(after_hooks, result, curry_args[1])
139
+ else
140
+ apply_hooks(after_hooks, result)
141
+ end
142
+ else
143
+ apply_hooks(after_hooks, result, *args[1..args.size - 1])
144
+ end
145
+ else
146
+ execute(*(curry_args + args), &block)
147
+ end
148
+
149
+ if one?
150
+ tuples.first
151
+ else
152
+ tuples
153
+ end
154
+ end
155
+ alias_method :[], :call
156
+
157
+ # Curry this command with provided args
158
+ #
159
+ # Curried command can be called without args. If argument is a graph input processor,
160
+ # lazy command will be returned, which is used for handling nested input hashes.
161
+ #
162
+ # @return [Command, Lazy]
163
+ #
164
+ # @api public
165
+ def curry(*args)
166
+ if curry_args.empty? && args.first.is_a?(Graph::InputEvaluator)
167
+ Lazy[self].new(self, *args)
168
+ else
169
+ self.class.build(relation, **options, curry_args: args)
170
+ end
171
+ end
172
+
173
+ # Compose this command with other commands
174
+ #
175
+ # Composed commands can handle nested input
176
+ #
177
+ # @return [Command::Graph]
178
+ #
179
+ # @api public
180
+ def combine(*others)
181
+ Graph.new(self, others)
182
+ end
183
+
184
+ # Check if this command is curried
185
+ #
186
+ # @return [TrueClass, FalseClass]
187
+ #
188
+ # @api public
189
+ def curried?
190
+ !curry_args.empty?
191
+ end
192
+
193
+ # Return a new command with appended before hooks
194
+ #
195
+ # @param [Array<Hash>] hooks A list of before hooks configurations
196
+ #
197
+ # @return [Command]
198
+ #
199
+ # @api public
200
+ def before(*hooks)
201
+ self.class.new(relation, **options, before: before_hooks + hooks)
202
+ end
203
+
204
+ # Return a new command with appended after hooks
205
+ #
206
+ # @param [Array<Hash>] hooks A list of after hooks configurations
207
+ #
208
+ # @return [Command]
209
+ #
210
+ # @api public
211
+ def after(*hooks)
212
+ self.class.new(relation, **options, after: after_hooks + hooks)
213
+ end
214
+
215
+ # List of before hooks
216
+ #
217
+ # @return [Array]
218
+ #
219
+ # @api public
220
+ def before_hooks
221
+ options[:before]
222
+ end
223
+
224
+ # List of after hooks
225
+ #
226
+ # @return [Array]
227
+ #
228
+ # @api public
229
+ def after_hooks
230
+ options[:after]
231
+ end
232
+
233
+ # Return a new command with other source relation
234
+ #
235
+ # This can be used to restrict command with a specific relation
236
+ #
237
+ # @return [Command]
238
+ #
239
+ # @api public
240
+ def new(new_relation)
241
+ self.class.build(new_relation, **options, source: relation)
242
+ end
243
+
244
+ # Check if this command has any hooks
245
+ #
246
+ # @api private
247
+ def hooks?
248
+ !before_hooks.empty? || !after_hooks.empty?
249
+ end
250
+
251
+ # Check if this command is lazy
252
+ #
253
+ # @return [false]
254
+ #
255
+ # @api private
256
+ def lazy?
257
+ false
258
+ end
259
+
260
+ # Check if this command is a graph
261
+ #
262
+ # @return [false]
263
+ #
264
+ # @api private
265
+ def graph?
266
+ false
267
+ end
268
+
269
+ # Check if this command returns a single tuple
270
+ #
271
+ # @return [TrueClass,FalseClass]
272
+ #
273
+ # @api private
274
+ def one?
275
+ result.equal?(:one)
276
+ end
277
+
278
+ # Check if this command returns many tuples
279
+ #
280
+ # @return [TrueClass,FalseClass]
281
+ #
282
+ # @api private
283
+ def many?
284
+ result.equal?(:many)
285
+ end
286
+
287
+ # Check if this command is restrictible through relation
288
+ #
289
+ # @return [TrueClass,FalseClass]
290
+ #
291
+ # @api private
292
+ def restrictible?
293
+ config.restrictable.equal?(true)
294
+ end
295
+
296
+ # Yields tuples for insertion or return an enumerator
297
+ #
298
+ # @api private
299
+ def map_input_tuples(tuples, &mapper)
300
+ return enum_for(:with_input_tuples, tuples) unless mapper
301
+
302
+ if tuples.respond_to? :merge
303
+ mapper[tuples]
304
+ else
305
+ tuples.map(&mapper)
306
+ end
307
+ end
308
+
309
+ private
310
+
311
+ # Hook called by Pipeline to get composite class for commands
312
+ #
313
+ # @return [Class]
314
+ #
315
+ # @api private
316
+ def composite_class
317
+ Command::Composite
318
+ end
319
+
320
+ # Apply provided hooks
321
+ #
322
+ # Used by #call
323
+ #
324
+ # @return [Array<Hash>]
325
+ #
326
+ # @api private
327
+ def apply_hooks(hooks, tuples, *args)
328
+ hooks.reduce(tuples) do |a, e|
329
+ if e.is_a?(Hash)
330
+ hook_meth, hook_args = e.to_a.flatten(1)
331
+ __send__(hook_meth, a, *args, **hook_args)
332
+ else
333
+ __send__(e, a, *args)
334
+ end
335
+ end
336
+ end
337
+
338
+ # Pipes a dataset through command's relation
339
+ #
340
+ # @return [Array]
341
+ #
342
+ # @api private
343
+ def wrap_dataset(dataset)
344
+ if relation.is_a?(Relation::Composite)
345
+ relation.new(dataset).to_a
346
+ else
347
+ dataset
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/initializer"
4
+ require "rom/commands/graph"
5
+ require "rom/command_proxy"
6
+ require "rom/registries/root"
7
+
8
+ module ROM
9
+ # Builds commands for relations.
10
+ #
11
+ # This class is used by repositories to automatically create commands for
12
+ # their relations. This is used both by `Repository#command` method and
13
+ # `commands` repository class macros.
14
+ #
15
+ # @api private
16
+ class CommandCompiler
17
+ extend Initializer
18
+
19
+ # @!attribute [r] id
20
+ # @return [Symbol] The command registry identifier
21
+ option :id, optional: true
22
+
23
+ # @!attribute [r] command_class
24
+ # @return [Symbol] The command command_class
25
+ option :command_class, optional: true
26
+
27
+ # @!attribute [r] registry
28
+ # @return [Registry::Root]
29
+ option :registry, default: -> { Registry::Root.new }
30
+
31
+ # @!attribute [r] plugins
32
+ # @return [Array<Symbol>] a list of optional plugins that will be enabled for commands
33
+ option :plugins, optional: true, default: -> { EMPTY_HASH }
34
+
35
+ # @!attribute [r] meta
36
+ # @return [Array<Symbol>] Meta data for a command
37
+ option :meta, optional: true
38
+
39
+ # @!attribute [r] cache
40
+ # @return [Cache] local cache instance
41
+ option :cache, default: -> { Cache.new }
42
+
43
+ # Return a specific command command_class for a given adapter and relation AST
44
+ #
45
+ # This class holds its own registry where all generated commands are being
46
+ # stored
47
+ #
48
+ # CommandProxy is returned for complex command graphs as they expect root
49
+ # relation name to be present in the input, which we don't want to have
50
+ # in repositories. It might be worth looking into removing this requirement
51
+ # from rom core Command::Graph API.
52
+ #
53
+ # @overload [](id, adapter, ast, plugins, meta)
54
+ # @param id [Symbol] The command identifier
55
+ # @param adapter [Symbol] The adapter identifier
56
+ # @param ast [Array] The AST representation of a relation
57
+ # @param plugins [Array<Symbol>] A list of optional command plugins that should be used
58
+ # @param meta [Hash] Meta data for a command
59
+ #
60
+ # @return [Command, CommandProxy]
61
+ #
62
+ # @api private
63
+ def call(*args)
64
+ cache.fetch_or_store(args.hash) do
65
+ id, adapter, ast, plugins, plugins_options, meta = args
66
+
67
+ component = registry.components.get(:commands, id: id)
68
+
69
+ command_class =
70
+ if component
71
+ component.constant
72
+ else
73
+ Command.adapter_namespace(adapter).const_get(Inflector.classify(id))
74
+ end
75
+
76
+ plugins_with_opts = Array(plugins)
77
+ .map { |plugin| [plugin, plugins_options.fetch(plugin) { EMPTY_HASH }] }
78
+ .to_h
79
+
80
+ compiler = with(
81
+ id: id,
82
+ command_class: command_class,
83
+ adapter: adapter,
84
+ plugins: plugins_with_opts,
85
+ meta: meta
86
+ )
87
+
88
+ graph_opts = compiler.visit(ast)
89
+ command = ROM::Commands::Graph.build(registry.root.commands, graph_opts)
90
+
91
+ if command.graph?
92
+ root = Inflector.singularize(command.name.relation).to_sym
93
+ CommandProxy.new(command, root)
94
+ elsif command.lazy?
95
+ command.unwrap
96
+ else
97
+ command
98
+ end
99
+ end
100
+ end
101
+ alias_method :[], :call
102
+
103
+ # @api private
104
+ def visit(ast, *args)
105
+ name, node = ast
106
+ __send__(:"visit_#{name}", node, *args)
107
+ end
108
+
109
+ # @api private
110
+ def relations
111
+ registry.root.relations
112
+ end
113
+
114
+ private
115
+
116
+ # @api private
117
+ def visit_relation(node, parent_relation = nil)
118
+ name, header, rel_meta = node
119
+ other = header.map { |attr| visit(attr, name) }.compact
120
+
121
+ key = register_command(name, rel_meta, parent_relation)
122
+
123
+ default_mapping =
124
+ if rel_meta[:combine_command_class] == :many
125
+ name
126
+ else
127
+ {Inflector.singularize(name).to_sym => name}
128
+ end
129
+
130
+ mapping =
131
+ if parent_relation
132
+ associations = relations[parent_relation].associations
133
+
134
+ assoc = associations[rel_meta[:combine_name]]
135
+
136
+ if assoc
137
+ {assoc.key => assoc.target.name.to_sym}
138
+ else
139
+ default_mapping
140
+ end
141
+ else
142
+ default_mapping
143
+ end
144
+
145
+ if other.empty?
146
+ [mapping, key]
147
+ else
148
+ [mapping, [key, other]]
149
+ end
150
+ end
151
+
152
+ # @api private
153
+ def visit_attribute(*)
154
+ nil
155
+ end
156
+
157
+ # Build a command object for a specific relation
158
+ #
159
+ # The command will be prepared for handling associations if it's a combined
160
+ # relation. Additional plugins will be enabled if they are configured for
161
+ # this compiler.
162
+ #
163
+ # @param [Symbol] rel_name A relation identifier from the container registry
164
+ # @param [Hash] rel_meta Meta information from relation AST
165
+ # @param [Symbol] parent_relation Optional parent relation identifier
166
+ #
167
+ # @return [ROM::Command]
168
+ #
169
+ # @api private
170
+ def register_command(rel_name, rel_meta, parent_relation = nil)
171
+ options = {
172
+ rel_name: rel_name,
173
+ meta: meta,
174
+ rel_meta: rel_meta,
175
+ parent_relation: parent_relation,
176
+ plugins: plugins
177
+ }
178
+
179
+ key = "commands.#{rel_name}.#{id}-compiled-#{options.hash}"
180
+
181
+ registry.fetch(key) do
182
+ relation = relations[rel_name]
183
+ klass = command_class.create_class(relation: relation, **options)
184
+
185
+ if gateways.key?(relation.gateway)
186
+ # TODO: add explicit specs covering this case
187
+ gateways[relation.gateway].command(klass, relation: relation, **options)
188
+ else
189
+ klass.build(relation, **options)
190
+ end
191
+ end
192
+
193
+ key
194
+ end
195
+
196
+ # @api private
197
+ def gateways
198
+ registry.root.gateways
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/support/inflector"
4
+
5
+ module ROM
6
+ # TODO: look into making command graphs work without the root key in the input
7
+ # so that we can get rid of this wrapper
8
+ #
9
+ # @api private
10
+ class CommandProxy
11
+ attr_reader :command
12
+
13
+ attr_reader :root
14
+
15
+ # @api private
16
+ def initialize(command, root)
17
+ @command = command
18
+ @root = root
19
+ end
20
+
21
+ # @api private
22
+ def call(input)
23
+ command.call(root => input)
24
+ end
25
+
26
+ # @api private
27
+ def >>(other)
28
+ self.class.new(command >> other, root)
29
+ end
30
+
31
+ # @api private
32
+ def restrictible?
33
+ command.restrictible?
34
+ end
35
+ end
36
+ end