dry-system 0.15.0 → 0.19.1

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