dry-system 0.18.1 → 0.19.2
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 +4 -4
- data/CHANGELOG.md +112 -0
- data/LICENSE +1 -1
- data/README.md +1 -1
- data/dry-system.gemspec +3 -4
- data/lib/dry/system/auto_registrar.rb +16 -58
- data/lib/dry/system/booter.rb +34 -15
- data/lib/dry/system/component.rb +56 -94
- data/lib/dry/system/component_dir.rb +128 -0
- data/lib/dry/system/config/component_dir.rb +202 -0
- data/lib/dry/system/config/component_dirs.rb +184 -0
- data/lib/dry/system/container.rb +79 -143
- data/lib/dry/system/errors.rb +21 -12
- data/lib/dry/system/identifier.rb +157 -0
- data/lib/dry/system/loader/autoloading.rb +26 -0
- data/lib/dry/system/loader.rb +40 -41
- data/lib/dry/system/plugins/logging.rb +1 -4
- data/lib/dry/system/version.rb +1 -1
- metadata +17 -27
- data/lib/dry/system/auto_registrar/configuration.rb +0 -43
data/lib/dry/system/container.rb
CHANGED
@@ -11,7 +11,6 @@ require "dry/core/deprecations"
|
|
11
11
|
|
12
12
|
require "dry/system"
|
13
13
|
require "dry/system/errors"
|
14
|
-
require "dry/system/loader"
|
15
14
|
require "dry/system/booter"
|
16
15
|
require "dry/system/auto_registrar"
|
17
16
|
require "dry/system/manual_registrar"
|
@@ -20,6 +19,9 @@ require "dry/system/component"
|
|
20
19
|
require "dry/system/constants"
|
21
20
|
require "dry/system/plugins"
|
22
21
|
|
22
|
+
require_relative "component_dir"
|
23
|
+
require_relative "config/component_dirs"
|
24
|
+
|
23
25
|
module Dry
|
24
26
|
module System
|
25
27
|
# Abstract container class to inherit from
|
@@ -61,7 +63,7 @@ module Dry
|
|
61
63
|
# end
|
62
64
|
#
|
63
65
|
# # this will configure $LOAD_PATH to include your `lib` dir
|
64
|
-
#
|
66
|
+
# add_dirs_to_load_paths!('lib')
|
65
67
|
# end
|
66
68
|
#
|
67
69
|
# @api public
|
@@ -71,14 +73,12 @@ module Dry
|
|
71
73
|
extend Dry::System::Plugins
|
72
74
|
|
73
75
|
setting :name
|
74
|
-
setting :default_namespace
|
75
76
|
setting(:root, Pathname.pwd.freeze) { |path| Pathname(path) }
|
76
77
|
setting :system_dir, "system"
|
77
78
|
setting :bootable_dirs, ["system/boot"]
|
78
79
|
setting :registrations_dir, "container"
|
79
|
-
setting :
|
80
|
+
setting :component_dirs, Config::ComponentDirs.new, cloneable: true
|
80
81
|
setting :inflector, Dry::Inflector.new
|
81
|
-
setting :loader, Dry::System::Loader
|
82
82
|
setting :booter, Dry::System::Booter
|
83
83
|
setting :auto_registrar, Dry::System::AutoRegistrar
|
84
84
|
setting :manual_registrar, Dry::System::ManualRegistrar
|
@@ -107,6 +107,7 @@ module Dry
|
|
107
107
|
config._settings << _settings[name]
|
108
108
|
self
|
109
109
|
end
|
110
|
+
ruby2_keywords(:setting) if respond_to?(:ruby2_keywords, true)
|
110
111
|
|
111
112
|
# Configures the container
|
112
113
|
#
|
@@ -125,7 +126,6 @@ module Dry
|
|
125
126
|
def configure(&block)
|
126
127
|
hooks[:before_configure].each { |hook| instance_eval(&hook) }
|
127
128
|
super(&block)
|
128
|
-
load_paths!(config.system_dir)
|
129
129
|
hooks[:after_configure].each { |hook| instance_eval(&hook) }
|
130
130
|
self
|
131
131
|
end
|
@@ -384,7 +384,7 @@ module Dry
|
|
384
384
|
self
|
385
385
|
end
|
386
386
|
|
387
|
-
#
|
387
|
+
# Adds the directories (relative to the container's root) to the Ruby load path
|
388
388
|
#
|
389
389
|
# @example
|
390
390
|
# class MyApp < Dry::System::Container
|
@@ -392,7 +392,7 @@ module Dry
|
|
392
392
|
# # ...
|
393
393
|
# end
|
394
394
|
#
|
395
|
-
#
|
395
|
+
# add_to_load_path!('lib')
|
396
396
|
# end
|
397
397
|
#
|
398
398
|
# @param [Array<String>] dirs
|
@@ -400,12 +400,9 @@ module Dry
|
|
400
400
|
# @return [self]
|
401
401
|
#
|
402
402
|
# @api public
|
403
|
-
def
|
404
|
-
dirs.map(&root.method(:join)).each do |path|
|
405
|
-
|
406
|
-
|
407
|
-
load_paths << path
|
408
|
-
$LOAD_PATH.unshift(path.to_s)
|
403
|
+
def add_to_load_path!(*dirs)
|
404
|
+
dirs.reverse.map(&root.method(:join)).each do |path|
|
405
|
+
$LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
|
409
406
|
end
|
410
407
|
self
|
411
408
|
end
|
@@ -416,47 +413,6 @@ module Dry
|
|
416
413
|
self
|
417
414
|
end
|
418
415
|
|
419
|
-
# Auto-registers components from the provided directory
|
420
|
-
#
|
421
|
-
# Typically you want to configure auto_register directories, and it will
|
422
|
-
# work automatically. Use this method in cases where you want to have an
|
423
|
-
# explicit way where some components are auto-registered, or if you want
|
424
|
-
# to exclude some components from being auto-registered
|
425
|
-
#
|
426
|
-
# @example
|
427
|
-
# class MyApp < Dry::System::Container
|
428
|
-
# configure do |config|
|
429
|
-
# # ...
|
430
|
-
# end
|
431
|
-
#
|
432
|
-
# # with a dir
|
433
|
-
# auto_register!('lib/core')
|
434
|
-
#
|
435
|
-
# # with a dir and a custom registration block
|
436
|
-
# auto_register!('lib/core') do |config|
|
437
|
-
# config.instance do |component|
|
438
|
-
# # custom way of initializing a component
|
439
|
-
# end
|
440
|
-
#
|
441
|
-
# config.exclude do |component|
|
442
|
-
# # return true to exclude component from auto-registration
|
443
|
-
# end
|
444
|
-
# end
|
445
|
-
# end
|
446
|
-
#
|
447
|
-
# @param [String] dir The dir name relative to the root dir
|
448
|
-
#
|
449
|
-
# @yield AutoRegistrar::Configuration
|
450
|
-
# @see AutoRegistrar::Configuration
|
451
|
-
#
|
452
|
-
# @return [self]
|
453
|
-
#
|
454
|
-
# @api public
|
455
|
-
def auto_register!(dir, &block)
|
456
|
-
auto_registrar.(dir, &block)
|
457
|
-
self
|
458
|
-
end
|
459
|
-
|
460
416
|
# Builds injector for this container
|
461
417
|
#
|
462
418
|
# An injector is a useful mixin which injects dependencies into
|
@@ -525,8 +481,8 @@ module Dry
|
|
525
481
|
end
|
526
482
|
|
527
483
|
# @api public
|
528
|
-
def resolve(key
|
529
|
-
load_component(key
|
484
|
+
def resolve(key)
|
485
|
+
load_component(key) unless finalized?
|
530
486
|
|
531
487
|
super
|
532
488
|
end
|
@@ -557,8 +513,8 @@ module Dry
|
|
557
513
|
end
|
558
514
|
|
559
515
|
# @api private
|
560
|
-
def
|
561
|
-
|
516
|
+
def component_dirs
|
517
|
+
config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
|
562
518
|
end
|
563
519
|
|
564
520
|
# @api private
|
@@ -594,67 +550,6 @@ module Dry
|
|
594
550
|
@importer ||= config.importer.new(self)
|
595
551
|
end
|
596
552
|
|
597
|
-
# @api private
|
598
|
-
def component(identifier, **options)
|
599
|
-
if (component = booter.components.detect { |c| c.identifier == identifier })
|
600
|
-
component
|
601
|
-
else
|
602
|
-
Component.new(
|
603
|
-
identifier,
|
604
|
-
loader: config.loader,
|
605
|
-
namespace: config.default_namespace,
|
606
|
-
separator: config.namespace_separator,
|
607
|
-
inflector: config.inflector,
|
608
|
-
**options
|
609
|
-
)
|
610
|
-
end
|
611
|
-
end
|
612
|
-
|
613
|
-
# @api private
|
614
|
-
def require_component(component)
|
615
|
-
return if registered?(component.identifier)
|
616
|
-
|
617
|
-
raise FileNotFoundError, component unless component.file_exists?(load_paths)
|
618
|
-
|
619
|
-
require_path(component.path)
|
620
|
-
|
621
|
-
yield
|
622
|
-
end
|
623
|
-
|
624
|
-
# Allows subclasses to use a different strategy for required files.
|
625
|
-
#
|
626
|
-
# E.g. apps that use `ActiveSupport::Dependencies::Loadable#require_dependency`
|
627
|
-
# will override this method to allow container managed dependencies to be reloaded
|
628
|
-
# for non-finalized containers.
|
629
|
-
#
|
630
|
-
# @api private
|
631
|
-
def require_path(path)
|
632
|
-
require path
|
633
|
-
end
|
634
|
-
|
635
|
-
# @api private
|
636
|
-
def load_component(key, &block)
|
637
|
-
return self if registered?(key)
|
638
|
-
|
639
|
-
component(key).tap do |component|
|
640
|
-
if component.bootable?
|
641
|
-
booter.start(component)
|
642
|
-
else
|
643
|
-
root_key = component.root_key
|
644
|
-
|
645
|
-
if (root_bootable = component(root_key)).bootable?
|
646
|
-
booter.start(root_bootable)
|
647
|
-
elsif importer.key?(root_key)
|
648
|
-
load_imported_component(component.namespaced(root_key))
|
649
|
-
end
|
650
|
-
|
651
|
-
load_local_component(component, &block) unless registered?(key)
|
652
|
-
end
|
653
|
-
end
|
654
|
-
|
655
|
-
self
|
656
|
-
end
|
657
|
-
|
658
553
|
# @api private
|
659
554
|
def after(event, &block)
|
660
555
|
hooks[:"after_#{event}"] << block
|
@@ -671,46 +566,87 @@ module Dry
|
|
671
566
|
|
672
567
|
# @api private
|
673
568
|
def inherited(klass)
|
674
|
-
new_hooks = Container.hooks.dup
|
675
|
-
|
676
569
|
hooks.each do |event, blocks|
|
677
|
-
|
678
|
-
new_hooks[event].concat(klass.hooks[event])
|
570
|
+
klass.hooks[event].concat blocks.dup
|
679
571
|
end
|
680
572
|
|
681
|
-
klass.instance_variable_set(:@hooks, new_hooks)
|
682
573
|
klass.instance_variable_set(:@__finalized__, false)
|
574
|
+
|
683
575
|
super
|
684
576
|
end
|
685
577
|
|
686
|
-
|
578
|
+
protected
|
687
579
|
|
688
580
|
# @api private
|
689
|
-
def
|
690
|
-
|
691
|
-
booter.boot_dependency(component) unless finalized?
|
581
|
+
def load_component(key)
|
582
|
+
return self if registered?(key)
|
692
583
|
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
584
|
+
component = component(key)
|
585
|
+
|
586
|
+
if component.bootable?
|
587
|
+
booter.start(component)
|
588
|
+
return self
|
589
|
+
end
|
590
|
+
|
591
|
+
booter.boot_dependency(component)
|
592
|
+
return self if registered?(key)
|
593
|
+
|
594
|
+
if component.file_exists?
|
595
|
+
load_local_component(component)
|
698
596
|
elsif manual_registrar.file_exists?(component)
|
699
597
|
manual_registrar.(component)
|
700
|
-
elsif
|
701
|
-
|
702
|
-
else
|
703
|
-
raise ComponentLoadError, component
|
598
|
+
elsif importer.key?(component.identifier.root_key)
|
599
|
+
load_imported_component(component.identifier)
|
704
600
|
end
|
601
|
+
|
602
|
+
self
|
705
603
|
end
|
706
604
|
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
605
|
+
private
|
606
|
+
|
607
|
+
def load_local_component(component)
|
608
|
+
if component.auto_register?
|
609
|
+
register(component.identifier, memoize: component.memoize?) { component.instance }
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
def load_imported_component(identifier)
|
614
|
+
import_namespace = identifier.root_key
|
615
|
+
|
616
|
+
container = importer[import_namespace]
|
617
|
+
|
618
|
+
container.load_component(identifier.dequalified(import_namespace).key)
|
619
|
+
|
620
|
+
importer.(import_namespace, container)
|
621
|
+
end
|
622
|
+
|
623
|
+
def component(identifier)
|
624
|
+
if (bootable_component = booter.find_component(identifier))
|
625
|
+
return bootable_component
|
626
|
+
end
|
627
|
+
|
628
|
+
# Find the first matching component from within the configured component dirs.
|
629
|
+
# If no matching component is found, return a plain component instance with no
|
630
|
+
# associated file path. This fallback is important because the component may
|
631
|
+
# still be loadable via the manual registrar or an imported container.
|
632
|
+
component_dirs.detect { |dir|
|
633
|
+
if (component = dir.component_for_identifier(identifier))
|
634
|
+
break component
|
635
|
+
end
|
636
|
+
} || Component.new(identifier)
|
712
637
|
end
|
713
638
|
end
|
639
|
+
|
640
|
+
# Default hooks
|
641
|
+
after :configure do
|
642
|
+
# Add appropriately configured component dirs to the load path
|
643
|
+
#
|
644
|
+
# Do this in a single pass to preserve ordering (i.e. earliest dirs win)
|
645
|
+
paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr|
|
646
|
+
arr << dir.path if dir.add_to_load_path
|
647
|
+
}
|
648
|
+
add_to_load_path!(*paths)
|
649
|
+
end
|
714
650
|
end
|
715
651
|
end
|
716
652
|
end
|
data/lib/dry/system/errors.rb
CHANGED
@@ -2,13 +2,22 @@
|
|
2
2
|
|
3
3
|
module Dry
|
4
4
|
module System
|
5
|
+
# Error raised when a component dir is added to configuration more than once
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
ComponentDirAlreadyAddedError = Class.new(StandardError) do
|
9
|
+
def initialize(dir)
|
10
|
+
super("Component directory #{dir.inspect} already added")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
5
14
|
# Error raised when the container tries to load a component with missing
|
6
15
|
# file
|
7
16
|
#
|
8
17
|
# @api public
|
9
18
|
FileNotFoundError = Class.new(StandardError) do
|
10
19
|
def initialize(component)
|
11
|
-
super("could not resolve require file for #{component.identifier}")
|
20
|
+
super("could not resolve require file for component '#{component.identifier}'")
|
12
21
|
end
|
13
22
|
end
|
14
23
|
|
@@ -18,20 +27,11 @@ module Dry
|
|
18
27
|
ComponentFileMismatchError = Class.new(StandardError) do
|
19
28
|
def initialize(component)
|
20
29
|
super(<<-STR)
|
21
|
-
Bootable component #{component.identifier
|
30
|
+
Bootable component '#{component.identifier}' not found
|
22
31
|
STR
|
23
32
|
end
|
24
33
|
end
|
25
34
|
|
26
|
-
# Error raised when a resolved component couldn't be found
|
27
|
-
#
|
28
|
-
# @api public
|
29
|
-
ComponentLoadError = Class.new(StandardError) do
|
30
|
-
def initialize(component)
|
31
|
-
super("could not load component #{component.inspect}")
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
35
|
# Error raised when resolved component couldn't be loaded
|
36
36
|
#
|
37
37
|
# @api public
|
@@ -81,8 +81,17 @@ module Dry
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
|
-
|
84
|
+
# Error raised when a configured component directory could not be found
|
85
|
+
#
|
86
|
+
# @api public
|
87
|
+
ComponentDirNotFoundError = Class.new(StandardError) do
|
88
|
+
def initialize(dir)
|
89
|
+
super("Component dir '#{dir}' not found")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
85
93
|
DuplicatedComponentKeyError = Class.new(ArgumentError)
|
94
|
+
|
86
95
|
InvalidSettingsError = Class.new(ArgumentError) do
|
87
96
|
# @api private
|
88
97
|
def initialize(attributes)
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/equalizer"
|
4
|
+
require_relative "constants"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module System
|
8
|
+
# An identifier representing a component to be registered.
|
9
|
+
#
|
10
|
+
# Components are eventually registered in the container using plain string
|
11
|
+
# identifiers, available as the `identifier` or `key` attribute here. Additional
|
12
|
+
# methods are provided to make it easier to evaluate or manipulate these identifiers.
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class Identifier
|
16
|
+
include Dry::Equalizer(:identifier, :namespace, :separator)
|
17
|
+
|
18
|
+
# @return [String] the identifier string
|
19
|
+
# @api public
|
20
|
+
attr_reader :identifier
|
21
|
+
|
22
|
+
# @return [String, nil] the namespace for the component
|
23
|
+
# @api public
|
24
|
+
attr_reader :namespace
|
25
|
+
|
26
|
+
# @return [String] the configured namespace separator
|
27
|
+
# @api public
|
28
|
+
attr_reader :separator
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
def initialize(identifier, namespace: nil, separator: DEFAULT_SEPARATOR)
|
32
|
+
@identifier = identifier.to_s
|
33
|
+
@namespace = namespace
|
34
|
+
@separator = separator
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!method key
|
38
|
+
# Returns the identifier string
|
39
|
+
#
|
40
|
+
# @return [String]
|
41
|
+
# @see #identifier
|
42
|
+
# @api public
|
43
|
+
alias_method :key, :identifier
|
44
|
+
|
45
|
+
# @!method to_s
|
46
|
+
# Returns the identifier string
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
# @see #identifier
|
50
|
+
# @api public
|
51
|
+
alias_method :to_s, :identifier
|
52
|
+
|
53
|
+
# Returns the root namespace segment of the identifier string, as a symbol
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# identifier.key # => "articles.operations.create"
|
57
|
+
# identifier.root_key # => :articles
|
58
|
+
#
|
59
|
+
# @return [Symbol] the root key
|
60
|
+
# @api public
|
61
|
+
def root_key
|
62
|
+
segments.first.to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns a path-delimited representation of the identifier, with the namespace
|
66
|
+
# incorporated. This path is intended for usage when requiring the component's
|
67
|
+
# source file.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# identifier.key # => "articles.operations.create"
|
71
|
+
# identifier.namespace # => "admin"
|
72
|
+
#
|
73
|
+
# identifier.path # => "admin/articles/operations/create"
|
74
|
+
#
|
75
|
+
# @return [String] the path
|
76
|
+
# @api public
|
77
|
+
def path
|
78
|
+
@require_path ||= identifier.gsub(separator, PATH_SEPARATOR).yield_self { |path|
|
79
|
+
if namespace
|
80
|
+
namespace_path = namespace.to_s.gsub(separator, PATH_SEPARATOR)
|
81
|
+
"#{namespace_path}#{PATH_SEPARATOR}#{path}"
|
82
|
+
else
|
83
|
+
path
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns true if the given namespace prefix is part of the identifier's leading
|
89
|
+
# namespaces
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# identifier.key # => "articles.operations.create"
|
93
|
+
#
|
94
|
+
# identifier.start_with?("articles.operations") # => true
|
95
|
+
# identifier.start_with?("articles") # => true
|
96
|
+
# identifier.start_with?("article") # => false
|
97
|
+
#
|
98
|
+
# @param leading_namespaces [String] the one or more leading namespaces to check
|
99
|
+
# @return [Boolean]
|
100
|
+
# @api public
|
101
|
+
def start_with?(leading_namespaces)
|
102
|
+
identifier.start_with?("#{leading_namespaces}#{separator}")
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns a copy of the identifier with the given leading namespaces removed from
|
106
|
+
# the identifier string.
|
107
|
+
#
|
108
|
+
# Additional options may be provided, which are passed to #initialize when
|
109
|
+
# constructing the new copy of the identifier
|
110
|
+
#
|
111
|
+
# @param leading_namespace [String] the one or more leading namespaces to remove
|
112
|
+
# @param options [Hash] additional options for initialization
|
113
|
+
#
|
114
|
+
# @return [Dry::System::Identifier] the copy of the identifier
|
115
|
+
#
|
116
|
+
# @see #initialize
|
117
|
+
# @api private
|
118
|
+
def dequalified(leading_namespaces, **options)
|
119
|
+
new_identifier = identifier.gsub(
|
120
|
+
/^#{Regexp.escape(leading_namespaces)}#{Regexp.escape(separator)}/,
|
121
|
+
EMPTY_STRING
|
122
|
+
)
|
123
|
+
|
124
|
+
return self if new_identifier == identifier
|
125
|
+
|
126
|
+
self.class.new(
|
127
|
+
new_identifier,
|
128
|
+
namespace: namespace,
|
129
|
+
separator: separator,
|
130
|
+
**options
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns a copy of the identifier with the given options applied
|
135
|
+
#
|
136
|
+
# @param namespace [String, nil] a new namespace to be used
|
137
|
+
#
|
138
|
+
# @return [Dry::System::Identifier] the copy of the identifier
|
139
|
+
#
|
140
|
+
# @see #initialize
|
141
|
+
# @api private
|
142
|
+
def with(namespace:)
|
143
|
+
self.class.new(
|
144
|
+
identifier,
|
145
|
+
namespace: namespace,
|
146
|
+
separator: separator
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def segments
|
153
|
+
@segments ||= identifier.split(separator)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|