dry-system 0.15.0 → 0.19.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -2
  3. data/LICENSE +1 -1
  4. data/README.md +1 -1
  5. data/dry-system.gemspec +5 -4
  6. data/lib/dry-system.rb +1 -1
  7. data/lib/dry/system.rb +2 -2
  8. data/lib/dry/system/auto_registrar.rb +17 -59
  9. data/lib/dry/system/booter.rb +68 -41
  10. data/lib/dry/system/component.rb +62 -100
  11. data/lib/dry/system/component_dir.rb +128 -0
  12. data/lib/dry/system/components.rb +2 -2
  13. data/lib/dry/system/components/bootable.rb +6 -34
  14. data/lib/dry/system/components/config.rb +2 -2
  15. data/lib/dry/system/config/component_dir.rb +202 -0
  16. data/lib/dry/system/config/component_dirs.rb +184 -0
  17. data/lib/dry/system/constants.rb +5 -5
  18. data/lib/dry/system/container.rb +133 -184
  19. data/lib/dry/system/errors.rb +21 -16
  20. data/lib/dry/system/identifier.rb +157 -0
  21. data/lib/dry/system/lifecycle.rb +2 -2
  22. data/lib/dry/system/loader.rb +40 -41
  23. data/lib/dry/system/loader/autoloading.rb +26 -0
  24. data/lib/dry/system/magic_comments_parser.rb +2 -2
  25. data/lib/dry/system/manual_registrar.rb +1 -1
  26. data/lib/dry/system/plugins.rb +7 -7
  27. data/lib/dry/system/plugins/bootsnap.rb +3 -3
  28. data/lib/dry/system/plugins/dependency_graph.rb +3 -3
  29. data/lib/dry/system/plugins/dependency_graph/strategies.rb +1 -1
  30. data/lib/dry/system/plugins/logging.rb +5 -5
  31. data/lib/dry/system/plugins/monitoring.rb +3 -3
  32. data/lib/dry/system/plugins/monitoring/proxy.rb +3 -3
  33. data/lib/dry/system/plugins/notifications.rb +1 -1
  34. data/lib/dry/system/provider.rb +3 -3
  35. data/lib/dry/system/settings.rb +6 -6
  36. data/lib/dry/system/settings/file_loader.rb +2 -2
  37. data/lib/dry/system/settings/file_parser.rb +1 -1
  38. data/lib/dry/system/stubs.rb +1 -1
  39. data/lib/dry/system/system_components/settings.rb +1 -1
  40. data/lib/dry/system/version.rb +1 -1
  41. metadata +21 -25
  42. data/lib/dry/system/auto_registrar/configuration.rb +0 -43
@@ -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
 
@@ -17,25 +26,12 @@ module Dry
17
26
  # @api public
18
27
  ComponentFileMismatchError = Class.new(StandardError) do
19
28
  def initialize(component)
20
- path = component.boot_path
21
- files = component.container_boot_files
22
-
23
29
  super(<<-STR)
24
- Boot file for component #{component.identifier.inspect} not found.
25
- Container boot files under #{path}: #{files.inspect}")
30
+ Bootable component '#{component.identifier}' not found
26
31
  STR
27
32
  end
28
33
  end
29
34
 
30
- # Error raised when a resolved component couldn't be found
31
- #
32
- # @api public
33
- ComponentLoadError = Class.new(StandardError) do
34
- def initialize(component)
35
- super("could not load component #{component.inspect}")
36
- end
37
- end
38
-
39
35
  # Error raised when resolved component couldn't be loaded
40
36
  #
41
37
  # @api public
@@ -85,8 +81,17 @@ module Dry
85
81
  end
86
82
  end
87
83
 
88
- 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
+
89
93
  DuplicatedComponentKeyError = Class.new(ArgumentError)
94
+
90
95
  InvalidSettingsError = Class.new(ArgumentError) do
91
96
  # @api private
92
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
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
3
+ require "concurrent/map"
4
4
 
5
- require 'dry/system/settings'
5
+ require "dry/system/settings"
6
6
 
7
7
  module Dry
8
8
  module System
@@ -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
@@ -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
@@ -7,8 +7,8 @@ module Dry
7
7
  COMMENT_RE = /^#\s+(?<name>[A-Za-z]{1}[A-Za-z0-9_]+):\s+(?<value>.+?)$/.freeze
8
8
 
9
9
  COERCIONS = {
10
- 'true' => true,
11
- 'false' => false
10
+ "true" => true,
11
+ "false" => false
12
12
  }.freeze
13
13
 
14
14
  def self.call(file_name)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/system/constants'
3
+ require "dry/system/constants"
4
4
 
5
5
  module Dry
6
6
  module System
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/system/constants'
3
+ require "dry/system/constants"
4
4
 
5
5
  module Dry
6
6
  module System
@@ -116,22 +116,22 @@ module Dry
116
116
  @enabled_plugins ||= []
117
117
  end
118
118
 
119
- require 'dry/system/plugins/bootsnap'
119
+ require "dry/system/plugins/bootsnap"
120
120
  register(:bootsnap, Plugins::Bootsnap)
121
121
 
122
- require 'dry/system/plugins/logging'
122
+ require "dry/system/plugins/logging"
123
123
  register(:logging, Plugins::Logging)
124
124
 
125
- require 'dry/system/plugins/env'
125
+ require "dry/system/plugins/env"
126
126
  register(:env, Plugins::Env)
127
127
 
128
- require 'dry/system/plugins/notifications'
128
+ require "dry/system/plugins/notifications"
129
129
  register(:notifications, Plugins::Notifications)
130
130
 
131
- require 'dry/system/plugins/monitoring'
131
+ require "dry/system/plugins/monitoring"
132
132
  register(:monitoring, Plugins::Monitoring)
133
133
 
134
- require 'dry/system/plugins/dependency_graph'
134
+ require "dry/system/plugins/dependency_graph"
135
135
  register(:dependency_graph, Plugins::DependencyGraph)
136
136
  end
137
137
  end
@@ -22,7 +22,7 @@ module Dry
22
22
 
23
23
  # @api private
24
24
  def self.dependencies
25
- { bootsnap: 'bootsnap' }
25
+ {bootsnap: "bootsnap"}
26
26
  end
27
27
 
28
28
  # Set up bootsnap for faster booting
@@ -31,12 +31,12 @@ module Dry
31
31
  def setup_bootsnap
32
32
  return unless bootsnap_available?
33
33
 
34
- ::Bootsnap.setup(config.bootsnap.merge(cache_dir: root.join('tmp/cache').to_s))
34
+ ::Bootsnap.setup(config.bootsnap.merge(cache_dir: root.join("tmp/cache").to_s))
35
35
  end
36
36
 
37
37
  # @api private
38
38
  def bootsnap_available?
39
- RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '2.3.0' && RUBY_VERSION < '2.5.0'
39
+ RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.3.0" && RUBY_VERSION < "2.5.0"
40
40
  end
41
41
  end
42
42
  end