dry-system 0.18.2 → 0.19.0

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.
@@ -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
- # load_paths!('lib')
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 :auto_register, []
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,7 +107,6 @@ module Dry
107
107
  config._settings << _settings[name]
108
108
  self
109
109
  end
110
- ruby2_keywords(:setting) if respond_to?(:ruby2_keywords, true)
111
110
 
112
111
  # Configures the container
113
112
  #
@@ -126,7 +125,6 @@ module Dry
126
125
  def configure(&block)
127
126
  hooks[:before_configure].each { |hook| instance_eval(&hook) }
128
127
  super(&block)
129
- load_paths!(config.system_dir)
130
128
  hooks[:after_configure].each { |hook| instance_eval(&hook) }
131
129
  self
132
130
  end
@@ -385,7 +383,7 @@ module Dry
385
383
  self
386
384
  end
387
385
 
388
- # Sets load paths relative to the container's root dir
386
+ # Adds the directories (relative to the container's root) to the Ruby load path
389
387
  #
390
388
  # @example
391
389
  # class MyApp < Dry::System::Container
@@ -393,7 +391,7 @@ module Dry
393
391
  # # ...
394
392
  # end
395
393
  #
396
- # load_paths!('lib')
394
+ # add_to_load_path!('lib')
397
395
  # end
398
396
  #
399
397
  # @param [Array<String>] dirs
@@ -401,12 +399,9 @@ module Dry
401
399
  # @return [self]
402
400
  #
403
401
  # @api public
404
- def load_paths!(*dirs)
405
- dirs.map(&root.method(:join)).each do |path|
406
- next if load_paths.include?(path)
407
-
408
- load_paths << path
409
- $LOAD_PATH.unshift(path.to_s)
402
+ def add_to_load_path!(*dirs)
403
+ dirs.reverse.map(&root.method(:join)).each do |path|
404
+ $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
410
405
  end
411
406
  self
412
407
  end
@@ -417,47 +412,6 @@ module Dry
417
412
  self
418
413
  end
419
414
 
420
- # Auto-registers components from the provided directory
421
- #
422
- # Typically you want to configure auto_register directories, and it will
423
- # work automatically. Use this method in cases where you want to have an
424
- # explicit way where some components are auto-registered, or if you want
425
- # to exclude some components from being auto-registered
426
- #
427
- # @example
428
- # class MyApp < Dry::System::Container
429
- # configure do |config|
430
- # # ...
431
- # end
432
- #
433
- # # with a dir
434
- # auto_register!('lib/core')
435
- #
436
- # # with a dir and a custom registration block
437
- # auto_register!('lib/core') do |config|
438
- # config.instance do |component|
439
- # # custom way of initializing a component
440
- # end
441
- #
442
- # config.exclude do |component|
443
- # # return true to exclude component from auto-registration
444
- # end
445
- # end
446
- # end
447
- #
448
- # @param [String] dir The dir name relative to the root dir
449
- #
450
- # @yield AutoRegistrar::Configuration
451
- # @see AutoRegistrar::Configuration
452
- #
453
- # @return [self]
454
- #
455
- # @api public
456
- def auto_register!(dir, &block)
457
- auto_registrar.(dir, &block)
458
- self
459
- end
460
-
461
415
  # Builds injector for this container
462
416
  #
463
417
  # An injector is a useful mixin which injects dependencies into
@@ -526,8 +480,8 @@ module Dry
526
480
  end
527
481
 
528
482
  # @api public
529
- def resolve(key, &block)
530
- load_component(key, &block) unless finalized?
483
+ def resolve(key)
484
+ load_component(key) unless finalized?
531
485
 
532
486
  super
533
487
  end
@@ -558,8 +512,8 @@ module Dry
558
512
  end
559
513
 
560
514
  # @api private
561
- def load_paths
562
- @load_paths ||= []
515
+ def component_dirs
516
+ config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
563
517
  end
564
518
 
565
519
  # @api private
@@ -595,67 +549,6 @@ module Dry
595
549
  @importer ||= config.importer.new(self)
596
550
  end
597
551
 
598
- # @api private
599
- def component(identifier, **options)
600
- if (component = booter.components.detect { |c| c.identifier == identifier })
601
- component
602
- else
603
- Component.new(
604
- identifier,
605
- loader: config.loader,
606
- namespace: config.default_namespace,
607
- separator: config.namespace_separator,
608
- inflector: config.inflector,
609
- **options
610
- )
611
- end
612
- end
613
-
614
- # @api private
615
- def require_component(component)
616
- return if registered?(component.identifier)
617
-
618
- raise FileNotFoundError, component unless component.file_exists?(load_paths)
619
-
620
- require_path(component.path)
621
-
622
- yield
623
- end
624
-
625
- # Allows subclasses to use a different strategy for required files.
626
- #
627
- # E.g. apps that use `ActiveSupport::Dependencies::Loadable#require_dependency`
628
- # will override this method to allow container managed dependencies to be reloaded
629
- # for non-finalized containers.
630
- #
631
- # @api private
632
- def require_path(path)
633
- require path
634
- end
635
-
636
- # @api private
637
- def load_component(key, &block)
638
- return self if registered?(key)
639
-
640
- component(key).tap do |component|
641
- if component.bootable?
642
- booter.start(component)
643
- else
644
- root_key = component.root_key
645
-
646
- if (root_bootable = component(root_key)).bootable?
647
- booter.start(root_bootable)
648
- elsif importer.key?(root_key)
649
- load_imported_component(component.namespaced(root_key))
650
- end
651
-
652
- load_local_component(component, &block) unless registered?(key)
653
- end
654
- end
655
-
656
- self
657
- end
658
-
659
552
  # @api private
660
553
  def after(event, &block)
661
554
  hooks[:"after_#{event}"] << block
@@ -672,46 +565,87 @@ module Dry
672
565
 
673
566
  # @api private
674
567
  def inherited(klass)
675
- new_hooks = Container.hooks.dup
676
-
677
568
  hooks.each do |event, blocks|
678
- new_hooks[event].concat(blocks)
679
- new_hooks[event].concat(klass.hooks[event])
569
+ klass.hooks[event].concat blocks.dup
680
570
  end
681
571
 
682
- klass.instance_variable_set(:@hooks, new_hooks)
683
572
  klass.instance_variable_set(:@__finalized__, false)
573
+
684
574
  super
685
575
  end
686
576
 
687
- private
577
+ protected
688
578
 
689
579
  # @api private
690
- def load_local_component(component, default_namespace_fallback = false, &block)
691
- if booter.bootable?(component) || component.file_exists?(load_paths)
692
- booter.boot_dependency(component) unless finalized?
580
+ def load_component(key)
581
+ return self if registered?(key)
693
582
 
694
- require_component(component) do
695
- register(component.identifier) { component.instance }
696
- end
697
- elsif !default_namespace_fallback
698
- load_local_component(component.prepend(config.default_namespace), true, &block)
583
+ component = component(key)
584
+
585
+ if component.bootable?
586
+ booter.start(component)
587
+ return self
588
+ end
589
+
590
+ booter.boot_dependency(component)
591
+ return self if registered?(key)
592
+
593
+ if component.file_exists?
594
+ load_local_component(component)
699
595
  elsif manual_registrar.file_exists?(component)
700
596
  manual_registrar.(component)
701
- elsif block_given?
702
- yield
703
- else
704
- raise ComponentLoadError, component
597
+ elsif importer.key?(component.identifier.root_key)
598
+ load_imported_component(component.identifier)
705
599
  end
600
+
601
+ self
706
602
  end
707
603
 
708
- # @api private
709
- def load_imported_component(component)
710
- container = importer[component.namespace]
711
- container.load_component(component.identifier)
712
- importer.(component.namespace, container)
604
+ private
605
+
606
+ def load_local_component(component)
607
+ if component.auto_register?
608
+ register(component.identifier, memoize: component.memoize?) { component.instance }
609
+ end
610
+ end
611
+
612
+ def load_imported_component(identifier)
613
+ import_namespace = identifier.root_key
614
+
615
+ container = importer[import_namespace]
616
+
617
+ container.load_component(identifier.dequalified(import_namespace).key)
618
+
619
+ importer.(import_namespace, container)
620
+ end
621
+
622
+ def component(identifier)
623
+ if (bootable_component = booter.find_component(identifier))
624
+ return bootable_component
625
+ end
626
+
627
+ # Find the first matching component from within the configured component dirs.
628
+ # If no matching component is found, return a plain component instance with no
629
+ # associated file path. This fallback is important because the component may
630
+ # still be loadable via the manual registrar or an imported container.
631
+ component_dirs.detect { |dir|
632
+ if (component = dir.component_for_identifier(identifier))
633
+ break component
634
+ end
635
+ } || Component.new(identifier)
713
636
  end
714
637
  end
638
+
639
+ # Default hooks
640
+ after :configure do
641
+ # Add appropriately configured component dirs to the load path
642
+ #
643
+ # Do this in a single pass to preserve ordering (i.e. earliest dirs win)
644
+ paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr|
645
+ arr << dir.path if dir.add_to_load_path
646
+ }
647
+ add_to_load_path!(*paths)
648
+ end
715
649
  end
716
650
  end
717
651
  end
@@ -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.inspect} not found
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
- ComponentsDirMissing = Class.new(StandardError)
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
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../loader"
4
+
5
+ module Dry
6
+ module System
7
+ class Loader
8
+ # Component loader for autoloading-enabled applications
9
+ #
10
+ # This behaves like the default loader, except instead of requiring the given path,
11
+ # it loads the respective constant, allowing the autoloader to load the
12
+ # corresponding file per its own configuration.
13
+ #
14
+ # @see Loader
15
+ # @api public
16
+ class Autoloading < Loader
17
+ class << self
18
+ def require!(component)
19
+ constant(component)
20
+ self
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/inflector"
4
-
5
3
  module Dry
6
4
  module System
7
5
  # Default component loader implementation
@@ -25,52 +23,53 @@ module Dry
25
23
  #
26
24
  # @api public
27
25
  class Loader
28
- # @!attribute [r] path
29
- # @return [String] Path to component's file
30
- attr_reader :path
26
+ class << self
27
+ # Requires the component's source file
28
+ #
29
+ # @api public
30
+ def require!(component)
31
+ require(component.path) if component.file_exists?
32
+ self
33
+ end
31
34
 
32
- # @!attribute [r] inflector
33
- # @return [Object] Inflector backend
34
- attr_reader :inflector
35
+ # Returns an instance of the component
36
+ #
37
+ # Provided optional args are passed to object's constructor
38
+ #
39
+ # @param [Array] args Optional constructor args
40
+ #
41
+ # @return [Object]
42
+ #
43
+ # @api public
44
+ def call(component, *args)
45
+ require!(component)
35
46
 
36
- # @api private
37
- def initialize(path, inflector = Dry::Inflector.new)
38
- @path = path
39
- @inflector = inflector
40
- end
47
+ constant = self.constant(component)
41
48
 
42
- # Returns component's instance
43
- #
44
- # Provided optional args are passed to object's constructor
45
- #
46
- # @param [Array] args Optional constructor args
47
- #
48
- # @return [Object]
49
- #
50
- # @api public
51
- def call(*args)
52
- if singleton?(constant)
53
- constant.instance(*args)
54
- else
55
- constant.new(*args)
49
+ if singleton?(constant)
50
+ constant.instance(*args)
51
+ else
52
+ constant.new(*args)
53
+ end
56
54
  end
57
- end
58
- ruby2_keywords(:call) if respond_to?(:ruby2_keywords, true)
55
+ ruby2_keywords(:call) if respond_to?(:ruby2_keywords, true)
59
56
 
60
- # Return component's class constant
61
- #
62
- # @return [Class]
63
- #
64
- # @api public
65
- def constant
66
- inflector.constantize(inflector.camelize(path))
67
- end
57
+ # Returns the component's class constant
58
+ #
59
+ # @return [Class]
60
+ #
61
+ # @api public
62
+ def constant(component)
63
+ inflector = component.inflector
64
+
65
+ inflector.constantize(inflector.camelize(component.path))
66
+ end
68
67
 
69
- private
68
+ private
70
69
 
71
- # @api private
72
- def singleton?(constant)
73
- constant.respond_to?(:instance) && !constant.respond_to?(:new)
70
+ def singleton?(constant)
71
+ constant.respond_to?(:instance) && !constant.respond_to?(:new)
72
+ end
74
73
  end
75
74
  end
76
75
  end
@@ -13,7 +13,10 @@ module Dry
13
13
 
14
14
  setting :log_dir, "log"
15
15
 
16
- setting :log_levels, {development: Logger::DEBUG, test: Logger::DEBUG, production: Logger::ERROR}
16
+ setting :log_levels,
17
+ development: Logger::DEBUG,
18
+ test: Logger::DEBUG,
19
+ production: Logger::ERROR
17
20
 
18
21
  setting :logger_class, ::Logger, reader: true
19
22
  end