dry-system 0.18.1 → 0.19.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7634be14c7b544e14f693cd158cd4cdf072e89ea5c947fafec45a0b31e58eb18
4
- data.tar.gz: 34a55fe75bbae237be83dbcf534c0c13947a66788b146ee2ec12a387d3976e38
3
+ metadata.gz: 2c09b2200301d76a2ac09297583d2a58711db80b43fbe191086bd859f1d3b4fe
4
+ data.tar.gz: 7f08ad0fb730b2a7281737f799722c30dc649926f36e18e2cd07144637edbb20
5
5
  SHA512:
6
- metadata.gz: 1802eb1c676eb114126b37fd1ac9607a446317e209de17c62018f6a61bd9dd6770093e673671685c6a8d665ec53c7d7897141c3a7896c4d64ee3f5161c47bc30
7
- data.tar.gz: '08fd659802b48151cf10598eb45fcb6555b8f9fa8b55340e6e9f393469608b965ecdefae143e79a880c394bcbede49b719f71f9bf6b96726af73921020121a35'
6
+ metadata.gz: 646a9ef47bce754608ae670ebb43aaad911e2c8b67d79a10f7c0f284cd379c4ce4992263fb22f48f42768e180cfdb6e9f1a3df8bb590245a9a29579858862000
7
+ data.tar.gz: 293fddcf0085814fc9628ee77ad6360d04ae076fb8b623117a1a336aceb39be1d5c1a8a44c92f10b7e48ca53e4939284ae91b8e6ed2a3514039f65c15d7d5d54
data/CHANGELOG.md CHANGED
@@ -1,3 +1,96 @@
1
+ <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
+
3
+ ## 0.19.0 2021-04-22
4
+
5
+ This release marks a huge step forward for dry-system, bringing support for Zeitwerk and other autoloaders, plus clearer configuration and improved consistency around component resolution for both finalized and lazy loading containers. [Read the announcement post](https://dry-rb.org/news/2021/04/22/dry-system-0-19-released-with-zeitwerk-support-and-more-leading-the-way-for-hanami-2-0/) for a high-level tour of the new features.
6
+
7
+ ### Added
8
+
9
+ - New `component_dirs` setting on `Dry::System::Container`, which must be used for specifying the directories which dry-system will search for component source files.
10
+
11
+ Each added component dir is relative to the container's `root`, and can have its own set of settings configured:
12
+
13
+ ```ruby
14
+ class MyApp::Container < Dry::System::Container
15
+ configure do |config|
16
+ config.root = __dir__
17
+
18
+ # Defaults for all component dirs can be configured separately
19
+ config.component_dirs.auto_register = true # default is already true
20
+
21
+ # Component dirs can be added and configured independently
22
+ config.component_dirs.add "lib" do |dir|
23
+ dir.add_to_load_path = true # defaults to true
24
+ dir.default_namespace = "my_app"
25
+ end
26
+
27
+ # All component dir settings are optional. Component dirs relying on default
28
+ # settings can be added like so:
29
+ config.component_dirs.add "custom_components"
30
+ end
31
+ end
32
+ ```
33
+
34
+ The following settings are available for configuring added `component_dirs`:
35
+
36
+ - `auto_register`, a boolean, or a proc accepting a `Dry::System::Component` instance and returning a truthy or falsey value. Providing a proc allows an auto-registration policy to apply on a per-component basis
37
+ - `add_to_load_path`, a boolean
38
+ - `default_namespace`, a string representing the leading namespace segments to be stripped from the component's identifier (given the identifier is derived from the component's fully qualified class name)
39
+ - `loader`, a custom replacement for the default `Dry::System::Loader` to be used for the component dir
40
+ - `memoize`, a boolean, to enable/disable memoizing all components in the directory, or a proc accepting a `Dry::System::Component` instance and returning a truthy or falsey value. Providing a proc allows a memoization policy to apply on a per-component basis
41
+
42
+ _All component dir settings are optional._
43
+
44
+ (@timriley in #155, #157, and #162)
45
+ - A new autoloading-friendly `Dry::System::Loader::Autoloading` is available, which is tested to work with [Zeitwerk](https://github.com/fxn/zeitwerk) 🎉
46
+
47
+ Configure this on the container (via a component dir `loader` setting), and the loader will no longer `require` any components, instead allowing missing constant resolution to trigger the loading of the required file.
48
+
49
+ This loader presumes an autoloading system like Zeitwerk has already been enabled and appropriately configured.
50
+
51
+ A recommended setup is as follows:
52
+
53
+ ```ruby
54
+ require "dry/system/container"
55
+ require "dry/system/loader/autoloading"
56
+ require "zeitwerk"
57
+
58
+ class MyApp::Container < Dry::System::Container
59
+ configure do |config|
60
+ config.root = __dir__
61
+
62
+ config.component_dirs.loader = Dry::System::Loader::Autoloading
63
+ config.component_dirs.add_to_load_path = false
64
+
65
+ config.component_dirs.add "lib" do |dir|
66
+ # ...
67
+ end
68
+ end
69
+ end
70
+
71
+ loader = Zeitwerk::Loader.new
72
+ loader.push_dir MyApp::Container.config.root.join("lib").realpath
73
+ loader.setup
74
+ ```
75
+
76
+ (@timriley in #153)
77
+ - [BREAKING] `Dry::System::Component` instances (which users of dry-system will interact with via custom loaders, as well as via the `auto_register` and `memoize` component dir settings described above) now return a `Dry::System::Identifier` from their `#identifier` method. The raw identifier string may be accessed via the identifier's own `#key` or `#to_s` methods. `Identifier` also provides a helpful namespace-aware `#start_with?` method for returning whether the identifier begins with the provided namespace(s) (@timriley in #158)
78
+
79
+ ### Changed
80
+
81
+ - Components with `# auto_register: false` magic comments in their source files are now properly ignored when lazy loading (@timriley in #155)
82
+ - `# memoize: true` and `# memoize: false` magic comments at top of component files are now respected (@timriley in #155)
83
+ - [BREAKING] `Dry::System::Container.load_paths!` has been renamed to `.add_to_load_path!`. This method now exists as a mere convenience only. Calling this method is no longer required for any configured `component_dirs`; these are now added to the load path automatically (@timriley in #153 and #155)
84
+ - [BREAKING] `auto_register` container setting has been removed. Configured directories to be auto-registered by adding `component_dirs` instead (@timriley in #155)
85
+ - [BREAKING] `default_namespace` container setting has been removed. Set it when adding `component_dirs` instead (@timriley in #155)
86
+ - [BREAKING] `loader` container setting has been nested under `component_dirs`, now available as `component_dirs.loader` to configure a default loader for all component dirs, as well as on individual component dirs when being added (@timriley in #162)
87
+ - [BREAKING] `Dry::System::ComponentLoadError` is no longer raised when a component could not be lazy loaded; this was only raised in a single specific failure condition. Instead, a `Dry::Container::Error` is raised in all cases of components failing to load (@timriley in #155)
88
+ - [BREAKING] `Dry::System::Container.auto_register!` has been removed. Configure `component_dirs` instead. (@timriley in #157)
89
+ - [BREAKING] The `Dry::System::Loader` interface has changed. It is now a static interface, no longer initialized with a component. The component is instead passed to each method as an argument: `.require!(component)`, `.call(component, *args)`, `.constant(component)` (@timriley in #157)
90
+ - [BREAKING] `Dry::System::Container.require_path` has been removed. Provide custom require behavior by configuring your own `loader` (@timriley in #153)
91
+
92
+ [Compare v0.18.1...v0.19.0](https://github.com/dry-rb/dry-system/compare/v0.18.1...v0.19.0)
93
+
1
94
  ## 0.18.1 2020-08-26
2
95
 
3
96
 
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2020 dry-rb team
3
+ Copyright (c) 2015-2021 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  This library officially supports the following Ruby versions:
23
23
 
24
- * MRI >= `2.4`
24
+ * MRI >= `2.5`
25
25
  * jruby >= `9.2`
26
26
 
27
27
  ## License
data/dry-system.gemspec CHANGED
@@ -25,15 +25,14 @@ Gem::Specification.new do |spec|
25
25
  spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-system'
26
26
  spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-system/issues'
27
27
 
28
- spec.required_ruby_version = ">= 2.4.0"
28
+ spec.required_ruby_version = ">= 2.5.0"
29
29
 
30
30
  # to update dependencies edit project.yml
31
31
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
32
32
  spec.add_runtime_dependency "dry-auto_inject", ">= 0.4.0"
33
- spec.add_runtime_dependency "dry-configurable", "~> 0.11", ">= 0.11.1"
33
+ spec.add_runtime_dependency "dry-configurable", "~> 0.12", ">= 0.12.1"
34
34
  spec.add_runtime_dependency "dry-container", "~> 0.7", ">= 0.7.2"
35
- spec.add_runtime_dependency "dry-core", "~> 0.3", ">= 0.3.1"
36
- spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
35
+ spec.add_runtime_dependency "dry-core", "~> 0.5", ">= 0.5"
37
36
  spec.add_runtime_dependency "dry-inflector", "~> 0.1", ">= 0.1.2"
38
37
  spec.add_runtime_dependency "dry-struct", "~> 1.0"
39
38
 
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/constants"
4
- require "dry/system/magic_comments_parser"
5
- require "dry/system/auto_registrar/configuration"
4
+ require_relative "component"
6
5
 
7
6
  module Dry
8
7
  module System
@@ -17,83 +16,42 @@ module Dry
17
16
  class AutoRegistrar
18
17
  attr_reader :container
19
18
 
20
- attr_reader :config
21
-
22
19
  def initialize(container)
23
20
  @container = container
24
- @config = container.config
25
21
  end
26
22
 
27
23
  # @api private
28
24
  def finalize!
29
- Array(config.auto_register).each { |dir| call(dir) }
25
+ container.component_dirs.each do |component_dir|
26
+ call(component_dir) if component_dir.auto_register?
27
+ end
30
28
  end
31
29
 
32
30
  # @api private
33
- def call(dir)
34
- registration_config = Configuration.new
35
- yield(registration_config) if block_given?
36
- components(dir).each do |component|
37
- next if !component.auto_register? || registration_config.exclude.(component)
31
+ def call(component_dir)
32
+ components(component_dir).each do |component|
33
+ next unless register_component?(component)
38
34
 
39
- container.require_component(component) do
40
- register(component.identifier, memoize: registration_config.memoize) {
41
- registration_config.instance.(component)
42
- }
43
- end
35
+ container.register(component.identifier, memoize: component.memoize?) { component.instance }
44
36
  end
45
37
  end
46
38
 
47
39
  private
48
40
 
49
- # @api private
50
- def components(dir)
51
- files(dir)
52
- .map { |file_name| [file_name, file_options(file_name)] }
53
- .map { |file_name, options| component(relative_path(dir, file_name), **options) }
54
- .reject { |component| registered?(component.identifier) }
41
+ def components(component_dir)
42
+ files(component_dir.full_path).map { |file_path|
43
+ component_dir.component_for_path(file_path)
44
+ }
55
45
  end
56
46
 
57
- # @api private
58
47
  def files(dir)
59
- components_dir = File.join(root, dir)
60
-
61
- unless ::Dir.exist?(components_dir)
62
- raise ComponentsDirMissing, "Components dir '#{components_dir}' not found"
63
- end
64
-
65
- ::Dir["#{components_dir}/**/#{RB_GLOB}"].sort
66
- end
67
-
68
- # @api private
69
- def relative_path(dir, file_path)
70
- dir_root = root.join(dir.to_s.split("/")[0])
71
- file_path.to_s.sub("#{dir_root}/", "").sub(RB_EXT, EMPTY_STRING)
72
- end
48
+ raise ComponentDirNotFoundError, dir unless Dir.exist?(dir)
73
49
 
74
- # @api private
75
- def file_options(file_name)
76
- MagicCommentsParser.(file_name)
77
- end
78
-
79
- # @api private
80
- def component(path, **options)
81
- container.component(path, **options)
50
+ Dir["#{dir}/**/#{RB_GLOB}"].sort
82
51
  end
83
52
 
84
- # @api private
85
- def root
86
- container.root
87
- end
88
-
89
- # @api private
90
- def registered?(name)
91
- container.registered?(name)
92
- end
93
-
94
- # @api private
95
- def register(*args, &block)
96
- container.register(*args, &block)
53
+ def register_component?(component)
54
+ !container.registered?(component) && component.auto_register?
97
55
  end
98
56
  end
99
57
  end
@@ -30,17 +30,28 @@ module Dry
30
30
  @components = ComponentRegistry.new
31
31
  end
32
32
 
33
- # @api private
34
- def bootable?(component)
35
- !boot_file(component).nil?
36
- end
37
-
38
33
  # @api private
39
34
  def register_component(component)
40
35
  components.register(component)
41
36
  self
42
37
  end
43
38
 
39
+ # Returns a bootable component if it can be found or loaded, otherwise nil
40
+ #
41
+ # @return [Dry::System::Components::Bootable, nil]
42
+ # @api private
43
+ def find_component(name)
44
+ name = name.to_sym
45
+
46
+ return components[name] if components.exists?(name)
47
+
48
+ return if finalized?
49
+
50
+ require_boot_file(name)
51
+
52
+ components[name] if components.exists?(name)
53
+ end
54
+
44
55
  # @api private
45
56
  def finalize!
46
57
  boot_files.each do |path|
@@ -54,6 +65,13 @@ module Dry
54
65
  freeze
55
66
  end
56
67
 
68
+ # @!method finalized?
69
+ # Returns true if the booter has been finalized
70
+ #
71
+ # @return [Boolean]
72
+ # @api private
73
+ alias_method :finalized?, :frozen?
74
+
57
75
  # @api private
58
76
  def shutdown
59
77
  components.each do |component|
@@ -115,12 +133,19 @@ module Dry
115
133
 
116
134
  # @api private
117
135
  def boot_dependency(component)
118
- boot_file = boot_file(component)
119
-
120
- start(boot_file.basename(".*").to_s.to_sym) if boot_file
136
+ if (component = find_component(component.root_key))
137
+ start(component)
138
+ end
121
139
  end
122
140
 
123
- # @api private
141
+ # Returns all boot files within the configured paths
142
+ #
143
+ # Searches for files in the order of the configured paths. In the case of multiple
144
+ # identically-named boot files within different paths, the file found first will be
145
+ # returned, and other matching files will be discarded.
146
+ #
147
+ # @return [Array<Pathname>]
148
+ # @api public
124
149
  def boot_files
125
150
  @boot_files ||= paths.each_with_object([[], []]) { |path, (boot_files, loaded)|
126
151
  files = Dir["#{path}/#{RB_GLOB}"].sort
@@ -161,12 +186,6 @@ module Dry
161
186
  self
162
187
  end
163
188
 
164
- def boot_file(name)
165
- name = name.respond_to?(:root_key) ? name.root_key.to_s : name
166
-
167
- find_boot_file(name)
168
- end
169
-
170
189
  def require_boot_file(identifier)
171
190
  boot_file = find_boot_file(identifier)
172
191
 
@@ -2,11 +2,12 @@
2
2
 
3
3
  require "concurrent/map"
4
4
 
5
- require "dry-equalizer"
5
+ require "dry/core/equalizer"
6
6
  require "dry/inflector"
7
7
  require "dry/system/loader"
8
8
  require "dry/system/errors"
9
9
  require "dry/system/constants"
10
+ require_relative "identifier"
10
11
 
11
12
  module Dry
12
13
  module System
@@ -14,106 +15,62 @@ module Dry
14
15
  # They expose an API to query this information and use a configurable
15
16
  # loader object to initialize class instances.
16
17
  #
17
- # Components are created automatically through auto-registration and can be
18
- # accessed through `Container.auto_register!` which yields them.
19
- #
20
18
  # @api public
21
19
  class Component
22
- include Dry::Equalizer(:identifier, :path)
20
+ include Dry::Equalizer(:identifier, :file_path, :options)
23
21
 
24
22
  DEFAULT_OPTIONS = {
25
23
  separator: DEFAULT_SEPARATOR,
26
- namespace: nil,
27
- inflector: Dry::Inflector.new
24
+ inflector: Dry::Inflector.new,
25
+ loader: Loader
28
26
  }.freeze
29
27
 
30
28
  # @!attribute [r] identifier
31
29
  # @return [String] component's unique identifier
32
30
  attr_reader :identifier
33
31
 
34
- # @!attribute [r] path
35
- # @return [String] component's relative path
36
- attr_reader :path
37
-
38
- # @!attribute [r] file
39
- # @return [String] component's file name
40
- attr_reader :file
32
+ # @!attribute [r] file_path
33
+ # @return [String, nil] full path to the component's file, if found
34
+ attr_reader :file_path
41
35
 
42
36
  # @!attribute [r] options
43
37
  # @return [Hash] component's options
44
38
  attr_reader :options
45
39
 
46
- # @!attribute [r] loader
47
- # @return [Object#call] component's loader object
48
- attr_reader :loader
49
-
50
40
  # @api private
51
- def self.new(*args, &block)
52
- cache.fetch_or_store([*args, block].hash) do
53
- name, options = args
54
- options = DEFAULT_OPTIONS.merge(options || EMPTY_HASH)
55
-
56
- ns, sep, inflector = options.values_at(:namespace, :separator, :inflector)
57
- identifier = extract_identifier(name, ns, sep)
58
-
59
- path = name.to_s.gsub(sep, PATH_SEPARATOR)
60
- loader = options.fetch(:loader, Loader).new(path, inflector)
61
-
62
- super(identifier, path, options.merge(loader: loader))
63
- end
64
- end
65
-
66
- # @api private
67
- def self.extract_identifier(name, ns, sep)
68
- name_s = name.to_s
69
- identifier = ns ? remove_namespace_from_name(name_s, ns) : name_s
70
-
71
- identifier.scan(WORD_REGEX).join(sep)
72
- end
73
-
74
- # @api private
75
- def self.remove_namespace_from_name(name, ns)
76
- match_value = name.match(/^(?<remove_namespace>#{ns})(?<separator>\W)(?<identifier>.*)/)
77
-
78
- match_value ? match_value[:identifier] : name
41
+ def self.new(identifier, options = EMPTY_HASH)
42
+ options = DEFAULT_OPTIONS.merge(options)
43
+
44
+ namespace = options.delete(:namespace)
45
+ separator = options.delete(:separator)
46
+
47
+ identifier =
48
+ if identifier.is_a?(Identifier)
49
+ identifier
50
+ else
51
+ Identifier.new(
52
+ identifier,
53
+ namespace: namespace,
54
+ separator: separator
55
+ )
56
+ end
57
+
58
+ super(identifier, **options)
79
59
  end
80
60
 
81
61
  # @api private
82
- def self.cache
83
- @cache ||= Concurrent::Map.new
84
- end
85
-
86
- # @api private
87
- def initialize(identifier, path, options)
62
+ def initialize(identifier, file_path: nil, **options)
88
63
  @identifier = identifier
89
- @path = path
64
+ @file_path = file_path
90
65
  @options = options
91
- @file = "#{path}#{RB_EXT}"
92
- @loader = options.fetch(:loader)
93
- freeze
94
66
  end
95
67
 
96
- # Returns components instance
97
- #
98
- # @example
99
- # class MyApp < Dry::System::Container
100
- # configure do |config|
101
- # config.name = :my_app
102
- # config.root = Pathname('/my/app')
103
- # end
104
- #
105
- # auto_register!('lib/clients') do |component|
106
- # # some custom initialization logic, ie:
107
- # constant = component.loader.constant
108
- # constant.create
109
- # end
110
- # end
68
+ # Returns the component's instance
111
69
  #
112
70
  # @return [Object] component's class instance
113
- #
114
71
  # @api public
115
72
  def instance(*args)
116
- loader.call(*args)
73
+ loader.call(self, *args)
117
74
  end
118
75
  ruby2_keywords(:instance) if respond_to?(:ruby2_keywords, true)
119
76
 
@@ -122,49 +79,54 @@ module Dry
122
79
  false
123
80
  end
124
81
 
125
- # @api private
126
- def file_exists?(paths)
127
- paths.any? { |path| path.join(file).exist? }
82
+ def key
83
+ identifier.to_s
128
84
  end
129
85
 
130
- # @api private
131
- def prepend(name)
132
- self.class.new(
133
- [name, identifier].join(separator), options.merge(loader: loader.class)
134
- )
86
+ def path
87
+ identifier.path
88
+ end
89
+
90
+ def root_key
91
+ identifier.root_key
135
92
  end
136
93
 
94
+ # Returns true if the component has a corresponding file
95
+ #
96
+ # @return [Boolean]
137
97
  # @api private
138
- def namespaced(namespace)
139
- self.class.new(
140
- path, options.merge(loader: loader.class, namespace: namespace)
141
- )
98
+ def file_exists?
99
+ !!file_path
142
100
  end
143
101
 
144
102
  # @api private
145
- def separator
146
- options[:separator]
103
+ def loader
104
+ options[:loader]
147
105
  end
148
106
 
149
107
  # @api private
150
- def namespace
151
- options[:namespace]
108
+ def inflector
109
+ options[:inflector]
152
110
  end
153
111
 
154
112
  # @api private
155
113
  def auto_register?
156
- !!options.fetch(:auto_register) { true }
114
+ callable_option?(options[:auto_register])
157
115
  end
158
116
 
159
117
  # @api private
160
- def root_key
161
- namespaces.first
118
+ def memoize?
119
+ callable_option?(options[:memoize])
162
120
  end
163
121
 
164
122
  private
165
123
 
166
- def namespaces
167
- identifier.split(separator).map(&:to_sym)
124
+ def callable_option?(value)
125
+ if value.respond_to?(:call)
126
+ !!value.call(self)
127
+ else
128
+ !!value
129
+ end
168
130
  end
169
131
  end
170
132
  end