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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +603 -0
- data/LICENSE +20 -0
- data/README.md +18 -0
- data/lib/rom-core.rb +1 -0
- data/lib/rom/array_dataset.rb +44 -0
- data/lib/rom/association_set.rb +16 -0
- data/lib/rom/associations/abstract.rb +135 -0
- data/lib/rom/associations/definitions.rb +5 -0
- data/lib/rom/associations/definitions/abstract.rb +116 -0
- data/lib/rom/associations/definitions/many_to_many.rb +24 -0
- data/lib/rom/associations/definitions/many_to_one.rb +11 -0
- data/lib/rom/associations/definitions/one_to_many.rb +11 -0
- data/lib/rom/associations/definitions/one_to_one.rb +11 -0
- data/lib/rom/associations/definitions/one_to_one_through.rb +11 -0
- data/lib/rom/associations/many_to_many.rb +81 -0
- data/lib/rom/associations/many_to_one.rb +37 -0
- data/lib/rom/associations/one_to_many.rb +37 -0
- data/lib/rom/associations/one_to_one.rb +8 -0
- data/lib/rom/associations/one_to_one_through.rb +8 -0
- data/lib/rom/associations/through_identifier.rb +39 -0
- data/lib/rom/auto_curry.rb +55 -0
- data/lib/rom/cache.rb +46 -0
- data/lib/rom/command.rb +488 -0
- data/lib/rom/command_compiler.rb +239 -0
- data/lib/rom/command_proxy.rb +24 -0
- data/lib/rom/command_registry.rb +141 -0
- data/lib/rom/commands.rb +3 -0
- data/lib/rom/commands/class_interface.rb +270 -0
- data/lib/rom/commands/composite.rb +53 -0
- data/lib/rom/commands/create.rb +13 -0
- data/lib/rom/commands/delete.rb +14 -0
- data/lib/rom/commands/graph.rb +88 -0
- data/lib/rom/commands/graph/class_interface.rb +62 -0
- data/lib/rom/commands/graph/input_evaluator.rb +62 -0
- data/lib/rom/commands/lazy.rb +99 -0
- data/lib/rom/commands/lazy/create.rb +23 -0
- data/lib/rom/commands/lazy/delete.rb +27 -0
- data/lib/rom/commands/lazy/update.rb +34 -0
- data/lib/rom/commands/result.rb +96 -0
- data/lib/rom/commands/update.rb +14 -0
- data/lib/rom/configuration.rb +114 -0
- data/lib/rom/configuration_dsl.rb +87 -0
- data/lib/rom/configuration_dsl/command.rb +41 -0
- data/lib/rom/configuration_dsl/command_dsl.rb +35 -0
- data/lib/rom/configuration_dsl/relation.rb +26 -0
- data/lib/rom/configuration_plugin.rb +17 -0
- data/lib/rom/constants.rb +64 -0
- data/lib/rom/container.rb +147 -0
- data/lib/rom/core.rb +46 -0
- data/lib/rom/create_container.rb +60 -0
- data/lib/rom/data_proxy.rb +94 -0
- data/lib/rom/enumerable_dataset.rb +68 -0
- data/lib/rom/environment.rb +70 -0
- data/lib/rom/gateway.rb +184 -0
- data/lib/rom/global.rb +58 -0
- data/lib/rom/global/plugin_dsl.rb +47 -0
- data/lib/rom/initializer.rb +64 -0
- data/lib/rom/lint/enumerable_dataset.rb +54 -0
- data/lib/rom/lint/gateway.rb +120 -0
- data/lib/rom/lint/linter.rb +78 -0
- data/lib/rom/lint/spec.rb +20 -0
- data/lib/rom/lint/test.rb +98 -0
- data/lib/rom/mapper_registry.rb +24 -0
- data/lib/rom/memory.rb +4 -0
- data/lib/rom/memory/associations.rb +4 -0
- data/lib/rom/memory/associations/many_to_many.rb +10 -0
- data/lib/rom/memory/associations/many_to_one.rb +10 -0
- data/lib/rom/memory/associations/one_to_many.rb +10 -0
- data/lib/rom/memory/associations/one_to_one.rb +10 -0
- data/lib/rom/memory/commands.rb +56 -0
- data/lib/rom/memory/dataset.rb +97 -0
- data/lib/rom/memory/gateway.rb +64 -0
- data/lib/rom/memory/relation.rb +62 -0
- data/lib/rom/memory/schema.rb +23 -0
- data/lib/rom/memory/storage.rb +59 -0
- data/lib/rom/memory/types.rb +9 -0
- data/lib/rom/pipeline.rb +105 -0
- data/lib/rom/plugin.rb +25 -0
- data/lib/rom/plugin_base.rb +45 -0
- data/lib/rom/plugin_registry.rb +197 -0
- data/lib/rom/plugins/command/schema.rb +37 -0
- data/lib/rom/plugins/configuration/configuration_dsl.rb +21 -0
- data/lib/rom/plugins/relation/instrumentation.rb +51 -0
- data/lib/rom/plugins/relation/registry_reader.rb +44 -0
- data/lib/rom/plugins/schema/timestamps.rb +58 -0
- data/lib/rom/registry.rb +71 -0
- data/lib/rom/relation.rb +548 -0
- data/lib/rom/relation/class_interface.rb +282 -0
- data/lib/rom/relation/commands.rb +23 -0
- data/lib/rom/relation/composite.rb +46 -0
- data/lib/rom/relation/curried.rb +103 -0
- data/lib/rom/relation/graph.rb +197 -0
- data/lib/rom/relation/loaded.rb +127 -0
- data/lib/rom/relation/materializable.rb +66 -0
- data/lib/rom/relation/name.rb +111 -0
- data/lib/rom/relation/view_dsl.rb +64 -0
- data/lib/rom/relation/wrap.rb +83 -0
- data/lib/rom/relation_registry.rb +10 -0
- data/lib/rom/schema.rb +437 -0
- data/lib/rom/schema/associations_dsl.rb +195 -0
- data/lib/rom/schema/attribute.rb +419 -0
- data/lib/rom/schema/dsl.rb +164 -0
- data/lib/rom/schema/inferrer.rb +66 -0
- data/lib/rom/schema_plugin.rb +27 -0
- data/lib/rom/setup.rb +68 -0
- data/lib/rom/setup/auto_registration.rb +74 -0
- data/lib/rom/setup/auto_registration_strategies/base.rb +16 -0
- data/lib/rom/setup/auto_registration_strategies/custom_namespace.rb +63 -0
- data/lib/rom/setup/auto_registration_strategies/no_namespace.rb +20 -0
- data/lib/rom/setup/auto_registration_strategies/with_namespace.rb +18 -0
- data/lib/rom/setup/finalize.rb +103 -0
- data/lib/rom/setup/finalize/finalize_commands.rb +60 -0
- data/lib/rom/setup/finalize/finalize_mappers.rb +56 -0
- data/lib/rom/setup/finalize/finalize_relations.rb +135 -0
- data/lib/rom/support/configurable.rb +85 -0
- data/lib/rom/support/memoizable.rb +58 -0
- data/lib/rom/support/notifications.rb +103 -0
- data/lib/rom/transaction.rb +24 -0
- data/lib/rom/types.rb +26 -0
- data/lib/rom/version.rb +5 -0
- 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
|
data/lib/rom/commands.rb
ADDED
@@ -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
|