hanami 2.0.0.alpha6 → 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.
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