rom-core 4.0.0.beta1

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +603 -0
  3. data/LICENSE +20 -0
  4. data/README.md +18 -0
  5. data/lib/rom-core.rb +1 -0
  6. data/lib/rom/array_dataset.rb +44 -0
  7. data/lib/rom/association_set.rb +16 -0
  8. data/lib/rom/associations/abstract.rb +135 -0
  9. data/lib/rom/associations/definitions.rb +5 -0
  10. data/lib/rom/associations/definitions/abstract.rb +116 -0
  11. data/lib/rom/associations/definitions/many_to_many.rb +24 -0
  12. data/lib/rom/associations/definitions/many_to_one.rb +11 -0
  13. data/lib/rom/associations/definitions/one_to_many.rb +11 -0
  14. data/lib/rom/associations/definitions/one_to_one.rb +11 -0
  15. data/lib/rom/associations/definitions/one_to_one_through.rb +11 -0
  16. data/lib/rom/associations/many_to_many.rb +81 -0
  17. data/lib/rom/associations/many_to_one.rb +37 -0
  18. data/lib/rom/associations/one_to_many.rb +37 -0
  19. data/lib/rom/associations/one_to_one.rb +8 -0
  20. data/lib/rom/associations/one_to_one_through.rb +8 -0
  21. data/lib/rom/associations/through_identifier.rb +39 -0
  22. data/lib/rom/auto_curry.rb +55 -0
  23. data/lib/rom/cache.rb +46 -0
  24. data/lib/rom/command.rb +488 -0
  25. data/lib/rom/command_compiler.rb +239 -0
  26. data/lib/rom/command_proxy.rb +24 -0
  27. data/lib/rom/command_registry.rb +141 -0
  28. data/lib/rom/commands.rb +3 -0
  29. data/lib/rom/commands/class_interface.rb +270 -0
  30. data/lib/rom/commands/composite.rb +53 -0
  31. data/lib/rom/commands/create.rb +13 -0
  32. data/lib/rom/commands/delete.rb +14 -0
  33. data/lib/rom/commands/graph.rb +88 -0
  34. data/lib/rom/commands/graph/class_interface.rb +62 -0
  35. data/lib/rom/commands/graph/input_evaluator.rb +62 -0
  36. data/lib/rom/commands/lazy.rb +99 -0
  37. data/lib/rom/commands/lazy/create.rb +23 -0
  38. data/lib/rom/commands/lazy/delete.rb +27 -0
  39. data/lib/rom/commands/lazy/update.rb +34 -0
  40. data/lib/rom/commands/result.rb +96 -0
  41. data/lib/rom/commands/update.rb +14 -0
  42. data/lib/rom/configuration.rb +114 -0
  43. data/lib/rom/configuration_dsl.rb +87 -0
  44. data/lib/rom/configuration_dsl/command.rb +41 -0
  45. data/lib/rom/configuration_dsl/command_dsl.rb +35 -0
  46. data/lib/rom/configuration_dsl/relation.rb +26 -0
  47. data/lib/rom/configuration_plugin.rb +17 -0
  48. data/lib/rom/constants.rb +64 -0
  49. data/lib/rom/container.rb +147 -0
  50. data/lib/rom/core.rb +46 -0
  51. data/lib/rom/create_container.rb +60 -0
  52. data/lib/rom/data_proxy.rb +94 -0
  53. data/lib/rom/enumerable_dataset.rb +68 -0
  54. data/lib/rom/environment.rb +70 -0
  55. data/lib/rom/gateway.rb +184 -0
  56. data/lib/rom/global.rb +58 -0
  57. data/lib/rom/global/plugin_dsl.rb +47 -0
  58. data/lib/rom/initializer.rb +64 -0
  59. data/lib/rom/lint/enumerable_dataset.rb +54 -0
  60. data/lib/rom/lint/gateway.rb +120 -0
  61. data/lib/rom/lint/linter.rb +78 -0
  62. data/lib/rom/lint/spec.rb +20 -0
  63. data/lib/rom/lint/test.rb +98 -0
  64. data/lib/rom/mapper_registry.rb +24 -0
  65. data/lib/rom/memory.rb +4 -0
  66. data/lib/rom/memory/associations.rb +4 -0
  67. data/lib/rom/memory/associations/many_to_many.rb +10 -0
  68. data/lib/rom/memory/associations/many_to_one.rb +10 -0
  69. data/lib/rom/memory/associations/one_to_many.rb +10 -0
  70. data/lib/rom/memory/associations/one_to_one.rb +10 -0
  71. data/lib/rom/memory/commands.rb +56 -0
  72. data/lib/rom/memory/dataset.rb +97 -0
  73. data/lib/rom/memory/gateway.rb +64 -0
  74. data/lib/rom/memory/relation.rb +62 -0
  75. data/lib/rom/memory/schema.rb +23 -0
  76. data/lib/rom/memory/storage.rb +59 -0
  77. data/lib/rom/memory/types.rb +9 -0
  78. data/lib/rom/pipeline.rb +105 -0
  79. data/lib/rom/plugin.rb +25 -0
  80. data/lib/rom/plugin_base.rb +45 -0
  81. data/lib/rom/plugin_registry.rb +197 -0
  82. data/lib/rom/plugins/command/schema.rb +37 -0
  83. data/lib/rom/plugins/configuration/configuration_dsl.rb +21 -0
  84. data/lib/rom/plugins/relation/instrumentation.rb +51 -0
  85. data/lib/rom/plugins/relation/registry_reader.rb +44 -0
  86. data/lib/rom/plugins/schema/timestamps.rb +58 -0
  87. data/lib/rom/registry.rb +71 -0
  88. data/lib/rom/relation.rb +548 -0
  89. data/lib/rom/relation/class_interface.rb +282 -0
  90. data/lib/rom/relation/commands.rb +23 -0
  91. data/lib/rom/relation/composite.rb +46 -0
  92. data/lib/rom/relation/curried.rb +103 -0
  93. data/lib/rom/relation/graph.rb +197 -0
  94. data/lib/rom/relation/loaded.rb +127 -0
  95. data/lib/rom/relation/materializable.rb +66 -0
  96. data/lib/rom/relation/name.rb +111 -0
  97. data/lib/rom/relation/view_dsl.rb +64 -0
  98. data/lib/rom/relation/wrap.rb +83 -0
  99. data/lib/rom/relation_registry.rb +10 -0
  100. data/lib/rom/schema.rb +437 -0
  101. data/lib/rom/schema/associations_dsl.rb +195 -0
  102. data/lib/rom/schema/attribute.rb +419 -0
  103. data/lib/rom/schema/dsl.rb +164 -0
  104. data/lib/rom/schema/inferrer.rb +66 -0
  105. data/lib/rom/schema_plugin.rb +27 -0
  106. data/lib/rom/setup.rb +68 -0
  107. data/lib/rom/setup/auto_registration.rb +74 -0
  108. data/lib/rom/setup/auto_registration_strategies/base.rb +16 -0
  109. data/lib/rom/setup/auto_registration_strategies/custom_namespace.rb +63 -0
  110. data/lib/rom/setup/auto_registration_strategies/no_namespace.rb +20 -0
  111. data/lib/rom/setup/auto_registration_strategies/with_namespace.rb +18 -0
  112. data/lib/rom/setup/finalize.rb +103 -0
  113. data/lib/rom/setup/finalize/finalize_commands.rb +60 -0
  114. data/lib/rom/setup/finalize/finalize_mappers.rb +56 -0
  115. data/lib/rom/setup/finalize/finalize_relations.rb +135 -0
  116. data/lib/rom/support/configurable.rb +85 -0
  117. data/lib/rom/support/memoizable.rb +58 -0
  118. data/lib/rom/support/notifications.rb +103 -0
  119. data/lib/rom/transaction.rb +24 -0
  120. data/lib/rom/types.rb +26 -0
  121. data/lib/rom/version.rb +5 -0
  122. metadata +289 -0
@@ -0,0 +1,239 @@
1
+ require 'dry/core/inflector'
2
+ require 'dry/core/cache'
3
+
4
+ require 'rom/initializer'
5
+ require 'rom/commands'
6
+ require 'rom/command_proxy'
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 Dry::Core::Cache
18
+ extend Initializer
19
+
20
+ # @api private
21
+ def self.registry
22
+ @__registry__ ||= Hash.new { |h, k| h[k] = {} }
23
+ end
24
+
25
+ # @!attribute [r] gateways
26
+ # @return [ROM::Registry]
27
+ param :gateways
28
+
29
+ # @!attribute [r] relations
30
+ # @return [ROM::RelationRegistry]
31
+ param :relations
32
+
33
+ # @!attribute [r] relations
34
+ # @return [ROM::Registry]
35
+ param :commands
36
+
37
+ # @!attribute [r] notifications
38
+ # @return [Notifications::EventBus] Configuration notifications event bus
39
+ param :notifications
40
+
41
+ # @!attribute [r] id
42
+ # @return [Symbol] The command type registry identifier
43
+ option :id, optional: true
44
+
45
+ # @!attribute [r] adapter
46
+ # @return [Symbol] The adapter identifier ie :sql or :http
47
+ option :adapter, optional: true
48
+
49
+ # @!attribute [r] registry
50
+ # @return [Hash] local registry where commands will be stored during compilation
51
+ option :registry, optional: true, default: -> { self.class.registry }
52
+
53
+ # @!attribute [r] plugins
54
+ # @return [Array<Symbol>] a list of optional plugins that will be enabled for commands
55
+ option :plugins, optional: true, default: -> { EMPTY_ARRAY }
56
+
57
+ # @!attribute [r] meta
58
+ # @return [Array<Symbol>] Meta data for a command
59
+ option :meta, optional: true
60
+
61
+ # Return a specific command type for a given adapter and relation AST
62
+ #
63
+ # This class holds its own registry where all generated commands are being
64
+ # stored
65
+ #
66
+ # CommandProxy is returned for complex command graphs as they expect root
67
+ # relation name to be present in the input, which we don't want to have
68
+ # in repositories. It might be worth looking into removing this requirement
69
+ # from rom core Command::Graph API.
70
+ #
71
+ # @overload [](type, adapter, ast, plugins, meta)
72
+ # @param type [Symbol] The type of command
73
+ # @param adapter [Symbol] The adapter identifier
74
+ # @param ast [Array] The AST representation of a relation
75
+ # @param plugins [Array<Symbol>] A list of optional command plugins that should be used
76
+ # @param meta [Hash] Meta data for a command
77
+ #
78
+ # @return [Command, CommandProxy]
79
+ #
80
+ # @api private
81
+ def call(*args)
82
+ fetch_or_store(args.hash) do
83
+ type, adapter, ast, plugins, meta = args
84
+
85
+ compiler = with(
86
+ id: type,
87
+ adapter: adapter,
88
+ plugins: Array(plugins),
89
+ meta: meta
90
+ )
91
+
92
+ graph_opts = compiler.visit(ast)
93
+ command = ROM::Commands::Graph.build(registry, graph_opts)
94
+
95
+ if command.graph?
96
+ CommandProxy.new(command)
97
+ elsif command.lazy?
98
+ command.unwrap
99
+ else
100
+ command
101
+ end
102
+ end
103
+ end
104
+ alias_method :[], :call
105
+
106
+ # @api private
107
+ def type
108
+ @_type ||= Commands.const_get(Dry::Core::Inflector.classify(id))[adapter]
109
+ rescue NameError
110
+ nil
111
+ end
112
+
113
+ # @api private
114
+ def visit(ast, *args)
115
+ name, node = ast
116
+ __send__(:"visit_#{name}", node, *args)
117
+ end
118
+
119
+ private
120
+
121
+ # @api private
122
+ def visit_relation(node, parent_relation = nil)
123
+ name, header, meta = node
124
+ other = header.map { |attr| visit(attr, name) }.compact
125
+
126
+ if type
127
+ register_command(name, type, meta, parent_relation)
128
+
129
+ default_mapping =
130
+ if meta[:combine_type] == :many
131
+ name
132
+ else
133
+ { Dry::Core::Inflector.singularize(name).to_sym => name }
134
+ end
135
+
136
+ mapping =
137
+ if parent_relation
138
+ associations = relations[parent_relation].associations
139
+
140
+ assoc = associations[meta[:combine_name]]
141
+
142
+ if assoc
143
+ { assoc.key => assoc.target.name.to_sym }
144
+ else
145
+ default_mapping
146
+ end
147
+ else
148
+ default_mapping
149
+ end
150
+
151
+ if other.size > 0
152
+ [mapping, [type, other]]
153
+ else
154
+ [mapping, type]
155
+ end
156
+ else
157
+ registry[name][id] = commands[name][id]
158
+ [name, id]
159
+ end
160
+ end
161
+
162
+ # @api private
163
+ def visit_attribute(*args)
164
+ nil
165
+ end
166
+
167
+ # Build a command object for a specific relation
168
+ #
169
+ # The command will be prepared for handling associations if it's a combined
170
+ # relation. Additional plugins will be enabled if they are configured for
171
+ # this compiler.
172
+ #
173
+ # @param [Symbol] rel_name A relation identifier from the container registry
174
+ # @param [Symbol] type The command type
175
+ # @param [Hash] meta Meta information from relation AST
176
+ # @param [Symbol] parent_relation Optional parent relation identifier
177
+ #
178
+ # @return [ROM::Command]
179
+ #
180
+ # @api private
181
+ def register_command(rel_name, type, meta, parent_relation = nil)
182
+ relation = relations[rel_name]
183
+
184
+ type.create_class(rel_name, type) do |klass|
185
+ klass.result(meta.fetch(:combine_type, result))
186
+
187
+ if meta[:combine_type]
188
+ setup_associates(klass, relation, meta, parent_relation)
189
+ end
190
+
191
+ plugins.each do |plugin|
192
+ klass.use(plugin)
193
+ end
194
+
195
+ gateway = gateways[relation.gateway]
196
+
197
+ notifications.trigger(
198
+ 'configuration.commands.class.before_build',
199
+ command: klass, gateway: gateway, dataset: relation.dataset
200
+ )
201
+
202
+ klass.extend_for_relation(relation) if klass.restrictable
203
+
204
+ registry[rel_name][type] = klass.build(relation, input: relation.input_schema)
205
+ end
206
+ end
207
+
208
+ # Return default result type
209
+ #
210
+ # @return [Symbol]
211
+ #
212
+ # @api private
213
+ def result
214
+ meta.fetch(:result, :one)
215
+ end
216
+
217
+ # Sets up `associates` plugin for a given command class and relation
218
+ #
219
+ # @param [Class] klass The command class
220
+ # @param [Relation] relation The relation for the command
221
+ #
222
+ # @api private
223
+ def setup_associates(klass, relation, meta, parent_relation)
224
+ assoc_name =
225
+ if relation.associations.key?(parent_relation)
226
+ parent_relation
227
+ else
228
+ singular_name = Dry::Core::Inflector.singularize(parent_relation).to_sym
229
+ singular_name if relation.associations.key?(singular_name)
230
+ end
231
+
232
+ if assoc_name
233
+ klass.associates(assoc_name)
234
+ else
235
+ klass.associates(parent_relation)
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,24 @@
1
+ require 'dry/core/inflector'
2
+
3
+ module ROM
4
+ # TODO: look into making command graphs work without the root key in the input
5
+ # so that we can get rid of this wrapper
6
+ #
7
+ # @api private
8
+ class CommandProxy
9
+ attr_reader :command, :root
10
+
11
+ def initialize(command)
12
+ @command = command
13
+ @root = Dry::Core::Inflector.singularize(command.name.relation).to_sym
14
+ end
15
+
16
+ def call(input)
17
+ command.call(root => input)
18
+ end
19
+
20
+ def >>(other)
21
+ self.class.new(command >> other)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,141 @@
1
+ require 'concurrent/map'
2
+
3
+ require 'rom/constants'
4
+ require 'rom/registry'
5
+ require 'rom/commands/result'
6
+
7
+ module ROM
8
+ # Specialized registry class for commands
9
+ #
10
+ # @api public
11
+ class CommandRegistry < Registry
12
+ include Commands
13
+
14
+ # Internal command registry
15
+ #
16
+ # @return [Registry]
17
+ #
18
+ # @api private
19
+ param :elements
20
+
21
+ # Name of the relation from which commands are under
22
+ #
23
+ # @return [String]
24
+ #
25
+ # @api private
26
+ option :relation_name
27
+
28
+ option :mappers, optional: true
29
+
30
+ option :mapper, optional: true
31
+
32
+ option :compiler, optional: true
33
+
34
+ def self.element_not_found_error
35
+ CommandNotFoundError
36
+ end
37
+
38
+ # Try to execute a command in a block
39
+ #
40
+ # @yield [command] Passes command to the block
41
+ #
42
+ # @example
43
+ #
44
+ # rom.commands[:users].try { create(name: 'Jane') }
45
+ # rom.commands[:users].try { update(:by_id, 1).call(name: 'Jane Doe') }
46
+ # rom.commands[:users].try { delete(:by_id, 1) }
47
+ #
48
+ # @return [Commands::Result]
49
+ #
50
+ # @api public
51
+ def try(&block)
52
+ response = block.call
53
+
54
+ if response.is_a?(Command) || response.is_a?(Composite)
55
+ try { response.call }
56
+ else
57
+ Result::Success.new(response)
58
+ end
59
+ rescue CommandError => e
60
+ Result::Failure.new(e)
61
+ end
62
+
63
+ # Return a command from the registry
64
+ #
65
+ # If mapper is set command will be turned into a composite command with
66
+ # auto-mapping
67
+ #
68
+ # @example
69
+ # create_user = rom.commands[:users][:create]
70
+ # create_user[name: 'Jane']
71
+ #
72
+ # # with mapping, assuming :entity mapper is registered for :users relation
73
+ # create_user = rom.commands[:users].map_with(:entity)[:create]
74
+ # create_user[name: 'Jane'] # => result is send through :entity mapper
75
+ #
76
+ # @param [Symbol] name The name of a registered command
77
+ #
78
+ # @return [Command,Command::Composite]
79
+ #
80
+ # @api public
81
+ def [](*args)
82
+ if args.size.equal?(1)
83
+ command = super
84
+ mapper = options[:mapper]
85
+
86
+ if mapper
87
+ command.curry >> mapper
88
+ else
89
+ command
90
+ end
91
+ else
92
+ cache.fetch_or_store(args.hash) { compiler.(*args) }
93
+ end
94
+ end
95
+
96
+ # Specify a mapper that should be used for commands from this registry
97
+ #
98
+ # @example
99
+ # entity_commands = rom.commands[:users].map_with(:entity)
100
+ #
101
+ #
102
+ # @param [Symbol] mapper_name The name of a registered mapper
103
+ #
104
+ # @return [CommandRegistry]
105
+ #
106
+ # @api public
107
+ def map_with(mapper_name)
108
+ with(mapper: mappers[mapper_name])
109
+ end
110
+
111
+ # @api private
112
+ def set_compiler(compiler)
113
+ options[:compiler] = @compiler = compiler
114
+ end
115
+
116
+ # @api private
117
+ def set_mappers(mappers)
118
+ options[:mappers] = @mappers = mappers
119
+ end
120
+
121
+ private
122
+
123
+ # Allow checking if a certain command is available using dot-notation
124
+ #
125
+ # @api private
126
+ def respond_to_missing?(name, include_private = false)
127
+ key?(name) || super
128
+ end
129
+
130
+ # Allow retrieving commands using dot-notation
131
+ #
132
+ # @api private
133
+ def method_missing(name, *)
134
+ if key?(name)
135
+ self[name]
136
+ else
137
+ super
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,3 @@
1
+ require 'rom/commands/create'
2
+ require 'rom/commands/update'
3
+ require 'rom/commands/delete'
@@ -0,0 +1,270 @@
1
+ require 'dry/core/class_builder'
2
+ require 'dry/core/inflector'
3
+
4
+ module ROM
5
+ # Base command class with factory class-level interface and setup-related logic
6
+ #
7
+ # @private
8
+ class Command
9
+ module ClassInterface
10
+ # This hook sets up default class state
11
+ #
12
+ # @api private
13
+ def inherited(klass)
14
+ super
15
+ klass.instance_variable_set(:'@before', before.dup)
16
+ klass.instance_variable_set(:'@after', after.dup)
17
+ end
18
+
19
+ # Sets up the base class
20
+ #
21
+ # @api private
22
+ def self.extended(klass)
23
+ super
24
+ klass.set_hooks(:before, [])
25
+ klass.set_hooks(:after, [])
26
+ end
27
+
28
+ # Return adapter specific sub-class based on the adapter identifier
29
+ #
30
+ # This is a syntax sugar to make things consistent
31
+ #
32
+ # @example
33
+ # ROM::Commands::Create[:memory]
34
+ # # => ROM::Memory::Commands::Create
35
+ #
36
+ # @param [Symbol] adapter identifier
37
+ #
38
+ # @return [Class]
39
+ #
40
+ # @api public
41
+ def [](adapter)
42
+ adapter_namespace(adapter).const_get(Dry::Core::Inflector.demodulize(name))
43
+ end
44
+
45
+ # Return namespaces that contains command subclasses of a specific adapter
46
+ #
47
+ # @param [Symbol] adapter identifier
48
+ #
49
+ # @return [Module]
50
+ #
51
+ # @api private
52
+ def adapter_namespace(adapter)
53
+ ROM.adapters.fetch(adapter).const_get(:Commands)
54
+ rescue KeyError
55
+ raise AdapterNotPresentError.new(adapter, :relation)
56
+ end
57
+
58
+ # Build a command class for a specific relation with options
59
+ #
60
+ # @example
61
+ # class CreateUser < ROM::Commands::Create[:memory]
62
+ # end
63
+ #
64
+ # command = CreateUser.build(rom.relations[:users])
65
+ #
66
+ # @param [Relation] relation
67
+ # @param [Hash] options
68
+ #
69
+ # @return [Command]
70
+ #
71
+ # @api public
72
+ def build(relation, options = EMPTY_HASH)
73
+ new(relation, self.options.merge(options))
74
+ end
75
+
76
+ # Create a command class with a specific type
77
+ #
78
+ # @param [Symbol] command name
79
+ # @param [Class] parent class
80
+ #
81
+ # @yield [Class] create class
82
+ #
83
+ # @return [Class, Object] return result of the block if it was provided
84
+ #
85
+ # @api public
86
+ def create_class(name, type, &block)
87
+ klass = Dry::Core::ClassBuilder
88
+ .new(name: "#{Dry::Core::Inflector.classify(type)}[:#{name}]", parent: type)
89
+ .call
90
+
91
+ if block
92
+ yield(klass)
93
+ else
94
+ klass
95
+ end
96
+ end
97
+
98
+ # Use a configured plugin in this relation
99
+ #
100
+ # @example
101
+ # class CreateUser < ROM::Commands::Create[:memory]
102
+ # use :pagintion
103
+ #
104
+ # per_page 30
105
+ # end
106
+ #
107
+ # @param [Symbol] plugin
108
+ # @param [Hash] options
109
+ # @option options [Symbol] :adapter (:default) first adapter to check for plugin
110
+ #
111
+ # @api public
112
+ def use(plugin, _options = EMPTY_HASH)
113
+ ROM.plugin_registry.commands.fetch(plugin, adapter).apply_to(self)
114
+ end
115
+
116
+ # Extend a command class with relation view methods
117
+ #
118
+ # @param [Relation]
119
+ #
120
+ # @return [Class]
121
+ #
122
+ # @api public
123
+ def extend_for_relation(relation)
124
+ include(relation_methods_mod(relation.class))
125
+ end
126
+
127
+ # Set before-execute hooks
128
+ #
129
+ # @overload before(hook)
130
+ # Set an before hook as a method name
131
+ #
132
+ # @example
133
+ # class CreateUser < ROM::Commands::Create[:sql]
134
+ # relation :users
135
+ # register_as :create
136
+ #
137
+ # before :my_hook
138
+ #
139
+ # def my_hook(tuple, *)
140
+ # puts "hook called#
141
+ # end
142
+ # end
143
+ #
144
+ # @overload before(hook_opts)
145
+ # Set an before hook as a method name with arguments
146
+ #
147
+ # @example
148
+ # class CreateUser < ROM::Commands::Create[:sql]
149
+ # relation :users
150
+ # register_as :create
151
+ #
152
+ # before my_hook: { arg1: 1, arg1: 2 }
153
+ #
154
+ # def my_hook(tuple, arg1:, arg2:)
155
+ # puts "hook called with args: #{arg1} and #{arg2}"
156
+ # end
157
+ # end
158
+ #
159
+ # @param [Hash<Symbol=>Hash>] hook Options with method name and pre-set args
160
+ #
161
+ # @return [Array<Hash, Symbol>] A list of all configured before hooks
162
+ #
163
+ # @api public
164
+ def before(*hooks)
165
+ if hooks.size > 0
166
+ set_hooks(:before, hooks)
167
+ else
168
+ @before
169
+ end
170
+ end
171
+
172
+ # Set after-execute hooks
173
+ #
174
+ # @overload after(hook)
175
+ # Set an after hook as a method name
176
+ #
177
+ # @example
178
+ # class CreateUser < ROM::Commands::Create[:sql]
179
+ # relation :users
180
+ # register_as :create
181
+ #
182
+ # after :my_hook
183
+ #
184
+ # def my_hook(tuple, *)
185
+ # puts "hook called#
186
+ # end
187
+ # end
188
+ #
189
+ # @overload after(hook_opts)
190
+ # Set an after hook as a method name with arguments
191
+ #
192
+ # @example
193
+ # class CreateUser < ROM::Commands::Create[:sql]
194
+ # relation :users
195
+ # register_as :create
196
+ #
197
+ # after my_hook: { arg1: 1, arg1: 2 }
198
+ #
199
+ # def my_hook(tuple, arg1:, arg2:)
200
+ # puts "hook called with args: #{arg1} and #{arg2}"
201
+ # end
202
+ # end
203
+ #
204
+ # @param [Hash<Symbol=>Hash>] hook Options with method name and pre-set args
205
+ #
206
+ # @return [Array<Hash, Symbol>] A list of all configured after hooks
207
+ #
208
+ # @api public
209
+ def after(*hooks)
210
+ if hooks.size > 0
211
+ set_hooks(:after, hooks)
212
+ else
213
+ @after
214
+ end
215
+ end
216
+
217
+ # Set new or more hooks
218
+ #
219
+ # @api private
220
+ def set_hooks(type, hooks)
221
+ ivar = :"@#{type}"
222
+
223
+ if instance_variable_defined?(ivar)
224
+ instance_variable_get(ivar).concat(hooks)
225
+ else
226
+ instance_variable_set(ivar, hooks)
227
+ end
228
+ end
229
+
230
+ # Return default name of the command class based on its name
231
+ #
232
+ # During setup phase this is used by defalut as `register_as` option
233
+ #
234
+ # @return [Symbol]
235
+ #
236
+ # @api private
237
+ def default_name
238
+ Dry::Core::Inflector.underscore(Dry::Core::Inflector.demodulize(name)).to_sym
239
+ end
240
+
241
+ # Return default options based on class macros
242
+ #
243
+ # @return [Hash]
244
+ #
245
+ # @api private
246
+ def options
247
+ { input: input, result: result, before: before, after: after }
248
+ end
249
+
250
+ # @api private
251
+ def relation_methods_mod(relation_class)
252
+ Module.new do
253
+ relation_class.view_methods.each do |meth|
254
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
255
+ def #{meth}(*args)
256
+ response = relation.public_send(:#{meth}, *args)
257
+
258
+ if response.is_a?(relation.class)
259
+ new(response)
260
+ else
261
+ response
262
+ end
263
+ end
264
+ RUBY
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end