hanami 2.0.0.alpha6 → 2.0.0.alpha8

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