hanami 2.0.0.alpha4 → 2.0.0.alpha7

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