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.
@@ -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,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
- # Sets load paths relative to the container's root dir
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
- # load_paths!('lib')
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 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)
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, &block)
529
- load_component(key, &block) unless finalized?
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 load_paths
561
- @load_paths ||= []
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
- new_hooks[event].concat(blocks)
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
- private
578
+ protected
687
579
 
688
580
  # @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?
581
+ def load_component(key)
582
+ return self if registered?(key)
692
583
 
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)
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 block_given?
701
- yield
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
- # @api private
708
- def load_imported_component(component)
709
- container = importer[component.namespace]
710
- container.load_component(component.identifier)
711
- importer.(component.namespace, container)
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
@@ -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