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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/hanami.gemspec +1 -2
- data/lib/hanami/app.rb +76 -16
- data/lib/hanami/assets/{application_configuration.rb → app_configuration.rb} +1 -1
- data/lib/hanami/configuration.rb +20 -20
- data/lib/hanami/extensions/action/slice_configured_action.rb +44 -1
- data/lib/hanami/extensions/view/slice_configured_view.rb +47 -7
- data/lib/hanami/providers/rack.rb +2 -0
- data/lib/hanami/providers/settings.rb +81 -6
- data/lib/hanami/settings/env_store.rb +32 -0
- data/lib/hanami/settings.rb +8 -12
- data/lib/hanami/setup.rb +1 -6
- data/lib/hanami/slice/routing/middleware/stack.rb +26 -5
- data/lib/hanami/slice.rb +38 -45
- data/lib/hanami/slice_configurable.rb +14 -1
- data/lib/hanami/slice_registrar.rb +65 -5
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami.rb +53 -2
- data/spec/new_integration/action/slice_configuration_spec.rb +287 -0
- data/spec/new_integration/code_loading/loading_from_lib_spec.rb +208 -0
- data/spec/new_integration/dotenv_loading_spec.rb +137 -0
- data/spec/new_integration/settings/access_to_constants_spec.rb +169 -0
- data/spec/new_integration/settings/loading_from_env_spec.rb +187 -0
- data/spec/new_integration/settings/settings_component_loading_spec.rb +113 -0
- data/spec/new_integration/settings/using_types_spec.rb +87 -0
- data/spec/new_integration/setup_spec.rb +145 -0
- data/spec/new_integration/slices/slice_loading_spec.rb +171 -0
- data/spec/new_integration/view/context/settings_spec.rb +5 -1
- data/spec/new_integration/view/slice_configuration_spec.rb +289 -0
- data/spec/support/app_integration.rb +4 -5
- data/spec/unit/hanami/configuration/slices_spec.rb +34 -0
- data/spec/unit/hanami/settings/env_store_spec.rb +52 -0
- data/spec/unit/hanami/slice_configurable_spec.rb +2 -2
- data/spec/unit/hanami/version_spec.rb +1 -1
- metadata +30 -28
- data/lib/hanami/settings/dotenv_store.rb +0 -58
- data/spec/new_integration/action/configuration_spec.rb +0 -26
- data/spec/new_integration/settings_spec.rb +0 -115
- data/spec/new_integration/view/configuration_spec.rb +0 -49
- 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
|
-
#
|
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
|
-
#
|
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
|
252
|
-
# Load settings
|
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:
|
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.
|
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:
|
336
|
+
register_provider(:routes, source: Providers::Routes.for_slice(self))
|
332
337
|
end
|
333
338
|
|
334
|
-
|
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
|
-
#
|
342
|
-
#
|
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
|
-
|
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.
|
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
|
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
|
-
|
112
|
-
|
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
|
-
|
116
|
-
|
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
|
data/lib/hanami/version.rb
CHANGED
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
|
-
|
76
|
+
instance_variable_defined?(:@_app)
|
26
77
|
end
|
27
78
|
|
28
79
|
def self.app=(klass)
|
29
80
|
@_mutex.synchronize do
|
30
|
-
if
|
81
|
+
if instance_variable_defined?(:@_app)
|
31
82
|
raise AppLoadError, "Hanami.app is already configured."
|
32
83
|
end
|
33
84
|
|