Skip to content

Commit 48144d3

Browse files
committed
Refactor setup/configuration/finalization code
- Use components directly in the loader - Use components when finalizing schemas - Use components when finalizing relations - Use components when finalizing commands - Use components when finalizing mappers - Remove usage for *_classes methods - Components => Components::Registry - Introduce Components::Relation - Drastically simplify handling of environment and notifications - Remove unused environment config from finalize - Remove unused repo_adapter - Use configuration in Finalize instead of options - Remove redundant configuration.finalize - Move setting default command registry to relation class method
1 parent 7255136 commit 48144d3

31 files changed

+676
-543
lines changed

lib/rom/compat/setup.rb

+18-3
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,25 @@ class Setup
2121
# @api public
2222
def auto_registration(directory, **options)
2323
auto_registration = AutoRegistration.new(directory, inflector: inflector, **options)
24-
auto_registration.relations.map { |r| register_relation(r) }
25-
auto_registration.commands.map { |r| register_command(r) }
26-
auto_registration.mappers.map { |r| register_mapper(r) }
24+
auto_registration.relations.each { |r| register_relation(r) }
25+
auto_registration.commands.each { |r| register_command(r) }
26+
auto_registration.mappers.each { |r| register_mapper(r) }
2727
self
2828
end
29+
30+
# @api public
31+
def relation_classes
32+
components.relations.map(&:constant)
33+
end
34+
35+
# @api public
36+
def command_classes
37+
components.commands.map(&:constant)
38+
end
39+
40+
# @api public
41+
def mapper_classes
42+
components.mappers.map(&:constant)
43+
end
2944
end
3045
end

lib/rom/components.rb

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require "rom/constants"
4+
require "rom/components/schema"
5+
require "rom/components/command"
6+
require "rom/components/relation"
7+
require "rom/components/mapper"
8+
9+
module ROM
10+
# Setup objects collect component classes during setup/finalization process
11+
#
12+
# @api public
13+
module Components
14+
CORE_TYPES = %i[schemas relations commands mappers].freeze
15+
16+
HANDLERS = {
17+
relations: Relation,
18+
commands: Command,
19+
mappers: Mapper,
20+
schemas: Schema
21+
}.freeze
22+
23+
# @api public
24+
def components
25+
@components ||= Registry.new
26+
end
27+
28+
class Registry
29+
# @api private
30+
attr_reader :types
31+
32+
# @api private
33+
attr_reader :store
34+
35+
# @api private
36+
attr_reader :handlers
37+
38+
# @api private
39+
def initialize(types: CORE_TYPES.dup, handlers: HANDLERS)
40+
@types = types
41+
@store = types.map { |type| [type, EMPTY_ARRAY.dup] }.to_h
42+
@handlers = handlers
43+
end
44+
45+
# @api private
46+
def [](type)
47+
store[type]
48+
end
49+
50+
# @api private
51+
def update(other)
52+
store.each { |type, items| items.concat(other[type]) }
53+
self
54+
end
55+
56+
# @api private
57+
def add(type, **options)
58+
component = handlers.fetch(type).new(**options)
59+
60+
if component.respond_to?(:id) && store[type].map(&:id).include?(component.id)
61+
case type
62+
when :mappers
63+
raise MapperAlreadyDefinedError, "+#{component.id}+ is already defined"
64+
when :relations
65+
raise RelationAlreadyDefinedError, "+#{component.id}+ is already defined"
66+
end
67+
end
68+
69+
# TODO: this needs a nicer abstraction
70+
# TODO: respond_to? is only needed because auto_register specs use POROs :(
71+
update(component.constant.components) if component.constant.respond_to?(:components)
72+
73+
store[type] << component
74+
75+
component
76+
end
77+
78+
CORE_TYPES.each do |type|
79+
define_method(type) { self[type] }
80+
end
81+
end
82+
end
83+
end

lib/rom/components/command.rb

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "core"
4+
5+
module ROM
6+
module Components
7+
# @api public
8+
class Command < Core
9+
id :command
10+
11+
# @!attribute [r] relation_name
12+
# @return [Symbol] The relation identifier
13+
# @api public
14+
option :relation_name, type: Types.Instance(Symbol), default: -> {
15+
# TODO: another workaround for auto_register specs not using actual rom classes
16+
constant.respond_to?(:relation) ? constant.relation : constant.name.to_sym
17+
}
18+
19+
# @api public
20+
def build(relation:)
21+
trigger(
22+
"commands.class.before_build",
23+
command: constant,
24+
gateway: gateways[relation.gateway],
25+
dataset: relation.dataset,
26+
adapter: relation.adapter
27+
)
28+
29+
constant.extend_for_relation(relation) if constant.restrictable
30+
31+
constant.build(relation)
32+
end
33+
end
34+
end
35+
end

lib/rom/components/core.rb

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# frozen_string_literal: true
2+
3+
require "dry/effects"
4+
require "dry/core/class_attributes"
5+
6+
require "rom/constants"
7+
require "rom/initializer"
8+
require "rom/types"
9+
10+
module ROM
11+
module Components
12+
# Abstract component class
13+
#
14+
# @api public
15+
class Core
16+
extend Initializer
17+
extend Dry::Core::ClassAttributes
18+
19+
include Dry::Effects.Reader(:configuration)
20+
21+
defines :id
22+
23+
# @!attribute [r] constant
24+
# @return [Class] Component's target class
25+
# @api public
26+
option :constant, optional: true, type: Types.Interface(:new)
27+
alias_method :relation, :constant
28+
29+
# This method is meant to return a run-time component instance
30+
#
31+
# @api public
32+
def build(**)
33+
raise NotImplementedError
34+
end
35+
36+
# @api public
37+
def trigger(event, payload)
38+
notifications.trigger("configuration.#{event}", payload)
39+
end
40+
41+
# @api public
42+
def relations
43+
configuration.relations
44+
end
45+
46+
# @api public
47+
def command_compiler
48+
configuration.command_compiler
49+
end
50+
51+
# @api public
52+
def notifications
53+
configuration.notifications
54+
end
55+
56+
# @api public
57+
def gateways
58+
configuration.gateways
59+
end
60+
61+
# @api public
62+
def components
63+
configuration.components
64+
end
65+
66+
# @api public
67+
def apply_plugins
68+
plugins.each do |plugin|
69+
plugin.apply_to(constant)
70+
end
71+
end
72+
73+
# @api public
74+
def plugins
75+
configuration.plugins.select { |plugin| plugin.type == self.class.id }
76+
end
77+
78+
# @api public
79+
def plugin_options
80+
plugins.map(&:config).map(&:to_hash).reduce(:merge) || EMPTY_HASH
81+
end
82+
83+
private
84+
85+
# @api private
86+
def gateway
87+
gateways.fetch(relation.gateway) do
88+
raise "+#{constant}+ gateway not found #{constant.gateway.inspect}"
89+
end
90+
end
91+
end
92+
end
93+
end

lib/rom/components/mapper.rb

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "core"
4+
5+
module ROM
6+
module Components
7+
# @api public
8+
class Mapper < Core
9+
id :mapper
10+
11+
# @!attribute [r] key
12+
# @return [Symbol] The mapper identifier
13+
# @api public
14+
option :key, type: Types.Instance(Symbol), default: -> {
15+
# TODO: another workaround for auto_register specs not using actual rom classes
16+
constant.respond_to?(:register_as) ?
17+
(constant.register_as || constant.relation) : constant.name.to_sym
18+
}
19+
20+
# @!attribute [r] base_relation
21+
# @return [Symbol] The base relation identifier
22+
# @api public
23+
option :base_relation, type: Types.Instance(Symbol), default: -> {
24+
# TODO: another workaround for auto_register specs not using actual rom classes
25+
constant.respond_to?(:base_relation) ? constant.base_relation : constant.name.to_sym
26+
}
27+
28+
# @!attribute [r] object
29+
# @return [Class] Pre-initialize object that should be used instead of the constant
30+
# @api public
31+
option :object, optional: true
32+
33+
# @api public
34+
def id
35+
"#{base_relation}.#{key}"
36+
end
37+
38+
# @api public
39+
def build
40+
object || constant.build
41+
end
42+
end
43+
end
44+
end

lib/rom/components/relation.rb

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "core"
4+
5+
module ROM
6+
module Components
7+
# @api public
8+
class Relation < Core
9+
id :relation
10+
11+
# @!attribute [r] key
12+
# @return [Symbol] The relation identifier
13+
# @api public
14+
option :key, type: Types.Instance(Symbol), default: -> {
15+
# TODO: another workaround for auto_register specs not using actual rom classes
16+
constant.respond_to?(:relation_name) ? constant.relation_name.to_sym : constant.name.to_sym
17+
}
18+
alias_method :id, :key
19+
20+
# @api public
21+
def adapter
22+
constant.adapter
23+
end
24+
25+
# @return [ROM::Relation]
26+
#
27+
# @api public
28+
def build
29+
unless adapter
30+
raise MissingAdapterIdentifierError,
31+
"Relation class +#{constant}+ is missing the adapter identifier"
32+
end
33+
34+
relation_names = components.relations.map(&:key)
35+
36+
# TODO: this should become a built-in feature, no neeed to use a plugin
37+
constant.use(:registry_reader, relations: relation_names)
38+
39+
trigger("relations.class.ready", relation: constant, adapter: adapter)
40+
41+
schema = finalize_schema
42+
43+
apply_plugins
44+
45+
dataset = gateway.dataset(schema.name.dataset).instance_exec(constant, &constant.dataset)
46+
47+
trigger("relations.dataset.allocated", dataset: dataset, relation: constant, adapter: adapter)
48+
49+
# TODO: this will be removed once mapper registry is lazy-by-default
50+
mappers = finalize_mappers
51+
52+
options = {
53+
__registry__: relations,
54+
mappers: mappers,
55+
schema: schema,
56+
inflector: configuration.inflector,
57+
**plugin_options
58+
}
59+
60+
relation = constant.new(dataset, **options)
61+
62+
# TODO: this will be removed once command registry is lazy-by-default
63+
finalize_commands(relation)
64+
65+
relation
66+
end
67+
68+
private
69+
70+
# @api private
71+
def finalize_schema
72+
components.schemas
73+
.select { |schema| schema.relation == constant }
74+
.last # TODO: relation DSL auto-defines an empty schema so this is a workaround
75+
.build
76+
end
77+
78+
# @api private
79+
def finalize_mappers
80+
mappers = components[:mappers]
81+
.map { |mapper| [mapper.key, mapper.build] if mapper.base_relation == key }
82+
.compact
83+
.to_h
84+
85+
constant.mapper_registry(cache: configuration.cache).merge(mappers)
86+
end
87+
88+
# @api private
89+
def finalize_commands(relation)
90+
commands = components.commands
91+
.select { |command| command.relation_name == constant.relation_name.relation }
92+
.map { |command| command.build(relation: relation) }
93+
94+
commands.each do |command|
95+
identifier = command.class.register_as || command.class.default_name
96+
relation.commands.elements[identifier] = command
97+
end
98+
99+
command_compiler.commands.elements[constant.relation_name.relation] = relation.commands
100+
101+
relation.commands.set_compiler(command_compiler)
102+
relation.commands.set_mappers(relation.mappers)
103+
end
104+
end
105+
end
106+
end

0 commit comments

Comments
 (0)