hanami 2.0.0.beta3 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/hanami.gemspec +9 -8
  4. data/lib/hanami/app.rb +50 -39
  5. data/lib/hanami/assets/app_config.rb +61 -0
  6. data/lib/hanami/assets/{configuration.rb → config.rb} +9 -10
  7. data/lib/hanami/{configuration → config}/actions/content_security_policy.rb +3 -3
  8. data/lib/hanami/config/actions/cookies.rb +57 -0
  9. data/lib/hanami/config/actions/sessions.rb +83 -0
  10. data/lib/hanami/config/actions.rb +164 -0
  11. data/lib/hanami/config/logger.rb +176 -0
  12. data/lib/hanami/config/null_config.rb +14 -0
  13. data/lib/hanami/{configuration → config}/router.rb +8 -9
  14. data/lib/hanami/{configuration → config}/views.rb +16 -20
  15. data/lib/hanami/config.rb +396 -0
  16. data/lib/hanami/constants.rb +4 -0
  17. data/lib/hanami/errors.rb +20 -0
  18. data/lib/hanami/extensions/action/slice_configured_action.rb +10 -6
  19. data/lib/hanami/extensions/action.rb +59 -7
  20. data/lib/hanami/extensions/view/context.rb +3 -4
  21. data/lib/hanami/extensions/view/slice_configured_view.rb +4 -4
  22. data/lib/hanami/extensions/view.rb +7 -5
  23. data/lib/hanami/providers/inflector.rb +6 -2
  24. data/lib/hanami/providers/logger.rb +9 -3
  25. data/lib/hanami/providers/rack.rb +12 -2
  26. data/lib/hanami/providers/routes.rb +14 -6
  27. data/lib/hanami/routes.rb +36 -1
  28. data/lib/hanami/settings/env_store.rb +4 -4
  29. data/lib/hanami/settings.rb +169 -21
  30. data/lib/hanami/slice/router.rb +38 -16
  31. data/lib/hanami/slice/routing/middleware/stack.rb +108 -40
  32. data/lib/hanami/slice/routing/resolver.rb +10 -17
  33. data/lib/hanami/slice/view_name_inferrer.rb +1 -1
  34. data/lib/hanami/slice.rb +605 -51
  35. data/lib/hanami/slice_configurable.rb +1 -1
  36. data/lib/hanami/slice_registrar.rb +25 -14
  37. data/lib/hanami/version.rb +2 -3
  38. data/lib/hanami/web/rack_logger.rb +14 -4
  39. data/lib/hanami.rb +122 -24
  40. data/spec/integration/action/csrf_protection_spec.rb +1 -1
  41. data/spec/integration/container/application_routes_helper_spec.rb +3 -1
  42. data/spec/integration/container/prepare_container_spec.rb +2 -0
  43. data/spec/integration/container/provider_lifecycle_spec.rb +61 -0
  44. data/spec/integration/container/standard_providers/rack_provider_spec.rb +44 -0
  45. data/spec/integration/container/{standard_bootable_components_spec.rb → standard_providers_spec.rb} +3 -3
  46. data/spec/integration/rack_app/body_parser_spec.rb +111 -0
  47. data/spec/integration/rack_app/middleware_spec.rb +455 -3
  48. data/spec/integration/rack_app/non_booted_rack_app_spec.rb +2 -1
  49. data/spec/integration/rack_app/rack_app_spec.rb +39 -11
  50. data/spec/integration/settings/access_in_slice_class_body_spec.rb +82 -0
  51. data/spec/integration/settings/access_to_constants_spec.rb +23 -146
  52. data/spec/integration/{slices/slice_settings_spec.rb → settings/slice_registration_spec.rb} +5 -1
  53. data/spec/integration/settings/using_types_spec.rb +4 -11
  54. data/spec/integration/setup_spec.rb +4 -4
  55. data/spec/integration/slices/external_slice_spec.rb +2 -1
  56. data/spec/integration/slices/slice_configuration_spec.rb +3 -1
  57. data/spec/integration/slices/slice_loading_spec.rb +4 -4
  58. data/spec/integration/slices/slice_routing_spec.rb +4 -3
  59. data/spec/integration/slices_spec.rb +100 -0
  60. data/spec/isolation/hanami/boot/success_spec.rb +1 -1
  61. data/spec/support/app_integration.rb +10 -15
  62. data/spec/unit/hanami/{configuration → config}/actions/content_security_policy_spec.rb +16 -16
  63. data/spec/unit/hanami/{configuration → config}/actions/cookies_spec.rb +6 -6
  64. data/spec/unit/hanami/{configuration → config}/actions/csrf_protection_spec.rb +12 -12
  65. data/spec/unit/hanami/config/actions/default_values_spec.rb +54 -0
  66. data/spec/unit/hanami/{configuration → config}/actions/sessions_spec.rb +6 -8
  67. data/spec/unit/hanami/{configuration → config}/actions_spec.rb +8 -20
  68. data/spec/unit/hanami/{configuration → config}/base_url_spec.rb +2 -2
  69. data/spec/unit/hanami/{configuration → config}/inflector_spec.rb +2 -2
  70. data/spec/unit/hanami/{configuration → config}/logger_spec.rb +42 -59
  71. data/spec/unit/hanami/{configuration → config}/router_spec.rb +7 -8
  72. data/spec/unit/hanami/{configuration → config}/slices_spec.rb +2 -2
  73. data/spec/unit/hanami/{configuration → config}/views_spec.rb +13 -24
  74. data/spec/unit/hanami/settings_spec.rb +65 -10
  75. data/spec/unit/hanami/slice_configurable_spec.rb +21 -2
  76. data/spec/unit/hanami/slice_spec.rb +32 -0
  77. data/spec/unit/hanami/version_spec.rb +1 -1
  78. data/spec/unit/hanami/web/rack_logger_spec.rb +13 -2
  79. metadata +100 -76
  80. data/lib/hanami/assets/app_configuration.rb +0 -69
  81. data/lib/hanami/configuration/actions/cookies.rb +0 -29
  82. data/lib/hanami/configuration/actions/sessions.rb +0 -46
  83. data/lib/hanami/configuration/actions.rb +0 -101
  84. data/lib/hanami/configuration/logger.rb +0 -87
  85. data/lib/hanami/configuration/null_configuration.rb +0 -14
  86. data/lib/hanami/configuration/sessions.rb +0 -50
  87. data/lib/hanami/configuration.rb +0 -234
  88. data/lib/hanami/providers/settings.rb +0 -98
  89. data/spec/unit/hanami/configuration/actions/default_values_spec.rb +0 -52
  90. data/spec/unit/hanami/configuration_spec.rb +0 -43
data/lib/hanami/errors.rb CHANGED
@@ -1,15 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hanami
4
+ # Base class for all Hanami errors.
5
+ #
6
+ # @api public
4
7
  # @since 2.0.0
5
8
  Error = Class.new(StandardError)
6
9
 
10
+ # Error raised when {Hanami::App} fails to load.
11
+ #
12
+ # @api public
7
13
  # @since 2.0.0
8
14
  AppLoadError = Class.new(Error)
9
15
 
16
+ # Error raised when an {Hanami::Slice} fails to load.
17
+ #
18
+ # @api public
10
19
  # @since 2.0.0
11
20
  SliceLoadError = Class.new(Error)
12
21
 
22
+ # Error raised when an individual component fails to load.
23
+ #
24
+ # @api public
13
25
  # @since 2.0.0
14
26
  ComponentLoadError = Class.new(Error)
27
+
28
+ # Error raised when unsupported middleware configuration is given.
29
+ #
30
+ # @see Hanami::Slice::Routing::Middleware::Stack#use
31
+ #
32
+ # @api public
33
+ # @since 2.0.0
34
+ UnsupportedMiddlewareSpecError = Class.new(Error)
15
35
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/action"
4
-
5
3
  module Hanami
6
4
  module Extensions
7
5
  module Action
@@ -35,19 +33,21 @@ module Hanami
35
33
  resolve_view = method(:resolve_paired_view)
36
34
  resolve_view_context = method(:resolve_view_context)
37
35
  resolve_routes = method(:resolve_routes)
36
+ resolve_rack_monitor = method(:resolve_rack_monitor)
38
37
 
39
38
  define_method(:new) do |**kwargs|
40
39
  super(
41
40
  view: kwargs.fetch(:view) { resolve_view.(self) },
42
41
  view_context: kwargs.fetch(:view_context) { resolve_view_context.() },
43
42
  routes: kwargs.fetch(:routes) { resolve_routes.() },
43
+ rack_monitor: kwargs.fetch(:rack_monitor) { resolve_rack_monitor.() },
44
44
  **kwargs,
45
45
  )
46
46
  end
47
47
  end
48
48
 
49
49
  def configure_action(action_class)
50
- action_class.config.settings.each do |setting|
50
+ action_class.settings.each do |setting|
51
51
  # Configure the action from config on the slice, _unless it has already been configured
52
52
  # by a parent slice_, and re-configuring it for this slice would make no change.
53
53
  #
@@ -86,12 +86,12 @@ module Hanami
86
86
  # the `:json` value configured in its immediate superclass.
87
87
  #
88
88
  # This would be surprising behavior, and we want to avoid it.
89
- slice_value = slice.config.actions.public_send(:"#{setting}")
90
- parent_value = slice.parent.config.actions.public_send(:"#{setting}") if slice.parent
89
+ slice_value = slice.config.actions.public_send(setting.name)
90
+ parent_value = slice.parent.config.actions.public_send(setting.name) if slice.parent
91
91
 
92
92
  next if slice.parent && slice_value == parent_value
93
93
 
94
- action_class.config.public_send(:"#{setting}=", slice_value)
94
+ action_class.config.public_send(:"#{setting.name}=", slice_value)
95
95
  end
96
96
  end
97
97
 
@@ -137,6 +137,10 @@ module Hanami
137
137
  slice.app["routes"] if slice.app.key?("routes")
138
138
  end
139
139
 
140
+ def resolve_rack_monitor
141
+ slice.app["rack.monitor"] if slice.app.key?("rack.monitor")
142
+ end
143
+
140
144
  def actions_config
141
145
  slice.config.actions
142
146
  end
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "hanami/action"
4
- require_relative "../slice_configurable"
5
- require_relative "action/slice_configured_action"
6
4
 
7
5
  module Hanami
6
+ # @api private
8
7
  module Extensions
9
- # Extended behavior for actions intended for use within an Hanami app.
8
+ # Integrated behavior for `Hanami::Action` classes within Hanami apps.
10
9
  #
11
- # @see Hanami::Action
10
+ # @see InstanceMethods
11
+ # @see https://github.com/hanami/controller
12
12
  #
13
13
  # @api public
14
14
  # @since 2.0.0
15
15
  module Action
16
+ # @api private
16
17
  def self.included(action_class)
17
18
  super
18
19
 
@@ -21,46 +22,97 @@ module Hanami
21
22
  action_class.prepend(InstanceMethods)
22
23
  end
23
24
 
25
+ # Class methods for app-integrated actions.
26
+ #
27
+ # @since 2.0.0
24
28
  module ClassMethods
29
+ # @api private
25
30
  def configure_for_slice(slice)
26
31
  extend SliceConfiguredAction.new(slice)
27
32
  end
28
33
  end
29
34
 
35
+ # Instance methods for app-integrated actions.
36
+ #
37
+ # @since 2.0.0
30
38
  module InstanceMethods
31
- attr_reader :view, :view_context, :routes
39
+ # @api private
40
+ attr_reader :view
32
41
 
33
- def initialize(view: nil, view_context: nil, routes: nil, **kwargs)
42
+ # @api private
43
+ attr_reader :view_context
44
+
45
+ # Returns the app or slice's {Hanami::Slice::RoutesHelper RoutesHelper} for use within
46
+ # action instance methods.
47
+ #
48
+ # @return [Hanami::Slice::RoutesHelper]
49
+ #
50
+ # @api public
51
+ # @since 2.0.0
52
+ attr_reader :routes
53
+
54
+ # Returns the app or slice's `Dry::Monitor::Rack::Middleware` for use within
55
+ # action instance methods.
56
+ #
57
+ # @return [Dry::Monitor::Rack::Middleware]
58
+ #
59
+ # @api public
60
+ # @since 2.0.0
61
+ attr_reader :rack_monitor
62
+
63
+ # @overload def initialize(routes: nil, **kwargs)
64
+ # Returns a new `Hanami::Action` with app components injected as dependencies.
65
+ #
66
+ # These dependencies are injected automatically so that a call to `.new` (with no
67
+ # arguments) returns a fully integrated action.
68
+ #
69
+ # @param routes [Hanami::Slice::RoutesHelper]
70
+ #
71
+ # @api public
72
+ # @since 2.0.0
73
+ def initialize(view: nil, view_context: nil, rack_monitor: nil, routes: nil, **kwargs)
34
74
  @view = view
35
75
  @view_context = view_context
36
76
  @routes = routes
77
+ @rack_monitor = rack_monitor
37
78
 
38
79
  super(**kwargs)
39
80
  end
40
81
 
41
82
  private
42
83
 
84
+ # @api private
43
85
  def build_response(**options)
44
86
  options = options.merge(view_options: method(:view_options))
45
87
  super(**options)
46
88
  end
47
89
 
90
+ # @api private
48
91
  def finish(req, res, halted)
49
92
  res.render(view, **req.params) if !halted && auto_render?(res)
50
93
  super
51
94
  end
52
95
 
96
+ # @api private
97
+ def _handle_exception(request, _response, exception)
98
+ rack_monitor&.instrument(:error, exception: exception, env: request.env)
99
+
100
+ super
101
+ end
102
+
103
+ # @api private
53
104
  def view_options(req, res)
54
105
  {context: view_context&.with(**view_context_options(req, res))}.compact
55
106
  end
56
107
 
108
+ # @api private
57
109
  def view_context_options(req, res)
58
110
  {request: req, response: res}
59
111
  end
60
112
 
61
113
  # Returns true if a view should automatically be rendered onto the response body.
62
114
  #
63
- # This may be overridden to enable/disable automatic rendering.
115
+ # This may be overridden to enable or disable automatic rendering.
64
116
  #
65
117
  # @param res [Hanami::Action::Response]
66
118
  #
@@ -3,16 +3,15 @@
3
3
  require "hanami/view"
4
4
  require "hanami/view/context"
5
5
  require_relative "../../errors"
6
- require_relative "../../slice_configurable"
7
- require_relative "slice_configured_context"
8
6
 
9
7
  module Hanami
10
8
  module Extensions
11
9
  module View
12
10
  # View context for views in Hanami apps.
13
11
  #
14
- # @api public
15
- # @since 2.0.0
12
+ # This is NOT RELEASED as of 2.0.0.
13
+ #
14
+ # @api private
16
15
  module Context
17
16
  def self.included(context_class)
18
17
  super
@@ -32,7 +32,7 @@ module Hanami
32
32
  # rubocop:disable Metrics/AbcSize
33
33
  def configure_view(view_class)
34
34
  view_class.settings.each do |setting|
35
- next unless slice.config.views.respond_to?(:"#{setting}")
35
+ next unless slice.config.views.respond_to?(setting.name)
36
36
 
37
37
  # Configure the view from config on the slice, _unless it has already been configured by
38
38
  # a parent slice_, and re-configuring it for this slice would make no change.
@@ -72,12 +72,12 @@ module Hanami
72
72
  # in its immediate superclass.
73
73
  #
74
74
  # This would be surprising behavior, and we want to avoid it.
75
- slice_value = slice.config.views.public_send(:"#{setting}")
76
- parent_value = slice.parent.config.views.public_send(:"#{setting}") if slice.parent
75
+ slice_value = slice.config.views.public_send(setting.name)
76
+ parent_value = slice.parent.config.views.public_send(setting.name) if slice.parent
77
77
 
78
78
  next if slice.parent && slice_value == parent_value
79
79
 
80
- view_class.config.public_send(:"#{setting}=", slice_value)
80
+ view_class.config.public_send(:"#{setting.name}=", slice_value)
81
81
  end
82
82
 
83
83
  view_class.config.inflector = inflector
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "hanami/view"
4
- require_relative "../slice_configurable"
5
- require_relative "view/slice_configured_view"
6
4
 
7
5
  module Hanami
6
+ # @api private
8
7
  module Extensions
9
- # Extended behavior for actions intended for use within an Hanami app.
8
+ # Integrated behavior for `Hanami::View` classes within Hanami apps.
9
+ #
10
+ # This is NOT RELEASED as of 2.0.0.
10
11
  #
11
12
  # @see Hanami::View
12
13
  #
13
- # @api public
14
- # @since 2.0.0
14
+ # @api private
15
15
  module View
16
+ # @api private
16
17
  def self.included(view_class)
17
18
  super
18
19
 
@@ -20,6 +21,7 @@ module Hanami
20
21
  view_class.extend(ClassMethods)
21
22
  end
22
23
 
24
+ # @api private
23
25
  module ClassMethods
24
26
  # @api private
25
27
  def configure_for_slice(slice)
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/system/provider/source"
4
-
5
3
  module Hanami
4
+ # @api private
6
5
  module Providers
6
+ # Provider source to register inflector component in Hanami slices.
7
+ #
8
+ # @api private
9
+ # @since 2.0.0
7
10
  class Inflector < Dry::System::Provider::Source
11
+ # @api private
8
12
  def start
9
13
  register :inflector, Hanami.app.inflector
10
14
  end
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/system/provider/source"
4
-
5
3
  module Hanami
4
+ # @api private
6
5
  module Providers
6
+ # Provider source to register logger component in Hanami slices.
7
+ #
8
+ # @see Hanami::Config#logger
9
+ #
10
+ # @api private
11
+ # @since 2.0.0
7
12
  class Logger < Dry::System::Provider::Source
13
+ # @api private
8
14
  def start
9
- register :logger, Hanami.app.configuration.logger_instance
15
+ register :logger, Hanami.app.config.logger_instance
10
16
  end
11
17
  end
12
18
  end
@@ -1,10 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/system/provider/source"
4
-
5
3
  module Hanami
4
+ # @api private
6
5
  module Providers
6
+ # Provider source to register Rack integration components in Hanami slices.
7
+ #
8
+ # @see Hanami::Providers::Logger
9
+ # @see Hanami::Web::RackLogger
10
+ # @see https://github.com/rack/rack
11
+ # @see https://dry-rb.org/gems/dry-monitor/
12
+ #
13
+ # @api private
14
+ # @since 2.0.0
7
15
  class Rack < Dry::System::Provider::Source
16
+ # @api private
8
17
  def prepare
9
18
  require "dry/monitor"
10
19
  require "hanami/web/rack_logger"
@@ -12,6 +21,7 @@ module Hanami
12
21
  Dry::Monitor.load_extensions(:rack)
13
22
  end
14
23
 
24
+ # @api private
15
25
  def start
16
26
  target.start :logger
17
27
 
@@ -1,29 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/system/provider/source"
4
-
5
3
  module Hanami
4
+ # @api private
6
5
  module Providers
6
+ # Provider source to register routes helper component in Hanami slices.
7
+ #
8
+ # @see Hanami::Slice::RoutesHelper
9
+ #
10
+ # @api private
11
+ # @since 2.0.0
7
12
  class Routes < Dry::System::Provider::Source
13
+ # @api private
8
14
  def self.for_slice(slice)
9
15
  Class.new(self) do |klass|
10
16
  klass.instance_variable_set(:@slice, slice)
11
17
  end
12
18
  end
13
19
 
20
+ # @api private
14
21
  def self.slice
15
22
  @slice || Hanami.app
16
23
  end
17
24
 
25
+ # @api private
18
26
  def prepare
19
27
  require "hanami/slice/routes_helper"
20
28
  end
21
29
 
30
+ # @api private
22
31
  def start
23
- # Register a lazy instance of RoutesHelper to ensure we don't load prematurely
24
- # load the router during the process of booting. This ensures the router's
25
- # resolver can run strict action key checks once when it runs on a fully booted
26
- # slice.
32
+ # Register a lazy instance of RoutesHelper to ensure we don't load prematurely load the
33
+ # router during the process of booting. This ensures the router's resolver can run strict
34
+ # action key checks once when it runs on a fully booted slice.
27
35
  register :routes do
28
36
  Hanami::Slice::RoutesHelper.new(self.class.slice.router)
29
37
  end
data/lib/hanami/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/slice/router"
3
+ require_relative "constants"
4
+ require_relative "errors"
4
5
 
5
6
  module Hanami
6
7
  # App routes
@@ -25,6 +26,40 @@ module Hanami
25
26
  # @see Hanami::Slice::Router
26
27
  # @since 2.0.0
27
28
  class Routes
29
+ # Error raised when no action could be found in an app or slice container for the key given in a
30
+ # routes file.
31
+ #
32
+ # @api public
33
+ # @since 2.0.0
34
+ class MissingActionError < Hanami::Error
35
+ # @api private
36
+ def initialize(action_key, slice)
37
+ action_path = action_key.gsub(CONTAINER_KEY_DELIMITER, PATH_DELIMITER)
38
+ action_constant = slice.inflector.camelize(
39
+ "#{slice.inflector.underscore(slice.namespace.to_s)}#{PATH_DELIMITER}#{action_path}"
40
+ )
41
+ action_file = slice.root.join("#{action_path}#{RB_EXT}")
42
+
43
+ super(<<~MSG)
44
+ Could not find action with key #{action_key.inspect} in #{slice}
45
+
46
+ To fix this, define the action class #{action_constant} in #{action_file}
47
+ MSG
48
+ end
49
+ end
50
+
51
+ # Error raised when a given routes endpoint does not implement the `#call` interface required
52
+ # for Rack.
53
+ #
54
+ # @api public
55
+ # @since 2.0.0
56
+ class NotCallableEndpointError < Hanami::Error
57
+ # @api private
58
+ def initialize(endpoint)
59
+ super("#{endpoint.inspect} is not compatible with Rack. Please make sure it implements #call.")
60
+ end
61
+ end
62
+
28
63
  # @api private
29
64
  def self.routes
30
65
  @routes ||= build_routes
@@ -4,7 +4,7 @@ require "dry/core/constants"
4
4
 
5
5
  module Hanami
6
6
  class Settings
7
- # The default store for {Hanami:Settings}, loading setting values from `ENV`.
7
+ # The default store for {Hanami::Settings}, loading setting values from `ENV`.
8
8
  #
9
9
  # If your app loads the dotenv gem, then `ENV` will also be populated from various `.env` files when
10
10
  # you subclass `Hanami::App`.
@@ -12,7 +12,7 @@ module Hanami
12
12
  # @since 2.0.0
13
13
  # @api private
14
14
  class EnvStore
15
- Undefined = Dry::Core::Constants::Undefined
15
+ NO_ARG = Object.new.freeze
16
16
 
17
17
  attr_reader :store, :hanami_env
18
18
 
@@ -21,9 +21,9 @@ module Hanami
21
21
  @hanami_env = hanami_env
22
22
  end
23
23
 
24
- def fetch(name, default_value = Undefined, &block)
24
+ def fetch(name, default_value = NO_ARG, &block)
25
25
  name = name.to_s.upcase
26
- args = default_value == Undefined ? [name] : [name, default_value]
26
+ args = default_value.eql?(NO_ARG) ? [name] : [name, default_value]
27
27
 
28
28
  store.fetch(*args, &block)
29
29
  end