hanami 2.0.0.alpha6 → 2.0.0.alpha8

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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+ require_relative "../slice_configurable"
5
+ require_relative "view/slice_configured_view"
6
+
7
+ module Hanami
8
+ class Application
9
+ # Superclass for views intended for use within an Hanami application.
10
+ #
11
+ # @see Hanami::View
12
+ #
13
+ # @api public
14
+ # @since 2.0.0
15
+ class View < Hanami::View
16
+ extend Hanami::SliceConfigurable
17
+
18
+ # @api private
19
+ def self.configure_for_slice(slice)
20
+ extend SliceConfiguredView.new(slice)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+
5
+ module Hanami
6
+ class Application
7
+ # Infers a view name for automatically rendering within actions.
8
+ #
9
+ # @api private
10
+ # @since 2.0.0
11
+ class ViewNameInferrer
12
+ ALTERNATIVE_NAMES = {
13
+ "create" => "new",
14
+ "update" => "edit"
15
+ }.freeze
16
+
17
+ class << self
18
+ # Returns an array of container keys for views matching the given action.
19
+ #
20
+ # Also provides alternative view keys for common RESTful actions.
21
+ #
22
+ # @example
23
+ # ViewNameInferrer.call(action_name: "Main::Actions::Posts::Create", slice: Main::Slice)
24
+ # # => ["views.posts.create", "views.posts.new"]
25
+ #
26
+ # @param action_name [String] action class name
27
+ # @param slice [Hanami::Slice, Hanami::Application] Hanami slice containing the action
28
+ #
29
+ # @return [Array<string>] array of paired view container keys
30
+ def call(action_class_name:, slice:)
31
+ action_key_base = slice.application.config.actions.name_inference_base
32
+ view_key_base = slice.application.config.actions.view_name_inference_base
33
+
34
+ action_name_key = action_name_key(action_class_name, slice, action_key_base)
35
+
36
+ view_key = [view_key_base, action_name_key].compact.join(CONTAINER_KEY_DELIMITER)
37
+
38
+ [view_key, alternative_view_key(view_key)].compact
39
+ end
40
+
41
+ private
42
+
43
+ def action_name_key(action_name, slice, key_base)
44
+ slice
45
+ .inflector
46
+ .underscore(action_name)
47
+ .sub(%r{^#{slice.slice_name.path}#{PATH_DELIMITER}}, "")
48
+ .sub(%r{^#{key_base}#{PATH_DELIMITER}}, "")
49
+ .gsub("/", CONTAINER_KEY_DELIMITER)
50
+ end
51
+
52
+ def alternative_view_key(view_key)
53
+ parts = view_key.split(CONTAINER_KEY_DELIMITER)
54
+
55
+ alternative_name = ALTERNATIVE_NAMES[parts.last]
56
+ return unless alternative_name
57
+
58
+ [parts[0..-2], alternative_name].join(CONTAINER_KEY_DELIMITER)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,12 +1,14 @@
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 "slice_name"
11
+ require_relative "application/slice_registrar"
10
12
 
11
13
  module Hanami
12
14
  # Hanami application class
@@ -16,19 +18,25 @@ module Hanami
16
18
  @_mutex = Mutex.new
17
19
 
18
20
  class << self
19
- def inherited(klass)
21
+ def inherited(subclass)
20
22
  super
23
+
21
24
  @_mutex.synchronize do
22
- klass.class_eval do
23
- @_mutex = Mutex.new
24
- @_configuration = Hanami::Configuration.new(application_name: name, env: Hanami.env)
25
+ subclass.class_eval do
26
+ @_mutex = Mutex.new
27
+ @application_name = SliceName.new(subclass, inflector: -> { subclass.inflector })
28
+ @configuration = Hanami::Configuration.new(application_name: @application_name, env: Hanami.env)
29
+ @autoloader = Zeitwerk::Loader.new
30
+ @container = Class.new(Dry::System::Container)
31
+
32
+ @prepared = @booted = false
25
33
 
26
34
  extend ClassMethods
27
35
  end
28
36
 
29
- klass.send :prepare_base_load_path
37
+ subclass.send :prepare_base_load_path
30
38
 
31
- Hanami.application = klass
39
+ Hanami.application = subclass
32
40
  end
33
41
  end
34
42
  end
@@ -37,39 +45,24 @@ module Hanami
37
45
  #
38
46
  # rubocop:disable Metrics/ModuleLength
39
47
  module ClassMethods
40
- def self.extended(klass)
41
- klass.class_eval do
42
- @prepared = @booted = false
43
- end
44
- end
48
+ attr_reader :application_name, :configuration, :autoloader, :container
45
49
 
46
- def configuration
47
- @_configuration
48
- end
50
+ alias_method :slice_name, :application_name
49
51
 
50
52
  alias_method :config, :configuration
51
53
 
54
+ def application
55
+ self
56
+ end
57
+
52
58
  def prepare(provider_name = nil)
53
- if provider_name
54
- container.prepare(provider_name)
55
- return self
56
- end
59
+ container.prepare(provider_name) and return self if provider_name
57
60
 
58
61
  return self if prepared?
59
62
 
60
63
  configuration.finalize!
61
64
 
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
65
+ prepare_all
73
66
 
74
67
  @prepared = true
75
68
  self
@@ -82,40 +75,24 @@ module Hanami
82
75
 
83
76
  container.finalize!(&block)
84
77
 
85
- slices.values.each(&:boot)
78
+ slices.each(&:boot)
86
79
 
87
80
  @booted = true
88
81
  self
89
82
  end
90
83
 
91
84
  def shutdown
85
+ slices.each(&:shutdown)
92
86
  container.shutdown!
87
+ self
93
88
  end
94
89
 
95
90
  def prepared?
96
- @prepared
91
+ !!@prepared
97
92
  end
98
93
 
99
94
  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
95
+ !!@booted
119
96
  end
120
97
 
121
98
  def router
@@ -131,15 +108,11 @@ module Hanami
131
108
  end
132
109
 
133
110
  def slices
134
- @slices ||= {}
111
+ @slices ||= SliceRegistrar.new(self)
135
112
  end
136
113
 
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
114
+ def register_slice(...)
115
+ slices.register(...)
143
116
  end
144
117
 
145
118
  def register(...)
@@ -175,19 +148,7 @@ module Hanami
175
148
  end
176
149
 
177
150
  def namespace
178
- configuration.namespace
179
- end
180
-
181
- def namespace_name
182
- namespace.name
183
- end
184
-
185
- def namespace_path
186
- inflector.underscore(namespace)
187
- end
188
-
189
- def application_name
190
- configuration.application_name
151
+ application_name.namespace
191
152
  end
192
153
 
193
154
  def root
@@ -198,21 +159,6 @@ module Hanami
198
159
  configuration.inflector
199
160
  end
200
161
 
201
- # @api private
202
- def component_provider(component)
203
- raise "Hanami.application must be prepared before detecting providers" unless prepared?
204
-
205
- # [Admin, Main, MyApp] or [MyApp::Admin, MyApp::Main, MyApp]
206
- providers = slices.values + [self]
207
-
208
- component_class = component.is_a?(Class) ? component : component.class
209
- component_name = component_class.name
210
-
211
- return unless component_name
212
-
213
- providers.detect { |provider| component_name.include?(provider.namespace.to_s) }
214
- end
215
-
216
162
  private
217
163
 
218
164
  def prepare_base_load_path
@@ -220,20 +166,25 @@ module Hanami
220
166
  $LOAD_PATH.unshift base_path unless $LOAD_PATH.include?(base_path)
221
167
  end
222
168
 
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
169
+ def prepare_all
170
+ load_settings
171
+ prepare_container_plugins
172
+ prepare_container_base_config
173
+ prepare_container_consts
174
+ container.configured!
175
+ prepare_slices
176
+ # For the application, the autoloader must be prepared after the slices, since
177
+ # they'll be configuring the autoloader with their own dirs
178
+ prepare_autoloader
179
+ end
232
180
 
233
- container.use :env, inferrer: -> { Hanami.env }
234
- container.use :zeitwerk, loader: autoloader, run_setup: false, eager_load: false
235
- container.use :notifications
181
+ def prepare_container_plugins
182
+ container.use(:env, inferrer: -> { Hanami.env })
183
+ container.use(:zeitwerk, loader: autoloader, run_setup: false, eager_load: false)
184
+ container.use(:notifications)
185
+ end
236
186
 
187
+ def prepare_container_base_config
237
188
  container.config.root = configuration.root
238
189
  container.config.inflector = configuration.inflector
239
190
 
@@ -241,63 +192,32 @@ module Hanami
241
192
  "config/providers",
242
193
  Pathname(__dir__).join("application/container/providers").realpath,
243
194
  ]
195
+ end
244
196
 
197
+ def prepare_autoload_paths
245
198
  # Autoload classes defined in lib/[app_namespace]/
246
- if root.join("lib", namespace_path).directory?
247
- container.autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
199
+ if root.join("lib", application_name.name).directory?
200
+ autoloader.push_dir(root.join("lib", application_name.name), namespace: namespace)
248
201
  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
202
  end
258
- # rubocop:enable Metrics/AbcSize
259
203
 
260
- def prepare_deps_module
261
- define_deps_module
262
- end
263
-
264
- def define_deps_module
265
- require "#{application_name}/deps"
266
- namespace.const_get :Deps
267
- rescue LoadError, NameError
204
+ def prepare_container_consts
205
+ namespace.const_set :Container, container
268
206
  namespace.const_set :Deps, container.injector
269
207
  end
270
208
 
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)
209
+ def prepare_slices
210
+ slices.load_slices.each(&:prepare)
211
+ slices.freeze
279
212
  end
280
213
 
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
214
+ def prepare_autoloader
215
+ # Autoload classes defined in lib/[app_namespace]/
216
+ if root.join("lib", application_name.name).directory?
217
+ autoloader.push_dir(root.join("lib", application_name.name), namespace: namespace)
294
218
  end
295
219
 
296
- register_slice(
297
- slice_name,
298
- namespace: slice_module,
299
- root: slice_path.realpath
300
- )
220
+ autoloader.setup
301
221
  end
302
222
 
303
223
  def load_settings
@@ -311,11 +231,8 @@ module Hanami
311
231
  Settings.new
312
232
  end
313
233
 
314
- MODULE_DELIMITER = "::"
315
- private_constant :MODULE_DELIMITER
316
-
317
234
  def autodiscover_application_constant(constants)
318
- inflector.constantize([namespace_name, *constants].join(MODULE_DELIMITER))
235
+ inflector.constantize([application_name.namespace_name, *constants].join(MODULE_DELIMITER))
319
236
  end
320
237
 
321
238
  def load_router
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Configuration
5
+ class Actions
6
+ # Configuration for Content Security Policy in Hanami applications
7
+ #
8
+ # @since 2.0.0
9
+ class ContentSecurityPolicy
10
+ # @since 2.0.0
11
+ # @api private
12
+ def initialize(&blk)
13
+ @policy = {
14
+ base_uri: "'self'",
15
+ child_src: "'self'",
16
+ connect_src: "'self'",
17
+ default_src: "'none'",
18
+ font_src: "'self'",
19
+ form_action: "'self'",
20
+ frame_ancestors: "'self'",
21
+ frame_src: "'self'",
22
+ img_src: "'self' https: data:",
23
+ media_src: "'self'",
24
+ object_src: "'none'",
25
+ plugin_types: "application/pdf",
26
+ script_src: "'self'",
27
+ style_src: "'self' 'unsafe-inline' https:"
28
+ }
29
+
30
+ blk&.(self)
31
+ end
32
+
33
+ # @since 2.0.0
34
+ # @api private
35
+ def initialize_copy(original_object)
36
+ @policy = original_object.instance_variable_get(:@policy).dup
37
+ super
38
+ end
39
+
40
+ # Get a CSP setting
41
+ #
42
+ # @param key [Symbol] the underscored name of the CPS setting
43
+ # @return [String,NilClass] the CSP setting, if any
44
+ #
45
+ # @since 2.0.0
46
+ # @api public
47
+ #
48
+ # @example
49
+ # module MyApp
50
+ # class Application < Hanami::Application
51
+ # config.actions.content_security_policy[:base_uri] # => "'self'"
52
+ # end
53
+ # end
54
+ def [](key)
55
+ @policy[key]
56
+ end
57
+
58
+ # Set a CSP setting
59
+ #
60
+ # @param key [Symbol] the underscored name of the CPS setting
61
+ # @param value [String] the CSP setting value
62
+ #
63
+ # @since 2.0.0
64
+ # @api public
65
+ #
66
+ # @example Replace a default value
67
+ # module MyApp
68
+ # class Application < Hanami::Application
69
+ # config.actions.content_security_policy[:plugin_types] = nil
70
+ # end
71
+ # end
72
+ #
73
+ # @example Append to a default value
74
+ # module MyApp
75
+ # class Application < Hanami::Application
76
+ # config.actions.content_security_policy[:script_src] += " https://my.cdn.test"
77
+ # end
78
+ # end
79
+ def []=(key, value)
80
+ @policy[key] = value
81
+ end
82
+
83
+ # Deletes a CSP key
84
+ #
85
+ # @param key [Symbol] the underscored name of the CPS setting
86
+ #
87
+ # @since 2.0.0
88
+ # @api public
89
+ #
90
+ # @example
91
+ # module MyApp
92
+ # class Application < Hanami::Application
93
+ # config.actions.content_security_policy.delete(:object_src)
94
+ # end
95
+ # end
96
+ def delete(key)
97
+ @policy.delete(key)
98
+ end
99
+
100
+ # @since 2.0.0
101
+ # @api private
102
+ def to_str
103
+ @policy.map do |key, value|
104
+ "#{dasherize(key)} #{value}"
105
+ end.join(";\n")
106
+ end
107
+
108
+ private
109
+
110
+ # @since 2.0.0
111
+ # @api private
112
+ def dasherize(key)
113
+ key.to_s.gsub("_", "-")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Configuration
5
+ class Actions
6
+ # Wrapper for application-level configuration of HTTP cookies for Hanami actions.
7
+ # This decorates the hash of cookie options that is otherwise directly configurable
8
+ # on actions, and adds the `enabled?` method to allow `ApplicationAction` to
9
+ # determine whether to include the `Action::Cookies` module.
10
+ #
11
+ # @since 2.0.0
12
+ class Cookies
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def enabled?
20
+ !options.nil?
21
+ end
22
+
23
+ def to_h
24
+ options.to_h
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/constants"
4
+ require "hanami/utils/string"
5
+ require "hanami/utils/class"
6
+
7
+ module Hanami
8
+ class Configuration
9
+ class Actions
10
+ # Configuration for HTTP sessions in Hanami actions
11
+ #
12
+ # @since 2.0.0
13
+ class Sessions
14
+ attr_reader :storage, :options
15
+
16
+ def initialize(storage = nil, *options)
17
+ @storage = storage
18
+ @options = options
19
+ end
20
+
21
+ def enabled?
22
+ !storage.nil?
23
+ end
24
+
25
+ def middleware
26
+ return [] if !enabled?
27
+
28
+ [[storage_middleware, options]]
29
+ end
30
+
31
+ private
32
+
33
+ def storage_middleware
34
+ require_storage
35
+
36
+ name = Utils::String.classify(storage)
37
+ Utils::Class.load!(name, ::Rack::Session)
38
+ end
39
+
40
+ def require_storage
41
+ require "rack/session/#{storage}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end