hanami 2.0.0.alpha6 → 2.0.0.alpha7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fc76408b7d97db32194d24bf73739bde8405fbd13855b7e4ea744f634d0739e
4
- data.tar.gz: 30d954f8332ea7579898a87187a89a0ae2a1b34b5d9f564a86b6f61c0844f2af
3
+ metadata.gz: d8a7910a3dd4b6e7d82711df4f181de1e8c63fbb91981542d2eb067d2cfe06b4
4
+ data.tar.gz: 2d09e35743119e1e630f196780df68cf7b7685c2d6e3208fe0c7804816ccb45f
5
5
  SHA512:
6
- metadata.gz: f64124d027449d451f5be703f4c00850c826e4d55ebef1e6c396be63dea59a9019cb74438f7844f1e68a187299ea45c7f89ba1e98267380e7d8e1b1f8666e4b9
7
- data.tar.gz: 8ea32aae47f0fe9536726c977af03904fa23f7258ef747b2cdd1a3b324eb064466beccc9340b7dddfb46d3d6a49c8aba899b1b488c6016377581599efd594577
6
+ metadata.gz: 51ecc6d32c036bfc2fae43498ca6f88bb937317e610d24e499a069b02ff4c8a49b253684ba4fe47d6f67e2c2fc2b0a989460fc1462b397ddd1a5ec539ace2b7c
7
+ data.tar.gz: aa3a1f17f9304605135391c3cb915f61cabbd5cbb2e4351e70be122353d36a85af979bbf4710ffdbfa27e1e77aea023f3de7ddda2b547399e1d96b396134da9c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,65 @@
1
1
  # Hanami
2
2
  The web, with simplicity.
3
3
 
4
+ ## v2.0.0.alpha7 - 2020-03-08
5
+
6
+ ## Added
7
+ - [Tim Riley] Introduced `Hanami::ApplicationLoadError` and `Hanami::SliceLoadError` exceptions to represent errors encountered during application and slice loading.
8
+ - [Tim Riley] `Hanami::Slice.shutdown` can be used to stop all the providers in a slice
9
+
10
+ ## Changed
11
+ - [Tim Riley] Slices are now represented as concrete classes (such as `Main::Slice`) inheriting from `Hanami::Slice`, as opposed to _instances_ of `Hanami::Slice`. You may create your own definitions for these slices in `config/slices/[slice_name].rb`, which you can then use for customising per-slice config and behavior, e.g.
12
+
13
+ ```ruby
14
+ # config/slices/main.rb:
15
+
16
+ module Main
17
+ class Slice < Hanami::Slice
18
+ # slice config here
19
+ end
20
+ end
21
+ ```
22
+ - [Tim Riley] Application-level `config.slice(slice_name, &block)` setting has been removed in favour of slice configuration within concrete slice class definitions
23
+ - [Tim Riley] You can configure your slice imports inside your slice classes, e.g.
24
+
25
+ ```ruby
26
+ # config/slices/main.rb:
27
+
28
+ module Main
29
+ class Slice < Hanami::Slice
30
+ # Import all exported components from "search" slice
31
+ import from: :search
32
+ end
33
+ end
34
+ ```
35
+ - [Tim Riley] You can configure your slice exports inside your slice classes, e.g.
36
+
37
+ ```ruby
38
+ # config/slices/search.rb:
39
+
40
+ module Search
41
+ class Slice < Hanami::Slice
42
+ # Export the "index_entity" component only
43
+ export ["index_entity"]
44
+ end
45
+ end
46
+ ```
47
+ - [Tim Riley] For advanced cases, you can configure your slice's container via a `prepare_container` block:
48
+
49
+ ```ruby
50
+ # config/slices/search.rb:
51
+
52
+ module Search
53
+ class Slice < Hanami::Slice
54
+ prepare_container do |container|
55
+ # `container` object is available here, with
56
+ # slice-specific configuration already applied
57
+ end
58
+ end
59
+ end
60
+ ```
61
+ - [Tim Riley] `Hanami::Application.shutdown` will now also shutdown all registered slices
62
+
4
63
  ## v2.0.0.alpha6 - 2022-02-10
5
64
  ### Added
6
65
  - [Luca Guidi] Official support for Ruby: MRI 3.1
@@ -0,0 +1,36 @@
1
+ # # frozen_string_literal: true
2
+
3
+ # require "hanami/application/router"
4
+
5
+ # Hanami.application.register_provider :router do
6
+ # start do
7
+ # configuration = Hanami.application.configuration
8
+
9
+ # routes = begin
10
+ # require File.join(configuration.root, configuration.router.routes_path)
11
+ # routes_class = Hanami.application.send(:autodiscover_application_constant, configuration.router.routes_class_name) # WIP private
12
+ # routes_class.routes
13
+ # rescue LoadError
14
+ # proc {}
15
+ # end
16
+
17
+ # resolver = configuration.router.resolver.new(
18
+ # slices: Hanami.application.slices,
19
+ # inflector: Hanami.application.inflector # TODO: use container[:inflector]?
20
+ # )
21
+
22
+ # router = Hanami::Application::Router.new(
23
+ # routes: routes,
24
+ # resolver: resolver,
25
+ # **configuration.router.options,
26
+ # ) do
27
+ # use Hanami.application[:rack_monitor]
28
+
29
+ # Hanami.application.config.for_each_middleware do |m, *args, &block|
30
+ # use(m, *args, &block)
31
+ # end
32
+ # end
33
+
34
+ # register :router, router
35
+ # end
36
+ # end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+ require_relative "../slice"
5
+
6
+ module Hanami
7
+ class Application
8
+ # @api private
9
+ class SliceRegistrar
10
+ attr_reader :application, :slices
11
+ private :application, :slices
12
+
13
+ def initialize(application)
14
+ @application = application
15
+ @slices = {}
16
+ end
17
+
18
+ def register(name, slice_class = nil, &block)
19
+ if slices.key?(name.to_sym)
20
+ raise SliceLoadError, "Slice '#{name}' is already registered"
21
+ end
22
+
23
+ # TODO: raise error unless name meets format (i.e. single level depth only)
24
+
25
+ slices[name.to_sym] = slice_class || build_slice(name, &block)
26
+ end
27
+
28
+ def [](name)
29
+ slices.fetch(name) do
30
+ raise SliceLoadError, "Slice '#{name}' not found"
31
+ end
32
+ end
33
+
34
+ def freeze
35
+ slices.freeze
36
+ super
37
+ end
38
+
39
+ def load_slices
40
+ slice_configs = Dir[root.join(CONFIG_DIR, "slices", "*#{RB_EXT}")]
41
+ .map { |file| File.basename(file, RB_EXT) }
42
+
43
+ slice_dirs = Dir[File.join(root, SLICES_DIR, "*")]
44
+ .select { |path| File.directory?(path) }
45
+ .map { |path| File.basename(path) }
46
+
47
+ (slice_dirs + slice_configs).uniq.sort.each do |slice_name|
48
+ load_slice(slice_name)
49
+ end
50
+
51
+ self
52
+ end
53
+
54
+ def each(&block)
55
+ slices.each_value(&block)
56
+ end
57
+
58
+ def to_a
59
+ slices.values
60
+ end
61
+
62
+ private
63
+
64
+ # Attempts to load a slice class defined in `config/slices/[slice_name].rb`, then
65
+ # registers the slice with the matching class, if found.
66
+ def load_slice(slice_name)
67
+ slice_const_name = inflector.camelize(slice_name)
68
+ slice_require_path = root.join("config", "slices", slice_name).to_s
69
+
70
+ begin
71
+ require(slice_require_path)
72
+ rescue LoadError => e
73
+ raise e unless e.path == slice_require_path
74
+ end
75
+
76
+ slice_class =
77
+ begin
78
+ inflector.constantize("#{slice_const_name}::Slice")
79
+ rescue NameError => e
80
+ raise e unless e.name == :Slice
81
+ end
82
+
83
+ register(slice_name, slice_class)
84
+ end
85
+
86
+ def build_slice(slice_name, &block)
87
+ slice_module =
88
+ begin
89
+ slice_module_name = inflector.camelize(slice_name.to_s)
90
+ inflector.constantize(slice_module_name)
91
+ rescue NameError
92
+ Object.const_set(inflector.camelize(slice_module_name), Module.new)
93
+ end
94
+
95
+ slice_module.const_set(:Slice, Class.new(Hanami::Slice, &block))
96
+ end
97
+
98
+ def root
99
+ application.root
100
+ end
101
+
102
+ def inflector
103
+ application.inflector
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,12 +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/slice_registrar"
10
11
 
11
12
  module Hanami
12
13
  # Hanami application class
@@ -20,8 +21,10 @@ module Hanami
20
21
  super
21
22
  @_mutex.synchronize do
22
23
  klass.class_eval do
23
- @_mutex = Mutex.new
24
+ @_mutex = Mutex.new
24
25
  @_configuration = Hanami::Configuration.new(application_name: name, env: Hanami.env)
26
+ @autoloader = Zeitwerk::Loader.new
27
+ @container = Class.new(Dry::System::Container)
25
28
 
26
29
  extend ClassMethods
27
30
  end
@@ -37,6 +40,8 @@ module Hanami
37
40
  #
38
41
  # rubocop:disable Metrics/ModuleLength
39
42
  module ClassMethods
43
+ attr_reader :autoloader, :container
44
+
40
45
  def self.extended(klass)
41
46
  klass.class_eval do
42
47
  @prepared = @booted = false
@@ -50,26 +55,13 @@ module Hanami
50
55
  alias_method :config, :configuration
51
56
 
52
57
  def prepare(provider_name = nil)
53
- if provider_name
54
- container.prepare(provider_name)
55
- return self
56
- end
58
+ container.prepare(provider_name) and return self if provider_name
57
59
 
58
60
  return self if prepared?
59
61
 
60
62
  configuration.finalize!
61
63
 
62
- load_settings
63
-
64
- @autoloader = Zeitwerk::Loader.new
65
- @container = prepare_container
66
- @deps_module = prepare_deps_module
67
-
68
- load_slices
69
- slices.each_value(&:prepare)
70
- slices.freeze
71
-
72
- @autoloader.setup
64
+ prepare_all
73
65
 
74
66
  @prepared = true
75
67
  self
@@ -82,40 +74,24 @@ module Hanami
82
74
 
83
75
  container.finalize!(&block)
84
76
 
85
- slices.values.each(&:boot)
77
+ slices.each(&:boot)
86
78
 
87
79
  @booted = true
88
80
  self
89
81
  end
90
82
 
91
83
  def shutdown
84
+ slices.each(&:shutdown)
92
85
  container.shutdown!
86
+ self
93
87
  end
94
88
 
95
89
  def prepared?
96
- @prepared
90
+ !!@prepared
97
91
  end
98
92
 
99
93
  def booted?
100
- @booted
101
- end
102
-
103
- def autoloader
104
- raise "Application not yet prepared" unless defined?(@autoloader)
105
-
106
- @autoloader
107
- end
108
-
109
- def container
110
- raise "Application not yet prepared" unless defined?(@container)
111
-
112
- @container
113
- end
114
-
115
- def deps
116
- raise "Application not yet prepared" unless defined?(@deps_module)
117
-
118
- @deps_module
94
+ !!@booted
119
95
  end
120
96
 
121
97
  def router
@@ -131,15 +107,11 @@ module Hanami
131
107
  end
132
108
 
133
109
  def slices
134
- @slices ||= {}
110
+ @slices ||= SliceRegistrar.new(self)
135
111
  end
136
112
 
137
- def register_slice(name, **slice_args)
138
- raise "Slice +#{name}+ already registered" if slices.key?(name.to_sym)
139
-
140
- slice = Slice.new(self, name: name, **slice_args)
141
- slice.namespace.const_set :Slice, slice if slice.namespace # rubocop:disable Style/SafeNavigation
142
- slices[name.to_sym] = slice
113
+ def register_slice(...)
114
+ slices.register(...)
143
115
  end
144
116
 
145
117
  def register(...)
@@ -202,8 +174,8 @@ module Hanami
202
174
  def component_provider(component)
203
175
  raise "Hanami.application must be prepared before detecting providers" unless prepared?
204
176
 
205
- # [Admin, Main, MyApp] or [MyApp::Admin, MyApp::Main, MyApp]
206
- providers = slices.values + [self]
177
+ # e.g. [Admin, Main, MyApp]
178
+ providers = slices.to_a + [self]
207
179
 
208
180
  component_class = component.is_a?(Class) ? component : component.class
209
181
  component_name = component_class.name
@@ -220,20 +192,25 @@ module Hanami
220
192
  $LOAD_PATH.unshift base_path unless $LOAD_PATH.include?(base_path)
221
193
  end
222
194
 
223
- # rubocop:disable Metrics/AbcSize
224
- def prepare_container
225
- container =
226
- begin
227
- require "#{application_name}/container"
228
- namespace.const_get :Container
229
- rescue LoadError, NameError
230
- namespace.const_set :Container, Class.new(Dry::System::Container)
231
- end
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
205
+ end
232
206
 
233
- container.use :env, inferrer: -> { Hanami.env }
234
- container.use :zeitwerk, loader: autoloader, run_setup: false, eager_load: false
235
- container.use :notifications
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)
211
+ end
236
212
 
213
+ def prepare_container_base_config
237
214
  container.config.root = configuration.root
238
215
  container.config.inflector = configuration.inflector
239
216
 
@@ -241,63 +218,32 @@ module Hanami
241
218
  "config/providers",
242
219
  Pathname(__dir__).join("application/container/providers").realpath,
243
220
  ]
221
+ end
244
222
 
223
+ def prepare_autoload_paths
245
224
  # Autoload classes defined in lib/[app_namespace]/
246
225
  if root.join("lib", namespace_path).directory?
247
- container.autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
226
+ autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
248
227
  end
249
-
250
- # Add lib/ to to the $LOAD_PATH so any files there (outside the app namespace) can
251
- # be required
252
- container.add_to_load_path!("lib") if root.join("lib").directory?
253
-
254
- container.configured!
255
-
256
- container
257
- end
258
- # rubocop:enable Metrics/AbcSize
259
-
260
- def prepare_deps_module
261
- define_deps_module
262
228
  end
263
229
 
264
- def define_deps_module
265
- require "#{application_name}/deps"
266
- namespace.const_get :Deps
267
- rescue LoadError, NameError
230
+ def prepare_container_consts
231
+ namespace.const_set :Container, container
268
232
  namespace.const_set :Deps, container.injector
269
233
  end
270
234
 
271
- def load_slices
272
- Dir[File.join(slices_path, "*")]
273
- .select(&File.method(:directory?))
274
- .each(&method(:load_slice))
275
- end
276
-
277
- def slices_path
278
- File.join(root, config.slices_dir)
235
+ def prepare_slices
236
+ slices.load_slices.each(&:prepare)
237
+ slices.freeze
279
238
  end
280
239
 
281
- def load_slice(slice_path)
282
- slice_path = Pathname(slice_path)
283
-
284
- slice_name = slice_path.relative_path_from(Pathname(slices_path)).to_s
285
- slice_const_name = inflector.camelize(slice_name)
286
-
287
- if config.slices_namespace.const_defined?(slice_const_name)
288
- slice_module = config.slices_namespace.const_get(slice_const_name)
289
-
290
- raise "Cannot use slice +#{slice_const_name}+ since it is not a module" unless slice_module.is_a?(Module)
291
- else
292
- slice_module = Module.new
293
- 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)
294
244
  end
295
245
 
296
- register_slice(
297
- slice_name,
298
- namespace: slice_module,
299
- root: slice_path.realpath
300
- )
246
+ autoloader.setup
301
247
  end
302
248
 
303
249
  def load_settings
@@ -311,9 +257,6 @@ module Hanami
311
257
  Settings.new
312
258
  end
313
259
 
314
- MODULE_DELIMITER = "::"
315
- private_constant :MODULE_DELIMITER
316
-
317
260
  def autodiscover_application_constant(constants)
318
261
  inflector.constantize([namespace_name, *constants].join(MODULE_DELIMITER))
319
262
  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
@@ -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,6 +13,7 @@ require_relative "configuration/middleware"
13
13
  require_relative "configuration/router"
14
14
  require_relative "configuration/sessions"
15
15
  require_relative "configuration/source_dirs"
16
+ require_relative "constants"
16
17
 
17
18
  module Hanami
18
19
  # Hanami application configuration
@@ -26,8 +27,7 @@ module Hanami
26
27
  DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
27
28
  private_constant :DEFAULT_ENVIRONMENTS
28
29
 
29
- MODULE_DELIMITER = "::"
30
- private_constant :MODULE_DELIMITER
30
+ attr_reader :env
31
31
 
32
32
  attr_reader :actions
33
33
  attr_reader :middleware
@@ -41,7 +41,7 @@ module Hanami
41
41
  @namespace = application_name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER)
42
42
 
43
43
  @environments = DEFAULT_ENVIRONMENTS.clone
44
- config.env = env
44
+ @env = env
45
45
 
46
46
  # Some default setting values must be assigned at initialize-time to ensure they
47
47
  # have appropriate values for the current application
@@ -117,13 +117,6 @@ module Hanami
117
117
  inflector.underscore(@namespace).to_sym
118
118
  end
119
119
 
120
- setting :env
121
-
122
- def env=(new_env)
123
- config.env = env
124
- apply_env_config(new_env)
125
- end
126
-
127
120
  setting :root, constructor: -> path { Pathname(path) }
128
121
 
129
122
  setting :inflector, default: Dry::Inflector.new
@@ -148,20 +141,8 @@ module Hanami
148
141
 
149
142
  setting :settings_store, default: Application::Settings::DotenvStore
150
143
 
151
- setting :slices_dir, default: "slices"
152
-
153
- setting :slices_namespace, default: Object
154
-
155
- # TODO: convert into a dedicated object with explicit behaviour around blocks per
156
- # slice, etc.
157
- setting :slices, default: {}, constructor: :dup.to_proc
158
-
159
144
  setting :source_dirs, default: Configuration::SourceDirs.new, cloneable: true
160
145
 
161
- def slice(slice_name, &block)
162
- slices[slice_name] = block
163
- end
164
-
165
146
  setting :base_url, default: "http://0.0.0.0:2300", constructor: -> url { URI(url) }
166
147
 
167
148
  def for_each_middleware(&blk)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ # @api private
5
+ MODULE_DELIMITER = "::"
6
+ private_constant :MODULE_DELIMITER
7
+
8
+ # @api private
9
+ CONFIG_DIR = "config"
10
+ private_constant :CONFIG_DIR
11
+
12
+ # @api private
13
+ SLICES_DIR = "slices"
14
+ private_constant :SLICES_DIR
15
+
16
+ # @api private
17
+ LIB_DIR = "lib"
18
+ private_constant :LIB_DIR
19
+
20
+ # @api private
21
+ RB_EXT = ".rb"
22
+ private_constant :RB_EXT
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ # @since 2.0.0
5
+ Error = Class.new(StandardError)
6
+
7
+ # @since 2.0.0
8
+ ApplicationLoadError = Class.new(Error)
9
+
10
+ # @since 2.0.0
11
+ SliceLoadError = Class.new(Error)
12
+ end
data/lib/hanami/slice.rb CHANGED
@@ -1,130 +1,190 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/container"
4
+ require "hanami/errors"
4
5
  require "pathname"
6
+ require_relative "constants"
5
7
 
6
8
  module Hanami
7
9
  # Distinct area of concern within an Hanami application
8
10
  #
9
11
  # @since 2.0.0
10
12
  class Slice
11
- attr_reader :application, :name, :namespace, :root
12
-
13
- def initialize(application, name:, namespace: nil, root: nil, container: nil)
14
- @application = application
15
- @name = name.to_sym
16
- @namespace = namespace
17
- @root = root ? Pathname(root) : root
18
- @container = container
19
- end
13
+ def self.inherited(klass)
14
+ super
20
15
 
21
- def inflector
22
- application.inflector
23
- end
16
+ klass.extend(ClassMethods)
24
17
 
25
- def namespace_path
26
- @namespace_path ||= inflector.underscore(namespace.to_s)
18
+ # Eagerly initialize any variables that may be accessed inside the subclass body
19
+ klass.instance_variable_set(:@application, Hanami.application)
20
+ klass.instance_variable_set(:@container, Class.new(Dry::System::Container))
27
21
  end
28
22
 
29
- def prepare(provider_name = nil)
30
- if provider_name
31
- container.prepare(provider_name)
32
- return self
23
+ # rubocop:disable Metrics/ModuleLength
24
+ module ClassMethods
25
+ attr_reader :application, :container
26
+
27
+ def slice_name
28
+ inflector.underscore(name.split(MODULE_DELIMITER)[-2]).to_sym
33
29
  end
34
30
 
35
- @container ||= define_container
31
+ def namespace
32
+ inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER))
33
+ end
36
34
 
37
- container.import from: application.container, as: :application
35
+ def namespace_path
36
+ inflector.underscore(namespace)
37
+ end
38
38
 
39
- slice_block = application.configuration.slices[name]
40
- instance_eval(&slice_block) if slice_block
39
+ def root
40
+ application.root.join(SLICES_DIR, slice_name.to_s)
41
+ end
41
42
 
42
- # This is here and not inside define_container to allow for the slice block to
43
- # interact with container config
44
- container.configured!
43
+ def inflector
44
+ application.inflector
45
+ end
45
46
 
46
- self
47
- end
47
+ def prepare(provider_name = nil)
48
+ container.prepare(provider_name) and return self if provider_name
48
49
 
49
- def boot
50
- container.finalize!
50
+ return self if prepared?
51
51
 
52
- @booted = true
53
- self
54
- end
52
+ ensure_slice_name
53
+ ensure_slice_consts
55
54
 
56
- # rubocop:disable Style/DoubleNegation
57
- def booted?
58
- !!@booted
59
- end
60
- # rubocop:enable Style/DoubleNegation
55
+ prepare_all
61
56
 
62
- def container
63
- @container ||= define_container
64
- end
57
+ @prepared = true
58
+ self
59
+ end
60
+
61
+ def prepare_container(&block)
62
+ @prepare_container_block = block
63
+ end
64
+
65
+ def boot
66
+ return self if booted?
67
+
68
+ container.finalize!
65
69
 
66
- def import(from:, **kwargs)
67
- # TODO: This should be handled via dry-system (see dry-rb/dry-system#228)
68
- raise "Cannot import after booting" if booted?
70
+ @booted = true
69
71
 
70
- if from.is_a?(Symbol) || from.is_a?(String)
71
- slice_name = from
72
- # TODO: better error than the KeyError from fetch if the slice doesn't exist
73
- from = application.slices.fetch(from.to_sym).container
72
+ self
74
73
  end
75
74
 
76
- as = kwargs[:as] || slice_name
75
+ def shutdown
76
+ container.shutdown!
77
+ self
78
+ end
77
79
 
78
- container.import(from: from, as: as, **kwargs)
79
- end
80
+ def prepared?
81
+ !!@prepared
82
+ end
80
83
 
81
- def register(*args, &block)
82
- container.register(*args, &block)
83
- end
84
+ def booted?
85
+ !!@booted
86
+ end
84
87
 
85
- def register_provider(...)
86
- container.register_provider(...)
87
- end
88
+ def register(...)
89
+ container.register(...)
90
+ end
88
91
 
89
- def start(...)
90
- container.start(...)
91
- end
92
+ def register_provider(...)
93
+ container.register_provider(...)
94
+ end
92
95
 
93
- def key?(...)
94
- container.key?(...)
95
- end
96
+ def start(...)
97
+ container.start(...)
98
+ end
96
99
 
97
- def keys
98
- container.keys
99
- end
100
+ def key?(...)
101
+ container.key?(...)
102
+ end
100
103
 
101
- def [](...)
102
- container.[](...)
103
- end
104
+ def keys
105
+ container.keys
106
+ end
104
107
 
105
- def resolve(...)
106
- container.resolve(...)
107
- end
108
+ def [](...)
109
+ container.[](...)
110
+ end
111
+
112
+ def resolve(...)
113
+ container.resolve(...)
114
+ end
115
+
116
+ def export(keys)
117
+ container.config.exports = keys
118
+ end
119
+
120
+ def import(from:, **kwargs)
121
+ # TODO: This should be handled via dry-system (see dry-rb/dry-system#228)
122
+ raise "Cannot import after booting" if booted?
123
+
124
+ application = self.application
125
+
126
+ container.after(:configure) do
127
+ if from.is_a?(Symbol) || from.is_a?(String)
128
+ slice_name = from
129
+ from = application.slices[from.to_sym].container
130
+ end
131
+
132
+ as = kwargs[:as] || slice_name
133
+
134
+ import(from: from, as: as, **kwargs)
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def ensure_slice_name
141
+ unless name
142
+ raise SliceLoadError, "Slice must have a class name before it can be prepared"
143
+ end
144
+ end
108
145
 
109
- private
146
+ def ensure_slice_consts
147
+ if namespace.const_defined?(:Container) || namespace.const_defined?(:Deps)
148
+ raise(
149
+ SliceLoadError,
150
+ "#{namespace}::Container and #{namespace}::Deps constants must not already be defined"
151
+ )
152
+ end
153
+ end
110
154
 
111
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
112
- def define_container
113
- container = Class.new(Dry::System::Container)
155
+ def prepare_all
156
+ prepare_container_plugins
157
+ prepare_container_base_config
158
+ prepare_container_component_dirs
159
+ prepare_autoloader
160
+ prepare_container_imports
161
+ prepare_container_consts
162
+ instance_exec(container, &@prepare_container_block) if @prepare_container_block
163
+ container.configured!
164
+ end
114
165
 
115
- container.use :env
116
- container.use :zeitwerk,
117
- loader: application.autoloader,
118
- run_setup: false,
119
- eager_load: false
166
+ def prepare_container_plugins
167
+ container.use(:env, inferrer: -> { Hanami.env })
120
168
 
121
- container.config.name = name
122
- container.config.env = application.configuration.env
123
- container.config.inflector = application.configuration.inflector
169
+ container.use(
170
+ :zeitwerk,
171
+ loader: application.autoloader,
172
+ run_setup: false,
173
+ eager_load: false
174
+ )
175
+ end
124
176
 
125
- if root&.directory?
177
+ def prepare_container_base_config
178
+ container.config.name = slice_name
126
179
  container.config.root = root
127
- container.config.provider_dirs = ["config/providers"]
180
+ container.config.provider_dirs = [File.join("config", "providers")]
181
+
182
+ container.config.env = application.configuration.env
183
+ container.config.inflector = application.configuration.inflector
184
+ end
185
+
186
+ def prepare_container_component_dirs # rubocop:disable Metrics/AbcSize
187
+ return unless root&.directory?
128
188
 
129
189
  # Add component dirs for each configured component path
130
190
  application.configuration.source_dirs.component_dirs.each do |component_dir|
@@ -132,8 +192,7 @@ module Hanami
132
192
 
133
193
  component_dir = component_dir.dup
134
194
 
135
- # TODO: this `== "lib"` check should be codified into a method somewhere
136
- if component_dir.path == "lib"
195
+ if component_dir.path == LIB_DIR
137
196
  # Expect component files in the root of the lib/ component dir to define
138
197
  # classes inside the slice's namespace.
139
198
  #
@@ -141,8 +200,6 @@ module Hanami
141
200
  # "foo"
142
201
  component_dir.namespaces.delete_root
143
202
  component_dir.namespaces.add_root(key: nil, const: namespace_path)
144
-
145
- container.config.component_dirs.add(component_dir)
146
203
  else
147
204
  # Expect component files in the root of non-lib/ component dirs to define
148
205
  # classes inside a namespace matching that dir.
@@ -154,13 +211,15 @@ module Hanami
154
211
 
155
212
  component_dir.namespaces.delete_root
156
213
  component_dir.namespaces.add_root(const: dir_namespace_path, key: component_dir.path)
157
-
158
- container.config.component_dirs.add(component_dir)
159
214
  end
215
+
216
+ container.config.component_dirs.add(component_dir)
160
217
  end
161
218
  end
162
219
 
163
- if root&.directory?
220
+ def prepare_autoloader # rubocop:disable Metrics/AbcSize
221
+ return unless root&.directory?
222
+
164
223
  # Pass configured autoload dirs to the autoloader
165
224
  application.configuration.source_dirs.autoload_paths.each do |autoload_path|
166
225
  next unless root.join(autoload_path).directory?
@@ -180,13 +239,15 @@ module Hanami
180
239
  end
181
240
  end
182
241
 
183
- if namespace
242
+ def prepare_container_imports
243
+ container.import from: application.container, as: :application
244
+ end
245
+
246
+ def prepare_container_consts
184
247
  namespace.const_set :Container, container
185
248
  namespace.const_set :Deps, container.injector
186
249
  end
187
-
188
- container
189
250
  end
190
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
251
+ # rubocop:enable Metrics/ModuleLength
191
252
  end
192
253
  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.alpha6"
11
+ VERSION = "2.0.0.alpha7"
12
12
 
13
13
  # @since 0.9.0
14
14
  # @api private
data/lib/hanami.rb CHANGED
@@ -1,19 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "hanami/application"
4
+ require_relative "hanami/errors"
5
+ require_relative "hanami/version"
6
+
7
+
3
8
  # A complete web framework for Ruby
4
9
  #
5
10
  # @since 0.1.0
6
11
  #
7
12
  # @see http://hanamirb.org
8
13
  module Hanami
9
- require "hanami/version"
10
- require "hanami/application"
11
-
12
14
  @_mutex = Mutex.new
13
15
 
14
16
  def self.application
15
17
  @_mutex.synchronize do
16
- raise "Hanami.application not configured" unless defined?(@_application)
18
+ unless defined?(@_application)
19
+ raise ApplicationLoadError,
20
+ "Hanami.application is not yet configured. " \
21
+ "You may need to `require \"hanami/setup\"` to load your config/application.rb file."
22
+ end
17
23
 
18
24
  @_application
19
25
  end
@@ -25,7 +31,9 @@ module Hanami
25
31
 
26
32
  def self.application=(klass)
27
33
  @_mutex.synchronize do
28
- raise "Hanami.application already configured" if defined?(@_application)
34
+ if defined?(@_application)
35
+ raise ApplicationLoadError, "Hanami.application is already configured."
36
+ end
29
37
 
30
38
  @_application = klass unless klass.name.nil?
31
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.alpha6
4
+ version: 2.0.0.alpha7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-10 00:00:00.000000000 Z
11
+ date: 2022-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -230,11 +230,14 @@ files:
230
230
  - lib/hanami/application/routing/resolver.rb
231
231
  - lib/hanami/application/routing/resolver/node.rb
232
232
  - lib/hanami/application/routing/resolver/trie.rb
233
+ - lib/hanami/application/routing/router.rb
233
234
  - lib/hanami/application/settings.rb
234
235
  - lib/hanami/application/settings/dotenv_store.rb
236
+ - lib/hanami/application/slice_registrar.rb
235
237
  - lib/hanami/assets/application_configuration.rb
236
238
  - lib/hanami/assets/configuration.rb
237
239
  - lib/hanami/boot.rb
240
+ - lib/hanami/boot/source_dirs.rb
238
241
  - lib/hanami/cli/application/cli.rb
239
242
  - lib/hanami/cli/application/command.rb
240
243
  - lib/hanami/cli/application/commands.rb
@@ -244,12 +247,15 @@ files:
244
247
  - lib/hanami/cli/commands/command.rb
245
248
  - lib/hanami/cli/commands/server.rb
246
249
  - lib/hanami/configuration.rb
250
+ - lib/hanami/configuration/actions.rb
247
251
  - lib/hanami/configuration/logger.rb
248
252
  - lib/hanami/configuration/middleware.rb
249
253
  - lib/hanami/configuration/null_configuration.rb
250
254
  - lib/hanami/configuration/router.rb
251
255
  - lib/hanami/configuration/sessions.rb
252
256
  - lib/hanami/configuration/source_dirs.rb
257
+ - lib/hanami/constants.rb
258
+ - lib/hanami/errors.rb
253
259
  - lib/hanami/prepare.rb
254
260
  - lib/hanami/server.rb
255
261
  - lib/hanami/setup.rb