dry-system 0.19.0 → 0.21.0

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