dry-system 0.18.0 → 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
@@ -125,7 +125,6 @@ module Dry
125
125
  def configure(&block)
126
126
  hooks[:before_configure].each { |hook| instance_eval(&hook) }
127
127
  super(&block)
128
- load_paths!(config.system_dir)
129
128
  hooks[:after_configure].each { |hook| instance_eval(&hook) }
130
129
  self
131
130
  end
@@ -384,7 +383,7 @@ module Dry
384
383
  self
385
384
  end
386
385
 
387
- # 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
388
387
  #
389
388
  # @example
390
389
  # class MyApp < Dry::System::Container
@@ -392,7 +391,7 @@ module Dry
392
391
  # # ...
393
392
  # end
394
393
  #
395
- # load_paths!('lib')
394
+ # add_to_load_path!('lib')
396
395
  # end
397
396
  #
398
397
  # @param [Array<String>] dirs
@@ -400,12 +399,9 @@ module Dry
400
399
  # @return [self]
401
400
  #
402
401
  # @api public
403
- def load_paths!(*dirs)
404
- dirs.map(&root.method(:join)).each do |path|
405
- next if load_paths.include?(path)
406
-
407
- load_paths << path
408
- $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)
409
405
  end
410
406
  self
411
407
  end
@@ -416,47 +412,6 @@ module Dry
416
412
  self
417
413
  end
418
414
 
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
415
  # Builds injector for this container
461
416
  #
462
417
  # An injector is a useful mixin which injects dependencies into
@@ -525,8 +480,8 @@ module Dry
525
480
  end
526
481
 
527
482
  # @api public
528
- def resolve(key, &block)
529
- load_component(key, &block) unless finalized?
483
+ def resolve(key)
484
+ load_component(key) unless finalized?
530
485
 
531
486
  super
532
487
  end
@@ -557,8 +512,8 @@ module Dry
557
512
  end
558
513
 
559
514
  # @api private
560
- def load_paths
561
- @load_paths ||= []
515
+ def component_dirs
516
+ config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
562
517
  end
563
518
 
564
519
  # @api private
@@ -594,67 +549,6 @@ module Dry
594
549
  @importer ||= config.importer.new(self)
595
550
  end
596
551
 
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
552
  # @api private
659
553
  def after(event, &block)
660
554
  hooks[:"after_#{event}"] << block
@@ -671,46 +565,87 @@ module Dry
671
565
 
672
566
  # @api private
673
567
  def inherited(klass)
674
- new_hooks = Container.hooks.dup
675
-
676
568
  hooks.each do |event, blocks|
677
- new_hooks[event].concat(blocks)
678
- new_hooks[event].concat(klass.hooks[event])
569
+ klass.hooks[event].concat blocks.dup
679
570
  end
680
571
 
681
- klass.instance_variable_set(:@hooks, new_hooks)
682
572
  klass.instance_variable_set(:@__finalized__, false)
573
+
683
574
  super
684
575
  end
685
576
 
686
- private
577
+ protected
687
578
 
688
579
  # @api private
689
- def load_local_component(component, default_namespace_fallback = false, &block)
690
- if booter.bootable?(component) || component.file_exists?(load_paths)
691
- booter.boot_dependency(component) unless finalized?
580
+ def load_component(key)
581
+ return self if registered?(key)
692
582
 
693
- require_component(component) do
694
- register(component.identifier) { component.instance }
695
- end
696
- elsif !default_namespace_fallback
697
- 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)
698
595
  elsif manual_registrar.file_exists?(component)
699
596
  manual_registrar.(component)
700
- elsif block_given?
701
- yield
702
- else
703
- raise ComponentLoadError, component
597
+ elsif importer.key?(component.identifier.root_key)
598
+ load_imported_component(component.identifier)
704
599
  end
600
+
601
+ self
705
602
  end
706
603
 
707
- # @api private
708
- def load_imported_component(component)
709
- container = importer[component.namespace]
710
- container.load_component(component.identifier)
711
- 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)
712
636
  end
713
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
714
649
  end
715
650
  end
716
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module System
5
- VERSION = "0.18.0"
5
+ VERSION = "0.19.0"
6
6
  end
7
7
  end