dry-system 0.18.1 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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