hanami 2.0.0.alpha4 → 2.0.0.alpha7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "dry/system/config/component_dirs"
5
+
6
+ module Hanami
7
+ class Configuration
8
+ # Configuration for slice source dirs
9
+ #
10
+ # @since 2.0.0
11
+ class SourceDirs
12
+ DEFAULT_COMPONENT_DIR_PATHS = %w[lib actions repositories views].freeze
13
+ private_constant :DEFAULT_COMPONENT_DIR_PATHS
14
+
15
+ include Dry::Configurable
16
+
17
+ setting :component_dirs,
18
+ default: Dry::System::Config::ComponentDirs.new.tap { |dirs|
19
+ DEFAULT_COMPONENT_DIR_PATHS.each do |path|
20
+ dirs.add path
21
+ end
22
+ },
23
+ cloneable: true
24
+
25
+ setting :autoload_paths, default: %w[entities]
26
+
27
+ private
28
+
29
+ def method_missing(name, *args, &block)
30
+ if config.respond_to?(name)
31
+ config.public_send(name, *args, &block)
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(name, _include_all = false)
38
+ config.respond_to?(name) || super
39
+ end
40
+ end
41
+ end
42
+ end
@@ -12,6 +12,8 @@ require_relative "configuration/logger"
12
12
  require_relative "configuration/middleware"
13
13
  require_relative "configuration/router"
14
14
  require_relative "configuration/sessions"
15
+ require_relative "configuration/source_dirs"
16
+ require_relative "constants"
15
17
 
16
18
  module Hanami
17
19
  # Hanami application configuration
@@ -25,6 +27,8 @@ module Hanami
25
27
  DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
26
28
  private_constant :DEFAULT_ENVIRONMENTS
27
29
 
30
+ attr_reader :env
31
+
28
32
  attr_reader :actions
29
33
  attr_reader :middleware
30
34
  attr_reader :router
@@ -33,15 +37,19 @@ module Hanami
33
37
  attr_reader :environments
34
38
  private :environments
35
39
 
36
- def initialize(env:)
40
+ def initialize(application_name:, env:)
41
+ @namespace = application_name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER)
42
+
37
43
  @environments = DEFAULT_ENVIRONMENTS.clone
38
- config.env = env
44
+ @env = env
39
45
 
40
46
  # Some default setting values must be assigned at initialize-time to ensure they
41
47
  # have appropriate values for the current application
42
48
  self.root = Dir.pwd
43
49
  self.settings_store = Application::Settings::DotenvStore.new.with_dotenv_loaded
44
50
 
51
+ config.logger = Configuration::Logger.new(env: env, application_name: method(:application_name))
52
+
45
53
  @assets = begin
46
54
  require_path = "hanami/assets/application_configuration"
47
55
  require require_path
@@ -101,29 +109,30 @@ module Hanami
101
109
  super
102
110
  end
103
111
 
104
- setting :env
112
+ def namespace
113
+ inflector.constantize(@namespace)
114
+ end
105
115
 
106
- def env=(new_env)
107
- config.env = env
108
- apply_env_config(new_env)
116
+ def application_name
117
+ inflector.underscore(@namespace).to_sym
109
118
  end
110
119
 
111
120
  setting :root, constructor: -> path { Pathname(path) }
112
121
 
113
- setting :inflector, default: Dry::Inflector.new, cloneable: true
122
+ setting :inflector, default: Dry::Inflector.new
114
123
 
115
124
  def inflections(&block)
116
125
  self.inflector = Dry::Inflector.new(&block)
117
126
  end
118
127
 
119
- setting :logger, default: Configuration::Logger.new, cloneable: true
128
+ setting :logger, cloneable: true
120
129
 
121
130
  def logger=(logger_instance)
122
131
  @logger_instance = logger_instance
123
132
  end
124
133
 
125
134
  def logger_instance
126
- @logger_instance || logger.logger_class.new(**logger.options)
135
+ @logger_instance || logger.instance
127
136
  end
128
137
 
129
138
  setting :settings_path, default: File.join("config", "settings")
@@ -132,21 +141,7 @@ module Hanami
132
141
 
133
142
  setting :settings_store, default: Application::Settings::DotenvStore
134
143
 
135
- setting :slices_dir, default: "slices"
136
-
137
- setting :slices_namespace, default: Object
138
-
139
- # TODO: convert into a dedicated object with explicit behaviour around blocks per
140
- # slice, etc.
141
- setting :slices, default: {}, constructor: :dup.to_proc
142
-
143
- # TODO: turn this into a richer "source dirs" setting that can support enabling
144
- # of container component loading as an opt in behvior
145
- setting :component_dir_paths, default: %w[actions repositories views]
146
-
147
- def slice(slice_name, &block)
148
- slices[slice_name] = block
149
- end
144
+ setting :source_dirs, default: Configuration::SourceDirs.new, cloneable: true
150
145
 
151
146
  setting :base_url, default: "http://0.0.0.0:2300", constructor: -> url { URI(url) }
152
147
 
@@ -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
@@ -2,4 +2,4 @@
2
2
 
3
3
  require_relative "setup"
4
4
 
5
- Hanami.init
5
+ Hanami.prepare
data/lib/hanami/slice.rb CHANGED
@@ -1,167 +1,253 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/container"
4
- require "dry/system/loader/autoloading"
4
+ require "hanami/errors"
5
5
  require "pathname"
6
+ require_relative "constants"
6
7
 
7
8
  module Hanami
8
9
  # Distinct area of concern within an Hanami application
9
10
  #
10
11
  # @since 2.0.0
11
12
  class Slice
12
- attr_reader :application, :name, :namespace, :root
13
-
14
- def initialize(application, name:, namespace: nil, root: nil, container: nil)
15
- @application = application
16
- @name = name.to_sym
17
- @namespace = namespace
18
- @root = root ? Pathname(root) : root
19
- @container = container || define_container
20
- end
13
+ def self.inherited(klass)
14
+ super
21
15
 
22
- def inflector
23
- application.inflector
24
- end
16
+ klass.extend(ClassMethods)
25
17
 
26
- def namespace_path
27
- @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))
28
21
  end
29
22
 
30
- def init
31
- container.import application: application.container
23
+ # rubocop:disable Metrics/ModuleLength
24
+ module ClassMethods
25
+ attr_reader :application, :container
32
26
 
33
- slice_block = application.configuration.slices[name]
34
- instance_eval(&slice_block) if slice_block
35
- end
27
+ def slice_name
28
+ inflector.underscore(name.split(MODULE_DELIMITER)[-2]).to_sym
29
+ end
36
30
 
37
- def boot
38
- container.finalize! do
39
- container.config.env = application.container.config.env
31
+ def namespace
32
+ inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER))
40
33
  end
41
34
 
42
- @booted = true
43
- self
44
- end
35
+ def namespace_path
36
+ inflector.underscore(namespace)
37
+ end
45
38
 
46
- # rubocop:disable Style/DoubleNegation
47
- def booted?
48
- !!@booted
49
- end
50
- # rubocop:enable Style/DoubleNegation
39
+ def root
40
+ application.root.join(SLICES_DIR, slice_name.to_s)
41
+ end
51
42
 
52
- def container
53
- @container ||= define_container
54
- end
43
+ def inflector
44
+ application.inflector
45
+ end
46
+
47
+ def prepare(provider_name = nil)
48
+ container.prepare(provider_name) and return self if provider_name
49
+
50
+ return self if prepared?
51
+
52
+ ensure_slice_name
53
+ ensure_slice_consts
55
54
 
56
- def import(*slice_names)
57
- raise "Cannot import after booting" if booted?
55
+ prepare_all
58
56
 
59
- slice_names.each do |slice_name|
60
- container.import slice_name.to_sym => application.slices.fetch(slice_name.to_sym).container
57
+ @prepared = true
58
+ self
61
59
  end
62
- end
63
60
 
64
- def register(*args, &block)
65
- container.register(*args, &block)
66
- end
61
+ def prepare_container(&block)
62
+ @prepare_container_block = block
63
+ end
67
64
 
68
- def register_bootable(*args, &block)
69
- container.boot(*args, &block)
70
- end
65
+ def boot
66
+ return self if booted?
71
67
 
72
- def init_bootable(*args)
73
- container.init(*args)
74
- end
68
+ container.finalize!
75
69
 
76
- def start_bootable(*args)
77
- container.start(*args)
78
- end
70
+ @booted = true
79
71
 
80
- def key?(*args)
81
- container.key?(*args)
82
- end
72
+ self
73
+ end
83
74
 
84
- def keys
85
- container.keys
86
- end
75
+ def shutdown
76
+ container.shutdown!
77
+ self
78
+ end
87
79
 
88
- def [](*args)
89
- container[*args]
90
- end
80
+ def prepared?
81
+ !!@prepared
82
+ end
91
83
 
92
- def resolve(*args)
93
- container.resolve(*args)
94
- end
84
+ def booted?
85
+ !!@booted
86
+ end
95
87
 
96
- private
88
+ def register(...)
89
+ container.register(...)
90
+ end
97
91
 
98
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
99
- def define_container
100
- container = Class.new(Dry::System::Container)
101
- container.use :env
92
+ def register_provider(...)
93
+ container.register_provider(...)
94
+ end
102
95
 
103
- container.configure do |config|
104
- config.name = name
105
- config.inflector = application.configuration.inflector
96
+ def start(...)
97
+ container.start(...)
98
+ end
106
99
 
107
- config.component_dirs.loader = Dry::System::Loader::Autoloading
108
- config.component_dirs.add_to_load_path = false
100
+ def key?(...)
101
+ container.key?(...)
102
+ end
109
103
 
110
- if root&.directory?
111
- config.root = root
112
- config.bootable_dirs = ["config/boot"]
104
+ def keys
105
+ container.keys
106
+ end
113
107
 
114
- # Add the "lib" component dir; all slices will load components from lib
115
- if root.join("lib").directory?
116
- config.component_dirs.add("lib") do |component_dir|
117
- # Expect component files in the root of the lib
118
- # component dir to define classes inside the slice's namespace.
119
- #
120
- # e.g. "lib/foo.rb" should define SliceNamespace::Foo, and will be
121
- # registered as "foo"
122
- component_dir.namespaces.root(key: nil, const: namespace_path)
108
+ def [](...)
109
+ container.[](...)
110
+ end
123
111
 
124
- application.autoloader.push_dir(root.join("lib"), namespace: namespace)
125
- end
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
126
130
  end
127
131
 
128
- # Add component dirs for each configured component path
129
- application.configuration.component_dir_paths.each do |slice_dir|
130
- next unless root.join(slice_dir).directory?
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
145
+
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
154
+
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
165
+
166
+ def prepare_container_plugins
167
+ container.use(:env, inferrer: -> { Hanami.env })
168
+
169
+ container.use(
170
+ :zeitwerk,
171
+ loader: application.autoloader,
172
+ run_setup: false,
173
+ eager_load: false
174
+ )
175
+ end
176
+
177
+ def prepare_container_base_config
178
+ container.config.name = slice_name
179
+ container.config.root = root
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?
188
+
189
+ # Add component dirs for each configured component path
190
+ application.configuration.source_dirs.component_dirs.each do |component_dir|
191
+ next unless root.join(component_dir.path).directory?
192
+
193
+ component_dir = component_dir.dup
194
+
195
+ if component_dir.path == LIB_DIR
196
+ # Expect component files in the root of the lib/ component dir to define
197
+ # classes inside the slice's namespace.
198
+ #
199
+ # e.g. "lib/foo.rb" should define SliceNamespace::Foo, to be registered as
200
+ # "foo"
201
+ component_dir.namespaces.delete_root
202
+ component_dir.namespaces.add_root(key: nil, const: namespace_path)
203
+ else
204
+ # Expect component files in the root of non-lib/ component dirs to define
205
+ # classes inside a namespace matching that dir.
206
+ #
207
+ # e.g. "actions/foo.rb" should define SliceNamespace::Actions::Foo, to be
208
+ # registered as "actions.foo"
209
+
210
+ dir_namespace_path = File.join(namespace_path, component_dir.path)
211
+
212
+ component_dir.namespaces.delete_root
213
+ component_dir.namespaces.add_root(const: dir_namespace_path, key: component_dir.path)
214
+ end
131
215
 
132
- config.component_dirs.add(slice_dir) do |component_dir|
133
- # Expect component files in the root of these component dirs to define
134
- # classes inside a namespace matching the dir.
135
- #
136
- # e.g. "actions/foo.rb" should define SliceNamespace::Actions::Foo, and
137
- # will be registered as "actions.foo"
216
+ container.config.component_dirs.add(component_dir)
217
+ end
218
+ end
138
219
 
139
- dir_namespace_path = File.join(namespace_path, slice_dir)
220
+ def prepare_autoloader # rubocop:disable Metrics/AbcSize
221
+ return unless root&.directory?
140
222
 
141
- autoloader_namespace = begin
142
- inflector.constantize(inflector.camelize(dir_namespace_path))
143
- rescue NameError
144
- namespace.const_set(inflector.camelize(slice_dir), Module.new)
145
- end
223
+ # Pass configured autoload dirs to the autoloader
224
+ application.configuration.source_dirs.autoload_paths.each do |autoload_path|
225
+ next unless root.join(autoload_path).directory?
146
226
 
147
- component_dir.namespaces.root(const: dir_namespace_path, key: slice_dir)
227
+ dir_namespace_path = File.join(namespace_path, autoload_path)
148
228
 
149
- application.autoloader.push_dir(
150
- container.root.join(slice_dir),
151
- namespace: autoloader_namespace
152
- )
153
- end
229
+ autoloader_namespace = begin
230
+ inflector.constantize(inflector.camelize(dir_namespace_path))
231
+ rescue NameError
232
+ namespace.const_set(inflector.camelize(autoload_path), Module.new)
154
233
  end
234
+
235
+ container.config.autoloader.push_dir(
236
+ container.root.join(autoload_path),
237
+ namespace: autoloader_namespace
238
+ )
155
239
  end
156
240
  end
157
241
 
158
- if namespace
242
+ def prepare_container_imports
243
+ container.import from: application.container, as: :application
244
+ end
245
+
246
+ def prepare_container_consts
159
247
  namespace.const_set :Container, container
160
248
  namespace.const_set :Deps, container.injector
161
249
  end
162
-
163
- container
164
250
  end
165
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
251
+ # rubocop:enable Metrics/ModuleLength
166
252
  end
167
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.alpha4"
11
+ VERSION = "2.0.0.alpha7"
12
12
 
13
13
  # @since 0.9.0
14
14
  # @api private
@@ -1,19 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "rack/request"
5
- require "hanami/utils/hash"
6
-
7
3
  module Hanami
8
4
  module Web
9
5
  # Rack logger for Hanami applications
10
6
  class RackLogger
11
- attr_reader :logger
12
- attr_reader :filter_params
7
+ REQUEST_METHOD = "REQUEST_METHOD"
8
+ private_constant :REQUEST_METHOD
9
+
10
+ HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
11
+ private_constant :HTTP_X_FORWARDED_FOR
12
+
13
+ REMOTE_ADDR = "REMOTE_ADDR"
14
+ private_constant :REMOTE_ADDR
15
+
16
+ SCRIPT_NAME = "SCRIPT_NAME"
17
+ private_constant :SCRIPT_NAME
18
+
19
+ PATH_INFO = "PATH_INFO"
20
+ private_constant :PATH_INFO
21
+
22
+ ROUTER_PARAMS = "router.params"
23
+ private_constant :ROUTER_PARAMS
24
+
25
+ CONTENT_LENGTH = "Content-Length"
26
+ private_constant :CONTENT_LENGTH
13
27
 
14
- def initialize(logger, filter_params: [])
28
+ def initialize(logger)
15
29
  @logger = logger
16
- @filter_params = filter_params
17
30
  end
18
31
 
19
32
  def attach(rack_monitor)
@@ -26,22 +39,20 @@ module Hanami
26
39
  end
27
40
  end
28
41
 
29
- # rubocop:disable Metrics/MethodLength
30
- def log_request(env, status, time)
42
+ def log_request(env, status, elapsed)
31
43
  data = {
32
- http: env[HTTP_VERSION],
33
44
  verb: env[REQUEST_METHOD],
34
45
  status: status,
46
+ elapsed: "#{elapsed}ms",
35
47
  ip: env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR],
36
48
  path: env[SCRIPT_NAME] + env[PATH_INFO].to_s,
37
49
  length: extract_content_length(env),
38
- params: extract_params(env),
39
- elapsed: time,
50
+ params: env[ROUTER_PARAMS],
51
+ time: Time.now,
40
52
  }
41
53
 
42
- logger.info JSON.generate(data)
54
+ logger.info(data)
43
55
  end
44
- # rubocop:enable Metrics/MethodLength
45
56
 
46
57
  def log_exception(exception)
47
58
  logger.error exception.message
@@ -50,47 +61,12 @@ module Hanami
50
61
 
51
62
  private
52
63
 
53
- HTTP_VERSION = "HTTP_VERSION"
54
- REQUEST_METHOD = "REQUEST_METHOD"
55
- HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
56
- REMOTE_ADDR = "REMOTE_ADDR"
57
- SCRIPT_NAME = "SCRIPT_NAME"
58
- PATH_INFO = "PATH_INFO"
59
- RACK_ERRORS = "rack.errors"
60
- QUERY_HASH = "rack.request.query_hash"
61
- FORM_HASH = "rack.request.form_hash"
62
- ROUTER_PARAMS = "router.params"
63
- CONTENT_LENGTH = "Content-Length"
64
+ attr_reader :logger
64
65
 
65
66
  def extract_content_length(env)
66
67
  value = env[CONTENT_LENGTH]
67
68
  !value || value.to_s == "0" ? "-" : value
68
69
  end
69
-
70
- def extract_params(env)
71
- result = env.fetch(QUERY_HASH, {})
72
- result.merge!(env.fetch(FORM_HASH, {}))
73
- result.merge!(Hanami::Utils::Hash.deep_stringify(env.fetch(ROUTER_PARAMS, {})))
74
- result
75
- end
76
-
77
- FILTERED = "[FILTERED]"
78
-
79
- # rubocop:disable Metrics/MethodLength
80
- def filter(params)
81
- params.each_with_object({}) do |(k, v), h|
82
- if filter_params.include?(k)
83
- h.update(k => FILTERED)
84
- elsif v.is_a?(Hash)
85
- h.update(k => filter(v))
86
- elsif v.is_a?(Array)
87
- h.update(k => v.map { |m| m.is_a?(Hash) ? filter(m) : m })
88
- else
89
- h[k] = v
90
- end
91
- end
92
- end
93
- # rubocop:enable Metrics/MethodLength
94
70
  end
95
71
  end
96
72
  end