hanami 2.0.0.beta1.1 → 2.0.0.beta2

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 (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
@@ -1,20 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack/builder"
4
-
5
3
  module Hanami
6
4
  class Slice
7
5
  module Routing
8
- # Hanami::Applicatione::Router middleware stack
9
- #
10
6
  # @since 2.0.0
11
7
  # @api private
12
8
  module Middleware
13
- # Middleware stack
9
+ # Wraps a rack app with a middleware stack
10
+ #
11
+ # We use this class to add middlewares to the rack application generated
12
+ # from {Hanami::Slice::Router}.
13
+ #
14
+ # ```
15
+ # stack = Hanami::Slice::Routing::Middleware::Stack.new
16
+ # stack.use(Rack::ContentType, "text/html")
17
+ # stack.to_rack_app(a_rack_app)
18
+ # ```
19
+ #
20
+ # Middlewares can be mounted on specific paths:
21
+ #
22
+ # ```
23
+ # stack.with("/api") do
24
+ # stack.use(Rack::ContentType, "application/json")
25
+ # end
26
+ # ```
14
27
  #
15
28
  # @since 2.0.0
16
29
  # @api private
17
30
  class Stack
31
+ include Enumerable
32
+
18
33
  # @since 2.0.0
19
34
  # @api private
20
35
  ROOT_PREFIX = "/"
@@ -77,6 +92,12 @@ module Hanami
77
92
  # @since 2.0.0
78
93
  # @api private
79
94
  def to_rack_app(app)
95
+ unless Hanami.bundled?("rack")
96
+ raise "Add \"rack\" to your `Gemfile` to run Hanami as a rack app"
97
+ end
98
+
99
+ require "rack/builder"
100
+
80
101
  s = self
81
102
 
82
103
  Rack::Builder.new do
data/lib/hanami/slice.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/container"
4
+ require "zeitwerk"
4
5
  require_relative "constants"
5
6
  require_relative "errors"
6
7
  require_relative "slice_name"
7
8
  require_relative "slice_registrar"
9
+ require_relative "providers/settings"
8
10
 
9
11
  module Hanami
10
12
  # A slice represents any distinct area of concern within an Hanami app.
@@ -40,6 +42,7 @@ module Hanami
40
42
  @_mutex.synchronize do
41
43
  subclass.class_eval do
42
44
  @_mutex = Mutex.new
45
+ @autoloader = Zeitwerk::Loader.new
43
46
  @container = Class.new(Dry::System::Container)
44
47
  end
45
48
  end
@@ -47,15 +50,14 @@ module Hanami
47
50
 
48
51
  # rubocop:disable Metrics/ModuleLength
49
52
  module ClassMethods
50
- attr_reader :parent, :container
53
+ attr_reader :parent, :autoloader, :container
51
54
 
52
55
  def app
53
56
  Hanami.app
54
57
  end
55
58
 
56
- # A slice's configuration is copied from the app configuration at time of
57
- # first access. The app should have its configuration completed before
58
- # slices are loaded.
59
+ # A slice's configuration is copied from the app configuration at time of first access. The
60
+ # app should have its configuration completed before slices are loaded.
59
61
  def configuration
60
62
  @configuration ||= app.configuration.dup.tap do |config|
61
63
  # Remove specific values from app that will not apply to this slice
@@ -178,10 +180,6 @@ module Hanami
178
180
  end
179
181
  end
180
182
 
181
- def settings
182
- @settings ||= load_settings
183
- end
184
-
185
183
  def routes
186
184
  @routes ||= load_routes
187
185
  end
@@ -222,6 +220,15 @@ module Hanami
222
220
  instance_exec(container, &@prepare_container_block) if @prepare_container_block
223
221
  container.configured!
224
222
 
223
+ prepare_autoloader
224
+
225
+ ensure_prepared
226
+
227
+ # Load child slices last, ensuring their parent is fully prepared beforehand
228
+ # (useful e.g. for slices that may wish to access constants defined in the
229
+ # parent's autoloaded directories)
230
+ prepare_slices
231
+
225
232
  @prepared = true
226
233
 
227
234
  self
@@ -248,19 +255,18 @@ module Hanami
248
255
  end
249
256
  end
250
257
 
251
- def prepare_all
252
- # Load settings first, to fail early in case of missing/unexpected values
253
- settings
258
+ def ensure_prepared
259
+ # Load settings so we can fail early in case of non-conformant values
260
+ self[:settings] if key?(:settings)
261
+ end
254
262
 
263
+ def prepare_all
255
264
  prepare_container_consts
256
265
  prepare_container_plugins
257
266
  prepare_container_base_config
258
267
  prepare_container_component_dirs
259
268
  prepare_container_imports
260
269
  prepare_container_providers
261
- prepare_autoloader
262
-
263
- prepare_slices
264
270
  end
265
271
 
266
272
  def prepare_container_plugins
@@ -268,7 +274,7 @@ module Hanami
268
274
 
269
275
  container.use(
270
276
  :zeitwerk,
271
- loader: app.autoloader,
277
+ loader: autoloader,
272
278
  run_setup: false,
273
279
  eager_load: false
274
280
  )
@@ -309,14 +315,13 @@ module Hanami
309
315
  dir.auto_register = -> component {
310
316
  relative_path = component.file_path.relative_path_from(root).to_s
311
317
  !relative_path.start_with?(*no_auto_register_paths)
312
-
313
318
  }
314
319
  end
315
320
  end
316
321
 
317
322
  def prepare_container_imports
318
323
  import(
319
- keys: config.slices.shared_component_keys,
324
+ keys: config.shared_app_component_keys,
320
325
  from: app.container,
321
326
  as: nil
322
327
  )
@@ -328,21 +333,29 @@ module Hanami
328
333
  # point we're still in the process of preparing.
329
334
  if routes
330
335
  require_relative "providers/routes"
331
- register_provider(:routes, source: Hanami::Providers::Routes.for_slice(self))
336
+ register_provider(:routes, source: Providers::Routes.for_slice(self))
332
337
  end
333
338
 
334
- if settings
335
- require_relative "providers/settings"
336
- register_provider(:settings, source: Hanami::Providers::Settings.for_slice(self))
337
- end
339
+ Providers::Settings.register_with_slice(self)
338
340
  end
339
341
 
340
342
  def prepare_autoloader
341
- # Everything in the slice directory can be autoloaded _except_ `config/`, which is
342
- # where we keep files loaded specially by the framework as part of slice setup.
343
+ # Component dirs are automatically pushed to the autoloader by dry-system's
344
+ # zeitwerk plugin. This method adds other dirs that are not otherwise configured
345
+ # as component dirs.
346
+
347
+ # Everything in the slice root can be autoloaded except `config/` and `slices/`,
348
+ # which are framework-managed directories
349
+
343
350
  if root.join(CONFIG_DIR)&.directory?
344
- container.config.autoloader.ignore(root.join(CONFIG_DIR))
351
+ autoloader.ignore(root.join(CONFIG_DIR))
352
+ end
353
+
354
+ if root.join(SLICES_DIR)&.directory?
355
+ autoloader.ignore(root.join(SLICES_DIR))
345
356
  end
357
+
358
+ autoloader.setup
346
359
  end
347
360
 
348
361
  def prepare_container_consts
@@ -355,26 +368,6 @@ module Hanami
355
368
  slices.freeze
356
369
  end
357
370
 
358
- def load_settings
359
- if root.directory?
360
- settings_require_path = File.join(root, SETTINGS_PATH)
361
-
362
- begin
363
- require_relative "./settings"
364
- require settings_require_path
365
- rescue LoadError => e
366
- raise e unless e.path == settings_require_path
367
- end
368
- end
369
-
370
- begin
371
- settings_class = namespace.const_get(SETTINGS_CLASS_NAME)
372
- settings_class.new(configuration.settings_store)
373
- rescue NameError => e
374
- raise e unless e.name == SETTINGS_CLASS_NAME.to_sym
375
- end
376
- end
377
-
378
371
  def load_routes
379
372
  if root.directory?
380
373
  routes_require_path = File.join(root, ROUTES_PATH)
@@ -36,10 +36,15 @@ module Hanami
36
36
 
37
37
  super(subclass)
38
38
 
39
+ subclass.instance_variable_set(:@configured_for_slices, configured_for_slices.dup)
40
+
39
41
  slice = slice_for.(subclass)
40
42
  return unless slice
41
43
 
42
- subclass.configure_for_slice(slice)
44
+ unless subclass.configured_for_slice?(slice)
45
+ subclass.configure_for_slice(slice)
46
+ subclass.configured_for_slices << slice # WIP
47
+ end
43
48
  end
44
49
  end
45
50
 
@@ -58,5 +63,13 @@ module Hanami
58
63
  end
59
64
 
60
65
  def configure_for_slice(slice); end
66
+
67
+ def configured_for_slice?(slice)
68
+ configured_for_slices.include?(slice)
69
+ end
70
+
71
+ def configured_for_slices
72
+ @configured_for_slices ||= []
73
+ end
61
74
  end
62
75
  end
@@ -6,6 +6,8 @@ require_relative "slice"
6
6
  module Hanami
7
7
  # @api private
8
8
  class SliceRegistrar
9
+ SLICE_DELIMITER = CONTAINER_KEY_DELIMITER
10
+
9
11
  attr_reader :parent, :slices
10
12
  private :parent, :slices
11
13
 
@@ -15,6 +17,8 @@ module Hanami
15
17
  end
16
18
 
17
19
  def register(name, slice_class = nil, &block)
20
+ return unless filter_slice_names([name]).any?
21
+
18
22
  if slices.key?(name.to_sym)
19
23
  raise SliceLoadError, "Slice '#{name}' is already registered"
20
24
  end
@@ -49,7 +53,10 @@ module Hanami
49
53
  .select { |path| File.directory?(path) }
50
54
  .map { |path| File.basename(path) }
51
55
 
52
- (slice_dirs + slice_configs).uniq.sort.each do |slice_name|
56
+ slice_names = (slice_dirs + slice_configs).uniq.sort
57
+ .then { filter_slice_names(_1) }
58
+
59
+ slice_names.each do |slice_name|
53
60
  load_slice(slice_name)
54
61
  end
55
62
 
@@ -60,12 +67,24 @@ module Hanami
60
67
  slices.each_value(&block)
61
68
  end
62
69
 
70
+ def keys
71
+ slices.keys
72
+ end
73
+
63
74
  def to_a
64
75
  slices.values
65
76
  end
66
77
 
67
78
  private
68
79
 
80
+ def root
81
+ parent.root
82
+ end
83
+
84
+ def inflector
85
+ parent.inflector
86
+ end
87
+
69
88
  # Runs when a slice file has been found at `config/slices/[slice_name].rb`, or a slice
70
89
  # directory at `slices/[slice_name]`. Attempts to require the slice class, if defined,
71
90
  # or generates a new slice class for the given slice name.
@@ -106,14 +125,55 @@ module Hanami
106
125
 
107
126
  # Slices require a root, so provide a sensible default based on the slice's parent
108
127
  slice.config.root ||= root.join(SLICES_DIR, slice_name.to_s)
128
+
129
+ slice.config.slices = child_slice_names(slice_name, parent.config.slices)
109
130
  end
110
131
 
111
- def root
112
- parent.root
132
+ # Returns a filtered array of slice names based on the parent's `config.slices`
133
+ #
134
+ # This works with both singular slice names (e.g. `"admin"`) as well as dot-delimited nested
135
+ # slice names (e.g. `"admin.shop"`).
136
+ #
137
+ # It will consider only the base names of the slices (since in this case, a parent slice must be
138
+ # loaded in order for its children to be loaded).
139
+ #
140
+ # @example
141
+ # parent.config.slices # => ["admin.shop"]
142
+ # filter_slice_names(["admin", "main"]) # => ["admin"]
143
+ #
144
+ # parent.config.slices # => ["admin"]
145
+ # filter_slice_names(["admin", "main"]) # => ["admin"]
146
+ def filter_slice_names(slice_names)
147
+ slice_names = slice_names.map(&:to_s)
148
+
149
+ if parent.config.slices
150
+ slice_names & parent.config.slices.map { base_slice_name(_1) }
151
+ else
152
+ slice_names
153
+ end
113
154
  end
114
155
 
115
- def inflector
116
- parent.inflector
156
+ # Returns the base slice name from an (optionally) dot-delimited nested slice name.
157
+ #
158
+ # @example
159
+ # base_slice_name("admin") # => "admin"
160
+ # base_slice_name("admin.users") # => "admin"
161
+ def base_slice_name(name)
162
+ name.to_s.split(SLICE_DELIMITER).first
163
+ end
164
+
165
+ # Returns an array of slice names specific to the given child slice.
166
+ #
167
+ # @example
168
+ # child_local_slice_names("admin", ["main", "admin.users"]) # => ["users"]
169
+ def child_slice_names(parent_slice_name, slice_names)
170
+ slice_names
171
+ &.select { |name|
172
+ name.include?(SLICE_DELIMITER) && name.split(SLICE_DELIMITER)[0] == parent_slice_name.to_s
173
+ }
174
+ &.map { |name|
175
+ name.split(SLICE_DELIMITER)[1..].join(SLICE_DELIMITER) # which version of Ruby supports this?
176
+ }
117
177
  end
118
178
  end
119
179
  end
@@ -8,7 +8,7 @@ module Hanami
8
8
  module Version
9
9
  # @since 0.9.0
10
10
  # @api private
11
- VERSION = "2.0.0.beta1.1"
11
+ VERSION = "2.0.0.beta2"
12
12
 
13
13
  # @since 0.9.0
14
14
  # @api private
data/lib/hanami.rb CHANGED
@@ -9,6 +9,57 @@ module Hanami
9
9
  @_mutex = Mutex.new
10
10
  @_bundled = {}
11
11
 
12
+ # Finds and loads the Hanami app file (`config/app.rb`).
13
+ #
14
+ # Raises an exception if the app file cannot be found.
15
+ #
16
+ # @return [Hanami::App] the loaded app class
17
+ #
18
+ # @api public
19
+ # @since 2.0.0
20
+ def self.setup(raise_exception: true)
21
+ return app if app?
22
+
23
+ app_path = self.app_path
24
+
25
+ if app_path
26
+ require(app_path)
27
+ app
28
+ elsif raise_exception
29
+ raise(
30
+ AppLoadError,
31
+ "Could not locate your Hanami app file.\n\n" \
32
+ "Your app file should be at `config/app.rb` in your project's root directory."
33
+ )
34
+ end
35
+ end
36
+
37
+ # Finds and returns the absolute path for the Hanami app file (`config/app.rb`).
38
+ #
39
+ # Searches within the given directory, then searches upwards through parent directories until the
40
+ # app file can be found.
41
+ #
42
+ # @param dir [String] The directory from which to start searching. Defaults to the current
43
+ # directory.
44
+ #
45
+ # @return [String, nil] the app file path, or nil if not found.
46
+ #
47
+ # @api public
48
+ # @since 2.0.0
49
+ def self.app_path(dir = Dir.pwd)
50
+ dir = Pathname(dir).expand_path
51
+ path = dir.join(APP_PATH)
52
+
53
+ if path.file?
54
+ path.to_s
55
+ elsif !dir.root?
56
+ app_path(dir.parent)
57
+ end
58
+ end
59
+
60
+ APP_PATH = "config/app.rb"
61
+ private_constant :APP_PATH
62
+
12
63
  def self.app
13
64
  @_mutex.synchronize do
14
65
  unless defined?(@_app)
@@ -22,12 +73,12 @@ module Hanami
22
73
  end
23
74
 
24
75
  def self.app?
25
- defined?(@_app)
76
+ instance_variable_defined?(:@_app)
26
77
  end
27
78
 
28
79
  def self.app=(klass)
29
80
  @_mutex.synchronize do
30
- if defined?(@_app)
81
+ if instance_variable_defined?(:@_app)
31
82
  raise AppLoadError, "Hanami.app is already configured."
32
83
  end
33
84