hanami 2.0.0.alpha1 → 2.0.0.alpha5

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +306 -5
  3. data/FEATURES.md +9 -1
  4. data/LICENSE.md +1 -1
  5. data/README.md +9 -6
  6. data/hanami.gemspec +12 -11
  7. data/lib/hanami/application/autoloader/inflector_adapter.rb +22 -0
  8. data/lib/hanami/application/container/boot/inflector.rb +7 -0
  9. data/lib/hanami/application/container/boot/logger.rb +7 -0
  10. data/lib/hanami/application/container/boot/rack_logger.rb +19 -0
  11. data/lib/hanami/application/container/boot/rack_monitor.rb +12 -0
  12. data/lib/hanami/application/container/boot/routes_helper.rb +9 -0
  13. data/lib/hanami/application/container/boot/settings.rb +7 -0
  14. data/lib/hanami/application/router.rb +59 -0
  15. data/lib/hanami/application/routes.rb +55 -0
  16. data/lib/hanami/application/routes_helper.rb +34 -0
  17. data/lib/hanami/application/routing/middleware/stack.rb +89 -0
  18. data/lib/hanami/application/routing/resolver/node.rb +50 -0
  19. data/lib/hanami/application/routing/resolver/trie.rb +59 -0
  20. data/lib/hanami/application/routing/resolver.rb +87 -0
  21. data/lib/hanami/application/routing/router.rb +36 -0
  22. data/lib/hanami/application/settings/dotenv_store.rb +60 -0
  23. data/lib/hanami/application/settings.rb +93 -0
  24. data/lib/hanami/application.rb +330 -34
  25. data/lib/hanami/assets/application_configuration.rb +63 -0
  26. data/lib/hanami/assets/configuration.rb +54 -0
  27. data/lib/hanami/boot/source_dirs.rb +44 -0
  28. data/lib/hanami/boot.rb +1 -2
  29. data/lib/hanami/cli/application/cli.rb +40 -0
  30. data/lib/hanami/cli/application/command.rb +47 -0
  31. data/lib/hanami/cli/application/commands/console.rb +81 -0
  32. data/lib/hanami/cli/application/commands.rb +16 -0
  33. data/lib/hanami/cli/base_command.rb +48 -0
  34. data/lib/hanami/cli/commands/command.rb +4 -4
  35. data/lib/hanami/cli/commands.rb +3 -2
  36. data/lib/hanami/configuration/logger.rb +84 -0
  37. data/lib/hanami/configuration/middleware.rb +4 -4
  38. data/lib/hanami/configuration/null_configuration.rb +14 -0
  39. data/lib/hanami/configuration/router.rb +52 -0
  40. data/lib/hanami/configuration/sessions.rb +5 -5
  41. data/lib/hanami/configuration/source_dirs.rb +42 -0
  42. data/lib/hanami/configuration.rb +122 -131
  43. data/lib/hanami/init.rb +5 -0
  44. data/lib/hanami/setup.rb +9 -0
  45. data/lib/hanami/slice.rb +189 -0
  46. data/lib/hanami/version.rb +1 -1
  47. data/lib/hanami/web/rack_logger.rb +96 -0
  48. data/lib/hanami.rb +17 -30
  49. metadata +116 -50
  50. data/bin/hanami +0 -8
  51. data/lib/hanami/configuration/cookies.rb +0 -24
  52. data/lib/hanami/configuration/security.rb +0 -141
  53. data/lib/hanami/container.rb +0 -107
  54. data/lib/hanami/frameworks.rb +0 -28
  55. data/lib/hanami/routes.rb +0 -31
@@ -1,70 +1,366 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/system/container"
4
+ require "dry/system/loader/autoloading"
3
5
  require "hanami/configuration"
4
- require "hanami/routes"
5
- require "hanami/router"
6
+ require "pathname"
7
+ require "rack"
8
+ require "zeitwerk"
9
+ require_relative "slice"
10
+ require_relative "application/autoloader/inflector_adapter"
6
11
 
7
12
  module Hanami
8
- # Hanami application
13
+ # Hanami application class
9
14
  #
10
15
  # @since 2.0.0
11
16
  class Application
12
17
  @_mutex = Mutex.new
13
18
 
14
- def self.inherited(app)
15
- @_mutex.synchronize do
16
- app.class_eval do
17
- @_mutex = Mutex.new
18
- @_configuration = Hanami::Configuration.new(env: Hanami.env)
19
+ class << self
20
+ def inherited(klass)
21
+ @_mutex.synchronize do
22
+ klass.class_eval do
23
+ @_mutex = Mutex.new
24
+ @_configuration = Hanami::Configuration.new(application_name: name, env: Hanami.env)
19
25
 
20
- extend ClassMethods
21
- include InstanceMethods
22
- end
26
+ extend ClassMethods
27
+ end
23
28
 
24
- Hanami.application = app
29
+ klass.send :prepare_base_load_path
30
+
31
+ Hanami.application = klass
32
+ end
25
33
  end
26
34
  end
27
35
 
28
- # Class method interface
36
+ # Application class interface
29
37
  #
30
- # @since 2.0.0
38
+ # rubocop:disable Metrics/ModuleLength
31
39
  module ClassMethods
40
+ def self.extended(klass)
41
+ klass.class_eval do
42
+ @inited = @booted = false
43
+ end
44
+ end
45
+
32
46
  def configuration
33
47
  @_configuration
34
48
  end
35
49
 
36
50
  alias config configuration
37
51
 
38
- def routes(&blk)
52
+ def init # rubocop:disable Metrics/MethodLength
53
+ return self if inited?
54
+
55
+ configuration.finalize!
56
+
57
+ @autoloader = Zeitwerk::Loader.new
58
+ autoloader.inflector = Autoloader::InflectorAdapter.new(inflector)
59
+
60
+ load_settings
61
+
62
+ @container = prepare_container
63
+ @deps_module = prepare_deps_module
64
+
65
+ load_slices
66
+ slices.values.each(&:init)
67
+ slices.freeze
68
+
69
+ autoloader.setup
70
+
71
+ @inited = true
72
+ self
73
+ end
74
+
75
+ def boot(&block)
76
+ return self if booted?
77
+
78
+ init
79
+
80
+ container.finalize!(&block)
81
+
82
+ slices.values.each(&:boot)
83
+
84
+ @booted = true
85
+ self
86
+ end
87
+
88
+ def shutdown
89
+ container.shutdown!
90
+ end
91
+
92
+ def inited?
93
+ @inited
94
+ end
95
+
96
+ def booted?
97
+ @booted
98
+ end
99
+
100
+ def autoloader
101
+ raise "Application not init'ed" unless defined?(@autoloader)
102
+
103
+ @autoloader
104
+ end
105
+
106
+ def container
107
+ raise "Application not init'ed" unless defined?(@container)
108
+
109
+ @container
110
+ end
111
+
112
+ def deps
113
+ raise "Application not init'ed" unless defined?(@deps_module)
114
+
115
+ @deps_module
116
+ end
117
+
118
+ def router
119
+ raise "Application not init'ed" unless inited?
120
+
39
121
  @_mutex.synchronize do
40
- if blk.nil?
41
- raise "Hanami.application.routes not configured" unless defined?(@_routes)
122
+ @_router ||= load_router
123
+ end
124
+ end
42
125
 
43
- @_routes
44
- else
45
- @_routes = Routes.new(&blk)
46
- end
126
+ def rack_app
127
+ @rack_app ||= router.to_rack_app
128
+ end
129
+
130
+ def slices
131
+ @slices ||= {}
132
+ end
133
+
134
+ def register_slice(name, **slice_args)
135
+ raise "Slice +#{name}+ already registered" if slices.key?(name.to_sym)
136
+
137
+ slice = Slice.new(self, name: name, **slice_args)
138
+ slice.namespace.const_set :Slice, slice if slice.namespace # rubocop:disable Style/SafeNavigation
139
+ slices[name.to_sym] = slice
140
+ end
141
+
142
+ def register(*args, **opts, &block)
143
+ container.register(*args, **opts, &block)
144
+ end
145
+
146
+ def register_bootable(*args, **opts, &block)
147
+ container.boot(*args, **opts, &block)
148
+ end
149
+
150
+ def init_bootable(*args)
151
+ container.init(*args)
152
+ end
153
+
154
+ def start_bootable(*args)
155
+ container.start(*args)
156
+ end
157
+
158
+ def key?(*args)
159
+ container.key?(*args)
160
+ end
161
+
162
+ def keys
163
+ container.keys
164
+ end
165
+
166
+ def [](*args)
167
+ container[*args]
168
+ end
169
+
170
+ def resolve(*args)
171
+ container.resolve(*args)
172
+ end
173
+
174
+ def settings
175
+ @_settings ||= load_settings
176
+ end
177
+
178
+ def namespace
179
+ configuration.namespace
180
+ end
181
+
182
+ def namespace_name
183
+ namespace.name
184
+ end
185
+
186
+ def namespace_path
187
+ inflector.underscore(namespace)
188
+ end
189
+
190
+ def application_name
191
+ configuration.application_name
192
+ end
193
+
194
+ def root
195
+ configuration.root
196
+ end
197
+
198
+ def inflector
199
+ configuration.inflector
200
+ end
201
+
202
+ # @api private
203
+ def component_provider(component)
204
+ raise "Hanami.application must be inited before detecting providers" unless inited?
205
+
206
+ # [Admin, Main, MyApp] or [MyApp::Admin, MyApp::Main, MyApp]
207
+ providers = slices.values + [self]
208
+
209
+ component_class = component.is_a?(Class) ? component : component.class
210
+ component_name = component_class.name
211
+
212
+ return unless component_name
213
+
214
+ providers.detect { |provider| component_name.include?(provider.namespace.to_s) }
215
+ end
216
+
217
+ private
218
+
219
+ def prepare_base_load_path
220
+ base_path = File.join(root, "lib")
221
+ $LOAD_PATH.unshift base_path unless $LOAD_PATH.include?(base_path)
222
+ end
223
+
224
+ def prepare_container
225
+ define_container.tap do |container|
226
+ configure_container container
47
227
  end
48
228
  end
49
- end
50
229
 
51
- # Instance method interface
52
- #
53
- # @since 2.0.0
54
- module InstanceMethods
55
- def initialize(configuration: self.class.configuration, routes: self.class.routes)
56
- @app = Rack::Builder.new do
57
- configuration.for_each_middleware do |m, *args|
58
- use m, *args
59
- end
230
+ def prepare_deps_module
231
+ define_deps_module
232
+ end
233
+
234
+ def define_container
235
+ require "#{application_name}/container"
236
+ namespace.const_get :Container
237
+ rescue LoadError, NameError
238
+ namespace.const_set :Container, Class.new(Dry::System::Container)
239
+ end
240
+
241
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
242
+ def configure_container(container)
243
+ container.use :env, inferrer: -> { Hanami.env }
244
+ container.use :notifications
245
+
246
+ container.configure do |config|
247
+ config.inflector = configuration.inflector
248
+
249
+ config.root = configuration.root
250
+ config.bootable_dirs = [
251
+ "config/boot",
252
+ Pathname(__dir__).join("application/container/boot").realpath,
253
+ ]
60
254
 
61
- run Hanami::Router.new(**configuration.router_settings, &routes)
255
+ config.component_dirs.loader = Dry::System::Loader::Autoloading
256
+ config.component_dirs.add_to_load_path = false
62
257
  end
258
+
259
+ # Autoload classes defined in lib/[app_namespace]/
260
+ if root.join("lib", namespace_path).directory?
261
+ autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
262
+ end
263
+
264
+ # Add lib/ to to the $LOAD_PATH so other files there (outside the app namespace)
265
+ # are require-able
266
+ container.add_to_load_path!("lib") if root.join("lib").directory?
267
+
268
+ container
269
+ end
270
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
271
+
272
+ def define_deps_module
273
+ require "#{application_name}/deps"
274
+ namespace.const_get :Deps
275
+ rescue LoadError, NameError
276
+ namespace.const_set :Deps, container.injector
277
+ end
278
+
279
+ def load_slices
280
+ Dir[File.join(slices_path, "*")]
281
+ .select(&File.method(:directory?))
282
+ .each(&method(:load_slice))
283
+ end
284
+
285
+ def slices_path
286
+ File.join(root, config.slices_dir)
287
+ end
288
+
289
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
290
+ def load_slice(slice_path)
291
+ slice_path = Pathname(slice_path)
292
+
293
+ slice_name = slice_path.relative_path_from(Pathname(slices_path)).to_s
294
+ slice_const_name = inflector.camelize(slice_name)
295
+
296
+ if config.slices_namespace.const_defined?(slice_const_name)
297
+ slice_module = config.slices_namespace.const_get(slice_const_name)
298
+
299
+ raise "Cannot use slice +#{slice_const_name}+ since it is not a module" unless slice_module.is_a?(Module)
300
+ else
301
+ slice_module = Module.new
302
+ config.slices_namespace.const_set inflector.camelize(slice_name), slice_module
303
+ end
304
+
305
+ register_slice(
306
+ slice_name,
307
+ namespace: slice_module,
308
+ root: slice_path.realpath
309
+ )
310
+ end
311
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
312
+
313
+ def load_settings
314
+ require_relative "application/settings"
315
+
316
+ prepare_base_load_path
317
+ require File.join(configuration.root, configuration.settings_path)
318
+ settings_class = autodiscover_application_constant(configuration.settings_class_name)
319
+ settings_class.new(configuration.settings_store)
320
+ rescue LoadError
321
+ Settings.new
322
+ end
323
+
324
+ MODULE_DELIMITER = "::"
325
+ private_constant :MODULE_DELIMITER
326
+
327
+ def autodiscover_application_constant(constants)
328
+ inflector.constantize([namespace_name, *constants].join(MODULE_DELIMITER))
329
+ end
330
+
331
+ def load_router
332
+ require_relative "application/router"
333
+
334
+ Router.new(
335
+ routes: load_routes,
336
+ resolver: router_resolver,
337
+ **configuration.router.options,
338
+ ) do
339
+ use Hanami.application[:rack_monitor]
340
+
341
+ Hanami.application.config.for_each_middleware do |m, *args, &block|
342
+ use(m, *args, &block)
343
+ end
344
+ end
345
+ end
346
+
347
+ def load_routes
348
+ require_relative "application/routes"
349
+
350
+ require File.join(configuration.root, configuration.router.routes_path)
351
+ routes_class = autodiscover_application_constant(configuration.router.routes_class_name)
352
+ routes_class.routes
353
+ rescue LoadError
354
+ proc {}
63
355
  end
64
356
 
65
- def call(env)
66
- @app.call(env)
357
+ def router_resolver
358
+ config.router.resolver.new(
359
+ slices: slices,
360
+ inflector: inflector
361
+ )
67
362
  end
68
363
  end
364
+ # rubocop:enable Metrics/ModuleLength
69
365
  end
70
366
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/assets/configuration"
4
+ require "dry/configurable"
5
+
6
+ module Hanami
7
+ module Assets
8
+ # @since 2.0.0
9
+ # @api public
10
+ class ApplicationConfiguration
11
+ include Dry::Configurable
12
+
13
+ setting :server_url, default: "http://localhost:8080"
14
+
15
+ # @since 2.0.0
16
+ # @api private
17
+ def initialize(*)
18
+ super
19
+
20
+ @base_configuration = Assets::Configuration.new
21
+ end
22
+
23
+ # @since 2.0.0
24
+ # @api private
25
+ def finalize!
26
+ end
27
+
28
+ # Returns the list of available settings
29
+ #
30
+ # @return [Set]
31
+ #
32
+ # @since 2.0.0
33
+ # @api private
34
+ def settings
35
+ base_configuration.settings + self.class.settings
36
+ end
37
+
38
+ private
39
+
40
+ # @since 2.0.0
41
+ # @api private
42
+ attr_reader :base_configuration
43
+
44
+ # @since 2.0.0
45
+ # @api private
46
+ def method_missing(name, *args, &block)
47
+ if config.respond_to?(name)
48
+ config.public_send(name, *args, &block)
49
+ elsif base_configuration.respond_to?(name)
50
+ base_configuration.public_send(name, *args, &block)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ # @since 2.0.0
57
+ # @api private
58
+ def respond_to_missing?(name, _incude_all = false)
59
+ config.respond_to?(name) || base_configuration.respond_to?(name) || super
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+
5
+ module Hanami
6
+ module Assets
7
+ # @since 2.0.0
8
+ # @api public
9
+ class Configuration
10
+ include Dry::Configurable
11
+
12
+ # Initialize the Configuration
13
+ #
14
+ # @yield [config] the configuration object
15
+ #
16
+ # @return [Configuration]
17
+ #
18
+ # @since 2.0.0
19
+ # @api private
20
+ def initialize(*)
21
+ super
22
+ yield self if block_given?
23
+ end
24
+
25
+ # Returns the list of available settings
26
+ #
27
+ # @return [Set]
28
+ #
29
+ # @since 2.0.0
30
+ # @api private
31
+ def settings
32
+ self.class.settings
33
+ end
34
+
35
+ private
36
+
37
+ # @since 2.0.0
38
+ # @api private
39
+ def method_missing(name, *args, &block)
40
+ if config.respond_to?(name)
41
+ config.public_send(name, *args, &block)
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ # @since 2.0.0
48
+ # @api private
49
+ def respond_to_missing?(name, _incude_all = false)
50
+ config.respond_to?(name) || super
51
+ end
52
+ end
53
+ end
54
+ 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
data/lib/hanami/boot.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/setup"
4
- require "hanami"
3
+ require_relative "setup"
5
4
 
6
5
  Hanami.boot
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/cli"
4
+ require_relative "commands"
5
+
6
+ module Hanami
7
+ class CLI
8
+ module Application
9
+ # Hanami application CLI
10
+ class CLI < Hanami::CLI
11
+ attr_reader :application
12
+
13
+ def initialize(application: nil, commands: Commands)
14
+ super(commands)
15
+ @application = application
16
+ end
17
+
18
+ private
19
+
20
+ # TODO: we should make a prepare_command method upstream
21
+ def parse(result, out)
22
+ command, arguments = super
23
+
24
+ if command.respond_to?(:with_application)
25
+ # Set HANAMI_ENV before the application inits to ensure all aspects
26
+ # of the boot process respect the provided env
27
+ ENV["HANAMI_ENV"] = arguments[:env] if arguments[:env]
28
+
29
+ require "hanami/init"
30
+ application = Hanami.application
31
+
32
+ [command.with_application(application), arguments]
33
+ else
34
+ [command, arguments]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_command"
4
+
5
+ module Hanami
6
+ class CLI
7
+ module Application
8
+ # Hanami application CLI command (intended to run inside application directory)
9
+ class Command < BaseCommand
10
+ def self.inherited(klass)
11
+ super
12
+
13
+ klass.option :env, aliases: ["-e"], default: nil, desc: "Application environment"
14
+ end
15
+
16
+ attr_reader :application
17
+
18
+ def initialize(application: nil, **opts)
19
+ super(**opts)
20
+ @application = application
21
+ end
22
+
23
+ def with_application(application)
24
+ self.class.new(
25
+ command_name: @command_name,
26
+ application: application,
27
+ out: out,
28
+ inflector: application.inflector,
29
+ files: files,
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def run_command(klass, *args)
36
+ klass.new(
37
+ command_name: klass.name,
38
+ application: application,
39
+ out: out,
40
+ inflector: application.inflector,
41
+ files: files,
42
+ ).call(*args)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end