hanami 2.3.0.beta1 → 2.3.0

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -1
  3. data/README.md +1 -3
  4. data/hanami.gemspec +8 -8
  5. data/lib/hanami/config/actions/content_security_policy.rb +2 -2
  6. data/lib/hanami/config/actions.rb +3 -2
  7. data/lib/hanami/config/router.rb +1 -0
  8. data/lib/hanami/config.rb +11 -19
  9. data/lib/hanami/extensions/action.rb +1 -0
  10. data/lib/hanami/extensions/operation/slice_configured_db_operation.rb +88 -0
  11. data/lib/hanami/extensions/operation.rb +8 -23
  12. data/lib/hanami/extensions/view/context.rb +2 -11
  13. data/lib/hanami/extensions/view/part.rb +3 -0
  14. data/lib/hanami/extensions/view/scope.rb +3 -0
  15. data/lib/hanami/extensions/view/slice_configured_context.rb +0 -7
  16. data/lib/hanami/extensions/view.rb +1 -0
  17. data/lib/hanami/helpers/assets_helper.rb +2 -2
  18. data/lib/hanami/middleware/content_security_policy_nonce.rb +3 -3
  19. data/lib/hanami/routes.rb +4 -3
  20. data/lib/hanami/slice/router.rb +201 -12
  21. data/lib/hanami/slice.rb +13 -0
  22. data/lib/hanami/slice_configurable.rb +1 -3
  23. data/lib/hanami/version.rb +1 -1
  24. data/lib/hanami/web/rack_logger.rb +25 -8
  25. data/lib/hanami.rb +15 -1
  26. data/spec/integration/action/format_config_spec.rb +2 -67
  27. data/spec/integration/action/slice_configuration_spec.rb +36 -36
  28. data/spec/integration/logging/request_logging_spec.rb +16 -0
  29. data/spec/integration/operations/extension_spec.rb +63 -0
  30. data/spec/integration/rack_app/body_parser_spec.rb +3 -3
  31. data/spec/integration/rack_app/rack_app_spec.rb +2 -2
  32. data/spec/integration/router/resource_routes_spec.rb +281 -0
  33. data/spec/unit/hanami/config/actions_spec.rb +2 -2
  34. metadata +19 -20
  35. data/spec/integration/view/context/settings_spec.rb +0 -46
  36. data/spec/unit/hanami/version_spec.rb +0 -7
@@ -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
@@ -11,10 +11,13 @@ RSpec.describe "Logging / Request logging", :app_integration do
11
11
 
12
12
  let(:logger_stream) { StringIO.new }
13
13
 
14
+ let(:logger_level) { nil }
15
+
14
16
  let(:root) { make_tmp_directory }
15
17
 
16
18
  def configure_logger
17
19
  Hanami.app.config.logger.stream = logger_stream
20
+ Hanami.app.config.logger.level = logger_level if logger_level
18
21
  end
19
22
 
20
23
  def logs
@@ -84,6 +87,19 @@ RSpec.describe "Logging / Request logging", :app_integration do
84
87
  )
85
88
  end
86
89
  end
90
+
91
+ context "log level error" do
92
+ let(:logger_level) { :error }
93
+ before do
94
+ expect_any_instance_of(Hanami::Web::RackLogger).to_not receive(:data)
95
+ end
96
+
97
+ it "does not log info" do
98
+ get "/"
99
+
100
+ expect(logs.split("\n").length).to eq 0
101
+ end
102
+ end
87
103
  end
88
104
 
89
105
  describe "slice router" do
@@ -56,4 +56,67 @@ RSpec.describe "Operation / Extensions", :app_integration do
56
56
  expect(main.rom).to be Main::Slice["db.rom"]
57
57
  end
58
58
  end
59
+
60
+ context "hanami-db bundled, but no db configured" do
61
+ it "does not extend the operation class" do
62
+ with_tmp_directory(Dir.mktmpdir) do
63
+ write "config/app.rb", <<~RUBY
64
+ require "hanami"
65
+
66
+ module TestApp
67
+ class App < Hanami::App
68
+ end
69
+ end
70
+ RUBY
71
+
72
+ write "app/operation.rb", <<~RUBY
73
+ module TestApp
74
+ class Operation < Dry::Operation
75
+ end
76
+ end
77
+ RUBY
78
+
79
+ require "hanami/prepare"
80
+
81
+ operation = TestApp::Operation.new
82
+
83
+ expect(operation.rom).to be nil
84
+ expect { operation.transaction }.to raise_error Hanami::ComponentLoadError, "A configured db for TestApp::App is required to run transactions."
85
+ end
86
+ end
87
+ end
88
+
89
+ context "hanami-db not bundled" do
90
+ before do
91
+ allow(Hanami).to receive(:bundled?).and_call_original
92
+ allow(Hanami).to receive(:bundled?).with("hanami-db").and_return false
93
+ end
94
+
95
+ it "does not extend the operation class" do
96
+ with_tmp_directory(Dir.mktmpdir) do
97
+ write "config/app.rb", <<~RUBY
98
+ require "hanami"
99
+
100
+ module TestApp
101
+ class App < Hanami::App
102
+ end
103
+ end
104
+ RUBY
105
+
106
+ write "app/operation.rb", <<~RUBY
107
+ module TestApp
108
+ class Operation < Dry::Operation
109
+ end
110
+ end
111
+ RUBY
112
+
113
+ require "hanami/prepare"
114
+
115
+ operation = TestApp::Operation.new
116
+
117
+ expect { operation.rom }.to raise_error NoMethodError
118
+ expect { operation.transaction }.to raise_error NoMethodError
119
+ end
120
+ end
121
+ end
59
122
  end
@@ -12,14 +12,13 @@ RSpec.describe "Hanami web app", :app_integration do
12
12
  with_tmp_directory(Dir.mktmpdir, &example)
13
13
  end
14
14
 
15
- specify "Setting middlewares in the config" do
15
+ specify "Default body parser" do
16
16
  write "config/app.rb", <<~RUBY
17
17
  require "hanami"
18
18
 
19
19
  module TestApp
20
20
  class App < Hanami::App
21
21
  config.actions.format :json
22
- config.middleware.use :body_parser, :json
23
22
  config.logger.stream = StringIO.new
24
23
  end
25
24
  end
@@ -65,7 +64,8 @@ RSpec.describe "Hanami web app", :app_integration do
65
64
 
66
65
  module TestApp
67
66
  class App < Hanami::App
68
- config.actions.formats.add :json, ["application/json+scim"]
67
+ config.actions.formats.register :json, "application/json+scim"
68
+ config.actions.formats.accept :json
69
69
  config.middleware.use :body_parser, [json: "application/json+scim"]
70
70
  config.logger.stream = StringIO.new
71
71
  end
@@ -305,7 +305,7 @@ RSpec.describe "Hanami web app", :app_integration do
305
305
  expect { Hanami.app.rack_app }.to raise_error do |exception|
306
306
  expect(exception).to be_kind_of(Hanami::Routes::MissingActionError)
307
307
  expect(exception.message).to include("Could not find action with key \"actions.missing.action\" in TestApp::App")
308
- expect(exception.message).to match(%r{define the action class TestApp::Actions::Missing::Action.+actions/missing/action.rb})
308
+ expect(exception.message).to include("define the action class TestApp::Actions::Missing::Action in app/actions/missing/action.rb")
309
309
  end
310
310
  end
311
311
  end
@@ -337,7 +337,7 @@ RSpec.describe "Hanami web app", :app_integration do
337
337
  expect { Hanami.app.rack_app }.to raise_error do |exception|
338
338
  expect(exception).to be_kind_of(Hanami::Routes::MissingActionError)
339
339
  expect(exception.message).to include("Could not find action with key \"actions.missing.action\" in Admin::Slice")
340
- expect(exception.message).to match(%r{define the action class Admin::Actions::Missing::Action.+slices/admin/actions/missing/action.rb})
340
+ expect(exception.message).to include("define the action class Admin::Actions::Missing::Action in slices/admin/actions/missing/action.rb")
341
341
  end
342
342
  end
343
343
  end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+ require "hanami/slice/router"
5
+ require "json"
6
+
7
+ RSpec.describe "Router / Resource routes" do
8
+ let(:router) { Hanami::Slice::Router.new(routes:, resolver:, inflector: Dry::Inflector.new) { } }
9
+
10
+ let(:resolver) { Hanami::Slice::Routing::Resolver.new(slice:) }
11
+ let(:slice) {
12
+ Class.new(Hanami::Slice).tap { |slice|
13
+ allow(slice).to receive(:container) { actions_container }
14
+ allow(slice).to receive(:slices) { {reviews: child_slice} }
15
+ }
16
+ }
17
+ let(:child_slice) {
18
+ Class.new(Hanami::Slice).tap { |slice|
19
+ allow(slice).to receive(:container) { actions_container("[reviews]") }
20
+ }
21
+ }
22
+ def actions_container(prefix = nil)
23
+ Hash.new { |_hsh, key|
24
+ Class.new { |klass|
25
+ klass.define_method(:call) do |env|
26
+ body = key
27
+ body = "#{body} #{JSON.generate(env["router.params"])}" if env["router.params"].any?
28
+ body = "#{prefix}#{body}" if prefix
29
+ [200, {}, body]
30
+ end
31
+ }.new
32
+ }.tap { |container|
33
+ def container.resolve(key) = self[key]
34
+ }
35
+ end
36
+
37
+ let(:app) { Rack::MockRequest.new(router) }
38
+ def routed(method, url)
39
+ app.request(method, url).body
40
+ end
41
+
42
+ describe "resources" do
43
+ let(:routes) { proc { resources :posts } }
44
+
45
+ it "routes all RESTful actions to the resource" do
46
+ expect(routed("GET", "/posts")).to eq %(actions.posts.index)
47
+ expect(routed("GET", "/posts/new")).to eq %(actions.posts.new)
48
+ expect(routed("POST", "/posts")).to eq %(actions.posts.create)
49
+ expect(routed("GET", "/posts/1")).to eq %(actions.posts.show {"id":"1"})
50
+ expect(routed("GET", "/posts/1/edit")).to eq %(actions.posts.edit {"id":"1"})
51
+ expect(routed("PATCH", "/posts/1")).to eq %(actions.posts.update {"id":"1"})
52
+ expect(routed("DELETE", "/posts/1")).to eq %(actions.posts.destroy {"id":"1"})
53
+
54
+ expect(router.path("posts")).to eq "/posts"
55
+ expect(router.path("new_post")).to eq "/posts/new"
56
+ expect(router.path("edit_post", id: 1)).to eq "/posts/1/edit"
57
+ end
58
+
59
+ describe "with :only" do
60
+ let(:routes) { proc { resources :posts, only: %i(index show) } }
61
+
62
+ it "routes only the given actions to the resource" do
63
+ expect(routed("GET", "/posts")).to eq %(actions.posts.index)
64
+ expect(routed("GET", "/posts/1")).to eq %(actions.posts.show {"id":"1"})
65
+
66
+ expect(routed("GET", "/posts/new")).not_to eq %(actions.posts.new)
67
+ expect(routed("POST", "/posts")).to eq "Method Not Allowed"
68
+ expect(routed("GET", "/posts/1/edit")).to eq "Not Found"
69
+ expect(routed("PATCH", "/posts/1")).to eq "Method Not Allowed"
70
+ expect(routed("DELETE", "/posts/1")).to eq "Method Not Allowed"
71
+ end
72
+ end
73
+
74
+ describe "with :except" do
75
+ let(:routes) { proc { resources :posts, except: %i(edit update destroy) } }
76
+
77
+ it "routes all except the given actions to the resource" do
78
+ expect(routed("GET", "/posts")).to eq %(actions.posts.index)
79
+ expect(routed("GET", "/posts/new")).to eq %(actions.posts.new)
80
+ expect(routed("POST", "/posts")).to eq %(actions.posts.create)
81
+ expect(routed("GET", "/posts/1")).to eq %(actions.posts.show {"id":"1"})
82
+
83
+ expect(routed("GET", "/posts/1/edit")).to eq "Not Found"
84
+ expect(routed("PATCH", "/posts/1")).to eq "Method Not Allowed"
85
+ expect(routed("DELETE", "/posts/1")).to eq "Method Not Allowed"
86
+ end
87
+ end
88
+
89
+ describe "with :to" do
90
+ let(:routes) { proc { resources :posts, to: "articles" } }
91
+
92
+ it "uses actions from the given container key namespace" do
93
+ expect(routed("GET", "/posts")).to eq %(actions.articles.index)
94
+ expect(routed("GET", "/posts/new")).to eq %(actions.articles.new)
95
+ expect(routed("POST", "/posts")).to eq %(actions.articles.create)
96
+ expect(routed("GET", "/posts/1")).to eq %(actions.articles.show {"id":"1"})
97
+ expect(routed("GET", "/posts/1/edit")).to eq %(actions.articles.edit {"id":"1"})
98
+ expect(routed("PATCH", "/posts/1")).to eq %(actions.articles.update {"id":"1"})
99
+ expect(routed("DELETE", "/posts/1")).to eq %(actions.articles.destroy {"id":"1"})
100
+ end
101
+ end
102
+
103
+ describe "witih :path" do
104
+ let(:routes) { proc { resources :posts, path: "articles" } }
105
+
106
+ it "uses the given path for the routes" do
107
+ expect(routed("GET", "/articles")).to eq %(actions.posts.index)
108
+ expect(routed("GET", "/articles/new")).to eq %(actions.posts.new)
109
+ expect(routed("POST", "/articles")).to eq %(actions.posts.create)
110
+ expect(routed("GET", "/articles/1")).to eq %(actions.posts.show {"id":"1"})
111
+ expect(routed("GET", "/articles/1/edit")).to eq %(actions.posts.edit {"id":"1"})
112
+ expect(routed("PATCH", "/articles/1")).to eq %(actions.posts.update {"id":"1"})
113
+ expect(routed("DELETE", "/articles/1")).to eq %(actions.posts.destroy {"id":"1"})
114
+ end
115
+ end
116
+ end
117
+
118
+ describe "resource" do
119
+ let(:routes) { proc { resource :profile } }
120
+
121
+ it "routes all RESTful actions (except index) to the resource" do
122
+ expect(routed("GET", "/profile/new")).to eq %(actions.profile.new)
123
+ expect(routed("POST", "/profile")).to eq %(actions.profile.create)
124
+ expect(routed("GET", "/profile")).to eq %(actions.profile.show)
125
+ expect(routed("GET", "/profile/edit")).to eq %(actions.profile.edit)
126
+ expect(routed("PATCH", "/profile")).to eq %(actions.profile.update)
127
+ expect(routed("DELETE", "/profile")).to eq %(actions.profile.destroy)
128
+
129
+ expect(routed("GET", "/profiles")).to eq "Not Found"
130
+ expect(routed("GET", "/profiles/1")).to eq "Not Found"
131
+ expect(routed("GET", "/profile/1")).to eq "Not Found"
132
+
133
+ expect(router.path("profile")).to eq "/profile"
134
+ expect(router.path("new_profile")).to eq "/profile/new"
135
+ expect(router.path("edit_profile")).to eq "/profile/edit"
136
+ end
137
+
138
+ describe "with :only" do
139
+ let(:routes) { proc { resource :profile, only: %i(show edit update) } }
140
+
141
+ it "routes only the given actions to the resource" do
142
+ expect(routed("GET", "/profile")).to eq %(actions.profile.show)
143
+ expect(routed("GET", "/profile/edit")).to eq %(actions.profile.edit)
144
+ expect(routed("PATCH", "/profile")).to eq %(actions.profile.update)
145
+
146
+ expect(routed("GET", "/profile/new")).to eq "Not Found"
147
+ expect(routed("POST", "/profile")).to eq "Method Not Allowed"
148
+ expect(routed("DELETE", "/profile")).to eq "Method Not Allowed"
149
+ end
150
+ end
151
+
152
+ describe "with :except" do
153
+ let(:routes) { proc { resource :profile, except: %i(edit update destroy) } }
154
+
155
+ it "routes all except the given actions to the resource" do
156
+ expect(routed("GET", "/profile/new")).to eq %(actions.profile.new)
157
+ expect(routed("POST", "/profile")).to eq %(actions.profile.create)
158
+ expect(routed("GET", "/profile")).to eq %(actions.profile.show)
159
+
160
+ expect(routed("GET", "/profile/edit")).to eq "Not Found"
161
+ expect(routed("PATCH", "/profile")).to eq "Method Not Allowed"
162
+ expect(routed("DELETE", "/profile")).to eq "Method Not Allowed"
163
+ end
164
+ end
165
+
166
+ describe "with :to" do
167
+ let(:routes) { proc { resource :profile, to: "user" } }
168
+
169
+ it "uses actions from the given container key namespace" do
170
+ expect(routed("GET", "/profile/new")).to eq %(actions.user.new)
171
+ expect(routed("POST", "/profile")).to eq %(actions.user.create)
172
+ expect(routed("GET", "/profile")).to eq %(actions.user.show)
173
+ expect(routed("GET", "/profile/edit")).to eq %(actions.user.edit)
174
+ expect(routed("PATCH", "/profile")).to eq %(actions.user.update)
175
+ expect(routed("DELETE", "/profile")).to eq %(actions.user.destroy)
176
+ end
177
+ end
178
+
179
+ describe "with :path" do
180
+ let(:routes) { proc { resource :profile, path: "user"} }
181
+
182
+ it "uses the given path for the routes" do
183
+ expect(routed("GET", "/user/new")).to eq %(actions.profile.new)
184
+ expect(routed("POST", "/user")).to eq %(actions.profile.create)
185
+ expect(routed("GET", "/user")).to eq %(actions.profile.show)
186
+ expect(routed("GET", "/user/edit")).to eq %(actions.profile.edit)
187
+ expect(routed("PATCH", "/user")).to eq %(actions.profile.update)
188
+ expect(routed("DELETE", "/user")).to eq %(actions.profile.destroy)
189
+ end
190
+ end
191
+ end
192
+
193
+ describe "nested resources" do
194
+ let(:routes) {
195
+ proc {
196
+ resources :cafes, only: :show do
197
+ resources :reviews, only: :index do
198
+ resources :comments, only: [:index, :new, :create]
199
+ end
200
+ end
201
+
202
+ resource :profile, only: :show do
203
+ resource :avatar, only: :show do
204
+ resources :comments, only: :index
205
+ end
206
+ end
207
+ }
208
+ }
209
+
210
+ it "routes to the nested resources" do
211
+ expect(routed("GET", "/cafes/1")).to eq %(actions.cafes.show {"id":"1"})
212
+ expect(routed("GET", "/cafes/1/reviews")).to eq %(actions.cafes.reviews.index {"cafe_id":"1"})
213
+ expect(routed("GET", "/cafes/1/reviews/2/comments")).to eq %(actions.cafes.reviews.comments.index {"cafe_id":"1","review_id":"2"})
214
+
215
+ expect(router.path("cafe", id: 1)).to eq "/cafes/1"
216
+ expect(router.path("cafe_reviews", cafe_id: 1)).to eq "/cafes/1/reviews"
217
+ expect(router.path("cafe_review_comments", cafe_id: 1, review_id: 1)).to eq "/cafes/1/reviews/1/comments"
218
+ expect(router.path("new_cafe_review_comment", cafe_id: 1, review_id: 1)).to eq "/cafes/1/reviews/1/comments/new"
219
+
220
+ expect(routed("GET", "/profile")).to eq %(actions.profile.show)
221
+ expect(routed("GET", "/profile/avatar")).to eq %(actions.profile.avatar.show)
222
+ expect(routed("GET", "/profile/avatar/comments")).to eq %(actions.profile.avatar.comments.index)
223
+ end
224
+ end
225
+
226
+ describe "standalone routes nested under resources" do
227
+ let(:routes) {
228
+ proc {
229
+ resources :cafes, only: :show do
230
+ get "/top-reviews", to: "cafes.top_reviews.index", as: :top_reviews
231
+ end
232
+ }
233
+ }
234
+
235
+ it "nests the standalone route under the resource" do
236
+ expect(routed("GET", "/cafes/1")).to eq %(actions.cafes.show {"id":"1"})
237
+ expect(routed("GET", "/cafes/1/top-reviews")).to eq %(actions.cafes.top_reviews.index {"cafe_id":"1"})
238
+
239
+ expect(router.path("cafe_top_reviews", cafe_id: 1)).to eq "/cafes/1/top-reviews"
240
+ end
241
+ end
242
+
243
+ describe "resources nested under scopes" do
244
+ let(:routes) {
245
+ proc {
246
+ scope "coffee-lovers" do
247
+ resources :cafes, only: :show do
248
+ get "/top-reviews", to: "cafes.top_reviews.index", as: :top_reviews
249
+ end
250
+ end
251
+ }
252
+ }
253
+
254
+ it "routes to the resources under the scope" do
255
+ expect(routed("GET", "/coffee-lovers/cafes/1")).to eq %(actions.cafes.show {"id":"1"})
256
+ expect(routed("GET", "/coffee-lovers/cafes/1/top-reviews")).to eq %(actions.cafes.top_reviews.index {"cafe_id":"1"})
257
+
258
+ expect(router.path("coffee_lovers_cafe", id: 1)).to eq "/coffee-lovers/cafes/1"
259
+ expect(router.path("coffee_lovers_cafe_top_reviews", cafe_id: 1)).to eq "/coffee-lovers/cafes/1/top-reviews"
260
+ end
261
+ end
262
+
263
+ describe "slices nested under resources" do
264
+ let(:routes) {
265
+ proc {
266
+ resources :cafes, only: :show do
267
+ slice :reviews, at: "" do
268
+ resources :reviews, only: :index
269
+ end
270
+ end
271
+ }
272
+ }
273
+
274
+ it "routes to actions within the nested slice" do
275
+ expect(routed("GET", "/cafes/1")).to eq %(actions.cafes.show {"id":"1"})
276
+ expect(routed("GET", "/cafes/1/reviews")).to eq %([reviews]actions.cafes.reviews.index {"cafe_id":"1"})
277
+
278
+ expect(router.path("cafe_reviews", cafe_id: 1)).to eq "/cafes/1/reviews"
279
+ end
280
+ end
281
+ end
@@ -23,9 +23,9 @@ RSpec.describe Hanami::Config, "#actions" do
23
23
  end
24
24
 
25
25
  it "configures base actions settings using custom methods" do
26
- expect { actions.formats.add(:json, "app/json") }
26
+ expect { actions.formats.register(:json, "app/json") }
27
27
  .to change { actions.formats.mapping }
28
- .to include("app/json" => :json)
28
+ .to include(json: have_attributes(media_type: "app/json"))
29
29
  end
30
30
 
31
31
  it "can be finalized" do