hanami 2.0.0.alpha7.1 → 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,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
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # require_relative "application_configuration/cookies"
4
- # require_relative "application_configuration/sessions"
5
- # require_relative "application_configuration/content_security_policy"
6
- # require_relative "configuration"
7
- # require_relative "view_name_inferrer"
3
+ require "dry/configurable"
4
+ require "hanami/action/configuration"
5
+ require_relative "actions/cookies"
6
+ require_relative "actions/sessions"
7
+ require_relative "actions/content_security_policy"
8
+ require_relative "../application/view_name_inferrer"
8
9
 
9
10
  module Hanami
10
11
  class Configuration
12
+ # Hanami actions configuration
13
+ #
14
+ # @since 2.0.0
11
15
  class Actions
12
16
  include Dry::Configurable
13
17
 
@@ -17,15 +21,18 @@ module Hanami
17
21
 
18
22
  setting :name_inference_base, default: "actions"
19
23
  setting :view_context_identifier, default: "view.context"
20
- setting :view_name_inferrer, default: ViewNameInferrer
24
+ setting :view_name_inferrer, default: Application::ViewNameInferrer
21
25
  setting :view_name_inference_base, default: "views"
22
26
 
23
27
  attr_accessor :content_security_policy
24
28
 
29
+ attr_reader :base_configuration
30
+ private :base_configuration
31
+
25
32
  def initialize(*, **options)
26
33
  super()
27
34
 
28
- @base_configuration = Configuration.new
35
+ @base_configuration = Hanami::Action::Configuration.new
29
36
  @content_security_policy = ContentSecurityPolicy.new do |csp|
30
37
  if assets_server_url = options[:assets_server_url]
31
38
  csp[:script_src] += " #{assets_server_url}"
@@ -41,8 +48,8 @@ module Hanami
41
48
  # (neither true nor false), so we can default it to whether sessions are enabled
42
49
  self.csrf_protection = sessions.enabled? if csrf_protection.nil?
43
50
 
44
- if self.content_security_policy
45
- self.default_headers["Content-Security-Policy"] = self.content_security_policy.to_str
51
+ if content_security_policy
52
+ default_headers["Content-Security-Policy"] = content_security_policy.to_str
46
53
  end
47
54
  end
48
55
 
@@ -58,8 +65,6 @@ module Hanami
58
65
 
59
66
  private
60
67
 
61
- attr_reader :base_configuration
62
-
63
68
  # Apply defaults for base configuration settings
64
69
  def configure_defaults
65
70
  self.default_request_format = :html
@@ -11,9 +11,9 @@ module Hanami
11
11
  class Logger
12
12
  include Dry::Configurable
13
13
 
14
- protected :config
14
+ attr_reader :application_name
15
15
 
16
- setting :application_name
16
+ protected :config
17
17
 
18
18
  setting :level
19
19
 
@@ -30,7 +30,6 @@ module Hanami
30
30
  setting :logger_class, default: Hanami::Logger
31
31
 
32
32
  def initialize(env:, application_name:)
33
- @env = env
34
33
  @application_name = application_name
35
34
 
36
35
  config.level = case env
@@ -58,12 +57,16 @@ module Hanami
58
57
  end
59
58
  end
60
59
 
61
- def finalize!
62
- config.application_name = @application_name.call
63
- end
64
-
65
60
  def instance
66
- logger_class.new(application_name, *options, stream: stream, level: level, formatter: formatter, filter: filters, colorizer: colors)
61
+ logger_class.new(
62
+ application_name.name,
63
+ *options,
64
+ stream: stream,
65
+ level: level,
66
+ formatter: formatter,
67
+ filter: filters,
68
+ colorizer: colors
69
+ )
67
70
  end
68
71
 
69
72
  private
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "hanami/view"
5
+
6
+ module Hanami
7
+ class Configuration
8
+ # Hanami actions configuration
9
+ #
10
+ # @since 2.0.0
11
+ class Views
12
+ include Dry::Configurable
13
+
14
+ setting :parts_path, default: "view/parts"
15
+
16
+ attr_reader :base_configuration
17
+ private :base_configuration
18
+
19
+ def initialize(*)
20
+ super
21
+
22
+ @base_configuration = Hanami::View.config.dup
23
+
24
+ configure_defaults
25
+ end
26
+
27
+ # Returns the list of available settings
28
+ #
29
+ # @return [Set]
30
+ #
31
+ # @since 2.0.0
32
+ # @api private
33
+ def settings
34
+ self.class.settings + View.settings - NON_FORWARDABLE_METHODS
35
+ end
36
+
37
+ def finalize!
38
+ return self if frozen?
39
+
40
+ base_configuration.finalize!
41
+
42
+ super
43
+ end
44
+
45
+ private
46
+
47
+ def configure_defaults
48
+ self.paths = ["templates"]
49
+ self.template_inference_base = "views"
50
+ self.layout = "application"
51
+ end
52
+
53
+ # An inflector for views is not configurable via `config.views.inflector` on an
54
+ # `Hanami::Application`. The application-wide inflector is already configurable
55
+ # there as `config.inflector` and will be used as the default inflector for views.
56
+ #
57
+ # A custom inflector may still be provided in an `Hanami::View` subclass, via
58
+ # `config.inflector=`.
59
+ NON_FORWARDABLE_METHODS = %i[inflector inflector=].freeze
60
+ private_constant :NON_FORWARDABLE_METHODS
61
+
62
+ def method_missing(name, *args, &block)
63
+ return super if NON_FORWARDABLE_METHODS.include?(name)
64
+
65
+ if config.respond_to?(name)
66
+ config.public_send(name, *args, &block)
67
+ elsif base_configuration.respond_to?(name)
68
+ base_configuration.public_send(name, *args, &block)
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ def respond_to_missing?(name, _include_all = false)
75
+ return false if NON_FORWARDABLE_METHODS.include?(name)
76
+
77
+ config.respond_to?(name) || base_configuration.respond_to?(name) || super
78
+ end
79
+ end
80
+ end
81
+ end
@@ -19,14 +19,13 @@ module Hanami
19
19
  # Hanami application configuration
20
20
  #
21
21
  # @since 2.0.0
22
- #
23
- # rubocop:disable Metrics/ClassLength
24
22
  class Configuration
25
23
  include Dry::Configurable
26
24
 
27
25
  DEFAULT_ENVIRONMENTS = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
28
26
  private_constant :DEFAULT_ENVIRONMENTS
29
27
 
28
+ attr_reader :application_name
30
29
  attr_reader :env
31
30
 
32
31
  attr_reader :actions
@@ -37,8 +36,9 @@ module Hanami
37
36
  attr_reader :environments
38
37
  private :environments
39
38
 
39
+ # rubocop:disable Metrics/AbcSize
40
40
  def initialize(application_name:, env:)
41
- @namespace = application_name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER)
41
+ @application_name = application_name
42
42
 
43
43
  @environments = DEFAULT_ENVIRONMENTS.clone
44
44
  @env = env
@@ -48,46 +48,29 @@ module Hanami
48
48
  self.root = Dir.pwd
49
49
  self.settings_store = Application::Settings::DotenvStore.new.with_dotenv_loaded
50
50
 
51
- config.logger = Configuration::Logger.new(env: env, application_name: method(:application_name))
51
+ config.logger = Configuration::Logger.new(env: env, application_name: application_name)
52
52
 
53
- @assets = begin
54
- require_path = "hanami/assets/application_configuration"
55
- require require_path
53
+ @assets = load_dependent_config("hanami/assets/application_configuration") {
56
54
  Hanami::Assets::ApplicationConfiguration.new
57
- rescue LoadError => e
58
- raise e unless e.path == require_path
59
- require_relative "configuration/null_configuration"
60
- NullConfiguration.new
61
- end
55
+ }
62
56
 
63
- # Config for actions (same for views, below) may not be available if the gem isn't
64
- # loaded; fall back to a null config object if it's missing
65
- @actions = begin
66
- require_path = "hanami/action/application_configuration"
67
- require require_path
68
- Hanami::Action::ApplicationConfiguration.new(assets_server_url: assets.server_url)
69
- rescue LoadError => e
70
- raise e unless e.path == require_path
71
- require_relative "configuration/null_configuration"
72
- NullConfiguration.new
73
- end
57
+ @actions = load_dependent_config("hanami/action") {
58
+ require_relative "configuration/actions"
59
+ Actions.new
60
+ }
74
61
 
75
62
  @middleware = Middleware.new
76
63
 
77
64
  @router = Router.new(self)
78
65
 
79
- @views = begin
80
- require_path = "hanami/view/application_configuration"
81
- require require_path
82
- Hanami::View::ApplicationConfiguration.new
83
- rescue LoadError => e
84
- raise e unless e.path == require_path
85
- require_relative "configuration/null_configuration"
86
- NullConfiguration.new
87
- end
66
+ @views = load_dependent_config("hanami/view") {
67
+ require_relative "configuration/views"
68
+ Views.new
69
+ }
88
70
 
89
71
  yield self if block_given?
90
72
  end
73
+ # rubocop:enable Metrics/AbcSize
91
74
 
92
75
  def environment(env_name, &block)
93
76
  environments[env_name] << block
@@ -109,14 +92,6 @@ module Hanami
109
92
  super
110
93
  end
111
94
 
112
- def namespace
113
- inflector.constantize(@namespace)
114
- end
115
-
116
- def application_name
117
- inflector.underscore(@namespace).to_sym
118
- end
119
-
120
95
  setting :root, constructor: -> path { Pathname(path) }
121
96
 
122
97
  setting :inflector, default: Dry::Inflector.new
@@ -162,6 +137,16 @@ module Hanami
162
137
  end
163
138
  end
164
139
 
140
+ def load_dependent_config(require_path, &block)
141
+ require require_path
142
+ yield
143
+ rescue LoadError => e
144
+ raise e unless e.path == require_path
145
+
146
+ require_relative "configuration/null_configuration"
147
+ NullConfiguration.new
148
+ end
149
+
165
150
  def method_missing(name, *args, &block)
166
151
  if config.respond_to?(name)
167
152
  config.public_send(name, *args, &block)
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hanami
4
+ CONTAINER_KEY_DELIMITER = "."
5
+ private_constant :CONTAINER_KEY_DELIMITER
6
+
4
7
  # @api private
5
8
  MODULE_DELIMITER = "::"
6
9
  private_constant :MODULE_DELIMITER
7
10
 
11
+ PATH_DELIMITER = "/"
12
+ private_constant :PATH_DELIMITER
13
+
8
14
  # @api private
9
15
  CONFIG_DIR = "config"
10
16
  private_constant :CONFIG_DIR
data/lib/hanami/errors.rb CHANGED
@@ -9,4 +9,7 @@ module Hanami
9
9
 
10
10
  # @since 2.0.0
11
11
  SliceLoadError = Class.new(Error)
12
+
13
+ # @since 2.0.0
14
+ ComponentLoadError = Class.new(Error)
12
15
  end
data/lib/hanami/slice.rb CHANGED
@@ -4,20 +4,21 @@ require "dry/system/container"
4
4
  require "hanami/errors"
5
5
  require "pathname"
6
6
  require_relative "constants"
7
+ require_relative "slice_name"
7
8
 
8
9
  module Hanami
9
10
  # Distinct area of concern within an Hanami application
10
11
  #
11
12
  # @since 2.0.0
12
13
  class Slice
13
- def self.inherited(klass)
14
+ def self.inherited(subclass)
14
15
  super
15
16
 
16
- klass.extend(ClassMethods)
17
+ subclass.extend(ClassMethods)
17
18
 
18
19
  # 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))
20
+ subclass.instance_variable_set(:@application, Hanami.application)
21
+ subclass.instance_variable_set(:@container, Class.new(Dry::System::Container))
21
22
  end
22
23
 
23
24
  # rubocop:disable Metrics/ModuleLength
@@ -25,15 +26,11 @@ module Hanami
25
26
  attr_reader :application, :container
26
27
 
27
28
  def slice_name
28
- inflector.underscore(name.split(MODULE_DELIMITER)[-2]).to_sym
29
+ @slice_name ||= SliceName.new(self, inflector: method(:inflector))
29
30
  end
30
31
 
31
32
  def namespace
32
- inflector.constantize(name.split(MODULE_DELIMITER)[0..-2].join(MODULE_DELIMITER))
33
- end
34
-
35
- def namespace_path
36
- inflector.underscore(namespace)
33
+ slice_name.namespace
37
34
  end
38
35
 
39
36
  def root
@@ -175,7 +172,7 @@ module Hanami
175
172
  end
176
173
 
177
174
  def prepare_container_base_config
178
- container.config.name = slice_name
175
+ container.config.name = slice_name.to_sym
179
176
  container.config.root = root
180
177
  container.config.provider_dirs = [File.join("config", "providers")]
181
178
 
@@ -199,7 +196,7 @@ module Hanami
199
196
  # e.g. "lib/foo.rb" should define SliceNamespace::Foo, to be registered as
200
197
  # "foo"
201
198
  component_dir.namespaces.delete_root
202
- component_dir.namespaces.add_root(key: nil, const: namespace_path)
199
+ component_dir.namespaces.add_root(key: nil, const: slice_name.name)
203
200
  else
204
201
  # Expect component files in the root of non-lib/ component dirs to define
205
202
  # classes inside a namespace matching that dir.
@@ -207,7 +204,7 @@ module Hanami
207
204
  # e.g. "actions/foo.rb" should define SliceNamespace::Actions::Foo, to be
208
205
  # registered as "actions.foo"
209
206
 
210
- dir_namespace_path = File.join(namespace_path, component_dir.path)
207
+ dir_namespace_path = File.join(slice_name.name, component_dir.path)
211
208
 
212
209
  component_dir.namespaces.delete_root
213
210
  component_dir.namespaces.add_root(const: dir_namespace_path, key: component_dir.path)
@@ -224,7 +221,7 @@ module Hanami
224
221
  application.configuration.source_dirs.autoload_paths.each do |autoload_path|
225
222
  next unless root.join(autoload_path).directory?
226
223
 
227
- dir_namespace_path = File.join(namespace_path, autoload_path)
224
+ dir_namespace_path = File.join(slice_name.name, autoload_path)
228
225
 
229
226
  autoloader_namespace = begin
230
227
  inflector.constantize(inflector.camelize(dir_namespace_path))
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Hanami
6
+ # Calls `configure_for_slice(slice)` on the extended class whenever it is first
7
+ # subclassed within a module namespace corresponding to a slice.
8
+ #
9
+ # @example
10
+ # class BaseClass
11
+ # extend Hanami::SliceConfigurable
12
+ # end
13
+ #
14
+ # # slices/main/lib/my_class.rb
15
+ # module Main
16
+ # class MyClass < BaseClass
17
+ # # Will be called with `Main::Slice`
18
+ # def self.configure_for_slice(slice)
19
+ # # ...
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # @api private
25
+ # @since 2.0.0
26
+ module SliceConfigurable
27
+ class << self
28
+ def extended(klass)
29
+ slice_for = method(:slice_for)
30
+
31
+ inherited_mod = Module.new do
32
+ define_method(:inherited) do |subclass|
33
+ unless Hanami.application?
34
+ raise ComponentLoadError, "Class #{klass} must be defined within an Hanami application"
35
+ end
36
+
37
+ super(subclass)
38
+
39
+ subclass.instance_variable_set(:@configured_for_slices, configured_for_slices.dup)
40
+
41
+ slice = slice_for.(subclass)
42
+ return unless slice
43
+
44
+ unless subclass.configured_for_slice?(slice)
45
+ subclass.configure_for_slice(slice)
46
+ subclass.configured_for_slices << slice
47
+ end
48
+ end
49
+ end
50
+
51
+ klass.singleton_class.prepend(inherited_mod)
52
+ end
53
+
54
+ private
55
+
56
+ def slice_for(klass)
57
+ return unless klass.name
58
+
59
+ slices = Hanami.application.slices.to_a + [Hanami.application]
60
+
61
+ slices.detect { |slice| klass.name.include?(slice.namespace.to_s) }
62
+ end
63
+ end
64
+
65
+ def configure_for_slice(slice); end
66
+
67
+ def configured_for_slice?(slice)
68
+ configured_for_slices.include?(slice)
69
+ end
70
+
71
+ def configured_for_slices
72
+ @configured_for_slices ||= []
73
+ end
74
+ end
75
+ end