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
@@ -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