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