hanami 2.0.0.beta1.1 → 2.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/hanami.gemspec +1 -2
  4. data/lib/hanami/app.rb +76 -16
  5. data/lib/hanami/assets/{application_configuration.rb → app_configuration.rb} +1 -1
  6. data/lib/hanami/configuration.rb +20 -20
  7. data/lib/hanami/extensions/action/slice_configured_action.rb +44 -1
  8. data/lib/hanami/extensions/view/slice_configured_view.rb +47 -7
  9. data/lib/hanami/providers/rack.rb +2 -0
  10. data/lib/hanami/providers/settings.rb +81 -6
  11. data/lib/hanami/settings/env_store.rb +32 -0
  12. data/lib/hanami/settings.rb +8 -12
  13. data/lib/hanami/setup.rb +1 -6
  14. data/lib/hanami/slice/routing/middleware/stack.rb +26 -5
  15. data/lib/hanami/slice.rb +38 -45
  16. data/lib/hanami/slice_configurable.rb +14 -1
  17. data/lib/hanami/slice_registrar.rb +65 -5
  18. data/lib/hanami/version.rb +1 -1
  19. data/lib/hanami.rb +53 -2
  20. data/spec/new_integration/action/slice_configuration_spec.rb +287 -0
  21. data/spec/new_integration/code_loading/loading_from_lib_spec.rb +208 -0
  22. data/spec/new_integration/dotenv_loading_spec.rb +137 -0
  23. data/spec/new_integration/settings/access_to_constants_spec.rb +169 -0
  24. data/spec/new_integration/settings/loading_from_env_spec.rb +187 -0
  25. data/spec/new_integration/settings/settings_component_loading_spec.rb +113 -0
  26. data/spec/new_integration/settings/using_types_spec.rb +87 -0
  27. data/spec/new_integration/setup_spec.rb +145 -0
  28. data/spec/new_integration/slices/slice_loading_spec.rb +171 -0
  29. data/spec/new_integration/view/context/settings_spec.rb +5 -1
  30. data/spec/new_integration/view/slice_configuration_spec.rb +289 -0
  31. data/spec/support/app_integration.rb +4 -5
  32. data/spec/unit/hanami/configuration/slices_spec.rb +34 -0
  33. data/spec/unit/hanami/settings/env_store_spec.rb +52 -0
  34. data/spec/unit/hanami/slice_configurable_spec.rb +2 -2
  35. data/spec/unit/hanami/version_spec.rb +1 -1
  36. metadata +30 -28
  37. data/lib/hanami/settings/dotenv_store.rb +0 -58
  38. data/spec/new_integration/action/configuration_spec.rb +0 -26
  39. data/spec/new_integration/settings_spec.rb +0 -115
  40. data/spec/new_integration/view/configuration_spec.rb +0 -49
  41. data/spec/unit/hanami/settings/dotenv_store_spec.rb +0 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 768139dff49ba3a91a5aadccc7ae8aa1257ea04ce4497ba98e74780b84fd59c5
4
- data.tar.gz: 06b9195a50933ee13ab2046e223f20e61535a2e909a4a26a00c0198af7d5c017
3
+ metadata.gz: 9e9e5c35a72383ba29d2523a9b30da2a3170605bba5cee20562c897025a58524
4
+ data.tar.gz: 328e2e6aa30db94ce7dbae5f58d3ac0ce8435ec928ed71fa058345ca3a22553f
5
5
  SHA512:
6
- metadata.gz: 430e49a492be7463d755be92c580861d155586bc8ef009eba96b28a39670b576bc8230cc8d8529bd3e2ecc5950814f19bc5bb8d1f0210ccd466cd8f23b0ece7e
7
- data.tar.gz: fb3c215103035f5c9af333792f5df2833952f0d7a8175d46d5f49683a824149be154430899f5aa485cd79c51183d4169b5d302b859b88239d7694da87707a01f
6
+ metadata.gz: 769a484cea4c2594afaece8ae368e16003d02f988b6c015370b0b8236de477042611d71432da8d89d885ebabd3e0e013e7dd9299d0622897f44ac3d511980daf
7
+ data.tar.gz: e5808f8d9195cb242cbc02d136c993167ab124dabda9b239469841c981c621410d80ace966378157f22cdfd4aaf683f885257d96713561e2e12ce54cdadcab0f
data/CHANGELOG.md CHANGED
@@ -2,8 +2,79 @@
2
2
 
3
3
  The web, with simplicity.
4
4
 
5
+ ## v2.0.0.beta2 - 2022-08-16
6
+
7
+ ### Added
8
+
9
+ - [Tim Riley] Added `config.slices` setting to specify a list of slices to load. This may also be set via a comma-separated string in the `HANAMI_SLICES` env var. [#1189]
10
+
11
+ This is useful when only certain parts of your application are required for particular workloads. Specifying those slices directly improves boot time and minimizes memory usage, as well as ensuring clean boundaries between your slices.
12
+
13
+ For example, given an app with both `blog`, `shop` and `admin` slices, you can specify only the first two to load:
14
+
15
+ ```ruby
16
+ # config/app.rb
17
+
18
+ module AppName
19
+ class App < Hanami::App
20
+ config.slices = %w[blog shop]
21
+ end
22
+ end
23
+ ```
24
+
25
+ Or by setting `HANAMI_SLICES=blog,shop` in the env.
26
+
27
+ You can also specify arbitrarily nested slices using dot delimiters. For example, if you have an `admin` slice with nested `shop` and `blog` slices, you can specify `config.slices = %w[admin.shop]` to only load the admin slice and its nested shop slice.
28
+ - [Tim Riley] Added `App.prepare_load_path`, which need only be called if you explicitly change your app's `config.root`. The `$LOAD_PATH` is otherwise still prepared automatically for the default `root` of `Dir.pwd`. [#1188]
29
+ - [Marc Busqué, Tim Riley] Added `Hanami.setup` to find and load your Hanami app file (expected at `config/app.rb`). This will search in the current directory, and will search upwards through parent directories until the app file is found. [#1197]
30
+
31
+ This behavior allows `Hanami.setup` (and its counterpart convenience require, `require "hanami/setup"`) to work even when called
32
+ from a nested directory within your Hanami app.
33
+
34
+ Also added `Hanami.app_path`, returning the absolute path of the app file, if found.
35
+
36
+ ### Changed
37
+
38
+ - [Tim Riley] Allow access to autoloaded constants from within `config/settings.rb` [#1186]
39
+
40
+ New Hanami apps generate a types module at `lib/[app_name]/types.rb`. Now you can access it within your settings class to type check your settings, e.g.
41
+
42
+ ```ruby
43
+ # config/settings.rb
44
+
45
+ module AppName
46
+ class Settings < Hanami::Settings
47
+ # By default, AppName::Types is defined at `lib/[app_name]/types.rb`, and can be autoloaded here
48
+ setting :some_flag, constructor: Types::Params::Bool
49
+ end
50
+ end
51
+ ```
52
+
53
+ For settings defined within a slice, you can access types (or any constant) defined within the slice:
54
+
55
+ ```ruby
56
+ # slices/slice_name/config/settings.rb
57
+
58
+ module SliceName
59
+ class Settings < Hanami::Settings
60
+ # SliceName::Types is expected to be defined at `slices/[slice_name]/types.rb`
61
+ setting :some_slice_flag, constructor: Types::Params::bool
62
+ end
63
+ end
64
+ ```
65
+ - [Tim Riley] Every slice has a distinct `Zeitwerk::Loader` instance at `Slice.autoloader`. This change enabled the autoloading within settings described above. [#1186]
66
+ - [Tim Riley] `Slice.settings` reader method has been removed. Access settings via `App["settings"]` or `Slice["settings"]` (after the slice has been prepared) instead. [#1186]
67
+ - [Tim Riley] dry-types is no longer a dependency specified in the gemspec. This is made available to generated app via a line in their `Gemfile` instead. [#1186]
68
+
69
+ ### Fixed
70
+
71
+ - [Tim Riley] `config` explicitly set in `Hanami::Action` or `Hanami::View` base classes (either app- or slice-level) is no longer overridden by slice-level configuration [#1193]
72
+ - [Nikita Shilnikov] Load dry-monitor rack extension explicitly to avoid autoloading errors [#1198] [#1199]
73
+
5
74
  ## v2.0.0.beta1.1 - 2022-07-22
6
75
 
76
+ ### Changed
77
+
7
78
  - [Andrew Croome] Specified 2.0.0.beta dependencies for hanami-cli and hanami-utils (to prefer these over previously installed alpha versions) [#1187]
8
79
 
9
80
  ## v2.0.0.beta1 - 2022-07-20
@@ -57,10 +128,12 @@ The web, with simplicity.
57
128
  - [Luca Guidi] `Hanami.application` -> `Hanami.app`
58
129
  - [Luca Guidi] `config/application.rb` -> `config/app.rb`
59
130
  - [Luca Guidi] Remove `::Application::` namespace in public API class names (e.g. `Hanami::Application::Routes` -> `Hanami::Routes`) [#1172]
131
+ - [Tim Riley] Removed `Hanami::Configuration#source_dirs` due to the new autoloading and auto-registration features [#1174]
60
132
  - [Tim Riley] Removed `Hanami::Configuration#settings_path`, `#settings_class_name` [#1175]
61
133
  - [Tim Riley] Removed `Hanami::Configuration::Router#routes_path`, and `#routes_class_name` [#1175]
62
134
  - [Tim Riley] Make `Hanami::App` to inherit from `Hanami::Slice` [#1162]
63
135
 
136
+
64
137
  ## v2.0.0.alpha8 - 2022-05-19
65
138
 
66
139
  ### Added
data/hanami.gemspec CHANGED
@@ -34,10 +34,9 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "bundler", ">= 1.16", "< 3"
35
35
  spec.add_dependency "dry-configurable", "~> 0.15"
36
36
  spec.add_dependency "dry-core", "~> 0.7"
37
- spec.add_dependency "dry-types", "~> 1.5"
38
37
  spec.add_dependency "dry-inflector", "~> 0.2", ">= 0.2.1"
39
38
  spec.add_dependency "dry-system", "~> 0.25", ">= 0.25.0"
40
- spec.add_dependency "dry-monitor", "~> 0.6", ">= 0.6.0"
39
+ spec.add_dependency "dry-monitor", "~> 0.6", ">= 0.6.3"
41
40
  spec.add_dependency "hanami-cli", "~> 2.0.beta"
42
41
  spec.add_dependency "hanami-utils", "~> 2.0.beta"
43
42
  spec.add_dependency "zeitwerk", "~> 2.4"
data/lib/hanami/app.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zeitwerk"
4
3
  require_relative "configuration"
5
4
  require_relative "constants"
6
5
  require_relative "slice"
@@ -31,29 +30,97 @@ module Hanami
31
30
  @_mutex.synchronize do
32
31
  subclass.class_eval do
33
32
  @configuration = Hanami::Configuration.new(app_name: slice_name, env: Hanami.env)
34
- @autoloader = Zeitwerk::Loader.new
35
33
 
36
- prepare_base_load_path
34
+ # Prepare the load path (based on the default root of `Dir.pwd`) as early as
35
+ # possible, so you can make a `require` inside the body of an `App` subclass,
36
+ # which may be useful for certain kinds of app configuration.
37
+ prepare_load_path
38
+
39
+ load_dotenv
37
40
  end
38
41
  end
39
42
  end
40
43
 
41
44
  # App class interface
42
45
  module ClassMethods
43
- attr_reader :autoloader, :configuration
46
+ attr_reader :configuration
44
47
 
45
48
  def app_name
46
49
  slice_name
47
50
  end
48
51
 
52
+ # Prepares the $LOAD_PATH based on the app's configured root, prepending the `lib/`
53
+ # directory if it exists. If the lib directory is already added, this will do
54
+ # nothing.
55
+ #
56
+ # In ordinary circumstances, you should never have to call this method: this method
57
+ # is called immediately upon subclassing {Hanami::App}, as a convenicence to put
58
+ # lib/ (under the default root of `Dir.pwd`) on the load path automatically. This is
59
+ # helpful if you need to require files inside the subclass body for performing
60
+ # certain app configuration steps.
61
+ #
62
+ # If you change your app's `config.root` and you need to require files from its
63
+ # `lib/` directory within your {App} subclass body, you should call
64
+ # {.prepare_load_path} explicitly after setting the new root.
65
+ #
66
+ # Otherwise, this method is called again as part of the app {.prepare} step, so if
67
+ # you've changed your app's root and do _not_ need to require files within your {App}
68
+ # subclass body, then you don't need to call this method.
69
+ #
70
+ # @example
71
+ # module MyApp
72
+ # class App < Hanami::App
73
+ # config.root = Pathname(__dir__).join("../src")
74
+ # prepare_load_path
75
+ #
76
+ # # You can make requires for your files here
77
+ # end
78
+ # end
79
+ #
80
+ # @return [self]
81
+ #
82
+ # @api public
83
+ # @since 2.0.0
84
+ def prepare_load_path
85
+ if (lib_path = root.join(LIB_DIR)).directory?
86
+ path = lib_path.realpath.to_s
87
+ $LOAD_PATH.prepend(path) unless $LOAD_PATH.include?(path)
88
+ end
89
+
90
+ self
91
+ end
92
+
49
93
  private
50
94
 
51
- def prepare_base_load_path
52
- base_path = root.join(LIB_DIR)
53
- $LOAD_PATH.unshift(base_path) unless $LOAD_PATH.include?(base_path)
95
+ # Uses [dotenv](https://github.com/bkeepers/dotenv) (if available) to populate `ENV` from
96
+ # various `.env` files.
97
+ #
98
+ # For a given `HANAMI_ENV` environment, the `.env` files are looked up in the following order:
99
+ #
100
+ # - .env.{environment}.local
101
+ # - .env.local (unless the environment is `test`)
102
+ # - .env.{environment}
103
+ # - .env
104
+ #
105
+ # If dotenv is unavailable, the method exits and does nothing.
106
+ def load_dotenv
107
+ return unless Hanami.bundled?("dotenv")
108
+
109
+ hanami_env = Hanami.env
110
+ dotenv_files = [
111
+ ".env.#{hanami_env}.local",
112
+ (".env.local" unless hanami_env == :test),
113
+ ".env.#{hanami_env}",
114
+ ".env"
115
+ ].compact
116
+
117
+ require "dotenv"
118
+ Dotenv.load(*dotenv_files)
54
119
  end
55
120
 
56
121
  def prepare_all
122
+ prepare_load_path
123
+
57
124
  # Make app-wide notifications available as early as possible
58
125
  container.use(:notifications)
59
126
 
@@ -65,16 +132,9 @@ module Hanami
65
132
  # standard steps have been skipped via the empty method overrides below.
66
133
  prepare_app_component_dirs
67
134
  prepare_app_providers
68
-
69
- # The autoloader must be setup after the container is configured, which is the
70
- # point at which any component dirs from other slices are added to the autoloader
71
- app = self
72
- container.after(:configure) do
73
- app.send(:prepare_app_autoloader)
74
- end
75
135
  end
76
136
 
77
- # Skip standard slice prepare steps that do not apply to the app slice
137
+ # Skip standard slice prepare steps that do not apply to the app
78
138
  def prepare_container_component_dirs; end
79
139
  def prepare_container_imports; end
80
140
 
@@ -122,7 +182,7 @@ module Hanami
122
182
  register_provider(:rack, source: Hanami::Providers::Rack, namespace: true)
123
183
  end
124
184
 
125
- def prepare_app_autoloader
185
+ def prepare_autoloader
126
186
  # Component dirs are automatically pushed to the autoloader by dry-system's
127
187
  # zeitwerk plugin. This method adds other dirs that are not otherwise configured
128
188
  # as component dirs.
@@ -7,7 +7,7 @@ module Hanami
7
7
  module Assets
8
8
  # @since 2.0.0
9
9
  # @api public
10
- class ApplicationConfiguration
10
+ class AppConfiguration
11
11
  include Dry::Configurable
12
12
 
13
13
  # @since 2.0.0
@@ -11,7 +11,7 @@ require_relative "constants"
11
11
  require_relative "configuration/logger"
12
12
  require_relative "configuration/router"
13
13
  require_relative "configuration/sessions"
14
- require_relative "settings/dotenv_store"
14
+ require_relative "settings/env_store"
15
15
  require_relative "slice/routing/middleware/stack"
16
16
 
17
17
  module Hanami
@@ -27,18 +27,18 @@ module Hanami
27
27
 
28
28
  setting :inflector, default: Dry::Inflector.new
29
29
 
30
- setting :settings_store, default: Hanami::Settings::DotenvStore
31
-
32
- setting :slices do
33
- setting :shared_component_keys, default: %w[
34
- inflector
35
- logger
36
- notifications
37
- rack.monitor
38
- routes
39
- settings
40
- ]
41
- end
30
+ setting :settings_store, default: Hanami::Settings::EnvStore.new
31
+
32
+ setting :shared_app_component_keys, default: %w[
33
+ inflector
34
+ logger
35
+ notifications
36
+ rack.monitor
37
+ routes
38
+ settings
39
+ ]
40
+
41
+ setting :slices
42
42
 
43
43
  setting :base_url, default: "http://0.0.0.0:2300", constructor: ->(url) { URI(url) }
44
44
 
@@ -100,10 +100,9 @@ module Hanami
100
100
  @environments = DEFAULT_ENVIRONMENTS.clone
101
101
  @env = env
102
102
 
103
- # Some default setting values must be assigned at initialize-time to ensure they
104
- # have appropriate values for the current app
103
+ # Apply default values that are only knowable at initialize-time (vs require-time)
105
104
  self.root = Dir.pwd
106
- self.settings_store = Hanami::Settings::DotenvStore.new.with_dotenv_loaded
105
+ load_from_env
107
106
 
108
107
  config.logger = Configuration::Logger.new(env: env, app_name: app_name)
109
108
 
@@ -199,7 +198,10 @@ module Hanami
199
198
 
200
199
  private
201
200
 
202
- # @api private
201
+ def load_from_env
202
+ self.slices = ENV["HANAMI_SLICES"]&.split(",")&.map(&:strip)
203
+ end
204
+
203
205
  def apply_env_config(env = self.env)
204
206
  environments[env].each do |block|
205
207
  instance_eval(&block)
@@ -207,7 +209,7 @@ module Hanami
207
209
  end
208
210
 
209
211
  # @api private
210
- def load_dependent_config(require_path, &block)
212
+ def load_dependent_config(require_path)
211
213
  require require_path
212
214
  yield
213
215
  rescue LoadError => e
@@ -217,7 +219,6 @@ module Hanami
217
219
  NullConfiguration.new
218
220
  end
219
221
 
220
- # @api private
221
222
  def method_missing(name, *args, &block)
222
223
  if config.respond_to?(name)
223
224
  config.public_send(name, *args, &block)
@@ -226,7 +227,6 @@ module Hanami
226
227
  end
227
228
  end
228
229
 
229
- # @api private
230
230
  def respond_to_missing?(name, _incude_all = false)
231
231
  config.respond_to?(name) || super
232
232
  end
@@ -48,7 +48,50 @@ module Hanami
48
48
 
49
49
  def configure_action(action_class)
50
50
  action_class.config.settings.each do |setting|
51
- action_class.config.public_send :"#{setting}=", actions_config.public_send(:"#{setting}")
51
+ # Configure the action from config on the slice, _unless it has already been configured
52
+ # by a parent slice_, and re-configuring it for this slice would make no change.
53
+ #
54
+ # In the case of most slices, its actions config is likely to be the same as its parent
55
+ # (since each slice copies its `config` from its parent), and if we re-apply the config
56
+ # here, then it may possibly overwrite config customisations explicitly made in parent
57
+ # action classes.
58
+ #
59
+ # For example, given an app-level base action class, with custom config:
60
+ #
61
+ # module MyApp
62
+ # class Action < Hanami::Action
63
+ # config.default_response_format = :json
64
+ # end
65
+ # end
66
+ #
67
+ # And then an action in a slice inheriting from it:
68
+ #
69
+ # module MySlice
70
+ # module Actions
71
+ # class SomeAction < MyApp::Action
72
+ # end
73
+ # end
74
+ # end
75
+ #
76
+ # In this case, `SliceConfiguredAction` will be extended two times:
77
+ #
78
+ # 1. When `MyApp::Action` is defined
79
+ # 2. Again when `MySlice::Actions::SomeAction` is defined
80
+ #
81
+ # If we blindly re-configure all action settings each time `SliceConfiguredAction` is
82
+ # extended, then at the point of (2) above, we'd end up overwriting the custom
83
+ # `config.default_response_format` explicitly configured in the `MyApp::Action` base
84
+ # class, leaving `MySlice::Actions::SomeAction` with `config.default_response_format` of
85
+ # `:html` (the default at `Hanami.app.config.actions.default_response_format`), and not
86
+ # the `:json` value configured in its immediate superclass.
87
+ #
88
+ # This would be surprising behavior, and we want to avoid it.
89
+ slice_value = slice.config.actions.public_send(:"#{setting}")
90
+ parent_value = slice.parent.config.actions.public_send(:"#{setting}") if slice.parent
91
+
92
+ next if slice.parent && slice_value == parent_value
93
+
94
+ action_class.config.public_send(:"#{setting}=", slice_value)
52
95
  end
53
96
  end
54
97
 
@@ -32,12 +32,52 @@ module Hanami
32
32
  # rubocop:disable Metrics/AbcSize
33
33
  def configure_view(view_class)
34
34
  view_class.settings.each do |setting|
35
- if slice.config.views.respond_to?(:"#{setting}")
36
- view_class.config.public_send(
37
- :"#{setting}=",
38
- slice.config.views.public_send(:"#{setting}")
39
- )
40
- end
35
+ next unless slice.config.views.respond_to?(:"#{setting}")
36
+
37
+ # Configure the view from config on the slice, _unless it has already been configured by
38
+ # a parent slice_, and re-configuring it for this slice would make no change.
39
+ #
40
+ # In the case of most slices, its views config is likely to be the same as its parent
41
+ # (since each slice copies its `config` from its parent), and if we re-apply the config
42
+ # here, then it may possibly overwrite config customisations explicitly made in parent
43
+ # view classes.
44
+ #
45
+ # For example, given an app-level base view class, with custom config:
46
+ #
47
+ # module MyApp
48
+ # class View < Hanami::View
49
+ # config.layout = "custom_layout"
50
+ # end
51
+ # end
52
+ #
53
+ # And then a view in a slice inheriting from it:
54
+ #
55
+ # module MySlice
56
+ # module Views
57
+ # class SomeView < MyApp::View
58
+ # end
59
+ # end
60
+ # end
61
+ #
62
+ # In this case, `SliceConfiguredView` will be extended two times:
63
+ #
64
+ # 1. When `MyApp::View` is defined
65
+ # 2. Again when `MySlice::Views::SomeView` is defined
66
+ #
67
+ # If we blindly re-configure all view settings each time `SliceConfiguredView` is
68
+ # extended, then at the point of (2) above, we'd end up overwriting the custom
69
+ # `config.layout` explicitly configured in the `MyApp::View` base class, leaving
70
+ # `MySlice::Views::SomeView` with `config.layout` of `"app"` (the default as specified
71
+ # at `Hanami.app.config.views.layout`), and not the `"custom_layout"` value configured
72
+ # in its immediate superclass.
73
+ #
74
+ # This would be surprising behavior, and we want to avoid it.
75
+ slice_value = slice.config.views.public_send(:"#{setting}")
76
+ parent_value = slice.parent.config.views.public_send(:"#{setting}") if slice.parent
77
+
78
+ next if slice.parent && slice_value == parent_value
79
+
80
+ view_class.config.public_send(:"#{setting}=", slice_value)
41
81
  end
42
82
 
43
83
  view_class.config.inflector = inflector
@@ -86,7 +126,7 @@ module Hanami
86
126
 
87
127
  begin
88
128
  inflector.constantize(inflector.camelize(path))
89
- rescue NameError => exception
129
+ rescue NameError # rubocop: disable Lint/SuppressedException
90
130
  end
91
131
  end
92
132
 
@@ -8,6 +8,8 @@ module Hanami
8
8
  def prepare
9
9
  require "dry/monitor"
10
10
  require "hanami/web/rack_logger"
11
+
12
+ Dry::Monitor.load_extensions(:rack)
11
13
  end
12
14
 
13
15
  def start
@@ -1,22 +1,97 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/provider/source"
4
+ require_relative "../constants"
5
+ require_relative "../errors"
4
6
 
5
7
  module Hanami
6
8
  module Providers
9
+ # The settings provider loads and registers the "settings" component in app and slice
10
+ # containers.
11
+ #
12
+ # To register this provider with a slice container, you should use
13
+ # {.register_with_slice}, which will register the provider only if settings are
14
+ # defined for the slice.
15
+ #
16
+ # @see Slice::ClassMethods.prepare_container_providers
17
+ #
18
+ # @api private
19
+ # @since 2.0.0
7
20
  class Settings < Dry::System::Provider::Source
8
- def self.for_slice(slice)
9
- Class.new(self) do |klass|
10
- klass.instance_variable_set(:@slice, slice)
21
+ class << self
22
+ # Registers the provider with the slice's container, but only if settings are
23
+ # defined for the slice.
24
+ def register_with_slice(slice)
25
+ return unless settings_defined?(slice)
26
+
27
+ slice.register_provider(:settings, source: with_slice(slice))
28
+ end
29
+
30
+ # Creates a new subclass of the provider for the given slice.
31
+ #
32
+ # You must do this before registering the provider with a container. The provider
33
+ # uses the slice to locate the settings definition based on the slice's config.
34
+ def with_slice(slice)
35
+ Class.new(self) do |klass|
36
+ klass.instance_variable_set(:@slice, slice)
37
+ end
38
+ end
39
+
40
+ # Returns the slice for the provider
41
+ def slice
42
+ unless @slice
43
+ raise SliceLoadError, "a slice must be given to #{self} via `.with_slice(slice)`"
44
+ end
45
+
46
+ @slice
47
+ end
48
+
49
+ private
50
+
51
+ # Returns true if settings are defined for the slice.
52
+ #
53
+ # Settings are considered defined if a `Settings` class is already defined in the
54
+ # slice namespace, or a `config/settings.rb` exists under the slice root.
55
+ def settings_defined?(slice)
56
+ slice.namespace.const_defined?(SETTINGS_CLASS_NAME) ||
57
+ slice.root.join("#{SETTINGS_PATH}#{RB_EXT}").file?
11
58
  end
12
59
  end
13
60
 
14
- def self.slice
15
- @slice || Hanami.app
61
+ def prepare
62
+ require_slice_settings unless slice_settings_class?
16
63
  end
17
64
 
18
65
  def start
19
- register :settings, self.class.slice.settings
66
+ settings = slice_settings_class.new(slice.config.settings_store)
67
+
68
+ register :settings, settings
69
+ end
70
+
71
+ private
72
+
73
+ def slice
74
+ self.class.slice
75
+ end
76
+
77
+ def slice_settings_class?
78
+ slice.namespace.const_defined?(SETTINGS_CLASS_NAME)
79
+ end
80
+
81
+ def slice_settings_class
82
+ slice.namespace.const_get(SETTINGS_CLASS_NAME)
83
+ end
84
+
85
+ def require_slice_settings
86
+ require "hanami/settings"
87
+
88
+ slice_settings_require_path = File.join(slice.root, SETTINGS_PATH)
89
+
90
+ begin
91
+ require slice_settings_require_path
92
+ rescue LoadError => e
93
+ raise e unless e.path == slice_settings_require_path
94
+ end
20
95
  end
21
96
  end
22
97
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/constants"
4
+
5
+ module Hanami
6
+ class Settings
7
+ # The default store for {Hanami:Settings}, loading setting values from `ENV`.
8
+ #
9
+ # If your app loads the dotenv gem, then `ENV` will also be populated from various `.env` files when
10
+ # you subclass `Hanami::App`.
11
+ #
12
+ # @since 2.0.0
13
+ # @api private
14
+ class EnvStore
15
+ Undefined = Dry::Core::Constants::Undefined
16
+
17
+ attr_reader :store, :hanami_env
18
+
19
+ def initialize(store: ENV, hanami_env: Hanami.env)
20
+ @store = store
21
+ @hanami_env = hanami_env
22
+ end
23
+
24
+ def fetch(name, default_value = Undefined, &block)
25
+ name = name.to_s.upcase
26
+ args = default_value == Undefined ? [name] : [name, default_value]
27
+
28
+ store.fetch(*args, &block)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -22,26 +22,22 @@ module Hanami
22
22
  # end
23
23
  # end
24
24
  #
25
- # Settings are defined with
26
- # [dry-configurable](https://dry-rb.org/gems/dry-configurable/), so you can
27
- # take a look there to see the supported syntax.
25
+ # Settings are defined with [dry-configurable](https://dry-rb.org/gems/dry-configurable/), so you
26
+ # can take a look there to see the supported syntax.
28
27
  #
29
- # Users work with an instance of this class made available within the
30
- # `settings` key in the container. The instance gets its settings populated
31
- # from a configurable store, which defaults to
32
- # {Hanami::Settings::DotenvStore}.
28
+ # Users work with an instance of this class made available within the `settings` key in the
29
+ # container. The instance gets its settings populated from a configurable store, which defaults to
30
+ # {Hanami::Settings::EnvStore}.
33
31
  #
34
- # A different store can be set through the `settings_store` Hanami
35
- # configuration option. All it needs to do is implementing a `#fetch` method
36
- # with the same signature as `Hash#fetch`.
32
+ # A different store can be set through the `settings_store` Hanami configuration option. All it
33
+ # needs to do is implementing a `#fetch` method with the same signature as `Hash#fetch`.
37
34
  #
38
35
  # @see Hanami::Settings::DotenvStore
39
36
  # @since 2.0.0
40
37
  class Settings
41
38
  # Exception for errors in the definition of settings.
42
39
  #
43
- # Its message collects all the individual errors that can be raised for
44
- # each setting.
40
+ # Its message collects all the individual errors that can be raised for each setting.
45
41
  InvalidSettingsError = Class.new(StandardError) do
46
42
  def initialize(errors)
47
43
  @errors = errors
data/lib/hanami/setup.rb CHANGED
@@ -3,9 +3,4 @@
3
3
  require "bundler/setup"
4
4
  require "hanami"
5
5
 
6
- begin
7
- app_require_path = File.join(Dir.pwd, "config/app")
8
- require app_require_path
9
- rescue LoadError => e
10
- raise e unless e.path == app_require_path
11
- end
6
+ Hanami.setup