dry-system 0.19.0 → 0.21.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.
@@ -7,16 +7,19 @@ require "dry-configurable"
7
7
  require "dry-container"
8
8
  require "dry/inflector"
9
9
 
10
+ require "dry/core/constants"
10
11
  require "dry/core/deprecations"
11
12
 
12
13
  require "dry/system"
13
- require "dry/system/errors"
14
- require "dry/system/booter"
15
14
  require "dry/system/auto_registrar"
16
- require "dry/system/manual_registrar"
17
- require "dry/system/importer"
15
+ require "dry/system/booter"
18
16
  require "dry/system/component"
19
17
  require "dry/system/constants"
18
+ require "dry/system/errors"
19
+ require "dry/system/identifier"
20
+ require "dry/system/importer"
21
+ require "dry/system/indirect_component"
22
+ require "dry/system/manual_registrar"
20
23
  require "dry/system/plugins"
21
24
 
22
25
  require_relative "component_dir"
@@ -48,7 +51,7 @@ module Dry
48
51
  #
49
52
  # Every container needs to be configured with following settings:
50
53
  #
51
- # * `:name` - a unique container identifier
54
+ # * `:name` - a unique container name
52
55
  # * `:root` - a system root directory (defaults to `pwd`)
53
56
  #
54
57
  # @example
@@ -73,17 +76,17 @@ module Dry
73
76
  extend Dry::System::Plugins
74
77
 
75
78
  setting :name
76
- setting(:root, Pathname.pwd.freeze) { |path| Pathname(path) }
77
- setting :system_dir, "system"
78
- setting :bootable_dirs, ["system/boot"]
79
- setting :registrations_dir, "container"
80
- setting :component_dirs, Config::ComponentDirs.new, cloneable: true
81
- setting :inflector, Dry::Inflector.new
82
- setting :booter, Dry::System::Booter
83
- setting :auto_registrar, Dry::System::AutoRegistrar
84
- setting :manual_registrar, Dry::System::ManualRegistrar
85
- setting :importer, Dry::System::Importer
86
- setting(:components, {}, reader: true, &:dup)
79
+ setting :root, default: Pathname.pwd.freeze, constructor: -> path { Pathname(path) }
80
+ setting :system_dir, default: "system"
81
+ setting :bootable_dirs, default: ["system/boot"]
82
+ setting :registrations_dir, default: "container"
83
+ setting :component_dirs, default: Config::ComponentDirs.new, cloneable: true
84
+ setting :inflector, default: Dry::Inflector.new
85
+ setting :booter, default: Dry::System::Booter
86
+ setting :auto_registrar, default: Dry::System::AutoRegistrar
87
+ setting :manual_registrar, default: Dry::System::ManualRegistrar
88
+ setting :importer, default: Dry::System::Importer
89
+ setting :components, default: {}, reader: true, constructor: :dup.to_proc
87
90
 
88
91
  class << self
89
92
  def strategies(value = nil)
@@ -101,8 +104,8 @@ module Dry
101
104
  # @see https://dry-rb.org/gems/dry-configurable
102
105
  #
103
106
  # @api public
104
- def setting(name, *args, &block)
105
- super(name, *args, &block)
107
+ def setting(name, default = Dry::Core::Constants::Undefined, **options, &block)
108
+ super(name, default, **options, &block)
106
109
  # TODO: dry-configurable needs a public API for this
107
110
  config._settings << _settings[name]
108
111
  self
@@ -233,7 +236,7 @@ module Dry
233
236
  # persistence.register(:db, DB.new)
234
237
  # end
235
238
  #
236
- # @param name [Symbol] a unique identifier for a bootable component
239
+ # @param name [Symbol] a unique name for a bootable component
237
240
  #
238
241
  # @see Lifecycle
239
242
  #
@@ -261,17 +264,15 @@ module Dry
261
264
  deprecate :finalize, :boot
262
265
 
263
266
  # @api private
264
- def boot_external(identifier, from:, key: nil, namespace: nil, &block)
267
+ def boot_external(name, from:, key: nil, namespace: nil, &block)
265
268
  System.providers[from].component(
266
- identifier, key: key, namespace: namespace, finalize: block, container: self
269
+ name, key: key, namespace: namespace, finalize: block, container: self
267
270
  )
268
271
  end
269
272
 
270
273
  # @api private
271
- def boot_local(identifier, namespace: nil, &block)
272
- Components::Bootable.new(
273
- identifier, container: self, namespace: namespace, &block
274
- )
274
+ def boot_local(name, namespace: nil, &block)
275
+ Components::Bootable.new(name, container: self, namespace: namespace, &block)
275
276
  end
276
277
 
277
278
  # Return if a container was finalized
@@ -490,7 +491,7 @@ module Dry
490
491
  #
491
492
  # @!method registered?(key)
492
493
  # Whether a +key+ is registered (doesn't trigger loading)
493
- # @param [String,Symbol] key Identifier
494
+ # @param [String,Symbol] key The key
494
495
  # @return [Boolean]
495
496
  # @api public
496
497
  #
@@ -580,17 +581,17 @@ module Dry
580
581
  def load_component(key)
581
582
  return self if registered?(key)
582
583
 
583
- component = component(key)
584
-
585
- if component.bootable?
586
- booter.start(component)
584
+ if (bootable_component = booter.find_component(key))
585
+ booter.start(bootable_component)
587
586
  return self
588
587
  end
589
588
 
589
+ component = find_component(key)
590
+
590
591
  booter.boot_dependency(component)
591
592
  return self if registered?(key)
592
593
 
593
- if component.file_exists?
594
+ if component.loadable?
594
595
  load_local_component(component)
595
596
  elsif manual_registrar.file_exists?(component)
596
597
  manual_registrar.(component)
@@ -614,25 +615,21 @@ module Dry
614
615
 
615
616
  container = importer[import_namespace]
616
617
 
617
- container.load_component(identifier.dequalified(import_namespace).key)
618
+ container.load_component(identifier.namespaced(from: import_namespace, to: nil).key)
618
619
 
619
620
  importer.(import_namespace, container)
620
621
  end
621
622
 
622
- def component(identifier)
623
- if (bootable_component = booter.find_component(identifier))
624
- return bootable_component
625
- end
626
-
623
+ def find_component(key)
627
624
  # 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.
625
+ # If no matching component is found, return a null component; this fallback is
626
+ # important because the component may still be loadable via the manual registrar
627
+ # or an imported container.
631
628
  component_dirs.detect { |dir|
632
- if (component = dir.component_for_identifier(identifier))
629
+ if (component = dir.component_for_key(key))
633
630
  break component
634
631
  end
635
- } || Component.new(identifier)
632
+ } || IndirectComponent.new(Identifier.new(key, separator: config.namespace_separator))
636
633
  end
637
634
  end
638
635
 
@@ -11,13 +11,13 @@ module Dry
11
11
  end
12
12
  end
13
13
 
14
- # Error raised when the container tries to load a component with missing
15
- # file
16
- #
17
- # @api public
18
- FileNotFoundError = Class.new(StandardError) do
19
- def initialize(component)
20
- super("could not resolve require file for component '#{component.identifier}'")
14
+ # Error raised when a namespace for a component dir is added to configuration more
15
+ # than once
16
+ NamespaceAlreadyAddedError = Class.new(StandardError) do
17
+ def initialize(path)
18
+ path_label = path ? "path #{path.inspect}" : "root path"
19
+
20
+ super("Namespace for #{path_label} already added")
21
21
  end
22
22
  end
23
23
 
@@ -27,7 +27,7 @@ module Dry
27
27
  ComponentFileMismatchError = Class.new(StandardError) do
28
28
  def initialize(component)
29
29
  super(<<-STR)
30
- Bootable component '#{component.identifier}' not found
30
+ Bootable component '#{component.name}' not found
31
31
  STR
32
32
  end
33
33
  end
@@ -43,26 +43,17 @@ module Dry
43
43
  end
44
44
  end
45
45
 
46
- # Error raised when component's identifier is not valid
46
+ # Error raised when component's name is not valid
47
47
  #
48
48
  # @api public
49
- InvalidComponentIdentifierError = Class.new(ArgumentError) do
49
+ InvalidComponentNameError = Class.new(ArgumentError) do
50
50
  def initialize(name)
51
51
  super(
52
- "component identifier +#{name}+ is invalid or boot file is missing"
52
+ "component +#{name}+ is invalid or boot file is missing"
53
53
  )
54
54
  end
55
55
  end
56
56
 
57
- # Error raised when component's identifier for booting is not a symbol
58
- #
59
- # @api public
60
- InvalidComponentIdentifierTypeError = Class.new(ArgumentError) do
61
- def initialize(name)
62
- super("component identifier #{name.inspect} must be a symbol")
63
- end
64
- end
65
-
66
57
  # Error raised when trying to stop a component that hasn't started yet
67
58
  #
68
59
  # @api public
@@ -13,42 +13,29 @@ module Dry
13
13
  #
14
14
  # @api public
15
15
  class Identifier
16
- include Dry::Equalizer(:identifier, :namespace, :separator)
16
+ include Dry::Equalizer(:key, :separator)
17
17
 
18
- # @return [String] the identifier string
18
+ # @return [String] the identifier's string key
19
19
  # @api public
20
- attr_reader :identifier
21
-
22
- # @return [String, nil] the namespace for the component
23
- # @api public
24
- attr_reader :namespace
20
+ attr_reader :key
25
21
 
26
22
  # @return [String] the configured namespace separator
27
23
  # @api public
28
24
  attr_reader :separator
29
25
 
30
26
  # @api private
31
- def initialize(identifier, namespace: nil, separator: DEFAULT_SEPARATOR)
32
- @identifier = identifier.to_s
33
- @namespace = namespace
27
+ def initialize(key, separator: DEFAULT_SEPARATOR)
28
+ @key = key.to_s
34
29
  @separator = separator
35
30
  end
36
31
 
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
32
  # @!method to_s
46
- # Returns the identifier string
33
+ # Returns the identifier string key
47
34
  #
48
35
  # @return [String]
49
- # @see #identifier
36
+ # @see #key
50
37
  # @api public
51
- alias_method :to_s, :identifier
38
+ alias_method :to_s, :key
52
39
 
53
40
  # Returns the root namespace segment of the identifier string, as a symbol
54
41
  #
@@ -62,31 +49,11 @@ module Dry
62
49
  segments.first.to_sym
63
50
  end
64
51
 
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"
52
+ # Returns true if the given leading namespaces are a leading part of the
53
+ # identifier's key
72
54
  #
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
55
+ # Also returns true if nil is given (technically, from nothing everything is
56
+ # wrought)
90
57
  #
91
58
  # @example
92
59
  # identifier.key # => "articles.operations.create"
@@ -94,63 +61,74 @@ module Dry
94
61
  # identifier.start_with?("articles.operations") # => true
95
62
  # identifier.start_with?("articles") # => true
96
63
  # identifier.start_with?("article") # => false
64
+ # identifier.start_with?(nil) # => true
97
65
  #
98
66
  # @param leading_namespaces [String] the one or more leading namespaces to check
99
67
  # @return [Boolean]
100
68
  # @api public
101
69
  def start_with?(leading_namespaces)
102
- identifier.start_with?("#{leading_namespaces}#{separator}")
70
+ leading_namespaces.nil? ||
71
+ key.start_with?("#{leading_namespaces}#{separator}") ||
72
+ key.eql?(leading_namespaces)
103
73
  end
104
74
 
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
75
+ # Returns the key with its segments separated by the given separator
113
76
  #
114
- # @return [Dry::System::Identifier] the copy of the identifier
77
+ # @example
78
+ # identifier.key # => "articles.operations.create"
79
+ # identifier.key_with_separator("/") # => "articles/operations/create"
115
80
  #
116
- # @see #initialize
81
+ # @return [String] the key using the separator
117
82
  # @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
- )
83
+ def key_with_separator(separator)
84
+ segments.join(separator)
132
85
  end
133
86
 
134
- # Returns a copy of the identifier with the given options applied
87
+ # Returns a copy of the identifier with the key's leading namespace(s) replaced
88
+ #
89
+ # @example Changing a namespace
90
+ # identifier.key # => "articles.operations.create"
91
+ # identifier.namespaced(from: "articles", to: "posts").key # => "posts.commands.create"
92
+ #
93
+ # @example Removing a namespace
94
+ # identifier.key # => "articles.operations.create"
95
+ # identifier.namespaced(from: "articles", to: nil).key # => "operations.create"
135
96
  #
136
- # @param namespace [String, nil] a new namespace to be used
97
+ # @example Adding a namespace
98
+ # identifier.key # => "articles.operations.create"
99
+ # identifier.namespaced(from: nil, to: "admin").key # => "admin.articles.operations.create"
100
+ #
101
+ # @param from [String, nil] the leading namespace(s) to replace
102
+ # @param to [String, nil] the replacement for the leading namespace
137
103
  #
138
104
  # @return [Dry::System::Identifier] the copy of the identifier
139
105
  #
140
106
  # @see #initialize
141
107
  # @api private
142
- def with(namespace:)
143
- self.class.new(
144
- identifier,
145
- namespace: namespace,
146
- separator: separator
147
- )
108
+ def namespaced(from:, to:)
109
+ return self if from == to
110
+
111
+ separated_to = "#{to}#{separator}" if to
112
+
113
+ new_key =
114
+ if from.nil?
115
+ "#{separated_to}#{key}"
116
+ else
117
+ key.sub(
118
+ /^#{Regexp.escape(from.to_s)}#{Regexp.escape(separator)}/,
119
+ separated_to || EMPTY_STRING
120
+ )
121
+ end
122
+
123
+ return self if new_key == key
124
+
125
+ self.class.new(new_key, separator: separator)
148
126
  end
149
127
 
150
128
  private
151
129
 
152
130
  def segments
153
- @segments ||= identifier.split(separator)
131
+ @segments ||= key.split(separator)
154
132
  end
155
133
  end
156
134
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Dry
6
+ module System
7
+ # An indirect component is a component that cannot be directly from a source file
8
+ # directly managed by the container. It may be component that needs to be loaded
9
+ # indirectly, either via a manual registration file or an imported container
10
+ #
11
+ # Indirect components are an internal abstraction and, unlike ordinary components, are
12
+ # not exposed to users via component dir configuration hooks.
13
+ #
14
+ # @see Container#load_component
15
+ # @see Container#find_component
16
+ #
17
+ # @api private
18
+ class IndirectComponent
19
+ include Dry::Equalizer(:identifier)
20
+
21
+ # @!attribute [r] identifier
22
+ # @return [String] the component's unique identifier
23
+ attr_reader :identifier
24
+
25
+ # @api private
26
+ def initialize(identifier)
27
+ @identifier = identifier
28
+ end
29
+
30
+ # Returns false, indicating that the component is not directly loadable from the
31
+ # files managed by the container
32
+ #
33
+ # This is the inverse of {Component#loadable?}
34
+ #
35
+ # @return [FalseClass]
36
+ #
37
+ # @api private
38
+ def loadable?
39
+ false
40
+ end
41
+
42
+ # Returns the component's unique key
43
+ #
44
+ # @return [String] the key
45
+ #
46
+ # @see Identifier#key
47
+ #
48
+ # @api private
49
+ def key
50
+ identifier.to_s
51
+ end
52
+
53
+ # Returns the root namespace segment of the component's key, as a symbol
54
+ #
55
+ # @see Identifier#root_key
56
+ #
57
+ # @return [Symbol] the root key
58
+ #
59
+ # @api private
60
+ def root_key
61
+ identifier.root_key
62
+ end
63
+ end
64
+ end
65
+ end
@@ -17,7 +17,7 @@ module Dry
17
17
  # class MyApp < Dry::System::Container
18
18
  # configure do |config|
19
19
  # # ...
20
- # config.loader MyLoader
20
+ # config.component_dirs.loader = MyLoader
21
21
  # end
22
22
  # end
23
23
  #
@@ -28,7 +28,7 @@ module Dry
28
28
  #
29
29
  # @api public
30
30
  def require!(component)
31
- require(component.path) if component.file_exists?
31
+ require(component.require_path)
32
32
  self
33
33
  end
34
34
 
@@ -61,8 +61,7 @@ module Dry
61
61
  # @api public
62
62
  def constant(component)
63
63
  inflector = component.inflector
64
-
65
- inflector.constantize(inflector.camelize(component.path))
64
+ inflector.constantize(inflector.camelize(component.const_path))
66
65
  end
67
66
 
68
67
  private
@@ -30,16 +30,12 @@ module Dry
30
30
  end
31
31
 
32
32
  # @api private
33
- def call(name)
34
- name = name.respond_to?(:root_key) ? name.root_key.to_s : name
35
-
36
- require(root.join(config.registrations_dir, name))
33
+ def call(component)
34
+ require(root.join(config.registrations_dir, component.root_key.to_s))
37
35
  end
38
36
 
39
- def file_exists?(name)
40
- name = name.respond_to?(:root_key) ? name.root_key.to_s : name
41
-
42
- File.exist?(File.join(registrations_dir, "#{name}#{RB_EXT}"))
37
+ def file_exists?(component)
38
+ File.exist?(File.join(registrations_dir, "#{component.root_key}#{RB_EXT}"))
43
39
  end
44
40
 
45
41
  private
@@ -16,7 +16,7 @@ module Dry
16
16
  def self.extended(system)
17
17
  super
18
18
  system.use(:env)
19
- system.before(:configure) { setting :bootsnap, DEFAULT_OPTIONS }
19
+ system.before(:configure) { setting :bootsnap, default: DEFAULT_OPTIONS }
20
20
  system.after(:configure, &:setup_bootsnap)
21
21
  end
22
22
 
@@ -15,7 +15,7 @@ module Dry
15
15
  system.use(:notifications)
16
16
 
17
17
  system.before(:configure) do
18
- setting :ignored_dependencies, []
18
+ setting :ignored_dependencies, default: []
19
19
  end
20
20
 
21
21
  system.after(:configure) do
@@ -34,10 +34,10 @@ module Dry
34
34
  # @api private
35
35
  def register(key, contents = nil, options = {}, &block)
36
36
  super
37
-
38
- unless config.ignored_dependencies.include?(key.to_sym)
37
+ dependency_key = key.to_s
38
+ unless config.ignored_dependencies.include?(dependency_key)
39
39
  self[:notifications].instrument(
40
- :registered_dependency, key: key, class: self[key].class
40
+ :registered_dependency, key: dependency_key, class: self[dependency_key].class
41
41
  )
42
42
  end
43
43
 
@@ -20,7 +20,7 @@ module Dry
20
20
 
21
21
  # @api private
22
22
  def extended(system)
23
- system.setting :env, inferrer.(), reader: true
23
+ system.setting :env, default: inferrer.(), reader: true
24
24
  super
25
25
  end
26
26
  end
@@ -11,14 +11,15 @@ module Dry
11
11
  system.before(:configure) do
12
12
  setting :logger, reader: true
13
13
 
14
- setting :log_dir, "log"
14
+ setting :log_dir, default: "log"
15
15
 
16
- setting :log_levels,
17
- development: Logger::DEBUG,
18
- test: Logger::DEBUG,
19
- production: Logger::ERROR
16
+ setting :log_levels, default: {
17
+ development: Logger::DEBUG,
18
+ test: Logger::DEBUG,
19
+ production: Logger::ERROR
20
+ }
20
21
 
21
- setting :logger_class, ::Logger, reader: true
22
+ setting :logger_class, default: ::Logger, reader: true
22
23
  end
23
24
 
24
25
  system.after(:configure, &:register_logger)
@@ -84,9 +84,7 @@ module Dry
84
84
  # Enables a plugin if not already enabled.
85
85
  # Raises error if plugin cannot be found in the plugin registry.
86
86
  #
87
- # Plugin identifier
88
- #
89
- # @param [Symbol] name The plugin identifier
87
+ # @param [Symbol] name The plugin name
90
88
  # @param [Hash] options Plugin options
91
89
  #
92
90
  # @return [self]
@@ -7,14 +7,14 @@ require "dry/system/components/bootable"
7
7
  module Dry
8
8
  module System
9
9
  class Provider
10
- attr_reader :identifier
10
+ attr_reader :name
11
11
 
12
12
  attr_reader :options
13
13
 
14
14
  attr_reader :components
15
15
 
16
- def initialize(identifier, options)
17
- @identifier = identifier
16
+ def initialize(name, options)
17
+ @name = name
18
18
  @options = options
19
19
  @components = Concurrent::Map.new
20
20
  end
@@ -35,9 +35,9 @@ module Dry
35
35
  boot_files.detect { |path| Pathname(path).basename(RB_EXT).to_s == name.to_s }
36
36
  end
37
37
 
38
- def component(name, options = {})
39
- identifier = options[:key] || name
40
- components.fetch(identifier).new(name, options)
38
+ def component(component_name, options = {})
39
+ component_key = options[:key] || component_name
40
+ components.fetch(component_key).new(component_name, options)
41
41
  end
42
42
 
43
43
  def load_components
@@ -15,12 +15,12 @@ module Dry
15
15
  items.each(&block)
16
16
  end
17
17
 
18
- def register(identifier, options)
19
- items << Provider.new(identifier, options)
18
+ def register(name, options)
19
+ items << Provider.new(name, options)
20
20
  end
21
21
 
22
- def [](identifier)
23
- detect { |provider| provider.identifier == identifier }
22
+ def [](name)
23
+ detect { |provider| provider.name == name }
24
24
  end
25
25
  end
26
26
  end