hanami 2.2.1 → 2.3.0.beta2

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/README.md +20 -35
  4. data/hanami.gemspec +9 -8
  5. data/lib/hanami/app.rb +2 -0
  6. data/lib/hanami/config/actions/content_security_policy.rb +23 -0
  7. data/lib/hanami/config/actions.rb +21 -0
  8. data/lib/hanami/config/console.rb +79 -0
  9. data/lib/hanami/config/logger.rb +1 -1
  10. data/lib/hanami/config.rb +14 -1
  11. data/lib/hanami/constants.rb +3 -0
  12. data/lib/hanami/extensions/db/repo.rb +11 -6
  13. data/lib/hanami/extensions/view/context.rb +10 -10
  14. data/lib/hanami/extensions/view/slice_configured_context.rb +0 -7
  15. data/lib/hanami/helpers/assets_helper.rb +92 -25
  16. data/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
  17. data/lib/hanami/routes.rb +3 -3
  18. data/lib/hanami/slice.rb +34 -6
  19. data/lib/hanami/slice_registrar.rb +1 -1
  20. data/lib/hanami/version.rb +1 -1
  21. data/lib/hanami.rb +10 -2
  22. data/spec/integration/action/format_config_spec.rb +6 -3
  23. data/spec/integration/action/slice_configuration_spec.rb +36 -36
  24. data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -1
  25. data/spec/integration/assets/serve_static_assets_spec.rb +1 -1
  26. data/spec/integration/container/autoloader_spec.rb +2 -0
  27. data/spec/integration/db/db_spec.rb +1 -1
  28. data/spec/integration/db/logging_spec.rb +63 -0
  29. data/spec/integration/db/repo_spec.rb +87 -2
  30. data/spec/integration/logging/exception_logging_spec.rb +6 -1
  31. data/spec/integration/rack_app/body_parser_spec.rb +2 -1
  32. data/spec/integration/rack_app/middleware_spec.rb +4 -11
  33. data/spec/integration/rack_app/rack_app_spec.rb +2 -2
  34. data/spec/integration/view/helpers/form_helper_spec.rb +1 -1
  35. data/spec/integration/web/content_security_policy_nonce_spec.rb +251 -0
  36. data/spec/support/app_integration.rb +2 -1
  37. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +7 -0
  38. data/spec/unit/hanami/config/actions_spec.rb +2 -2
  39. data/spec/unit/hanami/config/console_spec.rb +22 -0
  40. data/spec/unit/hanami/env_spec.rb +10 -13
  41. data/spec/unit/hanami/slice_spec.rb +18 -0
  42. data/spec/unit/hanami/web/rack_logger_spec.rb +11 -4
  43. metadata +34 -29
  44. data/spec/integration/view/context/settings_spec.rb +0 -46
  45. data/spec/support/shared_examples/cli/generate/app.rb +0 -494
  46. data/spec/support/shared_examples/cli/generate/migration.rb +0 -32
  47. data/spec/support/shared_examples/cli/generate/model.rb +0 -81
  48. data/spec/support/shared_examples/cli/new.rb +0 -97
  49. data/spec/unit/hanami/version_spec.rb +0 -7
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
  require "hanami/view"
5
+ require_relative "../constants"
5
6
 
6
7
  # rubocop:disable Metrics/ModuleLength
7
8
 
@@ -61,7 +62,10 @@ module Hanami
61
62
 
62
63
  # @since 0.3.0
63
64
  # @api private
64
- ABSOLUTE_URL_MATCHER = URI::DEFAULT_PARSER.make_regexp
65
+ # TODO: we can drop the defined?-check and fallback once Ruby 3.3 becomes our minimum required version
66
+ ABSOLUTE_URL_MATCHER = (
67
+ defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
68
+ ).make_regexp
65
69
 
66
70
  # @since 1.1.0
67
71
  # @api private
@@ -85,7 +89,12 @@ module Hanami
85
89
  # name of the algorithm, then a hyphen, then the hash value of the file.
86
90
  # If more than one algorithm is used, they"ll be separated by a space.
87
91
  #
88
- # @param source_paths [Array<String, #url>] one or more assets by name or absolute URL
92
+ # If the Content Security Policy uses 'nonce' and the source is not
93
+ # absolute, the nonce value of the current request is automatically added
94
+ # as an attribute. You can override this with the `nonce: false` option.
95
+ # See {#content_security_policy_nonce} for more.
96
+ #
97
+ # @param sources [Array<String, #url>] one or more assets by name or absolute URL
89
98
  #
90
99
  # @return [Hanami::View::HTML::SafeString] the markup
91
100
  #
@@ -136,6 +145,10 @@ module Hanami
136
145
  #
137
146
  # # <script src="/assets/application.js" type="text/javascript" defer="defer"></script>
138
147
  #
148
+ # @example Disable nonce
149
+ #
150
+ # <%= javascript_tag "application", nonce: false %>
151
+ #
139
152
  # @example Absolute URL
140
153
  #
141
154
  # <%= javascript_tag "https://code.jquery.com/jquery-2.1.4.min.js" %>
@@ -154,13 +167,15 @@ module Hanami
154
167
  #
155
168
  # # <script src="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
156
169
  # # type="text/javascript"></script>
157
- def javascript_tag(*source_paths, **options)
170
+ def javascript_tag(*sources, **options)
158
171
  options = options.reject { |k, _| k.to_sym == :src }
172
+ nonce_option = options.delete(:nonce)
159
173
 
160
- _safe_tags(*source_paths) do |source|
174
+ _safe_tags(*sources) do |source|
161
175
  attributes = {
162
- src: _typed_path(source, JAVASCRIPT_EXT),
163
- type: JAVASCRIPT_MIME_TYPE
176
+ src: _typed_url(source, JAVASCRIPT_EXT),
177
+ type: JAVASCRIPT_MIME_TYPE,
178
+ nonce: _nonce(source, nonce_option)
164
179
  }
165
180
  attributes.merge!(options)
166
181
 
@@ -189,7 +204,12 @@ module Hanami
189
204
  # name of the algorithm, then a hyphen, then the hashed value of the file.
190
205
  # If more than one algorithm is used, they"ll be separated by a space.
191
206
  #
192
- # @param source_paths [Array<String, #url>] one or more assets by name or absolute URL
207
+ # If the Content Security Policy uses 'nonce' and the source is not
208
+ # absolute, the nonce value of the current request is automatically added
209
+ # as an attribute. You can override this with the `nonce: false` option.
210
+ # See {#content_security_policy_nonce} for more.
211
+ #
212
+ # @param sources [Array<String, #url>] one or more assets by name or absolute URL
193
213
  #
194
214
  # @return [Hanami::View::HTML::SafeString] the markup
195
215
  #
@@ -214,6 +234,10 @@ module Hanami
214
234
  # # <link href="/assets/application.css" type="text/css" rel="stylesheet">
215
235
  # # <link href="/assets/dashboard.css" type="text/css" rel="stylesheet">
216
236
  #
237
+ # @example Disable nonce
238
+ #
239
+ # <%= stylesheet_tag "application", nonce: false %>
240
+ #
217
241
  # @example Subresource Integrity
218
242
  #
219
243
  # <%= stylesheet_tag "application" %>
@@ -247,19 +271,21 @@ module Hanami
247
271
  #
248
272
  # # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
249
273
  # # type="text/css" rel="stylesheet">
250
- def stylesheet_tag(*source_paths, **options)
274
+ def stylesheet_tag(*sources, **options)
251
275
  options = options.reject { |k, _| k.to_sym == :href }
276
+ nonce_option = options.delete(:nonce)
252
277
 
253
- _safe_tags(*source_paths) do |source_path|
278
+ _safe_tags(*sources) do |source|
254
279
  attributes = {
255
- href: _typed_path(source_path, STYLESHEET_EXT),
280
+ href: _typed_url(source, STYLESHEET_EXT),
256
281
  type: STYLESHEET_MIME_TYPE,
257
- rel: STYLESHEET_REL
282
+ rel: STYLESHEET_REL,
283
+ nonce: _nonce(source, nonce_option)
258
284
  }
259
285
  attributes.merge!(options)
260
286
 
261
287
  if _context.assets.subresource_integrity? || attributes.include?(:integrity)
262
- attributes[:integrity] ||= _subresource_integrity_value(source_path, STYLESHEET_EXT)
288
+ attributes[:integrity] ||= _subresource_integrity_value(source, STYLESHEET_EXT)
263
289
  attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
264
290
  end
265
291
 
@@ -626,7 +652,7 @@ module Hanami
626
652
  #
627
653
  # If CDN mode is on, it returns the absolute URL of the asset.
628
654
  #
629
- # @param source_path [String, #url] the asset name or asset object
655
+ # @param source [String, #url] the asset name or asset object
630
656
  #
631
657
  # @return [String] the asset path
632
658
  #
@@ -665,42 +691,71 @@ module Hanami
665
691
  # <%= asset_url "application.js" %>
666
692
  #
667
693
  # # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
668
- def asset_url(source_path)
669
- return source_path.url if source_path.respond_to?(:url)
670
- return source_path if _absolute_url?(source_path)
694
+ def asset_url(source)
695
+ return source.url if source.respond_to?(:url)
696
+ return source if _absolute_url?(source)
671
697
 
672
- _context.assets[source_path].url
698
+ _context.assets[source].url
699
+ end
700
+
701
+ # Random per request nonce value for Content Security Policy (CSP) rules.
702
+ #
703
+ # If the `Hanami::Middleware::ContentSecurityPolicyNonce` middleware is
704
+ # in use, this helper returns the nonce value for the current request
705
+ # or `nil` otherwise.
706
+ #
707
+ # For this policy to work in the browser, you have to add the `'nonce'`
708
+ # placeholder to the script and/or style source policy rule. It will be
709
+ # substituted by the current nonce value like `'nonce-A12OggyZ'.
710
+ #
711
+ # @return [String, nil] nonce value of the current request
712
+ #
713
+ # @since x.x.x
714
+ #
715
+ # @example App configuration
716
+ #
717
+ # config.middleware.use Hanami::Middleware::ContentSecurityPolicyNonce
718
+ # config.actions.content_security_policy[:script_src] = "'self' 'nonce'"
719
+ # config.actions.content_security_policy[:style_src] = "'self' 'nonce'"
720
+ #
721
+ # @example View helper
722
+ #
723
+ # <script nonce="<%= content_security_policy_nonce %>">
724
+ def content_security_policy_nonce
725
+ return unless _context.request?
726
+
727
+ _context.request.env[CONTENT_SECURITY_POLICY_NONCE_REQUEST_KEY]
673
728
  end
674
729
 
675
730
  private
676
731
 
677
732
  # @since 2.1.0
678
733
  # @api private
679
- def _safe_tags(*source_paths, &blk)
734
+ def _safe_tags(*sources, &blk)
680
735
  ::Hanami::View::HTML::SafeString.new(
681
- source_paths.map(&blk).join(NEW_LINE_SEPARATOR)
736
+ sources.map(&blk).join(NEW_LINE_SEPARATOR)
682
737
  )
683
738
  end
684
739
 
685
740
  # @since 2.1.0
686
741
  # @api private
687
- def _typed_path(source, ext)
742
+ def _typed_url(source, ext)
688
743
  source = "#{source}#{ext}" if source.is_a?(String) && _append_extension?(source, ext)
689
744
  asset_url(source)
690
745
  end
691
746
 
692
747
  # @api private
693
- def _subresource_integrity_value(source_path, ext)
694
- return if _absolute_url?(source_path)
748
+ def _subresource_integrity_value(source, ext)
749
+ return if _absolute_url?(source)
695
750
 
696
- source_path = "#{source_path}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source_path)
697
- _context.assets[source_path].sri
751
+ source = "#{source}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source)
752
+ _context.assets[source].sri
698
753
  end
699
754
 
700
755
  # @since 2.1.0
701
756
  # @api private
702
757
  def _absolute_url?(source)
703
- ABSOLUTE_URL_MATCHER.match(source)
758
+ ABSOLUTE_URL_MATCHER.match?(source.respond_to?(:url) ? source.url : source)
704
759
  end
705
760
 
706
761
  # @since 1.2.0
@@ -711,6 +766,18 @@ module Hanami
711
766
  _context.assets.crossorigin?(source)
712
767
  end
713
768
 
769
+ # @since x.x.x
770
+ # @api private
771
+ def _nonce(source, nonce_option)
772
+ if nonce_option == false
773
+ nil
774
+ elsif nonce_option == true || (nonce_option.nil? && !_absolute_url?(source))
775
+ content_security_policy_nonce
776
+ else
777
+ nonce_option
778
+ end
779
+ end
780
+
714
781
  # @since 2.1.0
715
782
  # @api private
716
783
  def _source_options(src, options, &blk)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "securerandom"
5
+ require_relative "../constants"
6
+
7
+ module Hanami
8
+ module Middleware
9
+ # Generates a random per request nonce value like `mSMnSwfVZVe+LyQy1SPCGw==`, stores it as
10
+ # `"hanami.content_security_policy_nonce"` in the Rack environment, and replaces all occurrences
11
+ # of `'nonce'` in the `Content-Security-Policy header with the actual nonce value for the
12
+ # request, e.g. `'nonce-mSMnSwfVZVe+LyQy1SPCGw=='`.
13
+ #
14
+ # @see Hanami::Middleware::ContentSecurityPolicyNonce
15
+ #
16
+ # @api private
17
+ # @since x.x.x
18
+ class ContentSecurityPolicyNonce
19
+ # @api private
20
+ # @since x.x.x
21
+ def initialize(app)
22
+ @app = app
23
+ end
24
+
25
+ # @api private
26
+ # @since x.x.x
27
+ def call(env)
28
+ return @app.call(env) unless Hanami.app.config.actions.content_security_policy?
29
+
30
+ args = nonce_generator.arity == 1 ? [Rack::Request.new(env)] : []
31
+ request_nonce = nonce_generator.call(*args)
32
+
33
+ env[CONTENT_SECURITY_POLICY_NONCE_REQUEST_KEY] = request_nonce
34
+
35
+ _, headers, _ = response = @app.call(env)
36
+
37
+ headers["Content-Security-Policy"] = sub_nonce(headers["Content-Security-Policy"], request_nonce)
38
+
39
+ response
40
+ end
41
+
42
+ private
43
+
44
+ def nonce_generator
45
+ Hanami.app.config.actions.content_security_policy_nonce_generator
46
+ end
47
+
48
+ def sub_nonce(string, nonce)
49
+ string&.gsub("'nonce'", "'nonce-#{nonce}'")
50
+ end
51
+ end
52
+ end
53
+ end
data/lib/hanami/routes.rb CHANGED
@@ -36,14 +36,14 @@ module Hanami
36
36
  def initialize(action_key, slice)
37
37
  action_path = action_key.gsub(CONTAINER_KEY_DELIMITER, PATH_DELIMITER)
38
38
  action_constant = slice.inflector.camelize(
39
- "#{slice.inflector.underscore(slice.namespace.to_s)}#{PATH_DELIMITER}#{action_path}"
39
+ slice.inflector.underscore(slice.namespace.to_s) + PATH_DELIMITER + action_path
40
40
  )
41
- action_file = slice.root.join("#{action_path}#{RB_EXT}")
41
+ action_file_path = slice.relative_source_path.join(action_path).to_s.concat(RB_EXT)
42
42
 
43
43
  super(<<~MSG)
44
44
  Could not find action with key #{action_key.inspect} in #{slice}
45
45
 
46
- To fix this, define the action class #{action_constant} in #{action_file}
46
+ To fix this, define the action class #{action_constant} in #{action_file_path}
47
47
  MSG
48
48
  end
49
49
  end
data/lib/hanami/slice.rb CHANGED
@@ -223,6 +223,18 @@ module Hanami
223
223
  app? ? root.join(APP_DIR) : root
224
224
  end
225
225
 
226
+ # Returns the slice's root component directory, as a path relative to the app's root.
227
+ #
228
+ # @return [Pathname]
229
+ #
230
+ # @see #source_path
231
+ #
232
+ # @api public
233
+ # @since 2.3.0
234
+ def relative_source_path
235
+ source_path.relative_path_from(app.root)
236
+ end
237
+
226
238
  # Returns the slice's configured inflector.
227
239
  #
228
240
  # Unless explicitly re-configured for the slice, this will be the app's inflector.
@@ -729,7 +741,9 @@ module Hanami
729
741
  # @api public
730
742
  # @since 2.0.0
731
743
  def routes
732
- @routes ||= load_routes
744
+ return @routes if instance_variable_defined?(:@routes)
745
+
746
+ @routes = load_routes
733
747
  end
734
748
 
735
749
  # Returns the slice's router, if or nil if no routes are defined.
@@ -925,7 +939,7 @@ module Hanami
925
939
  # Check here for the `routes` definition only, not `router` itself, because the
926
940
  # `router` requires the slice to be prepared before it can be loaded, and at this
927
941
  # point we're still in the process of preparing.
928
- if routes
942
+ if routes?
929
943
  require_relative "providers/routes"
930
944
  register_provider(:routes, source: Providers::Routes)
931
945
  end
@@ -954,9 +968,10 @@ module Hanami
954
968
  end
955
969
 
956
970
  def prepare_autoloader
957
- # Component dirs are automatically pushed to the autoloader by dry-system's
958
- # zeitwerk plugin. This method adds other dirs that are not otherwise configured
959
- # as component dirs.
971
+ autoloader.tag = "hanami.slices.#{slice_name.to_s}"
972
+
973
+ # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
974
+ # This method adds other dirs that are not otherwise configured as component dirs.
960
975
 
961
976
  # Everything in the slice root can be autoloaded except `config/` and `slices/`,
962
977
  # which are framework-managed directories
@@ -977,11 +992,19 @@ module Hanami
977
992
  slices.freeze
978
993
  end
979
994
 
995
+ def routes?
996
+ return false unless Hanami.bundled?("hanami-router")
997
+
998
+ return true if namespace.const_defined?(ROUTES_CLASS_NAME)
999
+
1000
+ root.join("#{ROUTES_PATH}#{RB_EXT}").file?
1001
+ end
1002
+
980
1003
  def load_routes
981
1004
  return false unless Hanami.bundled?("hanami-router")
982
1005
 
983
1006
  if root.directory?
984
- routes_require_path = File.join(root, ROUTES_PATH)
1007
+ routes_require_path = root.join(ROUTES_PATH).to_s
985
1008
 
986
1009
  begin
987
1010
  require_relative "./routes"
@@ -1050,6 +1073,11 @@ module Hanami
1050
1073
  if config.actions.sessions.enabled?
1051
1074
  use(*config.actions.sessions.middleware)
1052
1075
  end
1076
+
1077
+ if config.actions.content_security_policy && # rubocop:disable Style/SafeNavigation
1078
+ config.actions.content_security_policy.nonce?
1079
+ use(*config.actions.content_security_policy.middleware)
1080
+ end
1053
1081
  end
1054
1082
 
1055
1083
  if Hanami.bundled?("hanami-assets") && config.assets.serve
@@ -5,7 +5,7 @@ require_relative "constants"
5
5
  module Hanami
6
6
  # @api private
7
7
  class SliceRegistrar
8
- VALID_SLICE_NAME_RE = /^[a-z][a-z0-9_]+$/
8
+ VALID_SLICE_NAME_RE = /^[a-z][a-z0-9_]*$/
9
9
  SLICE_DELIMITER = CONTAINER_KEY_DELIMITER
10
10
 
11
11
  attr_reader :parent, :slices
@@ -7,7 +7,7 @@ module Hanami
7
7
  # @api private
8
8
  module Version
9
9
  # @api public
10
- VERSION = "2.2.1"
10
+ VERSION = "2.3.0.beta2"
11
11
 
12
12
  # @since 0.9.0
13
13
  # @api private
data/lib/hanami.rb CHANGED
@@ -138,7 +138,15 @@ module Hanami
138
138
  end
139
139
  end
140
140
 
141
- # Returns the Hanami app environment as loaded from the `HANAMI_ENV` environment variable.
141
+ # Returns the Hanami app environment as determined from the environment.
142
+ #
143
+ # Checks the following environment variables in order:
144
+ #
145
+ # - `HANAMI_ENV`
146
+ # - `APP_ENV`
147
+ # - `RACK_ENV`
148
+ #
149
+ # Defaults to `:development` if no environment variable is set.
142
150
  #
143
151
  # @example
144
152
  # Hanami.env # => :development
@@ -148,7 +156,7 @@ module Hanami
148
156
  # @api public
149
157
  # @since 2.0.0
150
158
  def self.env(e: ENV)
151
- e.fetch("HANAMI_ENV") { e.fetch("RACK_ENV", "development") }.to_sym
159
+ (e["HANAMI_ENV"] || e["APP_ENV"] || e["RACK_ENV"] || :development).to_sym
152
160
  end
153
161
 
154
162
  # Returns true if {.env} matches any of the given names
@@ -20,7 +20,7 @@ RSpec.describe "App action / Format config", :app_integration do
20
20
  class App < Hanami::App
21
21
  config.logger.stream = StringIO.new
22
22
 
23
- config.actions.format :json
23
+ config.actions.formats.accept :json
24
24
  end
25
25
  end
26
26
  RUBY
@@ -76,7 +76,10 @@ RSpec.describe "App action / Format config", :app_integration do
76
76
  class App < Hanami::App
77
77
  config.logger.stream = StringIO.new
78
78
 
79
- config.actions.formats.add :json, ["application/json+scim", "application/json"]
79
+ config.actions.formats.register :json, "application/json",
80
+ accept_types: ["application/json", "application/json+scim"],
81
+ content_types: ["application/json", "application/json+scim"]
82
+ config.actions.formats.accept :json
80
83
  end
81
84
  end
82
85
  RUBY
@@ -141,7 +144,7 @@ RSpec.describe "App action / Format config", :app_integration do
141
144
  class App < Hanami::App
142
145
  config.logger.stream = StringIO.new
143
146
 
144
- config.actions.format :json
147
+ config.actions.formats.accept :json
145
148
  config.middleware.use :body_parser, [json: "application/json+custom"]
146
149
  end
147
150
  end
@@ -34,25 +34,25 @@ RSpec.describe "App action / Slice configuration", :app_integration do
34
34
  it "applies default actions config from the app", :aggregate_failures do
35
35
  prepare_app
36
36
 
37
- expect(TestApp::Action.config.formats.values).to eq []
37
+ expect(TestApp::Action.config.formats.accepted).to eq []
38
38
  end
39
39
 
40
40
  it "applies actions config from the app" do
41
- Hanami.app.config.actions.format :json
41
+ Hanami.app.config.actions.formats.accepted = [:json]
42
42
 
43
43
  prepare_app
44
44
 
45
- expect(TestApp::Action.config.formats.values).to eq [:json]
45
+ expect(TestApp::Action.config.formats.accepted).to eq [:json]
46
46
  end
47
47
 
48
48
  it "does not override config in the base class" do
49
- Hanami.app.config.actions.format :csv
49
+ Hanami.app.config.actions.formats.accepted = [:csv]
50
50
 
51
51
  prepare_app
52
52
 
53
- TestApp::Action.config.format :json
53
+ TestApp::Action.config.formats.accepted = [:json]
54
54
 
55
- expect(TestApp::Action.config.formats.values).to eq [:json]
55
+ expect(TestApp::Action.config.formats.accepted).to eq [:json]
56
56
  end
57
57
  end
58
58
 
@@ -75,23 +75,23 @@ RSpec.describe "App action / Slice configuration", :app_integration do
75
75
  it "applies default actions config from the app", :aggregate_failures do
76
76
  prepare_app
77
77
 
78
- expect(TestApp::Actions::Articles::Index.config.formats.values).to eq []
78
+ expect(TestApp::Actions::Articles::Index.config.formats.accepted).to eq []
79
79
  end
80
80
 
81
81
  it "applies actions config from the app" do
82
- Hanami.app.config.actions.format :json
82
+ Hanami.app.config.actions.formats.accepted = [:json]
83
83
 
84
84
  prepare_app
85
85
 
86
- expect(TestApp::Actions::Articles::Index.config.formats.values).to eq [:json]
86
+ expect(TestApp::Actions::Articles::Index.config.formats.accepted).to eq [:json]
87
87
  end
88
88
 
89
89
  it "applies config from the base class" do
90
90
  prepare_app
91
91
 
92
- TestApp::Action.config.format :json
92
+ TestApp::Action.config.formats.accepted = [:json]
93
93
 
94
- expect(TestApp::Actions::Articles::Index.config.formats.values).to eq [:json]
94
+ expect(TestApp::Actions::Articles::Index.config.formats.accepted).to eq [:json]
95
95
  end
96
96
  end
97
97
 
@@ -114,23 +114,23 @@ RSpec.describe "App action / Slice configuration", :app_integration do
114
114
  it "applies default actions config from the app", :aggregate_failures do
115
115
  prepare_app
116
116
 
117
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq []
117
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq []
118
118
  end
119
119
 
120
120
  it "applies actions config from the app" do
121
- Hanami.app.config.actions.format :json
121
+ Hanami.app.config.actions.formats.accepted = [:json]
122
122
 
123
123
  prepare_app
124
124
 
125
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq [:json]
125
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq [:json]
126
126
  end
127
127
 
128
128
  it "applies config from the base class" do
129
129
  prepare_app
130
130
 
131
- TestApp::Action.config.format :json
131
+ TestApp::Action.config.formats.accepted = [:json]
132
132
 
133
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq [:json]
133
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq [:json]
134
134
  end
135
135
  end
136
136
  end
@@ -151,23 +151,23 @@ RSpec.describe "App action / Slice configuration", :app_integration do
151
151
  it "applies default actions config from the app", :aggregate_failures do
152
152
  prepare_app
153
153
 
154
- expect(Admin::Action.config.formats.values).to eq []
154
+ expect(Admin::Action.config.formats.accepted).to eq []
155
155
  end
156
156
 
157
157
  it "applies actions config from the app" do
158
- Hanami.app.config.actions.format :json
158
+ Hanami.app.config.actions.formats.accepted = [:json]
159
159
 
160
160
  prepare_app
161
161
 
162
- expect(Admin::Action.config.formats.values).to eq [:json]
162
+ expect(Admin::Action.config.formats.accepted).to eq [:json]
163
163
  end
164
164
 
165
165
  it "applies config from the app base class" do
166
166
  prepare_app
167
167
 
168
- TestApp::Action.config.format :json
168
+ TestApp::Action.config.formats.accepted = [:json]
169
169
 
170
- expect(Admin::Action.config.formats.values).to eq [:json]
170
+ expect(Admin::Action.config.formats.accepted).to eq [:json]
171
171
  end
172
172
 
173
173
  context "slice actions config present" do
@@ -186,24 +186,24 @@ RSpec.describe "App action / Slice configuration", :app_integration do
186
186
  it "applies actions config from the slice" do
187
187
  prepare_app
188
188
 
189
- expect(Admin::Action.config.formats.values).to eq [:csv]
189
+ expect(Admin::Action.config.formats.accepted).to eq [:csv]
190
190
  end
191
191
 
192
192
  it "prefers actions config from the slice over config from the app-level base class" do
193
193
  prepare_app
194
194
 
195
- TestApp::Action.config.format :json
195
+ TestApp::Action.config.formats.accepted = [:json]
196
196
 
197
- expect(Admin::Action.config.formats.values).to eq [:csv]
197
+ expect(Admin::Action.config.formats.accepted).to eq [:csv]
198
198
  end
199
199
 
200
200
  it "prefers config from the base class over actions config from the slice" do
201
201
  prepare_app
202
202
 
203
- TestApp::Action.config.format :csv
204
- Admin::Action.config.format :json
203
+ TestApp::Action.config.formats.accepted = [:csv]
204
+ Admin::Action.config.formats.accepted = [:json]
205
205
 
206
- expect(Admin::Action.config.formats.values).to eq [:json]
206
+ expect(Admin::Action.config.formats.accepted).to eq [:json]
207
207
  end
208
208
  end
209
209
  end
@@ -227,15 +227,15 @@ RSpec.describe "App action / Slice configuration", :app_integration do
227
227
  it "applies default actions config from the app", :aggregate_failures do
228
228
  prepare_app
229
229
 
230
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq []
230
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq []
231
231
  end
232
232
 
233
233
  it "applies actions config from the app" do
234
- Hanami.app.config.actions.format :json
234
+ Hanami.app.config.actions.formats.accepted = [:json]
235
235
 
236
236
  prepare_app
237
237
 
238
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq [:json]
238
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq [:json]
239
239
  end
240
240
 
241
241
  it "applies actions config from the slice" do
@@ -243,7 +243,7 @@ RSpec.describe "App action / Slice configuration", :app_integration do
243
243
  write "config/slices/admin.rb", <<~'RUBY'
244
244
  module Admin
245
245
  class Slice < Hanami::Slice
246
- config.actions.format :json
246
+ config.actions.formats.accepted = [:json]
247
247
  end
248
248
  end
249
249
  RUBY
@@ -251,15 +251,15 @@ RSpec.describe "App action / Slice configuration", :app_integration do
251
251
 
252
252
  prepare_app
253
253
 
254
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq [:json]
254
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq [:json]
255
255
  end
256
256
 
257
257
  it "applies config from the slice base class" do
258
258
  prepare_app
259
259
 
260
- Admin::Action.config.format :json
260
+ Admin::Action.config.formats.accepted = [:json]
261
261
 
262
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq [:json]
262
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq [:json]
263
263
  end
264
264
 
265
265
  it "prefers config from the slice base class over actions config from the slice" do
@@ -275,9 +275,9 @@ RSpec.describe "App action / Slice configuration", :app_integration do
275
275
 
276
276
  prepare_app
277
277
 
278
- Admin::Action.config.format :json
278
+ Admin::Action.config.formats.accepted = [:json]
279
279
 
280
- expect(Admin::Actions::Articles::Index.config.formats.values).to eq [:json]
280
+ expect(Admin::Actions::Articles::Index.config.formats.accepted).to eq [:json]
281
281
  end
282
282
  end
283
283
  end
@@ -123,7 +123,6 @@ RSpec.describe "Cross-slice assets via helpers", :app_integration do
123
123
  compile_assets!
124
124
 
125
125
  output = Admin::Slice["views.posts.show"].call.to_s
126
-
127
126
  expect(output).to match(%r{<link href="/assets/app-[A-Z0-9]{8}.css" type="text/css" rel="stylesheet">})
128
127
  expect(output).to match(%r{<script src="/assets/app-[A-Z0-9]{8}.js" type="text/javascript"></script>})
129
128
  end