hanami 2.0.0.alpha4 → 2.0.0.alpha7

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.
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/container"
4
- require "dry/system/loader/autoloading"
5
4
  require "hanami/configuration"
6
5
  require "pathname"
7
6
  require "rack"
8
7
  require "zeitwerk"
8
+ require_relative "constants"
9
9
  require_relative "slice"
10
- require_relative "application/autoloader/inflector_adapter"
11
- require_relative "application/router"
12
- require_relative "application/routes"
13
- require_relative "application/settings"
10
+ require_relative "application/slice_registrar"
14
11
 
15
12
  module Hanami
16
13
  # Hanami application class
@@ -21,13 +18,15 @@ module Hanami
21
18
 
22
19
  class << self
23
20
  def inherited(klass)
21
+ super
24
22
  @_mutex.synchronize do
25
23
  klass.class_eval do
26
- @_mutex = Mutex.new
27
- @_configuration = Hanami::Configuration.new(env: Hanami.env)
24
+ @_mutex = Mutex.new
25
+ @_configuration = Hanami::Configuration.new(application_name: name, env: Hanami.env)
26
+ @autoloader = Zeitwerk::Loader.new
27
+ @container = Class.new(Dry::System::Container)
28
28
 
29
29
  extend ClassMethods
30
- include InstanceMethods
31
30
  end
32
31
 
33
32
  klass.send :prepare_base_load_path
@@ -41,9 +40,11 @@ module Hanami
41
40
  #
42
41
  # rubocop:disable Metrics/ModuleLength
43
42
  module ClassMethods
43
+ attr_reader :autoloader, :container
44
+
44
45
  def self.extended(klass)
45
46
  klass.class_eval do
46
- @inited = @booted = false
47
+ @prepared = @booted = false
47
48
  end
48
49
  end
49
50
 
@@ -51,129 +52,102 @@ module Hanami
51
52
  @_configuration
52
53
  end
53
54
 
54
- alias config configuration
55
+ alias_method :config, :configuration
56
+
57
+ def prepare(provider_name = nil)
58
+ container.prepare(provider_name) and return self if provider_name
55
59
 
56
- def init # rubocop:disable Metrics/MethodLength
57
- return self if inited?
60
+ return self if prepared?
58
61
 
59
62
  configuration.finalize!
60
63
 
61
- @autoloader = Zeitwerk::Loader.new
62
- autoloader.inflector = Autoloader::InflectorAdapter.new(inflector)
64
+ prepare_all
63
65
 
64
- load_settings
66
+ @prepared = true
67
+ self
68
+ end
65
69
 
66
- @container = prepare_container
67
- @deps_module = prepare_deps_module
70
+ def boot(&block)
71
+ return self if booted?
68
72
 
69
- load_slices
70
- slices.values.each(&:init)
71
- slices.freeze
73
+ prepare
72
74
 
73
- autoloader.setup
75
+ container.finalize!(&block)
74
76
 
75
- @inited = true
77
+ slices.each(&:boot)
78
+
79
+ @booted = true
76
80
  self
77
81
  end
78
82
 
79
- def inited?
80
- @inited
83
+ def shutdown
84
+ slices.each(&:shutdown)
85
+ container.shutdown!
86
+ self
81
87
  end
82
88
 
83
- def autoloader
84
- raise "Application not init'ed" unless defined?(@autoloader)
85
-
86
- @autoloader
89
+ def prepared?
90
+ !!@prepared
87
91
  end
88
92
 
89
- def container
90
- raise "Application not init'ed" unless defined?(@container)
91
-
92
- @container
93
+ def booted?
94
+ !!@booted
93
95
  end
94
96
 
95
- def deps
96
- raise "Application not init'ed" unless defined?(@deps_module)
97
+ def router
98
+ raise "Application not yet prepared" unless prepared?
97
99
 
98
- @deps_module
100
+ @_mutex.synchronize do
101
+ @_router ||= load_router
102
+ end
99
103
  end
100
104
 
101
- def slices
102
- @slices ||= {}
105
+ def rack_app
106
+ @rack_app ||= router.to_rack_app
103
107
  end
104
108
 
105
- def register_slice(name, **slice_args)
106
- raise "Slice +#{name}+ already registered" if slices.key?(name.to_sym)
107
-
108
- slice = Slice.new(self, name: name, **slice_args)
109
- slice.namespace.const_set :Slice, slice if slice.namespace # rubocop:disable Style/SafeNavigation
110
- slices[name.to_sym] = slice
109
+ def slices
110
+ @slices ||= SliceRegistrar.new(self)
111
111
  end
112
112
 
113
- def register(*args, **opts, &block)
114
- container.register(*args, **opts, &block)
113
+ def register_slice(...)
114
+ slices.register(...)
115
115
  end
116
116
 
117
- def register_bootable(*args, **opts, &block)
118
- container.boot(*args, **opts, &block)
117
+ def register(...)
118
+ container.register(...)
119
119
  end
120
120
 
121
- def init_bootable(*args)
122
- container.init(*args)
121
+ def register_provider(...)
122
+ container.register_provider(...)
123
123
  end
124
124
 
125
- def start_bootable(*args)
126
- container.start(*args)
125
+ def start(...)
126
+ container.start(...)
127
127
  end
128
128
 
129
- def key?(*args)
130
- container.key?(*args)
129
+ def key?(...)
130
+ container.key?(...)
131
131
  end
132
132
 
133
133
  def keys
134
134
  container.keys
135
135
  end
136
136
 
137
- def [](*args)
138
- container[*args]
137
+ def [](...)
138
+ container.[](...)
139
139
  end
140
140
 
141
- def resolve(*args)
142
- container.resolve(*args)
143
- end
144
-
145
- def boot(&block)
146
- return self if booted?
147
-
148
- init
149
-
150
- load_router
151
-
152
- container.finalize!(&block)
153
-
154
- slices.values.each(&:boot)
155
-
156
- @booted = true
157
- self
158
- end
159
-
160
- def booted?
161
- @booted
162
- end
163
-
164
- def shutdown
165
- container.shutdown!
141
+ def resolve(...)
142
+ container.resolve(...)
166
143
  end
167
144
 
168
145
  def settings
169
146
  @_settings ||= load_settings
170
147
  end
171
148
 
172
- MODULE_DELIMITER = "::"
173
- private_constant :MODULE_DELIMITER
174
-
175
149
  def namespace
176
- inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER))
150
+ configuration.namespace
177
151
  end
178
152
 
179
153
  def namespace_name
@@ -185,7 +159,7 @@ module Hanami
185
159
  end
186
160
 
187
161
  def application_name
188
- inflector.underscore(namespace).to_sym
162
+ configuration.application_name
189
163
  end
190
164
 
191
165
  def root
@@ -198,10 +172,10 @@ module Hanami
198
172
 
199
173
  # @api private
200
174
  def component_provider(component)
201
- raise "Hanami.application must be inited before detecting providers" unless inited?
175
+ raise "Hanami.application must be prepared before detecting providers" unless prepared?
202
176
 
203
- # [Admin, Main, MyApp] or [MyApp::Admin, MyApp::Main, MyApp]
204
- providers = slices.values + [self]
177
+ # e.g. [Admin, Main, MyApp]
178
+ providers = slices.to_a + [self]
205
179
 
206
180
  component_class = component.is_a?(Class) ? component : component.class
207
181
  component_name = component_class.name
@@ -211,41 +185,6 @@ module Hanami
211
185
  providers.detect { |provider| component_name.include?(provider.namespace.to_s) }
212
186
  end
213
187
 
214
- def router
215
- @_mutex.synchronize do
216
- @_router ||= load_router
217
- end
218
- end
219
-
220
- def load_router
221
- Router.new(
222
- routes: routes,
223
- resolver: resolver,
224
- **configuration.router.options,
225
- ) do
226
- use Hanami.application[:rack_monitor]
227
-
228
- Hanami.application.config.for_each_middleware do |m, *args, &block|
229
- use(m, *args, &block)
230
- end
231
- end
232
- end
233
-
234
- def routes
235
- require File.join(configuration.root, configuration.router.routes_path)
236
- routes_class = autodiscover_application_constant(configuration.router.routes_class_name)
237
- routes_class.routes
238
- rescue LoadError
239
- proc {}
240
- end
241
-
242
- def resolver
243
- config.router.resolver.new(
244
- slices: slices,
245
- inflector: inflector
246
- )
247
- end
248
-
249
188
  private
250
189
 
251
190
  def prepare_base_load_path
@@ -253,96 +192,63 @@ module Hanami
253
192
  $LOAD_PATH.unshift base_path unless $LOAD_PATH.include?(base_path)
254
193
  end
255
194
 
256
- def prepare_container
257
- define_container.tap do |container|
258
- configure_container container
259
- end
260
- end
261
-
262
- def prepare_deps_module
263
- define_deps_module
195
+ def prepare_all
196
+ load_settings
197
+ prepare_container_plugins
198
+ prepare_container_base_config
199
+ prepare_container_consts
200
+ container.configured!
201
+ prepare_slices
202
+ # For the application, the autoloader must be prepared after the slices, since
203
+ # they'll be configuring the autoloader with their own dirs
204
+ prepare_autoloader
264
205
  end
265
206
 
266
- def define_container
267
- require "#{application_name}/container"
268
- namespace.const_get :Container
269
- rescue LoadError, NameError
270
- namespace.const_set :Container, Class.new(Dry::System::Container)
207
+ def prepare_container_plugins
208
+ container.use(:env, inferrer: -> { Hanami.env })
209
+ container.use(:zeitwerk, loader: autoloader, run_setup: false, eager_load: false)
210
+ container.use(:notifications)
271
211
  end
272
212
 
273
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
274
- def configure_container(container)
275
- container.use :env, inferrer: -> { Hanami.env }
276
- container.use :notifications
277
-
278
- container.configure do |config|
279
- config.inflector = configuration.inflector
213
+ def prepare_container_base_config
214
+ container.config.root = configuration.root
215
+ container.config.inflector = configuration.inflector
280
216
 
281
- config.root = configuration.root
282
- config.bootable_dirs = [
283
- "config/boot",
284
- Pathname(__dir__).join("application/container/boot").realpath,
285
- ]
286
-
287
- config.component_dirs.loader = Dry::System::Loader::Autoloading
288
- config.component_dirs.add_to_load_path = false
289
- end
217
+ container.config.provider_dirs = [
218
+ "config/providers",
219
+ Pathname(__dir__).join("application/container/providers").realpath,
220
+ ]
221
+ end
290
222
 
223
+ def prepare_autoload_paths
291
224
  # Autoload classes defined in lib/[app_namespace]/
292
225
  if root.join("lib", namespace_path).directory?
293
226
  autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
294
227
  end
295
-
296
- # Add lib/ to to the $LOAD_PATH so other files there (outside the app namespace)
297
- # are require-able
298
- container.add_to_load_path!("lib") if root.join("lib").directory?
299
-
300
- container
301
228
  end
302
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
303
229
 
304
- def define_deps_module
305
- require "#{application_name}/deps"
306
- namespace.const_get :Deps
307
- rescue LoadError, NameError
230
+ def prepare_container_consts
231
+ namespace.const_set :Container, container
308
232
  namespace.const_set :Deps, container.injector
309
233
  end
310
234
 
311
- def load_slices
312
- Dir[File.join(slices_path, "*")]
313
- .select(&File.method(:directory?))
314
- .each(&method(:load_slice))
315
- end
316
-
317
- def slices_path
318
- File.join(root, config.slices_dir)
235
+ def prepare_slices
236
+ slices.load_slices.each(&:prepare)
237
+ slices.freeze
319
238
  end
320
239
 
321
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
322
- def load_slice(slice_path)
323
- slice_path = Pathname(slice_path)
324
-
325
- slice_name = slice_path.relative_path_from(Pathname(slices_path)).to_s
326
- slice_const_name = inflector.camelize(slice_name)
327
-
328
- if config.slices_namespace.const_defined?(slice_const_name)
329
- slice_module = config.slices_namespace.const_get(slice_const_name)
330
-
331
- raise "Cannot use slice +#{slice_const_name}+ since it is not a module" unless slice_module.is_a?(Module)
332
- else
333
- slice_module = Module.new
334
- config.slices_namespace.const_set inflector.camelize(slice_name), slice_module
240
+ def prepare_autoloader
241
+ # Autoload classes defined in lib/[app_namespace]/
242
+ if root.join("lib", namespace_path).directory?
243
+ autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
335
244
  end
336
245
 
337
- register_slice(
338
- slice_name,
339
- namespace: slice_module,
340
- root: slice_path.realpath
341
- )
246
+ autoloader.setup
342
247
  end
343
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
344
248
 
345
249
  def load_settings
250
+ require_relative "application/settings"
251
+
346
252
  prepare_base_load_path
347
253
  require File.join(configuration.root, configuration.settings_path)
348
254
  settings_class = autodiscover_application_constant(configuration.settings_class_name)
@@ -354,24 +260,40 @@ module Hanami
354
260
  def autodiscover_application_constant(constants)
355
261
  inflector.constantize([namespace_name, *constants].join(MODULE_DELIMITER))
356
262
  end
357
- end
358
- # rubocop:enable Metrics/ModuleLength
359
263
 
360
- # Application instance interface
361
- module InstanceMethods
362
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
363
- def initialize(application = self.class)
264
+ def load_router
364
265
  require_relative "application/router"
365
266
 
366
- application.boot
267
+ Router.new(
268
+ routes: load_routes,
269
+ resolver: router_resolver,
270
+ **configuration.router.options,
271
+ ) do
272
+ use Hanami.application[:rack_monitor]
273
+
274
+ Hanami.application.config.for_each_middleware do |m, *args, &block|
275
+ use(m, *args, &block)
276
+ end
277
+ end
278
+ end
279
+
280
+ def load_routes
281
+ require_relative "application/routes"
367
282
 
368
- @app = application.router.to_rack_app
283
+ require File.join(configuration.root, configuration.router.routes_path)
284
+ routes_class = autodiscover_application_constant(configuration.router.routes_class_name)
285
+ routes_class.routes
286
+ rescue LoadError
287
+ proc {}
369
288
  end
370
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
371
289
 
372
- def call(env)
373
- @app.call(env)
290
+ def router_resolver
291
+ config.router.resolver.new(
292
+ slices: slices,
293
+ inflector: inflector
294
+ )
374
295
  end
375
296
  end
297
+ # rubocop:enable Metrics/ModuleLength
376
298
  end
377
299
  end
@@ -0,0 +1,44 @@
1
+ module Hanami
2
+ module Boot
3
+ module SourceDirs
4
+ def self.setup_component_dir!(component_dir, slice, container)
5
+ # TODO: this `== "lib"` check should be codified into a method somewhere
6
+ if component_dir.path == "lib"
7
+ # Expect component files in the root of the lib
8
+ # component dir to define classes inside the slice's namespace.
9
+ #
10
+ # e.g. "lib/foo.rb" should define SliceNamespace::Foo, and will be
11
+ # registered as "foo"
12
+ component_dir.namespaces.root(key: nil, const: slice.namespace_path)
13
+
14
+ slice.application.autoloader.push_dir(slice.root.join("lib"), namespace: slice.namespace)
15
+
16
+ container.config.component_dirs.add(component_dir)
17
+ else
18
+ # Expect component files in the root of these component dirs to define
19
+ # classes inside a namespace matching the dir.
20
+ #
21
+ # e.g. "actions/foo.rb" should define SliceNamespace::Actions::Foo, and
22
+ # will be registered as "actions.foo"
23
+
24
+ dir_namespace_path = File.join(slice.namespace_path, component_dir.path)
25
+
26
+ autoloader_namespace = begin
27
+ slice.inflector.constantize(slice.inflector.camelize(dir_namespace_path))
28
+ rescue NameError
29
+ slice.namespace.const_set(slice.inflector.camelize(component_dir.path), Module.new)
30
+ end
31
+
32
+ # TODO: do we need to do something special to clear out any previously configured root namespace here?
33
+ component_dir.namespaces.root(const: dir_namespace_path, key: component_dir.path) # TODO: do we need to swap path delimiters for key delimiters here?
34
+ container.config.component_dirs.add(component_dir)
35
+
36
+ slice.application.autoloader.push_dir(
37
+ slice.root.join(component_dir.path),
38
+ namespace: autoloader_namespace
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -26,7 +26,7 @@ module Hanami
26
26
  # of the boot process respect the provided env
27
27
  ENV["HANAMI_ENV"] = arguments[:env] if arguments[:env]
28
28
 
29
- require "hanami/init"
29
+ require "hanami/prepare"
30
30
  application = Hanami.application
31
31
 
32
32
  [command.with_application(application), arguments]
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require_relative "application_configuration/cookies"
4
+ # require_relative "application_configuration/sessions"
5
+ # require_relative "application_configuration/content_security_policy"
6
+ # require_relative "configuration"
7
+ # require_relative "view_name_inferrer"
8
+
9
+ module Hanami
10
+ class Configuration
11
+ class Actions
12
+ include Dry::Configurable
13
+
14
+ setting :cookies, default: {}, constructor: -> options { Cookies.new(options) }
15
+ setting :sessions, constructor: proc { |storage, *options| Sessions.new(storage, *options) }
16
+ setting :csrf_protection
17
+
18
+ setting :name_inference_base, default: "actions"
19
+ setting :view_context_identifier, default: "view.context"
20
+ setting :view_name_inferrer, default: ViewNameInferrer
21
+ setting :view_name_inference_base, default: "views"
22
+
23
+ attr_accessor :content_security_policy
24
+
25
+ def initialize(*, **options)
26
+ super()
27
+
28
+ @base_configuration = Configuration.new
29
+ @content_security_policy = ContentSecurityPolicy.new do |csp|
30
+ if assets_server_url = options[:assets_server_url]
31
+ csp[:script_src] += " #{assets_server_url}"
32
+ csp[:style_src] += " #{assets_server_url}"
33
+ end
34
+ end
35
+
36
+ configure_defaults
37
+ end
38
+
39
+ def finalize!
40
+ # A nil value for `csrf_protection` means it has not been explicitly configured
41
+ # (neither true nor false), so we can default it to whether sessions are enabled
42
+ self.csrf_protection = sessions.enabled? if csrf_protection.nil?
43
+
44
+ if self.content_security_policy
45
+ self.default_headers["Content-Security-Policy"] = self.content_security_policy.to_str
46
+ end
47
+ end
48
+
49
+ # Returns the list of available settings
50
+ #
51
+ # @return [Set]
52
+ #
53
+ # @since 2.0.0
54
+ # @api private
55
+ def settings
56
+ base_configuration.settings + self.class.settings
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :base_configuration
62
+
63
+ # Apply defaults for base configuration settings
64
+ def configure_defaults
65
+ self.default_request_format = :html
66
+ self.default_response_format = :html
67
+
68
+ self.default_headers = {
69
+ "X-Frame-Options" => "DENY",
70
+ "X-Content-Type-Options" => "nosniff",
71
+ "X-XSS-Protection" => "1; mode=block"
72
+ }
73
+ end
74
+
75
+ def method_missing(name, *args, &block)
76
+ if config.respond_to?(name)
77
+ config.public_send(name, *args, &block)
78
+ elsif base_configuration.respond_to?(name)
79
+ base_configuration.public_send(name, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def respond_to_missing?(name, _incude_all = false)
86
+ config.respond_to?(name) || base_configuration.respond_to?(name) || super
87
+ end
88
+ end
89
+ end
90
+ end
@@ -13,14 +13,58 @@ module Hanami
13
13
 
14
14
  protected :config
15
15
 
16
+ setting :application_name
17
+
18
+ setting :level
19
+
20
+ setting :stream
21
+
22
+ setting :formatter
23
+
24
+ setting :colors
25
+
26
+ setting :filters, default: %w[_csrf password password_confirmation].freeze
27
+
28
+ setting :options, default: [], constructor: ->(value) { Array(value).flatten }, cloneable: true
29
+
16
30
  setting :logger_class, default: Hanami::Logger
17
31
 
18
- setting :options, default: {level: :debug}
32
+ def initialize(env:, application_name:)
33
+ @env = env
34
+ @application_name = application_name
35
+
36
+ config.level = case env
37
+ when :production
38
+ :info
39
+ else
40
+ :debug
41
+ end
19
42
 
20
- # Currently used for logging of Rack requests only.
21
- #
22
- # TODO: incorporate this into the standard logging some way or another
23
- setting :filter_params, default: %w[_csrf password password_confirmation].freeze
43
+ config.stream = case env
44
+ when :test
45
+ File.join("log", "#{env}.log")
46
+ else
47
+ $stdout
48
+ end
49
+
50
+ config.formatter = case env
51
+ when :production
52
+ :json
53
+ end
54
+
55
+ config.colors = case env
56
+ when :production, :test
57
+ false
58
+ end
59
+ end
60
+
61
+ def finalize!
62
+ config.application_name = @application_name.call
63
+ end
64
+
65
+ def instance
66
+ logger_class.new(application_name, *options, stream: stream, level: level, formatter: formatter, filter: filters, colorizer: colors)
67
+ end
24
68
 
25
69
  private
26
70