dry-system 0.18.2 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de2d1a6333450270f4b10f90b2dfc7c3436b9885504544224970e3075bae0a90
4
- data.tar.gz: a7506b0e02c0b3511b921d3390562d1ad59f67ffb8970c9573b5fd5f5437bbc8
3
+ metadata.gz: 2c09b2200301d76a2ac09297583d2a58711db80b43fbe191086bd859f1d3b4fe
4
+ data.tar.gz: 7f08ad0fb730b2a7281737f799722c30dc649926f36e18e2cd07144637edbb20
5
5
  SHA512:
6
- metadata.gz: ab86a3f981d2bf26468a950f31d90993f096e2c8523fd275762f42ab5728e74e2fbc038b30566eecfaf41f15792bc34fedc2bd8733ffc6c03bb7ede4715b0cc0
7
- data.tar.gz: 4969d94cc2e88c17646f96904e309892c02bbfbc311b08523ca01ff2bd3b08c3c132bf4f0eb641f7a6b6043f424fda0ffdb99ea3ef36ca899ce7a4790b258a43
6
+ metadata.gz: 646a9ef47bce754608ae670ebb43aaad911e2c8b67d79a10f7c0f284cd379c4ce4992263fb22f48f42768e180cfdb6e9f1a3df8bb590245a9a29579858862000
7
+ data.tar.gz: 293fddcf0085814fc9628ee77ad6360d04ae076fb8b623117a1a336aceb39be1d5c1a8a44c92f10b7e48ca53e4939284ae91b8e6ed2a3514039f65c15d7d5d54
data/CHANGELOG.md CHANGED
@@ -1,11 +1,95 @@
1
- ## 0.18.2 2021-08-30
1
+ <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
+ ## 0.19.0 2021-04-22
3
4
 
4
- ### Changed
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._
5
43
 
6
- - [internal] Improved compatibility with upcoming dry-configurable 0.13.0 release (@timriley in #187)
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
7
80
 
8
- [Compare v0.18.1...v0.18.2](https://github.com/dry-rb/dry-system/compare/v0.18.1...v0.18.2)
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)
9
93
 
10
94
  ## 0.18.1 2020-08-26
11
95
 
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