hanami 2.0.0.alpha4 → 2.0.0.alpha7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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