├── .github
└── workflows
│ ├── rubocop.yml
│ └── test.yml
├── .gitignore
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.md
├── Rakefile
├── gemfiles
├── Gemfile-rails-5-2
├── Gemfile-rails-6-0
├── Gemfile-rails-6-1
├── Gemfile-rails-7-0
└── Gemfile-rails-main
├── lib
├── action_controller
│ ├── respond_with.rb
│ └── responder.rb
├── generators
│ ├── rails
│ │ ├── USAGE
│ │ ├── responders_controller_generator.rb
│ │ └── templates
│ │ │ ├── api_controller.rb.tt
│ │ │ └── controller.rb.tt
│ └── responders
│ │ └── install_generator.rb
├── responders.rb
└── responders
│ ├── collection_responder.rb
│ ├── controller_method.rb
│ ├── flash_responder.rb
│ ├── http_cache_responder.rb
│ ├── locales
│ └── en.yml
│ └── version.rb
├── responders.gemspec
└── test
├── action_controller
├── respond_with_api_test.rb
├── respond_with_test.rb
└── verify_requested_format_test.rb
├── locales
└── en.yml
├── responders
├── collection_responder_test.rb
├── controller_method_test.rb
├── flash_responder_test.rb
└── http_cache_responder_test.rb
├── support
└── models.rb
├── test_helper.rb
└── views
├── addresses
├── create.js.erb
├── edit.html.erb
└── new.html.erb
├── locations
└── new.html.erb
└── respond_with
├── edit.html.erb
├── new.html.erb
├── respond_with_additional_params.html.erb
├── using_invalid_resource_with_template.xml.erb
├── using_options_with_template.xml.erb
├── using_resource.js.erb
└── using_resource_with_block.html.erb
/.github/workflows/rubocop.yml:
--------------------------------------------------------------------------------
1 | name: RuboCop
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v3
11 |
12 | - name: Set up Ruby 3.2
13 | uses: ruby/setup-ruby@v1
14 | with:
15 | ruby-version: 3.2
16 | bundler-cache: true
17 |
18 | - name: Run RuboCop
19 | run: bundle exec rubocop --parallel
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | strategy:
6 | fail-fast: false
7 | matrix:
8 | gemfile:
9 | - Gemfile
10 | - gemfiles/Gemfile-rails-main
11 | - gemfiles/Gemfile-rails-7-0
12 | - gemfiles/Gemfile-rails-6-1
13 | - gemfiles/Gemfile-rails-6-0
14 | - gemfiles/Gemfile-rails-5-2
15 | ruby:
16 | - '3.3'
17 | - '3.2'
18 | - '3.1'
19 | - '3.0'
20 | - '2.7'
21 | - '2.6'
22 | - '2.5'
23 | exclude:
24 | - gemfile: Gemfile
25 | ruby: '2.6'
26 | - gemfile: Gemfile
27 | ruby: '2.5'
28 | - gemfile: gemfiles/Gemfile-rails-main
29 | ruby: '3.0'
30 | - gemfile: gemfiles/Gemfile-rails-main
31 | ruby: '2.7'
32 | - gemfile: gemfiles/Gemfile-rails-main
33 | ruby: '2.6'
34 | - gemfile: gemfiles/Gemfile-rails-main
35 | ruby: '2.5'
36 | - gemfile: gemfiles/Gemfile-rails-7-0
37 | ruby: '2.6'
38 | - gemfile: gemfiles/Gemfile-rails-7-0
39 | ruby: '2.5'
40 | - gemfile: gemfiles/Gemfile-rails-6-0
41 | ruby: '3.3'
42 | - gemfile: gemfiles/Gemfile-rails-6-0
43 | ruby: '3.2'
44 | - gemfile: gemfiles/Gemfile-rails-6-0
45 | ruby: '3.1'
46 | - gemfile: gemfiles/Gemfile-rails-5-2
47 | ruby: '3.3'
48 | - gemfile: gemfiles/Gemfile-rails-5-2
49 | ruby: '3.2'
50 | - gemfile: gemfiles/Gemfile-rails-5-2
51 | ruby: '3.1'
52 | - gemfile: gemfiles/Gemfile-rails-5-2
53 | ruby: '3.0'
54 | - gemfile: gemfiles/Gemfile-rails-5-2
55 | ruby: '2.7'
56 | runs-on: ubuntu-latest
57 | env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
58 | BUNDLE_GEMFILE: ${{ matrix.gemfile }}
59 | steps:
60 | - uses: actions/checkout@v3
61 | - uses: ruby/setup-ruby@v1
62 | with:
63 | ruby-version: ${{ matrix.ruby }}
64 | bundler-cache: true # runs bundle install and caches installed gems automatically
65 | - run: bundle exec rake
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | gemfiles/*.lock
4 | pkg/*
5 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require:
2 | - rubocop-performance
3 |
4 | AllCops:
5 | TargetRubyVersion: 2.5
6 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
7 | # to ignore them, so only the ones explicitly set in this file are enabled.
8 | DisabledByDefault: true
9 |
10 | Performance:
11 | Exclude:
12 | - '**/test/**/*'
13 |
14 | # Prefer &&/|| over and/or.
15 | Style/AndOr:
16 | Enabled: true
17 |
18 | # Align `when` with `case`.
19 | Layout/CaseIndentation:
20 | Enabled: true
21 |
22 | # Align comments with method definitions.
23 | Layout/CommentIndentation:
24 | Enabled: true
25 |
26 | Layout/ElseAlignment:
27 | Enabled: true
28 |
29 | # Align `end` with the matching keyword or starting expression except for
30 | # assignments, where it should be aligned with the LHS.
31 | Layout/EndAlignment:
32 | Enabled: true
33 | EnforcedStyleAlignWith: variable
34 | AutoCorrect: true
35 |
36 | Layout/EmptyLineAfterMagicComment:
37 | Enabled: true
38 |
39 | Layout/EmptyLinesAroundAccessModifier:
40 | Enabled: true
41 |
42 | Layout/EmptyLinesAroundBlockBody:
43 | Enabled: true
44 |
45 | # In a regular class definition, no empty lines around the body.
46 | Layout/EmptyLinesAroundClassBody:
47 | Enabled: true
48 |
49 | # In a regular method definition, no empty lines around the body.
50 | Layout/EmptyLinesAroundMethodBody:
51 | Enabled: true
52 |
53 | # In a regular module definition, no empty lines around the body.
54 | Layout/EmptyLinesAroundModuleBody:
55 | Enabled: true
56 |
57 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
58 | Style/HashSyntax:
59 | Enabled: true
60 |
61 | Layout/FirstArgumentIndentation:
62 | Enabled: true
63 |
64 | Layout/IndentationConsistency:
65 | Enabled: true
66 |
67 | # Two spaces, no tabs (for indentation).
68 | Layout/IndentationWidth:
69 | Enabled: true
70 |
71 | Layout/LeadingCommentSpace:
72 | Enabled: true
73 |
74 | Layout/SpaceAfterColon:
75 | Enabled: true
76 |
77 | Layout/SpaceAfterComma:
78 | Enabled: true
79 |
80 | Layout/SpaceAfterSemicolon:
81 | Enabled: true
82 |
83 | Layout/SpaceAroundEqualsInParameterDefault:
84 | Enabled: true
85 |
86 | Layout/SpaceAroundKeyword:
87 | Enabled: true
88 |
89 | Layout/SpaceAroundOperators:
90 | Enabled: true
91 |
92 | Layout/SpaceBeforeComma:
93 | Enabled: true
94 |
95 | Layout/SpaceBeforeComment:
96 | Enabled: true
97 |
98 | Layout/SpaceBeforeFirstArg:
99 | Enabled: true
100 |
101 | Style/DefWithParentheses:
102 | Enabled: true
103 |
104 | # Defining a method with parameters needs parentheses.
105 | Style/MethodDefParentheses:
106 | Enabled: true
107 |
108 | Style/FrozenStringLiteralComment:
109 | Enabled: true
110 | EnforcedStyle: always
111 |
112 | Style/RedundantFreeze:
113 | Enabled: true
114 |
115 | # Use `foo {}` not `foo{}`.
116 | Layout/SpaceBeforeBlockBraces:
117 | Enabled: true
118 |
119 | # Use `foo { bar }` not `foo {bar}`.
120 | Layout/SpaceInsideBlockBraces:
121 | Enabled: true
122 | EnforcedStyleForEmptyBraces: space
123 |
124 | # Use `{ a: 1 }` not `{a:1}`.
125 | Layout/SpaceInsideHashLiteralBraces:
126 | Enabled: true
127 |
128 | Layout/SpaceInsideParens:
129 | Enabled: true
130 |
131 | # Check quotes usage according to lint rule below.
132 | Style/StringLiterals:
133 | Enabled: true
134 | EnforcedStyle: double_quotes
135 |
136 | # Detect hard tabs, no hard tabs.
137 | Layout/IndentationStyle:
138 | Enabled: true
139 |
140 | # Blank lines should not have any spaces.
141 | Layout/TrailingEmptyLines:
142 | Enabled: true
143 |
144 | # No trailing whitespace.
145 | Layout/TrailingWhitespace:
146 | Enabled: true
147 |
148 | # Use quotes for string literals when they are enough.
149 | Style/RedundantPercentQ:
150 | Enabled: true
151 |
152 | Lint/AmbiguousOperator:
153 | Enabled: true
154 |
155 | Lint/AmbiguousRegexpLiteral:
156 | Enabled: true
157 |
158 | Lint/ErbNewArguments:
159 | Enabled: true
160 |
161 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
162 | Lint/RequireParentheses:
163 | Enabled: true
164 |
165 | Lint/ShadowingOuterLocalVariable:
166 | Enabled: true
167 |
168 | Lint/RedundantStringCoercion:
169 | Enabled: true
170 |
171 | Lint/UriEscapeUnescape:
172 | Enabled: true
173 |
174 | Lint/UselessAssignment:
175 | Enabled: true
176 |
177 | Lint/DeprecatedClassMethods:
178 | Enabled: true
179 |
180 | Style/ParenthesesAroundCondition:
181 | Enabled: true
182 |
183 | Style/RedundantBegin:
184 | Enabled: true
185 |
186 | Style/RedundantReturn:
187 | Enabled: true
188 | AllowMultipleReturnValues: true
189 |
190 | Style/Semicolon:
191 | Enabled: true
192 | AllowAsExpressionSeparator: true
193 |
194 | # Prefer Foo.method over Foo::method
195 | Style/ColonMethodCall:
196 | Enabled: true
197 |
198 | Style/TrivialAccessors:
199 | Enabled: true
200 |
201 | Performance/FlatMap:
202 | Enabled: true
203 |
204 | Performance/RedundantMerge:
205 | Enabled: true
206 |
207 | Performance/StartWith:
208 | Enabled: true
209 |
210 | Performance/EndWith:
211 | Enabled: true
212 |
213 | Performance/RegexpMatch:
214 | Enabled: true
215 |
216 | Performance/ReverseEach:
217 | Enabled: true
218 |
219 | Performance/UnfreezeString:
220 | Enabled: true
221 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Unreleased
2 |
3 | * Add support to Ruby 3.3. (no changes required.)
4 |
5 | ## 3.1.1
6 |
7 | * Add support for Rails 7.1. (no changes required.)
8 |
9 | ## 3.1.0
10 |
11 | * Add config `responders.redirect_status` to allow overriding the redirect code/status used in redirects. The default is `302 Found`, which matches Rails, but it allows to change responders to redirect with `303 See Other` for example, to make it more compatible with how Hotwire/Turbo expects redirects to work.
12 | * Add config `responders.error_status` to allow overriding the status code used to respond to `HTML` or `JS` requests that have errors on the resource. The default is `200 OK`, but it allows to change the response to be `422 Unprocessable Entity` in such cases for example, which makes it more consistent with other statuses more commonly used in APIs (like JSON/XML), and works by default with Turbo/Hotwire which expects a 422 on form error HTML responses. Note that changing this may break your application if you're relying on the previous 2xx status to handle error cases.
13 | * Add support for Ruby 3.0, 3.1, and 3.2, drop support for Ruby < 2.5.
14 | * Add support for Rails 6.1 and 7.0, drop support for Rails < 5.2.
15 | * Move CI to GitHub Actions.
16 |
17 | ## 3.0.1
18 |
19 | * Add support to Ruby 2.7
20 |
21 | ## 3.0.0
22 |
23 | * Remove support for Rails 4.2
24 | * Remove support for Ruby < 2.4
25 |
26 | ## 2.4.1
27 |
28 | * Add support for Rails 6 beta
29 |
30 | ## 2.4.0
31 |
32 | * `respond_with` now accepts a new kwarg called `:render` which goes straight to the `render`
33 | call after an unsuccessful post request. Useful if for example you need to render a template
34 | which is outside of controller's path eg:
35 |
36 | `respond_with resource, render: { template: 'path/to/template' }`
37 |
38 | ## 2.3.0
39 |
40 | * `verify_request_format!` is aliased to `verify_requested_format!` now.
41 | * Implementing the `interpolation_options` method on your controller is deprecated
42 | in favor of naming it `flash_interpolation_options` instead.
43 |
44 | ## 2.2.0
45 |
46 | * Added the `verify_request_format!` method, that can be used as a `before_action`
47 | callback to prevent your actions from being invoked when the controller does
48 | not respond to the request mime type, preventing the execution of complex
49 | queries or creating/deleting records from your app.
50 |
51 | ## 2.1.2
52 |
53 | * Fix rendering when using `ActionController::API`. (by @eLod)
54 | * Added API controller template for the controller generator. (by @vestimir)
55 |
56 | ## 2.1.1
57 |
58 | * Added support for Rails 5.
59 |
60 | ## 2.1.0
61 |
62 | * No longer automatically set the responders generator as many projects may use this gem as a dependency. When upgrading, users will need to add `config.app_generators.scaffold_controller :responders_controller` to their application. The `responders:install` generator has been updated to automatically insert it in new applications
63 |
64 | ## 2.0.1
65 |
66 | * Require `rails/railtie` explicitly before using it
67 | * Require `action_controller` explicitly before using it
68 | * Remove unnecessary and limiting `resourceful?` check that required models to implement `to_#{format}` (such checks are responsibility of the rendering layer)
69 |
70 | ## 2.0.0
71 |
72 | * Import `respond_with` and class-level `respond_to` from Rails
73 | * Support only Rails ~> 4.2
74 | * `Responders::LocationResponder` is now included by the default responder (and therefore deprecated)
75 |
76 | ## 1.1.0
77 |
78 | * Support Rails 4.1.
79 | * Allow callable objects as the location.
80 |
81 | ## 1.0.0
82 |
83 | * Improve controller generator to work closer to the Rails 4 one, and make it
84 | compatible with strong parameters.
85 | * Drop support for Rails 3.1 and Ruby 1.8, keep support for Rails 3.2
86 | * Support for Rails 4.0 onward
87 | * Fix flash message on destroy failure. Fixes #61
88 |
89 | ## 0.9.3
90 |
91 | * Fix url generation for namespaced models
92 |
93 | ## 0.9.2
94 |
95 | * Properly inflect custom responders names
96 |
97 | ## 0.9.1
98 |
99 | * Fix bug with namespace lookup
100 |
101 | ## 0.9.0
102 |
103 | * Disable namespace lookup by default
104 |
105 | ## 0.8
106 |
107 | * Allow embedded HTML in flash messages
108 |
109 | ## 0.7
110 |
111 | * Support Rails 3.1 onward
112 | * Support namespaced engines
113 |
114 | ## 0.6
115 |
116 | * Allow engine detection in generators
117 | * HTTP Cache is no longer triggered for collections
118 | * `:js` now sets the `flash.now` by default, instead of `flash`
119 | * Renamed `responders_install` generator to `responders:install`
120 | * Added `CollectionResponder` which allows you to always redirect to the collection path
121 | (index action) after POST/PUT/DELETE
122 |
123 | ## 0.5
124 |
125 | * Added Railtie and better Rails 3 support
126 | * Added `:flash_now` as option
127 |
128 | ## 0.4
129 |
130 | * Added `Responders::FlashResponder.flash_keys` and default to `[ :notice, :alert ]`
131 | * Added support to `respond_with(@resource, :notice => "Yes!", :alert => "No!")``
132 |
133 | ## 0.1
134 |
135 | * Added `FlashResponder`
136 | * Added `HttpCacheResponder`
137 | * Added responders generators
138 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | gemspec
6 |
7 | gem "activemodel", "~> 7.1.0"
8 | gem "railties", "~> 7.1.0"
9 | gem "mocha"
10 | gem "rails-controller-testing"
11 | gem "rubocop"
12 | gem "rubocop-performance"
13 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | responders (3.1.1)
5 | actionpack (>= 5.2)
6 | railties (>= 5.2)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actionpack (7.1.3.2)
12 | actionview (= 7.1.3.2)
13 | activesupport (= 7.1.3.2)
14 | nokogiri (>= 1.8.5)
15 | racc
16 | rack (>= 2.2.4)
17 | rack-session (>= 1.0.1)
18 | rack-test (>= 0.6.3)
19 | rails-dom-testing (~> 2.2)
20 | rails-html-sanitizer (~> 1.6)
21 | actionview (7.1.3.2)
22 | activesupport (= 7.1.3.2)
23 | builder (~> 3.1)
24 | erubi (~> 1.11)
25 | rails-dom-testing (~> 2.2)
26 | rails-html-sanitizer (~> 1.6)
27 | activemodel (7.1.3.2)
28 | activesupport (= 7.1.3.2)
29 | activesupport (7.1.3.2)
30 | base64
31 | bigdecimal
32 | concurrent-ruby (~> 1.0, >= 1.0.2)
33 | connection_pool (>= 2.2.5)
34 | drb
35 | i18n (>= 1.6, < 2)
36 | minitest (>= 5.1)
37 | mutex_m
38 | tzinfo (~> 2.0)
39 | ast (2.4.2)
40 | base64 (0.2.0)
41 | bigdecimal (3.1.7)
42 | builder (3.2.4)
43 | concurrent-ruby (1.2.3)
44 | connection_pool (2.4.1)
45 | crass (1.0.6)
46 | drb (2.2.1)
47 | erubi (1.12.0)
48 | i18n (1.14.4)
49 | concurrent-ruby (~> 1.0)
50 | io-console (0.7.2)
51 | irb (1.12.0)
52 | rdoc
53 | reline (>= 0.4.2)
54 | json (2.7.2)
55 | language_server-protocol (3.17.0.3)
56 | loofah (2.22.0)
57 | crass (~> 1.0.2)
58 | nokogiri (>= 1.12.0)
59 | mini_portile2 (2.8.5)
60 | minitest (5.22.3)
61 | mocha (2.1.0)
62 | ruby2_keywords (>= 0.0.5)
63 | mutex_m (0.2.0)
64 | nokogiri (1.15.6)
65 | mini_portile2 (~> 2.8.2)
66 | racc (~> 1.4)
67 | parallel (1.24.0)
68 | parser (3.3.0.5)
69 | ast (~> 2.4.1)
70 | racc
71 | psych (5.1.2)
72 | stringio
73 | racc (1.7.3)
74 | rack (3.0.10)
75 | rack-session (2.0.0)
76 | rack (>= 3.0.0)
77 | rack-test (2.1.0)
78 | rack (>= 1.3)
79 | rackup (2.1.0)
80 | rack (>= 3)
81 | webrick (~> 1.8)
82 | rails-controller-testing (1.0.5)
83 | actionpack (>= 5.0.1.rc1)
84 | actionview (>= 5.0.1.rc1)
85 | activesupport (>= 5.0.1.rc1)
86 | rails-dom-testing (2.2.0)
87 | activesupport (>= 5.0.0)
88 | minitest
89 | nokogiri (>= 1.6)
90 | rails-html-sanitizer (1.6.0)
91 | loofah (~> 2.21)
92 | nokogiri (~> 1.14)
93 | railties (7.1.3.2)
94 | actionpack (= 7.1.3.2)
95 | activesupport (= 7.1.3.2)
96 | irb
97 | rackup (>= 1.0.0)
98 | rake (>= 12.2)
99 | thor (~> 1.0, >= 1.2.2)
100 | zeitwerk (~> 2.6)
101 | rainbow (3.1.1)
102 | rake (13.2.1)
103 | rdoc (6.6.3.1)
104 | psych (>= 4.0.0)
105 | regexp_parser (2.9.0)
106 | reline (0.5.1)
107 | io-console (~> 0.5)
108 | rexml (3.2.6)
109 | rubocop (1.63.0)
110 | json (~> 2.3)
111 | language_server-protocol (>= 3.17.0)
112 | parallel (~> 1.10)
113 | parser (>= 3.3.0.2)
114 | rainbow (>= 2.2.2, < 4.0)
115 | regexp_parser (>= 1.8, < 3.0)
116 | rexml (>= 3.2.5, < 4.0)
117 | rubocop-ast (>= 1.31.1, < 2.0)
118 | ruby-progressbar (~> 1.7)
119 | unicode-display_width (>= 2.4.0, < 3.0)
120 | rubocop-ast (1.31.2)
121 | parser (>= 3.3.0.4)
122 | rubocop-performance (1.21.0)
123 | rubocop (>= 1.48.1, < 2.0)
124 | rubocop-ast (>= 1.31.1, < 2.0)
125 | ruby-progressbar (1.13.0)
126 | ruby2_keywords (0.0.5)
127 | stringio (3.1.0)
128 | thor (1.3.1)
129 | tzinfo (2.0.6)
130 | concurrent-ruby (~> 1.0)
131 | unicode-display_width (2.5.0)
132 | webrick (1.8.1)
133 | zeitwerk (2.6.13)
134 |
135 | PLATFORMS
136 | ruby
137 |
138 | DEPENDENCIES
139 | activemodel (~> 7.1.0)
140 | mocha
141 | rails-controller-testing
142 | railties (~> 7.1.0)
143 | responders!
144 | rubocop
145 | rubocop-performance
146 |
147 | BUNDLED WITH
148 | 2.4.22
149 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-2024 Rafael França, Carlos Antônio da Silva
2 | Copyright (c) 2009-2019 Plataformatec
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Responders
2 |
3 | [](http://badge.fury.io/rb/responders)
4 |
5 | A set of responders modules to dry up your Rails app.
6 |
7 | ## Installation
8 |
9 | Add the responders gem to your Gemfile:
10 |
11 | gem "responders"
12 |
13 | Update your bundle and run the install generator:
14 |
15 | $ bundle install
16 | $ rails g responders:install
17 |
18 | If you are including this gem to support backwards compatibility for responders in previous releases of Rails, you only need to include the gem and bundle.
19 |
20 | $ bundle install
21 |
22 | ## Responders Types
23 |
24 | ### FlashResponder
25 |
26 | Sets the flash based on the controller action and resource status.
27 | For instance, if you do: `respond_with(@post)` on a POST request and the resource `@post`
28 | does not contain errors, it will automatically set the flash message to
29 | `"Post was successfully created"` as long as you configure your I18n file:
30 |
31 | ```yaml
32 | flash:
33 | actions:
34 | create:
35 | notice: "%{resource_name} was successfully created."
36 | update:
37 | notice: "%{resource_name} was successfully updated."
38 | destroy:
39 | notice: "%{resource_name} was successfully destroyed."
40 | alert: "%{resource_name} could not be destroyed."
41 | ```
42 |
43 | In case the resource contains errors, you should use the failure key on I18n. This is
44 | useful to dry up flash messages from your controllers. Note: by default alerts for `update`
45 | and `destroy` actions are commented in generated I18n file. If you need a specific message
46 | for a controller, let's say, for `PostsController`, you can also do:
47 |
48 | ```yaml
49 | flash:
50 | posts:
51 | create:
52 | notice: "Your post was created and will be published soon"
53 | ```
54 |
55 | This responder is activated in all non get requests. By default it will use the keys
56 | `:notice` and `:alert`, but they can be changed in your application:
57 |
58 | ```ruby
59 | config.responders.flash_keys = [ :success, :failure ]
60 | ```
61 |
62 | You can also have embedded HTML. Just create a `_html` scope.
63 |
64 | ```yaml
65 | flash:
66 | actions:
67 | create:
68 | alert_html: "OH NOES! You did it wrong!"
69 | posts:
70 | create:
71 | notice_html: "Yay! You did it!"
72 | ```
73 |
74 | See also the `namespace_lookup` option to search the full hierarchy of possible keys.
75 |
76 | ### HttpCacheResponder
77 |
78 | Automatically adds Last-Modified headers to API requests. This
79 | allows clients to easily query the server if a resource changed and if the client tries
80 | to retrieve a resource that has not been modified, it returns not_modified status.
81 |
82 | ### CollectionResponder
83 |
84 | Makes your create and update action redirect to the collection on success.
85 |
86 | ### LocationResponder
87 |
88 | This responder allows you to use callable objects as the redirect location.
89 | Useful when you want to use the `respond_with` method with
90 | a custom route that requires persisted objects, but the validation may fail.
91 |
92 | Note: this responder is included by default, and doesn't need to be included
93 | on the top of your controller (including it will issue a deprecation warning).
94 |
95 | ```ruby
96 | class ThingsController < ApplicationController
97 | respond_to :html
98 |
99 | def create
100 | @thing = Thing.create(params[:thing])
101 | respond_with @thing, location: -> { thing_path(@thing) }
102 | end
103 | end
104 | ```
105 |
106 | **Dealing with namespaced routes**
107 |
108 | In order for the LocationResponder to find the correct route helper for namespaced routes you need to pass the namespaces to `respond_with`:
109 |
110 | ```ruby
111 | class Api::V1::ThingsController < ApplicationController
112 | respond_to :json
113 |
114 | # POST /api/v1/things
115 | def create
116 | @thing = Thing.create(thing_params)
117 | respond_with :api, :v1, @thing
118 | end
119 | end
120 | ```
121 |
122 | ## Configuring your own responder
123 |
124 | Responders only provides a set of modules and to use them you have to create your own
125 | responder. After you run the install command, the following responder will be
126 | generated in your application:
127 |
128 | ```ruby
129 | # lib/application_responder.rb
130 | class ApplicationResponder < ActionController::Responder
131 | include Responders::FlashResponder
132 | include Responders::HttpCacheResponder
133 | end
134 | ```
135 |
136 | Your application also needs to be configured to use it:
137 |
138 | ```ruby
139 | # app/controllers/application_controller.rb
140 | require "application_responder"
141 |
142 | class ApplicationController < ActionController::Base
143 | self.responder = ApplicationResponder
144 | respond_to :html
145 | end
146 | ```
147 |
148 | ## Controller method
149 |
150 | This gem also includes the controller method `responders`, which allows you to cherry-pick which
151 | responders you want included in your controller.
152 |
153 | ```ruby
154 | class InvitationsController < ApplicationController
155 | responders :flash, :http_cache
156 | end
157 | ```
158 |
159 | ## Interpolation Options
160 |
161 | You can pass in extra interpolation options for the translation by adding an `flash_interpolation_options` method to your controller:
162 |
163 | ```ruby
164 | class InvitationsController < ApplicationController
165 | responders :flash, :http_cache
166 |
167 | def create
168 | @invitation = Invitation.create(params[:invitation])
169 | respond_with @invitation
170 | end
171 |
172 | private
173 |
174 | def flash_interpolation_options
175 | { resource_name: @invitation.email }
176 | end
177 | end
178 | ```
179 |
180 | Now you would see the message `"name@example.com was successfully created"` instead of the default `"Invitation was successfully created."`
181 |
182 | ## Generator
183 |
184 | This gem also includes a responders controller generator, so your scaffold can be customized
185 | to use `respond_with` instead of default `respond_to` blocks. From 2.1, you need to explicitly opt-in to use this generator by adding the following to your `config/application.rb`:
186 |
187 | ```ruby
188 | config.app_generators.scaffold_controller :responders_controller
189 | ```
190 |
191 | ## Failure handling
192 |
193 | Responders don't use `valid?` to check for errors in models to figure out if
194 | the request was successful or not, and relies on your controllers to call
195 | `save` or `create` to trigger the validations.
196 |
197 | ```ruby
198 | def create
199 | @widget = Widget.new(widget_params)
200 | # @widget will be a valid record for responders, as we haven't called `save`
201 | # on it, and will always redirect to the `widgets_path`.
202 | respond_with @widget, location: -> { widgets_path }
203 | end
204 | ```
205 |
206 | Responders will check if the `errors` object in your model is empty or not. Take
207 | this in consideration when implementing different actions or writing test
208 | assertions on this behavior for your controllers.
209 |
210 | ```ruby
211 | def create
212 | @widget = Widget.new(widget_params)
213 | @widget.errors.add(:base, :invalid)
214 | # `respond_with` will render the `new` template again,
215 | # and set the status based on the configured `error_status`.
216 | respond_with @widget
217 | end
218 | ```
219 |
220 | ## Verifying request formats
221 |
222 | `respond_with` will raise an `ActionController::UnknownFormat` if the request
223 | MIME type was not configured through the class level `respond_to`, but the
224 | action will still be executed and any side effects (like creating a new record)
225 | will still occur. To raise the `UnknownFormat` exception before your action
226 | is invoked you can set the `verify_requested_format!` method as a `before_action`
227 | on your controller.
228 |
229 | ```ruby
230 | class WidgetsController < ApplicationController
231 | respond_to :json
232 | before_action :verify_requested_format!
233 |
234 | # POST /widgets.html won't reach the `create` action.
235 | def create
236 | widget = Widget.create(widget_params)
237 | respond_with widget
238 | end
239 | end
240 | ```
241 |
242 | ## Configuring error and redirect statuses
243 |
244 | By default, `respond_with` will respond to errors on `HTML` & `JS` requests using the HTTP status code `200 OK`,
245 | and perform redirects using the HTTP status code `302 Found`, both for backwards compatibility reasons.
246 |
247 | You can configure this behavior by setting `config.responders.error_status` and `config.responders.redirect_status` to the desired status codes.
248 |
249 | ```ruby
250 | config.responders.error_status = :unprocessable_entity
251 | config.responders.redirect_status = :see_other
252 | ```
253 |
254 | These can also be set in your custom `ApplicationResponder` if you have generated one: (see install instructions)
255 |
256 | ```ruby
257 | class ApplicationResponder < ActionController::Responder
258 | self.error_status = :unprocessable_entity
259 | self.redirect_status = :see_other
260 | end
261 | ```
262 |
263 | _Note_: the application responder generated for new apps already configures a different set of defaults: `422 Unprocessable Entity` for errors, and `303 See Other` for redirects. _Responders may change the defaults to match these in a future major release._
264 |
265 | ### Hotwire/Turbo and fetch APIs
266 |
267 | Hotwire/Turbo expects successful redirects after form submissions to respond with HTTP status `303 See Other`, and error responses to be 4xx or 5xx statuses, for example `422 Unprocessable Entity` for displaying form validation errors and `500 Internal Server Error` for other server errors. [Turbo documentation: Redirecting After a Form Submission](https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission).
268 |
269 | The example configuration showed above matches the statuses that better integrate with Hotwire/Turbo.
270 |
271 | ## Examples
272 |
273 | Want more examples ? Check out these blog posts:
274 |
275 | * [Embracing REST with mind, body and soul](http://blog.plataformatec.com.br/2009/08/embracing-rest-with-mind-body-and-soul/)
276 | * [Three reasons to love ActionController::Responder](http://weblog.rubyonrails.org/2009/8/31/three-reasons-love-responder/)
277 | * [My five favorite things about Rails 3](https://web.archive.org/web/20201109041436/https://blog.engineyard.com/my-five-favorite-things-about-rails-3)
278 |
279 | ## Supported Ruby / Rails versions
280 |
281 | We intend to maintain support for all Ruby / Rails versions that haven't reached end-of-life.
282 |
283 | For more information about specific versions please check [Ruby](https://www.ruby-lang.org/en/downloads/branches/)
284 | and [Rails](https://guides.rubyonrails.org/maintenance_policy.html) maintenance policies, and our test matrix.
285 |
286 | ## Bugs and Feedback
287 |
288 | If you discover any bugs or want to drop a line, feel free to create an issue on GitHub.
289 |
290 | MIT License. Copyright 2020-2024 Rafael França, Carlos Antônio da Silva. Copyright 2009-2019 Plataformatec.
291 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # frozen_string_literal: true
3 |
4 | require "bundler/gem_tasks"
5 |
6 | require "rake/testtask"
7 | require "rdoc/task"
8 | require File.join(File.dirname(__FILE__), "lib", "responders", "version")
9 |
10 | desc "Default: run unit tests"
11 | task default: :test
12 |
13 | desc "Test Responders"
14 | Rake::TestTask.new(:test) do |t|
15 | t.libs << "test"
16 | t.pattern = "test/**/*_test.rb"
17 | t.verbose = true
18 | end
19 |
20 | desc "Generate documentation for Responders"
21 | Rake::RDocTask.new(:rdoc) do |rdoc|
22 | rdoc.rdoc_dir = "rdoc"
23 | rdoc.title = "Responders"
24 | rdoc.options << "--line-numbers" << "--inline-source"
25 | rdoc.rdoc_files.include("README.rdoc")
26 | rdoc.rdoc_files.include("lib/**/*.rb")
27 | end
28 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-rails-5-2:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "activemodel", "~> 5.2.0"
6 | gem "railties", "~> 5.2.0"
7 | gem "mocha"
8 | gem "rails-controller-testing"
9 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-rails-6-0:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "activemodel", "~> 6.0.0"
6 | gem "railties", "~> 6.0.0"
7 | gem "mocha"
8 | gem "rails-controller-testing"
9 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-rails-6-1:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec path: ".."
4 |
5 | gem "activemodel", "~> 6.1.0"
6 | gem "railties", "~> 6.1.0"
7 | gem "mocha"
8 | gem "rails-controller-testing"
9 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-rails-7-0:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | gemspec path: ".."
6 |
7 | gem "activemodel", "~> 7.0.0"
8 | gem "railties", "~> 7.0.0"
9 | gem "mocha"
10 | gem "rails-controller-testing"
11 | gem "rubocop"
12 | gem "rubocop-performance"
13 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-rails-main:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec path: '..'
4 |
5 | gem 'activemodel', github: 'rails/rails', branch: 'main'
6 | gem 'railties', github: 'rails/rails', branch: 'main'
7 | gem 'mocha'
8 | gem 'rails-controller-testing'
9 |
--------------------------------------------------------------------------------
/lib/action_controller/respond_with.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support/core_ext/array/extract_options"
4 | require "action_controller/metal/mime_responds"
5 |
6 | module ActionController # :nodoc:
7 | module RespondWith
8 | extend ActiveSupport::Concern
9 |
10 | included do
11 | class_attribute :responder, :mimes_for_respond_to
12 | self.responder = ActionController::Responder
13 | clear_respond_to
14 | end
15 |
16 | module ClassMethods
17 | # Defines mime types that are rendered by default when invoking
18 | # respond_with.
19 | #
20 | # respond_to :html, :xml, :json
21 | #
22 | # Specifies that all actions in the controller respond to requests
23 | # for :html, :xml and :json.
24 | #
25 | # To specify on per-action basis, use :only and
26 | # :except with an array of actions or a single action:
27 | #
28 | # respond_to :html
29 | # respond_to :xml, :json, except: [ :edit ]
30 | #
31 | # This specifies that all actions respond to :html
32 | # and all actions except :edit respond to :xml and
33 | # :json.
34 | #
35 | # respond_to :json, only: :create
36 | #
37 | # This specifies that the :create action and no other responds
38 | # to :json.
39 | def respond_to(*mimes)
40 | options = mimes.extract_options!
41 |
42 | only_actions = Array(options.delete(:only)).map(&:to_sym)
43 | except_actions = Array(options.delete(:except)).map(&:to_sym)
44 |
45 | hash = mimes_for_respond_to.dup
46 | mimes.each do |mime|
47 | mime = mime.to_sym
48 | hash[mime] = {}
49 | hash[mime][:only] = only_actions unless only_actions.empty?
50 | hash[mime][:except] = except_actions unless except_actions.empty?
51 | end
52 | self.mimes_for_respond_to = hash.freeze
53 | end
54 |
55 | # Clear all mime types in respond_to.
56 | #
57 | def clear_respond_to
58 | self.mimes_for_respond_to = Hash.new.freeze
59 | end
60 | end
61 |
62 | # For a given controller action, respond_with generates an appropriate
63 | # response based on the mime-type requested by the client.
64 | #
65 | # If the method is called with just a resource, as in this example -
66 | #
67 | # class PeopleController < ApplicationController
68 | # respond_to :html, :xml, :json
69 | #
70 | # def index
71 | # @people = Person.all
72 | # respond_with @people
73 | # end
74 | # end
75 | #
76 | # then the mime-type of the response is typically selected based on the
77 | # request's Accept header and the set of available formats declared
78 | # by previous calls to the controller's class method +respond_to+. Alternatively
79 | # the mime-type can be selected by explicitly setting request.format in
80 | # the controller.
81 | #
82 | # If an acceptable format is not identified, the application returns a
83 | # '406 - not acceptable' status. Otherwise, the default response is to render
84 | # a template named after the current action and the selected format,
85 | # e.g. index.html.erb. If no template is available, the behavior
86 | # depends on the selected format:
87 | #
88 | # * for an html response - if the request method is +get+, an exception
89 | # is raised but for other requests such as +post+ the response
90 | # depends on whether the resource has any validation errors (i.e.
91 | # assuming that an attempt has been made to save the resource,
92 | # e.g. by a +create+ action) -
93 | # 1. If there are no errors, i.e. the resource
94 | # was saved successfully, the response +redirect+'s to the resource
95 | # i.e. its +show+ action.
96 | # 2. If there are validation errors, the response
97 | # renders a default action, which is :new for a
98 | # +post+ request or :edit for +patch+ or +put+,
99 | # and the status is set based on the configured `error_status`.
100 | # (defaults to `422 Unprocessable Entity` on new apps,
101 | # `200 OK` for compatibility reasons on old apps.)
102 | # Thus an example like this -
103 | #
104 | # respond_to :html, :xml
105 | #
106 | # def create
107 | # @user = User.new(params[:user])
108 | # flash[:notice] = 'User was successfully created.' if @user.save
109 | # respond_with(@user)
110 | # end
111 | #
112 | # is equivalent, in the absence of create.html.erb, to -
113 | #
114 | # def create
115 | # @user = User.new(params[:user])
116 | # respond_to do |format|
117 | # if @user.save
118 | # flash[:notice] = 'User was successfully created.'
119 | # format.html { redirect_to(@user) }
120 | # format.xml { render xml: @user }
121 | # else
122 | # format.html { render action: "new", status: :unprocessable_entity }
123 | # format.xml { render xml: @user, status: :unprocessable_entity }
124 | # end
125 | # end
126 | # end
127 | #
128 | # * for a JavaScript request - if the template isn't found, an exception is
129 | # raised.
130 | # * for other requests - i.e. data formats such as xml, json, csv etc, if
131 | # the resource passed to +respond_with+ responds to to_
,
132 | # the method attempts to render the resource in the requested format
133 | # directly, e.g. for an xml request, the response is equivalent to calling
134 | # render xml: resource
.
135 | #
136 | # === Nested resources
137 | #
138 | # As outlined above, the +resources+ argument passed to +respond_with+
139 | # can play two roles. It can be used to generate the redirect url
140 | # for successful html requests (e.g. for +create+ actions when
141 | # no template exists), while for formats other than html and JavaScript
142 | # it is the object that gets rendered, by being converted directly to the
143 | # required format (again assuming no template exists).
144 | #
145 | # For redirecting successful html requests, +respond_with+ also supports
146 | # the use of nested resources, which are supplied in the same way as
147 | # in form_for
and polymorphic_url
. For example -
148 | #
149 | # def create
150 | # @project = Project.find(params[:project_id])
151 | # @task = @project.comments.build(params[:task])
152 | # flash[:notice] = 'Task was successfully created.' if @task.save
153 | # respond_with(@project, @task)
154 | # end
155 | #
156 | # This would cause +respond_with+ to redirect to project_task_url
157 | # instead of task_url
. For request formats other than html or
158 | # JavaScript, if multiple resources are passed in this way, it is the last
159 | # one specified that is rendered.
160 | #
161 | # === Customizing response behavior
162 | #
163 | # Like +respond_to+, +respond_with+ may also be called with a block that
164 | # can be used to overwrite any of the default responses, e.g. -
165 | #
166 | # def create
167 | # @user = User.new(params[:user])
168 | # flash[:notice] = "User was successfully created." if @user.save
169 | #
170 | # respond_with(@user) do |format|
171 | # format.html { render }
172 | # end
173 | # end
174 | #
175 | # The argument passed to the block is an ActionController::MimeResponds::Collector
176 | # object which stores the responses for the formats defined within the
177 | # block. Note that formats with responses defined explicitly in this way
178 | # do not have to first be declared using the class method +respond_to+.
179 | #
180 | # Also, a hash passed to +respond_with+ immediately after the specified
181 | # resource(s) is interpreted as a set of options relevant to all
182 | # formats. Any option accepted by +render+ can be used, e.g.
183 | #
184 | # respond_with @people, status: 200
185 | #
186 | # However, note that these options are ignored after an unsuccessful attempt
187 | # to save a resource, e.g. when automatically rendering :new
188 | # after a post request.
189 | #
190 | # Three additional options are relevant specifically to +respond_with+ -
191 | # 1. :location - overwrites the default redirect location used after
192 | # a successful html +post+ request.
193 | # 2. :action - overwrites the default render action used after an
194 | # unsuccessful html +post+ request.
195 | # 3. :render - allows to pass any options directly to the :render
196 | # call after unsuccessful html +post+ request. Useful if for example you
197 | # need to render a template which is outside of controller's path or you
198 | # want to override the default http :status code, e.g.
199 | #
200 | # respond_with(resource, render: { template: 'path/to/template', status: 418 })
201 | def respond_with(*resources, &block)
202 | if self.class.mimes_for_respond_to.empty?
203 | raise "In order to use respond_with, first you need to declare the " \
204 | "formats your controller responds to in the class level."
205 | end
206 |
207 | mimes = collect_mimes_from_class_level
208 | collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
209 | block.call(collector) if block_given?
210 |
211 | if format = collector.negotiate_format(request)
212 | _process_format(format)
213 | options = resources.size == 1 ? {} : resources.extract_options!
214 | options = options.clone
215 | options[:default_response] = collector.response
216 | (options.delete(:responder) || self.class.responder).call(self, resources, options)
217 | else
218 | raise ActionController::UnknownFormat
219 | end
220 | end
221 |
222 | protected
223 |
224 | # Before action callback that can be used to prevent requests that do not
225 | # match the mime types defined through respond_to from being executed.
226 | #
227 | # class PeopleController < ApplicationController
228 | # respond_to :html, :xml, :json
229 | #
230 | # before_action :verify_requested_format!
231 | # end
232 | def verify_requested_format!
233 | mimes = collect_mimes_from_class_level
234 | collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
235 |
236 | unless collector.negotiate_format(request)
237 | raise ActionController::UnknownFormat
238 | end
239 | end
240 |
241 | alias :verify_request_format! :verify_requested_format!
242 |
243 | # Collect mimes declared in the class method respond_to valid for the
244 | # current action.
245 | def collect_mimes_from_class_level # :nodoc:
246 | action = action_name.to_sym
247 |
248 | self.class.mimes_for_respond_to.keys.select do |mime|
249 | config = self.class.mimes_for_respond_to[mime]
250 |
251 | if config[:except]
252 | !config[:except].include?(action)
253 | elsif config[:only]
254 | config[:only].include?(action)
255 | else
256 | true
257 | end
258 | end
259 | end
260 | end
261 | end
262 |
--------------------------------------------------------------------------------
/lib/action_controller/responder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support/json"
4 |
5 | module ActionController # :nodoc:
6 | # Responsible for exposing a resource to different mime requests,
7 | # usually depending on the HTTP verb. The responder is triggered when
8 | # respond_with
is called. The simplest case to study is a GET request:
9 | #
10 | # class PeopleController < ApplicationController
11 | # respond_to :html, :xml, :json
12 | #
13 | # def index
14 | # @people = Person.all
15 | # respond_with(@people)
16 | # end
17 | # end
18 | #
19 | # When a request comes in, for example for an XML response, three steps happen:
20 | #
21 | # 1) the responder searches for a template at people/index.xml;
22 | #
23 | # 2) if the template is not available, it will invoke #to_xml
on the given resource;
24 | #
25 | # 3) if the responder does not respond_to :to_xml
, call #to_format
on it.
26 | #
27 | # === Built-in HTTP verb semantics
28 | #
29 | # The default \Rails responder holds semantics for each HTTP verb. Depending on the
30 | # content type, verb and the resource status, it will behave differently.
31 | #
32 | # Using \Rails default responder, a POST request for creating an object could
33 | # be written as:
34 | #
35 | # def create
36 | # @user = User.new(params[:user])
37 | # flash[:notice] = 'User was successfully created.' if @user.save
38 | # respond_with(@user)
39 | # end
40 | #
41 | # Which is exactly the same as:
42 | #
43 | # def create
44 | # @user = User.new(params[:user])
45 | #
46 | # respond_to do |format|
47 | # if @user.save
48 | # flash[:notice] = 'User was successfully created.'
49 | # format.html { redirect_to(@user) }
50 | # format.xml { render xml: @user, status: :created, location: @user }
51 | # else
52 | # format.html { render action: "new", status: :unprocessable_entity }
53 | # format.xml { render xml: @user.errors, status: :unprocessable_entity }
54 | # end
55 | # end
56 | # end
57 | #
58 | # The same happens for PATCH/PUT and DELETE requests.
59 | #
60 | # === Nested resources
61 | #
62 | # You can supply nested resources as you do in form_for
and polymorphic_url
.
63 | # Consider the project has many tasks example. The create action for
64 | # TasksController would be like:
65 | #
66 | # def create
67 | # @project = Project.find(params[:project_id])
68 | # @task = @project.tasks.build(params[:task])
69 | # flash[:notice] = 'Task was successfully created.' if @task.save
70 | # respond_with(@project, @task)
71 | # end
72 | #
73 | # Giving several resources ensures that the responder will redirect to
74 | # project_task_url
instead of task_url
.
75 | #
76 | # Namespaced and singleton resources require a symbol to be given, as in
77 | # polymorphic urls. If a project has one manager which has many tasks, it
78 | # should be invoked as:
79 | #
80 | # respond_with(@project, :manager, @task)
81 | #
82 | # Note that if you give an array, it will be treated as a collection,
83 | # so the following is not equivalent:
84 | #
85 | # respond_with [@project, :manager, @task]
86 | #
87 | # === Custom options
88 | #
89 | # respond_with
also allows you to pass options that are forwarded
90 | # to the underlying render call. Those options are only applied for success
91 | # scenarios. For instance, you can do the following in the create method above:
92 | #
93 | # def create
94 | # @project = Project.find(params[:project_id])
95 | # @task = @project.tasks.build(params[:task])
96 | # flash[:notice] = 'Task was successfully created.' if @task.save
97 | # respond_with(@project, @task, status: 201)
98 | # end
99 | #
100 | # This will return status 201 if the task was saved successfully. If not,
101 | # it will simply ignore the given options and return status 422 and the
102 | # resource errors. You can also override the location to redirect to:
103 | #
104 | # respond_with(@project, location: root_path)
105 | #
106 | # To customize the failure scenario, you can pass a block to
107 | # respond_with
:
108 | #
109 | # def create
110 | # @project = Project.find(params[:project_id])
111 | # @task = @project.tasks.build(params[:task])
112 | # respond_with(@project, @task, status: 201) do |format|
113 | # if @task.save
114 | # flash[:notice] = 'Task was successfully created.'
115 | # else
116 | # format.html { render "some_special_template", status: :unprocessable_entity }
117 | # end
118 | # end
119 | # end
120 | #
121 | # Using respond_with
with a block follows the same syntax as respond_to
.
122 | class Responder
123 | class_attribute :error_status, default: :ok, instance_writer: false, instance_predicate: false
124 | class_attribute :redirect_status, default: :found, instance_writer: false, instance_predicate: false
125 |
126 | attr_reader :controller, :request, :format, :resource, :resources, :options
127 |
128 | DEFAULT_ACTIONS_FOR_VERBS = {
129 | post: :new,
130 | patch: :edit,
131 | put: :edit
132 | }
133 |
134 | def initialize(controller, resources, options = {})
135 | @controller = controller
136 | @request = @controller.request
137 | @format = @controller.formats.first
138 | @resource = resources.last
139 | @resources = resources
140 | @options = options
141 | @action = options.delete(:action)
142 | @default_response = options.delete(:default_response)
143 |
144 | if options[:location].respond_to?(:call)
145 | location = options.delete(:location)
146 | options[:location] = location.call unless has_errors?
147 | end
148 | end
149 |
150 | delegate :head, :render, :redirect_to, to: :controller
151 | delegate :get?, :post?, :patch?, :put?, :delete?, to: :request
152 |
153 | # Undefine :to_json and :to_yaml since it's defined on Object
154 | undef_method(:to_json) if method_defined?(:to_json)
155 | undef_method(:to_yaml) if method_defined?(:to_yaml)
156 |
157 | # Initializes a new responder and invokes the proper format. If the format is
158 | # not defined, call to_format.
159 | #
160 | def self.call(*args)
161 | new(*args).respond
162 | end
163 |
164 | # Main entry point for responder responsible to dispatch to the proper format.
165 | #
166 | def respond
167 | method = "to_#{format}"
168 | respond_to?(method) ? send(method) : to_format
169 | end
170 |
171 | # HTML format does not render the resource, it always attempt to render a
172 | # template.
173 | #
174 | def to_html
175 | default_render
176 | rescue ActionView::MissingTemplate => e
177 | navigation_behavior(e)
178 | end
179 |
180 | # to_js simply tries to render a template. If no template is found, raises the error.
181 | def to_js
182 | default_render
183 | end
184 |
185 | # All other formats follow the procedure below. First we try to render a
186 | # template, if the template is not available, we verify if the resource
187 | # responds to :to_format and display it.
188 | #
189 | def to_format
190 | if !get? && has_errors? && !response_overridden?
191 | display_errors
192 | elsif has_view_rendering? || response_overridden?
193 | default_render
194 | else
195 | api_behavior
196 | end
197 | rescue ActionView::MissingTemplate
198 | api_behavior
199 | end
200 |
201 | protected
202 |
203 | # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
204 | def navigation_behavior(error)
205 | if get?
206 | raise error
207 | elsif has_errors? && default_action
208 | render error_rendering_options
209 | else
210 | redirect_to navigation_location, status: redirect_status
211 | end
212 | end
213 |
214 | # This is the common behavior for formats associated with APIs, such as :xml and :json.
215 | def api_behavior
216 | raise MissingRenderer.new(format) unless has_renderer?
217 |
218 | if get?
219 | display resource
220 | elsif post?
221 | display resource, status: :created, location: api_location
222 | else
223 | head :no_content
224 | end
225 | end
226 |
227 | # Returns the resource location by retrieving it from the options or
228 | # returning the resources array.
229 | #
230 | def resource_location
231 | options[:location] || resources
232 | end
233 | alias :navigation_location :resource_location
234 | alias :api_location :resource_location
235 |
236 | # If a response block was given, use it, otherwise call render on
237 | # controller.
238 | #
239 | def default_render
240 | if @default_response
241 | @default_response.call(options)
242 | elsif !get? && has_errors?
243 | controller.render({ status: error_status }.merge!(options))
244 | else
245 | controller.render(options)
246 | end
247 | end
248 |
249 | # Display is just a shortcut to render a resource with the current format.
250 | #
251 | # display @user, status: :ok
252 | #
253 | # For XML requests it's equivalent to:
254 | #
255 | # render xml: @user, status: :ok
256 | #
257 | # Options sent by the user are also used:
258 | #
259 | # respond_with(@user, status: :created)
260 | # display(@user, status: :ok)
261 | #
262 | # Results in:
263 | #
264 | # render xml: @user, status: :created
265 | #
266 | def display(resource, given_options = {})
267 | controller.render given_options.merge!(options).merge!(format => resource)
268 | end
269 |
270 | def display_errors
271 | # TODO: use `error_status` once we switch the default to be `unprocessable_entity`,
272 | # otherwise we'd be changing this behavior here now.
273 | controller.render format => resource_errors, :status => :unprocessable_entity
274 | end
275 |
276 | # Check whether the resource has errors.
277 | #
278 | def has_errors?
279 | resource.respond_to?(:errors) && !resource.errors.empty?
280 | end
281 |
282 | # Check whether the necessary Renderer is available
283 | def has_renderer?
284 | Renderers::RENDERERS.include?(format)
285 | end
286 |
287 | def has_view_rendering?
288 | controller.class.include? ActionView::Rendering
289 | end
290 |
291 | # By default, render the :edit
action for HTML requests with errors, unless
292 | # the verb was POST.
293 | #
294 | def default_action
295 | @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
296 | end
297 |
298 | def resource_errors
299 | respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
300 | end
301 |
302 | def json_resource_errors
303 | { errors: resource.errors }
304 | end
305 |
306 | def response_overridden?
307 | @default_response.present?
308 | end
309 |
310 | def error_rendering_options
311 | if options[:render]
312 | options[:render]
313 | else
314 | { action: default_action, status: error_status }
315 | end
316 | end
317 | end
318 | end
319 |
--------------------------------------------------------------------------------
/lib/generators/rails/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Stubs out a scaffolded controller and its views. Different from rails
3 | scaffold_controller, it uses respond_with instead of respond_to blocks.
4 | Pass the model name, either CamelCased or under_scored. The controller
5 | name is retrieved as a pluralized version of the model name.
6 |
7 | To create a controller within a module, specify the model name as a
8 | path like 'parent_module/controller_name'.
9 |
10 | This generates a controller class in app/controllers and invokes helper,
11 | template engine and test framework generators.
12 |
--------------------------------------------------------------------------------
/lib/generators/rails/responders_controller_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/rails/scaffold_controller/scaffold_controller_generator"
4 |
5 | module Rails
6 | module Generators
7 | class RespondersControllerGenerator < ScaffoldControllerGenerator
8 | source_root File.expand_path("../templates", __FILE__)
9 |
10 | protected
11 |
12 | def flash?
13 | if defined?(ApplicationController)
14 | !ApplicationController.responder.ancestors.include?(Responders::FlashResponder)
15 | else
16 | Rails.application.config.responders.flash_keys.blank?
17 | end
18 | end
19 |
20 | def attributes_params
21 | "#{singular_table_name}_params"
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/api_controller.rb.tt:
--------------------------------------------------------------------------------
1 | <% if namespaced? -%>
2 | require_dependency "<%= namespaced_file_path %>/application_controller"
3 |
4 | <% end -%>
5 | <% module_namespacing do -%>
6 | class <%= controller_class_name %>Controller < ApplicationController
7 | before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy]
8 |
9 | respond_to :json
10 |
11 | <% unless options[:singleton] -%>
12 | def index
13 | @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
14 | respond_with(@<%= plural_table_name %>)
15 | end
16 | <% end -%>
17 |
18 | def show
19 | respond_with(@<%= singular_table_name %>)
20 | end
21 |
22 | def create
23 | @<%= singular_table_name %> = <%= orm_class.build(class_name, attributes_params) %>
24 | <%= "flash[:notice] = '#{class_name} was successfully created.' if " if flash? %>@<%= orm_instance.save %>
25 | respond_with(@<%= singular_table_name %>)
26 | end
27 |
28 | def update
29 | <%= "flash[:notice] = '#{class_name} was successfully updated.' if " if flash? %>@<%= orm_instance.update(attributes_params) %>
30 | respond_with(@<%= singular_table_name %>)
31 | end
32 |
33 | def destroy
34 | @<%= orm_instance.destroy %>
35 | respond_with(@<%= singular_table_name %>)
36 | end
37 |
38 | private
39 | def set_<%= singular_table_name %>
40 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
41 | end
42 |
43 | def <%= "#{singular_table_name}_params" %>
44 | <%- if attributes_names.empty? -%>
45 | params[:<%= singular_table_name %>]
46 | <%- else -%>
47 | params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
48 | <%- end -%>
49 | end
50 | end
51 | <% end -%>
52 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/controller.rb.tt:
--------------------------------------------------------------------------------
1 | <% if namespaced? -%>
2 | require_dependency "<%= namespaced_file_path %>/application_controller"
3 |
4 | <% end -%>
5 | <% module_namespacing do -%>
6 | class <%= controller_class_name %>Controller < ApplicationController
7 | before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
8 |
9 | respond_to :html
10 |
11 | <% unless options[:singleton] -%>
12 | def index
13 | @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
14 | respond_with(@<%= plural_table_name %>)
15 | end
16 | <% end -%>
17 |
18 | def show
19 | respond_with(@<%= singular_table_name %>)
20 | end
21 |
22 | def new
23 | @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
24 | respond_with(@<%= singular_table_name %>)
25 | end
26 |
27 | def edit
28 | end
29 |
30 | def create
31 | @<%= singular_table_name %> = <%= orm_class.build(class_name, attributes_params) %>
32 | <%= "flash[:notice] = '#{class_name} was successfully created.' if " if flash? %>@<%= orm_instance.save %>
33 | respond_with(@<%= singular_table_name %>)
34 | end
35 |
36 | def update
37 | <%= "flash[:notice] = '#{class_name} was successfully updated.' if " if flash? %>@<%= orm_instance.update(attributes_params) %>
38 | respond_with(@<%= singular_table_name %>)
39 | end
40 |
41 | def destroy
42 | @<%= orm_instance.destroy %>
43 | respond_with(@<%= singular_table_name %>)
44 | end
45 |
46 | private
47 | def set_<%= singular_table_name %>
48 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
49 | end
50 |
51 | def <%= "#{singular_table_name}_params" %>
52 | <%- if attributes_names.empty? -%>
53 | params[:<%= singular_table_name %>]
54 | <%- else -%>
55 | params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
56 | <%- end -%>
57 | end
58 | end
59 | <% end -%>
60 |
--------------------------------------------------------------------------------
/lib/generators/responders/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Responders
4 | module Generators
5 | class InstallGenerator < Rails::Generators::Base
6 | source_root File.expand_path("..", __FILE__)
7 |
8 | desc "Creates an initializer with default responder configuration and copy locale file"
9 |
10 | def create_responder_file
11 | create_file "lib/application_responder.rb", <<~RUBY
12 | class ApplicationResponder < ActionController::Responder
13 | include Responders::FlashResponder
14 | include Responders::HttpCacheResponder
15 |
16 | # Redirects resources to the collection path (index action) instead
17 | # of the resource path (show action) for POST/PUT/DELETE requests.
18 | # include Responders::CollectionResponder
19 |
20 | # Configure default status codes for responding to errors and redirects.
21 | self.error_status = :unprocessable_entity
22 | self.redirect_status = :see_other
23 | end
24 | RUBY
25 | end
26 |
27 | def update_application
28 | inject_into_class "config/application.rb", "Application", <<-RUBY
29 | # Use the responders controller from the responders gem
30 | config.app_generators.scaffold_controller :responders_controller
31 |
32 | RUBY
33 | end
34 |
35 | def update_application_controller
36 | prepend_file "app/controllers/application_controller.rb", %{require "application_responder"\n\n}
37 | inject_into_class "app/controllers/application_controller.rb", "ApplicationController", <<-RUBY
38 | self.responder = ApplicationResponder
39 | respond_to :html
40 |
41 | RUBY
42 | end
43 |
44 | def copy_locale
45 | copy_file "../../responders/locales/en.yml", "config/locales/responders.en.yml"
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/responders.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_controller"
4 | require "rails/railtie"
5 |
6 | module ActionController
7 | autoload :Responder, "action_controller/responder"
8 | autoload :RespondWith, "action_controller/respond_with"
9 | end
10 |
11 | module Responders
12 | autoload :FlashResponder, "responders/flash_responder"
13 | autoload :HttpCacheResponder, "responders/http_cache_responder"
14 | autoload :CollectionResponder, "responders/collection_responder"
15 |
16 | require "responders/controller_method"
17 |
18 | class Railtie < ::Rails::Railtie
19 | config.responders = ActiveSupport::OrderedOptions.new
20 | config.responders.flash_keys = [:notice, :alert]
21 | config.responders.namespace_lookup = false
22 | config.responders.error_status = :ok
23 | config.responders.redirect_status = :found
24 |
25 | # Add load paths straight to I18n, so engines and application can overwrite it.
26 | require "active_support/i18n"
27 | I18n.load_path << File.expand_path("../responders/locales/en.yml", __FILE__)
28 |
29 | initializer "responders.flash_responder" do |app|
30 | Responders::FlashResponder.flash_keys = app.config.responders.flash_keys
31 | Responders::FlashResponder.namespace_lookup = app.config.responders.namespace_lookup
32 | ActionController::Responder.error_status = app.config.responders.error_status
33 | ActionController::Responder.redirect_status = app.config.responders.redirect_status
34 | end
35 | end
36 | end
37 |
38 | ActiveSupport.on_load(:action_controller) do
39 | include ActionController::RespondWith
40 | end
41 |
--------------------------------------------------------------------------------
/lib/responders/collection_responder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Responders
4 | # This responder modifies your current responder to redirect
5 | # to the collection page on POST/PUT/DELETE.
6 | module CollectionResponder
7 | protected
8 |
9 | # Returns the collection location for redirecting after POST/PUT/DELETE.
10 | # This method, converts the following resources array to the following:
11 | #
12 | # [:admin, @post] #=> [:admin, :posts]
13 | # [@user, @post] #=> [@user, :posts]
14 | #
15 | # When these new arrays are given to redirect_to, it will generate the
16 | # proper URL pointing to the index action.
17 | #
18 | # [:admin, @post] #=> admin_posts_url
19 | # [@user, @post] #=> user_posts_url(@user.to_param)
20 | #
21 | def navigation_location
22 | return options[:location] if options[:location]
23 |
24 | klass = resources.last.class
25 |
26 | if klass.respond_to?(:model_name)
27 | resources[0...-1] << klass.model_name.route_key.to_sym
28 | else
29 | resources
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/responders/controller_method.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Responders
4 | module ControllerMethod
5 | # Adds the given responders to the current controller's responder, allowing you to cherry-pick
6 | # which responders you want per controller.
7 | #
8 | # class InvitationsController < ApplicationController
9 | # responders :flash, :http_cache
10 | # end
11 | #
12 | # Takes symbols and strings and translates them to VariableResponder (eg. :flash becomes FlashResponder).
13 | # Also allows passing in the responders modules in directly, so you could do:
14 | #
15 | # responders FlashResponder, HttpCacheResponder
16 | #
17 | # Or a mix of both methods:
18 | #
19 | # responders :flash, MyCustomResponder
20 | #
21 | def responders(*responders)
22 | self.responder = responders.inject(Class.new(responder)) do |klass, responder|
23 | responder = \
24 | case responder
25 | when Module
26 | responder
27 | when String, Symbol
28 | Responders.const_get("#{responder.to_s.camelize}Responder")
29 | else
30 | raise "responder has to be a string, a symbol or a module"
31 | end
32 |
33 | klass.send(:include, responder)
34 | klass
35 | end
36 | end
37 | end
38 | end
39 |
40 | ActiveSupport.on_load(:action_controller_base) do
41 | ActionController::Base.extend Responders::ControllerMethod
42 | end
43 |
--------------------------------------------------------------------------------
/lib/responders/flash_responder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Responders
4 | # Responder to automatically set flash messages based on I18n API. It checks for
5 | # message based on the current action, but also allows defaults to be set, using
6 | # the following order:
7 | #
8 | # flash.controller_name.action_name.status
9 | # flash.actions.action_name.status
10 | #
11 | # So, if you have a CarsController, create action, it will check for:
12 | #
13 | # flash.cars.create.status
14 | # flash.actions.create.status
15 | #
16 | # The statuses by default are :notice (when the object can be created, updated
17 | # or destroyed with success) and :alert (when the object cannot be created
18 | # or updated).
19 | #
20 | # On I18n, the resource_name given is available as interpolation option,
21 | # this means you can set:
22 | #
23 | # flash:
24 | # actions:
25 | # create:
26 | # notice: "Hooray! %{resource_name} was successfully created!"
27 | #
28 | # But sometimes, flash messages are not that simple. Going back
29 | # to cars example, you might want to say the brand of the car when it's
30 | # updated. Well, that's easy also:
31 | #
32 | # flash:
33 | # cars:
34 | # update:
35 | # notice: "Hooray! You just tuned your %{car_brand}!"
36 | #
37 | # Since :car_name is not available for interpolation by default, you have
38 | # to overwrite `flash_interpolation_options` in your controller.
39 | #
40 | # def flash_interpolation_options
41 | # { :car_brand => @car.brand }
42 | # end
43 | #
44 | # Then you will finally have:
45 | #
46 | # 'Hooray! You just tuned your Aston Martin!'
47 | #
48 | # If your controller is namespaced, for example Admin::CarsController,
49 | # the messages will be checked in the following order:
50 | #
51 | # flash.admin.cars.create.status
52 | # flash.admin.actions.create.status
53 | # flash.cars.create.status
54 | # flash.actions.create.status
55 | #
56 | # You can also have flash messages with embedded HTML. Just create a scope that
57 | # ends with _html as the scopes below:
58 | #
59 | # flash.actions.create.notice_html
60 | # flash.cars.create.notice_html
61 | #
62 | # == Options
63 | #
64 | # FlashResponder also accepts some options through respond_with API.
65 | #
66 | # * :flash - When set to false, no flash message is set.
67 | #
68 | # respond_with(@user, :flash => true)
69 | #
70 | # * :notice - Supply the message to be set if the record has no errors.
71 | # * :alert - Supply the message to be set if the record has errors.
72 | #
73 | # respond_with(@user, :notice => "Hooray! Welcome!", :alert => "Woot! You failed.")
74 | #
75 | # * :flash_now - Sets the flash message using flash.now. Accepts true, :on_failure or :on_success.
76 | #
77 | # == Configure status keys
78 | #
79 | # As said previously, FlashResponder by default use :notice and :alert
80 | # keys. You can change that by setting the status_keys:
81 | #
82 | # Responders::FlashResponder.flash_keys = [ :success, :failure ]
83 | #
84 | # However, the options :notice and :alert to respond_with are kept :notice
85 | # and :alert.
86 | #
87 | module FlashResponder
88 | class << self
89 | attr_accessor :flash_keys, :namespace_lookup
90 | end
91 |
92 | self.flash_keys = [ :notice, :alert ]
93 | self.namespace_lookup = false
94 |
95 | def initialize(controller, resources, options = {})
96 | super
97 | @flash = options.delete(:flash)
98 | @notice = options.delete(:notice)
99 | @alert = options.delete(:alert)
100 | @flash_now = options.delete(:flash_now) { :on_failure }
101 | end
102 |
103 | def to_html
104 | set_flash_message! if set_flash_message?
105 | super
106 | end
107 |
108 | def to_js
109 | set_flash_message! if set_flash_message?
110 | defined?(super) ? super : to_format
111 | end
112 |
113 | protected
114 |
115 | def set_flash_message!
116 | if has_errors?
117 | status = Responders::FlashResponder.flash_keys.last
118 | set_flash(status, @alert)
119 | else
120 | status = Responders::FlashResponder.flash_keys.first
121 | set_flash(status, @notice)
122 | end
123 |
124 | return if controller.flash[status].present?
125 |
126 | options = mount_i18n_options(status)
127 | message = controller.helpers.t options[:default].shift, **options
128 | set_flash(status, message)
129 | end
130 |
131 | def set_flash(key, value)
132 | return if value.blank?
133 | flash = controller.flash
134 | flash = flash.now if set_flash_now?
135 | flash[key] ||= value
136 | end
137 |
138 | def set_flash_now?
139 | @flash_now == true || format == :js ||
140 | (default_action && (has_errors? ? @flash_now == :on_failure : @flash_now == :on_success))
141 | end
142 |
143 | def set_flash_message? # :nodoc:
144 | !get? && @flash != false
145 | end
146 |
147 | def mount_i18n_options(status) # :nodoc:
148 | options = {
149 | default: flash_defaults_by_namespace(status),
150 | resource_name: resource_name,
151 | downcase_resource_name: resource_name.downcase
152 | }
153 |
154 | controller_options = controller_interpolation_options
155 | if controller_options
156 | options.merge!(controller_options)
157 | end
158 |
159 | options
160 | end
161 |
162 | def controller_interpolation_options
163 | controller.send(:flash_interpolation_options) if controller.respond_to?(:flash_interpolation_options, true)
164 | end
165 |
166 | def resource_name
167 | if resource.class.respond_to?(:model_name)
168 | resource.class.model_name.human
169 | else
170 | resource.class.name.underscore.humanize
171 | end
172 | end
173 |
174 | def flash_defaults_by_namespace(status) # :nodoc:
175 | defaults = []
176 | slices = controller.controller_path.split("/")
177 | lookup = Responders::FlashResponder.namespace_lookup
178 |
179 | begin
180 | controller_scope = :"flash.#{slices.fill(controller.controller_name, -1).join(".")}.#{controller.action_name}.#{status}"
181 |
182 | actions_scope = lookup ? slices.fill("actions", -1).join(".") : :actions
183 | actions_scope = :"flash.#{actions_scope}.#{controller.action_name}.#{status}"
184 |
185 | defaults << :"#{controller_scope}_html"
186 | defaults << controller_scope
187 |
188 | defaults << :"#{actions_scope}_html"
189 | defaults << actions_scope
190 |
191 | slices.shift
192 | end while slices.size > 0 && lookup
193 |
194 | defaults << ""
195 | end
196 | end
197 | end
198 |
--------------------------------------------------------------------------------
/lib/responders/http_cache_responder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Responders
4 | # Set HTTP Last-Modified headers based on the given resource. It's used only
5 | # on API behavior (to_format) and is useful for a client to check in the server
6 | # if a resource changed after a specific date or not.
7 | #
8 | # This is not usually not used in html requests because pages contains a lot
9 | # information besides the resource information, as current_user, flash messages,
10 | # widgets... that are better handled with other strategies, as fragment caches and
11 | # the digest of the body.
12 | #
13 | module HttpCacheResponder
14 | def initialize(controller, resources, options = {})
15 | super
16 | @http_cache = options.delete(:http_cache)
17 | end
18 |
19 | def to_format
20 | return if do_http_cache? && do_http_cache!
21 | super
22 | end
23 |
24 | protected
25 |
26 | def do_http_cache!
27 | timestamp = resources.map do |resource|
28 | resource.updated_at.try(:utc) if resource.respond_to?(:updated_at)
29 | end.compact.max
30 |
31 | controller.response.last_modified ||= timestamp if timestamp
32 |
33 | head :not_modified if fresh = request.fresh?(controller.response)
34 | fresh
35 | end
36 |
37 | def do_http_cache?
38 | get? && @http_cache != false && ActionController::Base.perform_caching &&
39 | persisted? && resource.respond_to?(:updated_at)
40 | end
41 |
42 | def persisted?
43 | resource.respond_to?(:persisted?) ? resource.persisted? : true
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/responders/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | flash:
3 | actions:
4 | create:
5 | notice: '%{resource_name} was successfully created.'
6 | # alert: '%{resource_name} could not be created.'
7 | update:
8 | notice: '%{resource_name} was successfully updated.'
9 | # alert: '%{resource_name} could not be updated.'
10 | destroy:
11 | notice: '%{resource_name} was successfully destroyed.'
12 | alert: '%{resource_name} could not be destroyed.'
13 |
--------------------------------------------------------------------------------
/lib/responders/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Responders
4 | VERSION = "3.1.1"
5 | end
6 |
--------------------------------------------------------------------------------
/responders.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | # frozen_string_literal: true
3 |
4 | $:.unshift File.expand_path("../lib", __FILE__)
5 | require "responders/version"
6 |
7 | Gem::Specification.new do |s|
8 | s.name = "responders"
9 | s.version = Responders::VERSION.dup
10 | s.platform = Gem::Platform::RUBY
11 | s.summary = "A set of Rails responders to dry up your application"
12 | s.email = "heartcombo@googlegroups.com"
13 | s.homepage = "https://github.com/heartcombo/responders"
14 | s.description = "A set of Rails responders to dry up your application"
15 | s.authors = ["José Valim"]
16 | s.license = "MIT"
17 | s.metadata = {
18 | "homepage_uri" => "https://github.com/heartcombo/responders",
19 | "changelog_uri" => "https://github.com/heartcombo/responders/blob/main/CHANGELOG.md",
20 | "source_code_uri" => "https://github.com/heartcombo/responders",
21 | "bug_tracker_uri" => "https://github.com/heartcombo/responders/issues",
22 | }
23 |
24 | s.required_ruby_version = ">= 2.5.0"
25 |
26 | s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*"]
27 | s.require_paths = ["lib"]
28 |
29 | s.add_dependency "railties", ">= 5.2"
30 | s.add_dependency "actionpack", ">= 5.2"
31 | end
32 |
--------------------------------------------------------------------------------
/test/action_controller/respond_with_api_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 | require "support/models"
5 |
6 | class ApiRespondWithController < ActionController::API
7 | respond_to :json
8 |
9 | def index
10 | respond_with [
11 | Customer.new("Foo", 1),
12 | Customer.new("Bar", 2),
13 | ]
14 | end
15 |
16 | def create
17 | respond_with Customer.new("Foo", 1), location: "http://test.host/"
18 | end
19 | end
20 |
21 | class RespondWithAPITest < ActionController::TestCase
22 | tests ApiRespondWithController
23 |
24 | def test_api_controller_without_view_rendering
25 | @request.accept = "application/json"
26 |
27 | get :index
28 | assert_equal 200, @response.status
29 | expected = [{ name: "Foo", id: 1 }, { name: "Bar", id: 2 }]
30 | assert_equal expected.to_json, @response.body
31 |
32 | post :create
33 | assert_equal 201, @response.status
34 | expected = { name: "Foo", id: 1 }
35 | assert_equal expected.to_json, @response.body
36 |
37 | errors = { name: ["invalid"] }
38 | Customer.any_instance.stubs(:errors).returns(errors)
39 | post :create
40 | assert_equal 422, @response.status
41 | expected = { errors: errors }
42 | assert_equal expected.to_json, @response.body
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/action_controller/respond_with_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 | require "support/models"
5 |
6 | class RespondWithController < ApplicationController
7 | class CustomerWithJson < Customer
8 | def to_json(*); super; end
9 | end
10 |
11 | respond_to :html, :json, :touch
12 | respond_to :xml, except: :using_resource_with_block
13 | respond_to :js, only: [ :using_resource_with_block, :using_resource, "using_hash_resource", :using_resource_with_status ]
14 |
15 | def using_resource
16 | respond_with(resource)
17 | end
18 |
19 | def using_hash_resource
20 | respond_with(result: resource)
21 | end
22 |
23 | def using_resource_with_status
24 | respond_with(resource, status: 418, template: "respond_with/using_resource")
25 | end
26 |
27 | def using_resource_with_block
28 | respond_with(resource) do |format|
29 | format.csv { render body: "CSV", content_type: "text/csv" }
30 | end
31 | end
32 |
33 | def using_resource_with_overwrite_block
34 | respond_with(resource) do |format|
35 | format.html { render html: "HTML" }
36 | end
37 | end
38 |
39 | def using_resource_with_collection
40 | respond_with([resource, Customer.new("jamis", 9)])
41 | end
42 |
43 | def using_resource_with_parent
44 | respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13))
45 | end
46 |
47 | def using_resource_with_status_and_location
48 | respond_with(resource, location: "http://test.host/", status: :created)
49 | end
50 |
51 | def using_resource_with_json
52 | respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13))
53 | end
54 |
55 | def using_invalid_resource_with_template
56 | respond_with(resource)
57 | end
58 |
59 | def using_options_with_template
60 | @customer = resource
61 | respond_with(@customer, status: 123, location: "http://test.host/")
62 | end
63 |
64 | def using_resource_with_responder
65 | responder = proc { |c, r, o| c.render body: "Resource name is #{r.first.name}" }
66 | respond_with(resource, responder: responder)
67 | end
68 |
69 | def using_resource_with_action
70 | respond_with(resource, action: :foo) do |format|
71 | format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) }
72 | end
73 | end
74 |
75 | def using_resource_with_rendering_options
76 | rendering_options = { template: "addresses/edit", status: :unprocessable_entity }
77 | respond_with(resource, render: rendering_options) do |format|
78 | format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) }
79 | end
80 | end
81 |
82 | def using_responder_with_respond
83 | responder = Class.new(ActionController::Responder) do
84 | def respond; @controller.render body: "respond #{format}"; end
85 | end
86 | respond_with(resource, responder: responder)
87 | end
88 |
89 | def respond_with_additional_params
90 | @params = RespondWithController.params
91 | respond_with({ result: resource }, @params)
92 | end
93 |
94 | protected
95 |
96 | def self.params
97 | {
98 | foo: "bar"
99 | }
100 | end
101 |
102 | def resource
103 | Customer.new("david", request.delete? ? nil : 13)
104 | end
105 | end
106 |
107 | class InheritedRespondWithController < RespondWithController
108 | clear_respond_to
109 | respond_to :xml, :json
110 |
111 | def index
112 | respond_with(resource) do |format|
113 | format.json { render body: "JSON" }
114 | end
115 | end
116 | end
117 |
118 | class CsvRespondWithController < ApplicationController
119 | respond_to :csv
120 |
121 | class RespondWithCsv
122 | def to_csv
123 | "c,s,v"
124 | end
125 | end
126 |
127 | def index
128 | respond_with(RespondWithCsv.new)
129 | end
130 | end
131 |
132 | class EmptyRespondWithController < ApplicationController
133 | clear_respond_to
134 | def index
135 | respond_with(Customer.new("david", 13))
136 | end
137 | end
138 |
139 | class RespondWithControllerTest < ActionController::TestCase
140 | def setup
141 | super
142 | @request.host = "www.example.com"
143 | Mime::Type.register_alias("text/html", :iphone)
144 | Mime::Type.register_alias("text/html", :touch)
145 | Mime::Type.register("text/x-mobile", :mobile)
146 | end
147 |
148 | def teardown
149 | super
150 | Mime::Type.unregister(:iphone)
151 | Mime::Type.unregister(:touch)
152 | Mime::Type.unregister(:mobile)
153 | end
154 |
155 | def test_respond_with_shouldnt_modify_original_hash
156 | get :respond_with_additional_params
157 | assert_equal RespondWithController.params, assigns(:params)
158 | end
159 |
160 | def test_using_resource
161 | @request.accept = "application/xml"
162 | get :using_resource
163 | assert_equal "application/xml", @response.media_type
164 | assert_equal "david", @response.body
165 |
166 | @request.accept = "application/json"
167 | get :using_resource
168 | assert_equal "application/json", @response.media_type
169 | assert_equal "{\"name\":\"david\",\"id\":13}", @response.body
170 | end
171 |
172 | def test_using_resource_with_js_simply_tries_to_render_the_template
173 | @request.accept = "text/javascript"
174 | get :using_resource
175 | assert_equal "text/javascript", @response.media_type
176 | assert_equal "alert(\"Hi\");", @response.body
177 | assert_equal 200, @response.status
178 | end
179 |
180 | def test_using_resource_for_post_with_js_renders_the_template_on_failure
181 | @request.accept = "text/javascript"
182 | errors = { name: :invalid }
183 | Customer.any_instance.stubs(:errors).returns(errors)
184 | post :using_resource
185 | assert_equal "text/javascript", @response.media_type
186 | assert_equal "alert(\"Hi\");", @response.body
187 | assert_equal 200, @response.status
188 | end
189 |
190 | def test_using_resource_for_post_with_js_renders_the_template_and_yields_configured_error_status_on_failure
191 | @request.accept = "text/javascript"
192 | errors = { name: :invalid }
193 | Customer.any_instance.stubs(:errors).returns(errors)
194 | with_error_status(:unprocessable_entity) do
195 | post :using_resource
196 | end
197 | assert_equal "text/javascript", @response.media_type
198 | assert_equal "alert(\"Hi\");", @response.body
199 | assert_equal 422, @response.status
200 | end
201 |
202 | def test_using_resource_for_post_with_js_renders_the_template_and_yields_given_status_on_failure
203 | @request.accept = "text/javascript"
204 | errors = { name: :invalid }
205 | Customer.any_instance.stubs(:errors).returns(errors)
206 | post :using_resource_with_status
207 | assert_equal "text/javascript", @response.media_type
208 | assert_equal "alert(\"Hi\");", @response.body
209 | assert_equal 418, @response.status
210 | end
211 |
212 | def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found
213 | @request.accept = "text/javascript"
214 | assert_raise ActionView::MissingTemplate do
215 | get :using_hash_resource
216 | end
217 | end
218 |
219 | def test_using_hash_resource
220 | @request.accept = "application/xml"
221 | get :using_hash_resource
222 | assert_equal "application/xml", @response.media_type
223 | assert_equal "\n\n david\n\n", @response.body
224 |
225 | @request.accept = "application/json"
226 | get :using_hash_resource
227 | assert_equal "application/json", @response.media_type
228 | assert_includes @response.body, "result"
229 | assert_includes @response.body, '"name":"david"'
230 | assert_includes @response.body, '"id":13'
231 | end
232 |
233 | def test_using_hash_resource_with_post
234 | @request.accept = "application/json"
235 | assert_raise ArgumentError, "Nil location provided. Can't build URI." do
236 | post :using_hash_resource
237 | end
238 | end
239 |
240 | def test_using_resource_with_block
241 | @request.accept = "*/*"
242 | get :using_resource_with_block
243 | assert_equal "text/html", @response.media_type
244 | assert_equal "Hello world!", @response.body
245 |
246 | @request.accept = "text/csv"
247 | get :using_resource_with_block
248 | assert_equal "text/csv", @response.media_type
249 | assert_equal "CSV", @response.body
250 |
251 | @request.accept = "application/xml"
252 | get :using_resource
253 | assert_equal "application/xml", @response.media_type
254 | assert_equal "david", @response.body
255 | end
256 |
257 | def test_using_resource_with_overwrite_block
258 | get :using_resource_with_overwrite_block
259 | assert_equal "text/html", @response.media_type
260 | assert_equal "HTML", @response.body
261 | end
262 |
263 | def test_not_acceptable
264 | @request.accept = "application/xml"
265 | assert_raises(ActionController::UnknownFormat) do
266 | get :using_resource_with_block
267 | end
268 |
269 | @request.accept = "text/javascript"
270 | assert_raises(ActionController::UnknownFormat) do
271 | get :using_resource_with_overwrite_block
272 | end
273 | end
274 |
275 | def test_using_resource_for_post_with_html_redirects_on_success
276 | with_test_route_set do
277 | post :using_resource
278 | assert_equal "text/html", @response.media_type
279 | assert_equal 302, @response.status
280 | assert_equal "http://www.example.com/customers/13", @response.location
281 | assert @response.redirect?
282 | end
283 | end
284 |
285 | def test_using_resource_for_post_with_html_rerender_on_failure
286 | with_test_route_set do
287 | errors = { name: :invalid }
288 | Customer.any_instance.stubs(:errors).returns(errors)
289 | post :using_resource
290 | assert_equal "text/html", @response.media_type
291 | assert_equal 200, @response.status
292 | assert_equal "New world!\n", @response.body
293 | assert_nil @response.location
294 | end
295 | end
296 |
297 | def test_using_resource_for_post_with_html_rerender_and_yields_configured_error_status_on_failure
298 | with_test_route_set do
299 | errors = { name: :invalid }
300 | Customer.any_instance.stubs(:errors).returns(errors)
301 | with_error_status(:unprocessable_entity) do
302 | post :using_resource
303 | end
304 | assert_equal "text/html", @response.media_type
305 | assert_equal 422, @response.status
306 | assert_equal "New world!\n", @response.body
307 | assert_nil @response.location
308 | end
309 | end
310 |
311 | def test_using_resource_for_post_with_xml_yields_created_on_success
312 | with_test_route_set do
313 | @request.accept = "application/xml"
314 | post :using_resource
315 | assert_equal "application/xml", @response.media_type
316 | assert_equal 201, @response.status
317 | assert_equal "david", @response.body
318 | assert_equal "http://www.example.com/customers/13", @response.location
319 | end
320 | end
321 |
322 | def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure
323 | with_test_route_set do
324 | @request.accept = "application/xml"
325 | errors = { name: :invalid }
326 | Customer.any_instance.stubs(:errors).returns(errors)
327 | post :using_resource
328 | assert_equal "application/xml", @response.media_type
329 | assert_equal 422, @response.status
330 | assert_equal errors.to_xml, @response.body
331 | assert_nil @response.location
332 | end
333 | end
334 |
335 | def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure
336 | with_test_route_set do
337 | @request.accept = "application/json"
338 | errors = { name: :invalid }
339 | Customer.any_instance.stubs(:errors).returns(errors)
340 | post :using_resource
341 | assert_equal "application/json", @response.media_type
342 | assert_equal 422, @response.status
343 | errors = { errors: errors }
344 | assert_equal errors.to_json, @response.body
345 | assert_nil @response.location
346 | end
347 | end
348 |
349 | def test_using_resource_for_patch_with_html_redirects_on_success
350 | with_test_route_set do
351 | patch :using_resource
352 | assert_equal "text/html", @response.media_type
353 | assert_equal 302, @response.status
354 | assert_equal "http://www.example.com/customers/13", @response.location
355 | assert @response.redirect?
356 | end
357 | end
358 |
359 | def test_using_resource_for_patch_with_html_rerender_on_failure
360 | with_test_route_set do
361 | errors = { name: :invalid }
362 | Customer.any_instance.stubs(:errors).returns(errors)
363 | patch :using_resource
364 | assert_equal "text/html", @response.media_type
365 | assert_equal 200, @response.status
366 | assert_equal "Edit world!\n", @response.body
367 | assert_nil @response.location
368 | end
369 | end
370 |
371 | def test_using_resource_for_patch_with_html_rerender_and_yields_configured_error_status_on_failure
372 | with_test_route_set do
373 | errors = { name: :invalid }
374 | Customer.any_instance.stubs(:errors).returns(errors)
375 | with_error_status(:unprocessable_entity) do
376 | patch :using_resource
377 | end
378 | assert_equal "text/html", @response.media_type
379 | assert_equal 422, @response.status
380 | assert_equal "Edit world!\n", @response.body
381 | assert_nil @response.location
382 | end
383 | end
384 |
385 | def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override
386 | with_test_route_set do
387 | errors = { name: :invalid }
388 | Customer.any_instance.stubs(:errors).returns(errors)
389 | @request.env["rack.methodoverride.original_method"] = "POST"
390 | patch :using_resource
391 | assert_equal "text/html", @response.media_type
392 | assert_equal 200, @response.status
393 | assert_equal "Edit world!\n", @response.body
394 | assert_nil @response.location
395 | end
396 | end
397 |
398 | def test_using_resource_for_patch_with_html_rerender_and_yields_configured_error_status_on_failure_even_on_method_override
399 | with_test_route_set do
400 | errors = { name: :invalid }
401 | Customer.any_instance.stubs(:errors).returns(errors)
402 | @request.env["rack.methodoverride.original_method"] = "POST"
403 | with_error_status(:unprocessable_entity) do
404 | patch :using_resource
405 | end
406 | assert_equal "text/html", @response.media_type
407 | assert_equal 422, @response.status
408 | assert_equal "Edit world!\n", @response.body
409 | assert_nil @response.location
410 | end
411 | end
412 |
413 | def test_using_resource_for_put_with_html_redirects_on_success
414 | with_test_route_set do
415 | put :using_resource
416 | assert_equal "text/html", @response.media_type
417 | assert_equal 302, @response.status
418 | assert_equal "http://www.example.com/customers/13", @response.location
419 | assert @response.redirect?
420 | end
421 | end
422 |
423 | def test_using_resource_for_put_with_html_rerender_on_failure
424 | with_test_route_set do
425 | errors = { name: :invalid }
426 | Customer.any_instance.stubs(:errors).returns(errors)
427 | put :using_resource
428 | assert_equal "text/html", @response.media_type
429 | assert_equal 200, @response.status
430 | assert_equal "Edit world!\n", @response.body
431 | assert_nil @response.location
432 | end
433 | end
434 |
435 | def test_using_resource_for_put_with_html_rerender_and_yields_configured_error_status_on_failure
436 | with_test_route_set do
437 | errors = { name: :invalid }
438 | Customer.any_instance.stubs(:errors).returns(errors)
439 | with_error_status(:unprocessable_entity) do
440 | put :using_resource
441 | end
442 | assert_equal "text/html", @response.media_type
443 | assert_equal 422, @response.status
444 | assert_equal "Edit world!\n", @response.body
445 | assert_nil @response.location
446 | end
447 | end
448 |
449 | def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override
450 | with_test_route_set do
451 | errors = { name: :invalid }
452 | Customer.any_instance.stubs(:errors).returns(errors)
453 | @request.env["rack.methodoverride.original_method"] = "POST"
454 | put :using_resource
455 | assert_equal "text/html", @response.media_type
456 | assert_equal 200, @response.status
457 | assert_equal "Edit world!\n", @response.body
458 | assert_nil @response.location
459 | end
460 | end
461 |
462 | def test_using_resource_for_put_with_html_rerender_and_yields_configured_error_status_on_failure_even_on_method_override
463 | with_test_route_set do
464 | errors = { name: :invalid }
465 | Customer.any_instance.stubs(:errors).returns(errors)
466 | @request.env["rack.methodoverride.original_method"] = "POST"
467 | with_error_status(:unprocessable_entity) do
468 | put :using_resource
469 | end
470 | assert_equal "text/html", @response.media_type
471 | assert_equal 422, @response.status
472 | assert_equal "Edit world!\n", @response.body
473 | assert_nil @response.location
474 | end
475 | end
476 |
477 | def test_using_resource_for_put_with_xml_yields_no_content_on_success
478 | @request.accept = "application/xml"
479 | put :using_resource
480 | assert_equal 204, @response.status
481 | assert_equal "", @response.body
482 | end
483 |
484 | def test_using_resource_for_put_with_json_yields_no_content_on_success
485 | @request.accept = "application/json"
486 | put :using_resource_with_json
487 | assert_equal 204, @response.status
488 | assert_equal "", @response.body
489 | end
490 |
491 | def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure
492 | @request.accept = "application/xml"
493 | errors = { name: :invalid }
494 | Customer.any_instance.stubs(:errors).returns(errors)
495 | put :using_resource
496 | assert_equal "application/xml", @response.media_type
497 | assert_equal 422, @response.status
498 | assert_equal errors.to_xml, @response.body
499 | assert_nil @response.location
500 | end
501 |
502 | def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure
503 | @request.accept = "application/json"
504 | errors = { name: :invalid }
505 | Customer.any_instance.stubs(:errors).returns(errors)
506 | put :using_resource
507 | assert_equal "application/json", @response.media_type
508 | assert_equal 422, @response.status
509 | errors = { errors: errors }
510 | assert_equal errors.to_json, @response.body
511 | assert_nil @response.location
512 | end
513 |
514 | def test_using_resource_for_delete_with_html_redirects_on_success
515 | with_test_route_set do
516 | Customer.any_instance.stubs(:destroyed?).returns(true)
517 | delete :using_resource
518 | assert_equal "text/html", @response.media_type
519 | assert_equal 302, @response.status
520 | assert_equal "http://www.example.com/customers", @response.location
521 | end
522 | end
523 |
524 | def test_using_resource_for_delete_with_xml_yields_no_content_on_success
525 | Customer.any_instance.stubs(:destroyed?).returns(true)
526 | @request.accept = "application/xml"
527 | delete :using_resource
528 | assert_equal 204, @response.status
529 | assert_equal "", @response.body
530 | end
531 |
532 | def test_using_resource_for_delete_with_json_yields_no_content_on_success
533 | Customer.any_instance.stubs(:destroyed?).returns(true)
534 | @request.accept = "application/json"
535 | delete :using_resource_with_json
536 | assert_equal 204, @response.status
537 | assert_equal "", @response.body
538 | end
539 |
540 | def test_using_resource_for_delete_with_html_redirects_on_failure
541 | with_test_route_set do
542 | errors = { name: :invalid }
543 | Customer.any_instance.stubs(:errors).returns(errors)
544 | Customer.any_instance.stubs(:destroyed?).returns(false)
545 | delete :using_resource
546 | assert_equal "text/html", @response.media_type
547 | assert_equal 302, @response.status
548 | assert_equal "http://www.example.com/customers", @response.location
549 | end
550 | end
551 |
552 | def test_using_resource_with_parent_for_get
553 | @request.accept = "application/xml"
554 | get :using_resource_with_parent
555 | assert_equal "application/xml", @response.media_type
556 | assert_equal 200, @response.status
557 | assert_equal "david", @response.body
558 | end
559 |
560 | def test_using_resource_with_parent_for_post
561 | with_test_route_set do
562 | @request.accept = "application/xml"
563 |
564 | post :using_resource_with_parent
565 | assert_equal "application/xml", @response.media_type
566 | assert_equal 201, @response.status
567 | assert_equal "david", @response.body
568 | assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location
569 |
570 | errors = { name: :invalid }
571 | Customer.any_instance.stubs(:errors).returns(errors)
572 | post :using_resource
573 | assert_equal "application/xml", @response.media_type
574 | assert_equal 422, @response.status
575 | assert_equal errors.to_xml, @response.body
576 | assert_nil @response.location
577 | end
578 | end
579 |
580 | def test_using_resource_with_collection
581 | @request.accept = "application/xml"
582 | get :using_resource_with_collection
583 | assert_equal "application/xml", @response.media_type
584 | assert_equal 200, @response.status
585 | assert_match(/david<\/name>/, @response.body)
586 | assert_match(/jamis<\/name>/, @response.body)
587 | end
588 |
589 | def test_using_resource_with_action
590 | @controller.instance_eval do
591 | def render(params = {})
592 | self.response_body = "#{params[:action]} - #{formats}"
593 | end
594 | end
595 |
596 | errors = { name: :invalid }
597 | Customer.any_instance.stubs(:errors).returns(errors)
598 |
599 | post :using_resource_with_action
600 | assert_equal "foo - #{[:html]}", @controller.response.body
601 | end
602 |
603 | def test_using_resource_with_rendering_options
604 | Customer.any_instance.stubs(:errors).returns(name: :invalid)
605 |
606 | post :using_resource_with_rendering_options
607 |
608 | assert_response :unprocessable_entity
609 | assert_equal "edit.html.erb", @controller.response.body
610 | end
611 |
612 | def test_respond_as_responder_entry_point
613 | @request.accept = "text/html"
614 | get :using_responder_with_respond
615 | assert_equal "respond html", @response.body
616 |
617 | @request.accept = "application/xml"
618 | get :using_responder_with_respond
619 | assert_equal "respond xml", @response.body
620 | end
621 |
622 | def test_clear_respond_to
623 | @controller = InheritedRespondWithController.new
624 | @request.accept = "text/html"
625 | assert_raises(ActionController::UnknownFormat) do
626 | get :index
627 | end
628 | end
629 |
630 | def test_first_in_respond_to_has_higher_priority
631 | @controller = InheritedRespondWithController.new
632 | @request.accept = "*/*"
633 | get :index
634 | assert_equal "application/xml", @response.media_type
635 | assert_equal "david", @response.body
636 | end
637 |
638 | def test_block_inside_respond_with_is_rendered
639 | @controller = InheritedRespondWithController.new
640 | @request.accept = "application/json"
641 | get :index
642 | assert_equal "JSON", @response.body
643 | end
644 |
645 | def test_no_double_render_is_raised
646 | @request.accept = "text/html"
647 | assert_raise ActionView::MissingTemplate do
648 | get :using_resource
649 | end
650 | end
651 |
652 | def test_using_resource_with_status_and_location
653 | @request.accept = "text/html"
654 | post :using_resource_with_status_and_location
655 | assert @response.redirect?
656 | assert_equal "http://test.host/", @response.location
657 |
658 | @request.accept = "application/xml"
659 | get :using_resource_with_status_and_location
660 | assert_equal 201, @response.status
661 | end
662 |
663 | def test_using_resource_with_status_and_location_with_invalid_resource
664 | errors = { name: :invalid }
665 | Customer.any_instance.stubs(:errors).returns(errors)
666 |
667 | @request.accept = "text/xml"
668 |
669 | post :using_resource_with_status_and_location
670 | assert_equal errors.to_xml, @response.body
671 | assert_equal 422, @response.status
672 | assert_nil @response.location
673 |
674 | put :using_resource_with_status_and_location
675 | assert_equal errors.to_xml, @response.body
676 | assert_equal 422, @response.status
677 | assert_nil @response.location
678 | end
679 |
680 | def test_using_invalid_resource_with_template
681 | errors = { name: :invalid }
682 | Customer.any_instance.stubs(:errors).returns(errors)
683 |
684 | @request.accept = "text/xml"
685 |
686 | post :using_invalid_resource_with_template
687 | assert_equal errors.to_xml, @response.body
688 | assert_equal 422, @response.status
689 | assert_nil @response.location
690 |
691 | put :using_invalid_resource_with_template
692 | assert_equal errors.to_xml, @response.body
693 | assert_equal 422, @response.status
694 | assert_nil @response.location
695 | end
696 |
697 | def test_using_options_with_template
698 | @request.accept = "text/xml"
699 |
700 | post :using_options_with_template
701 | assert_equal "david", @response.body
702 | assert_equal 123, @response.status
703 | assert_equal "http://test.host/", @response.location
704 |
705 | put :using_options_with_template
706 | assert_equal "david", @response.body
707 | assert_equal 123, @response.status
708 | assert_equal "http://test.host/", @response.location
709 | end
710 |
711 | def test_using_resource_with_responder
712 | get :using_resource_with_responder
713 | assert_equal "Resource name is david", @response.body
714 | end
715 |
716 | def test_using_resource_with_set_responder
717 | RespondWithController.responder = proc { |c, r, o| c.render body: "Resource name is #{r.first.name}" }
718 | get :using_resource
719 | assert_equal "Resource name is david", @response.body
720 | ensure
721 | RespondWithController.responder = ActionController::Responder
722 | end
723 |
724 | def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer
725 | @controller = CsvRespondWithController.new
726 | assert_raise ActionController::MissingRenderer do
727 | get :index, format: "csv"
728 | end
729 | end
730 |
731 | def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
732 | @controller = EmptyRespondWithController.new
733 | @request.accept = "*/*"
734 | assert_raise RuntimeError do
735 | get :index
736 | end
737 | end
738 |
739 | def test_redirect_status_configured_as_see_other
740 | with_test_route_set do
741 | with_redirect_status(:see_other) do
742 | post :using_resource
743 | assert_equal 303, @response.status
744 | end
745 | end
746 | end
747 |
748 | def test_redirect_status_configured_as_moved_permanently
749 | with_test_route_set do
750 | with_redirect_status(:moved_permanently) do
751 | patch :using_resource
752 | assert_equal 301, @response.status
753 | end
754 | end
755 | end
756 |
757 | private
758 |
759 | def with_test_route_set
760 | with_routing do |set|
761 | set.draw do
762 | resources :customers
763 | resources :quiz_stores do
764 | resources :customers
765 | end
766 | (ActionDispatch.try(:deprecator) || ActiveSupport::Deprecation).silence do
767 | get ":controller/:action"
768 | end
769 | end
770 | yield
771 | end
772 | end
773 |
774 | def with_error_status(status)
775 | old_status = ActionController::Responder.error_status
776 | ActionController::Responder.error_status = status
777 | yield
778 | ensure
779 | ActionController::Responder.error_status = old_status
780 | end
781 |
782 | def with_redirect_status(status)
783 | old_status = ActionController::Responder.redirect_status
784 | ActionController::Responder.redirect_status = status
785 | yield
786 | ensure
787 | ActionController::Responder.redirect_status = old_status
788 | end
789 | end
790 |
791 | class LocationsController < ApplicationController
792 | respond_to :html
793 | before_action :set_resource
794 |
795 | def create
796 | respond_with @resource, location: -> { "given_location" }
797 | end
798 |
799 | def update
800 | respond_with @resource, location: "given_location"
801 | end
802 |
803 | def set_resource
804 | @resource = Address.new
805 | @resource.errors.add(:fail, "FAIL") if params[:fail]
806 | end
807 | end
808 |
809 | class LocationResponderTest < ActionController::TestCase
810 | tests LocationsController
811 |
812 | def test_redirects_to_block_location_on_success
813 | post :create
814 | assert_redirected_to "given_location"
815 | end
816 |
817 | def test_renders_page_on_fail
818 | post :create, params: { fail: true }
819 | assert_includes @response.body, "new.html.erb"
820 | end
821 |
822 | def test_redirects_to_plain_string
823 | post :update
824 | assert_redirected_to "given_location"
825 | end
826 | end
827 |
--------------------------------------------------------------------------------
/test/action_controller/verify_requested_format_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class ThingsController < ApplicationController
6 | clear_respond_to
7 |
8 | respond_to :js
9 | respond_to :html, only: [:show, :new]
10 |
11 | before_action :verify_requested_format!
12 |
13 | attr_reader :called
14 |
15 | def action
16 | @called = true
17 | render inline: action_name
18 | end
19 |
20 | alias :index :action
21 | alias :show :action
22 | alias :new :action
23 | end
24 |
25 | class VerifyRequestedFormatTest < ActionController::TestCase
26 | tests ThingsController
27 |
28 | def test_strict_mode_shouldnt_call_action
29 | assert_raises(ActionController::UnknownFormat) do
30 | get :index
31 | end
32 |
33 | refute @controller.called, "action should not be executed."
34 | end
35 |
36 | def test_strict_mode_calls_action_with_right_format
37 | get :index, format: :js
38 |
39 | assert @controller.called, "action should be executed."
40 | end
41 |
42 | def test_strict_mode_respects_only_option
43 | get :show, format: :html
44 |
45 | assert @controller.called, "action should be executed."
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | flash:
3 | actions:
4 | create:
5 | success: "Resource created with success"
6 | failure: "Resource could not be created"
7 | with_block:
8 | success: "Resource with block created with success"
9 | with_html:
10 | failure_html: "OH NOES! You did it wrong!"
11 | xss_html: "Yay! %{xss}"
12 | addresses:
13 | update:
14 | success: "Nice! %{resource_name} was updated with success!"
15 | failure: "Oh no! We could not update your address!"
16 | destroy:
17 | success: "Successfully deleted the address at %{reference}"
18 | notice: "Successfully deleted the chosen address at %{reference}"
19 | with_html:
20 | success_html: "Yay! You did it!"
21 | admin:
22 | actions:
23 | create:
24 | notice: "Admin created address with success"
25 | addresses:
26 | update:
27 | notice: "Admin updated address with success"
28 |
29 |
--------------------------------------------------------------------------------
/test/responders/collection_responder_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class CollectionResponder < ActionController::Responder
6 | include Responders::CollectionResponder
7 | end
8 |
9 | class CollectionController < ApplicationController
10 | self.responder = CollectionResponder
11 |
12 | def single
13 | respond_with Address.new
14 | end
15 |
16 | def namespaced
17 | respond_with :admin, Address.new
18 | end
19 |
20 | def nested
21 | respond_with User.new, Address.new
22 | end
23 |
24 | def only_symbols
25 | respond_with :admin, :addresses
26 | end
27 |
28 | def with_location
29 | respond_with Address.new, location: "given_location"
30 | end
31 |
32 | def isolated_namespace
33 | respond_with MyEngine::Business
34 | end
35 |
36 | def uncountable
37 | respond_with News.new
38 | end
39 | end
40 |
41 | class CollectionResponderTest < ActionController::TestCase
42 | tests CollectionController
43 |
44 | def test_collection_with_single_resource
45 | @controller.expects(:addresses_url).returns("addresses_url")
46 | post :single
47 | assert_redirected_to "addresses_url"
48 | end
49 |
50 | def test_collection_with_namespaced_resource
51 | @controller.expects(:admin_addresses_url).returns("admin_addresses_url")
52 | put :namespaced
53 | assert_redirected_to "admin_addresses_url"
54 | end
55 |
56 | def test_collection_with_nested_resource
57 | @controller.expects(:user_addresses_url).returns("user_addresses_url")
58 | delete :nested
59 | assert_redirected_to "user_addresses_url"
60 | end
61 |
62 | def test_collection_respects_location_option
63 | delete :with_location
64 | assert_redirected_to "given_location"
65 | end
66 |
67 | def test_collection_respects_only_symbols
68 | @controller.expects(:admin_addresses_url).returns("admin_addresses_url")
69 | post :only_symbols
70 | assert_redirected_to "admin_addresses_url"
71 | end
72 |
73 | def test_collection_respects_isolated_namespace
74 | @controller.expects(:businesses_url).returns("businesses_url")
75 | post :isolated_namespace
76 | assert_redirected_to "businesses_url"
77 | end
78 |
79 | def test_collection_respects_uncountable_resource
80 | @controller.expects(:news_index_url).returns("news_index_url")
81 | post :uncountable
82 | assert_redirected_to "news_index_url"
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/responders/controller_method_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | ActionController::Base.extend Responders::ControllerMethod
6 |
7 | module FooResponder
8 | def to_html
9 | @resource << "foo"
10 | super
11 | end
12 | end
13 |
14 | module BarResponder
15 | def to_html
16 | @resource << "bar"
17 | super
18 | end
19 | end
20 |
21 | module PeopleResponder
22 | def to_html
23 | @resource << "baz"
24 | super
25 | end
26 | end
27 |
28 | class PeopleController < ApplicationController
29 | responders :foo, BarResponder
30 |
31 | def index
32 | @array = []
33 | respond_with(@array) do |format|
34 | format.html { render body: "Success!" }
35 | end
36 | end
37 | end
38 |
39 | class SpecialPeopleController < PeopleController
40 | responders :people
41 | end
42 |
43 | class ControllerMethodTest < ActionController::TestCase
44 | tests PeopleController
45 |
46 | def setup
47 | @controller.stubs(:polymorphic_url).returns("/")
48 | end
49 |
50 | def test_foo_responder_gets_added
51 | get :index
52 | assert assigns(:array).include? "foo"
53 | end
54 |
55 | def test_bar_responder_gets_added
56 | get :index
57 | assert assigns(:array).include? "bar"
58 | end
59 | end
60 |
61 | class ControllerMethodInheritanceTest < ActionController::TestCase
62 | tests SpecialPeopleController
63 |
64 | def setup
65 | @controller.stubs(:polymorphic_url).returns("/")
66 | end
67 |
68 | def test_responder_is_inherited
69 | get :index
70 | assert assigns(:array).include? "foo"
71 | assert assigns(:array).include? "bar"
72 | assert assigns(:array).include? "baz"
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/test/responders/flash_responder_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class FlashResponder < ActionController::Responder
6 | include Responders::FlashResponder
7 | end
8 |
9 | class AddressesController < ApplicationController
10 | before_action :set_resource
11 | self.responder = FlashResponder
12 |
13 | respond_to :js, only: :create
14 |
15 | FLASH_PARAM_VALUES = {
16 | "true" => true,
17 | "false" => false
18 | }
19 |
20 | FLASH_NOW_PARAM_VALUES = {
21 | "true" => true,
22 | "false" => false,
23 | "on_success" => :on_success,
24 | "on_failure" => :on_failure
25 | }
26 |
27 | def action
28 | set_flash = FLASH_PARAM_VALUES[params[:flash].to_s]
29 | set_flash_now = FLASH_NOW_PARAM_VALUES[params[:flash_now].to_s]
30 |
31 | flash[:success] = "Flash is set" if params[:set_flash]
32 | respond_with(@resource, flash: set_flash, flash_now: set_flash_now)
33 | end
34 |
35 | alias :new :action
36 | alias :create :action
37 | alias :update :action
38 | alias :destroy :action
39 |
40 | def with_block
41 | respond_with(@resource) do |format|
42 | format.html { render html: "Success!" }
43 | end
44 | end
45 |
46 | def another
47 | respond_with(@resource, notice: "Yes, notice this!", alert: "Warning, warning!")
48 | end
49 |
50 | def with_html
51 | respond_with(@resource)
52 | end
53 |
54 | def flexible
55 | options = params[:responder_options] || {}
56 | flash_now, alert = options.values_at(:flash_now, :alert)
57 |
58 | respond_with(@resource, flash_now: flash_now, alert: alert)
59 | end
60 |
61 | protected
62 |
63 | def flash_interpolation_options
64 | { reference: "Ocean Avenue", xss: "" }
65 | end
66 |
67 | def set_resource
68 | @resource = Address.new
69 | @resource.errors.add(:fail, "FAIL") if params[:fail]
70 | end
71 | end
72 |
73 | class PolymorphicAddesssController < AddressesController
74 | def create
75 | respond_with(User.new, Address.new)
76 | end
77 | end
78 |
79 | module Admin
80 | class AddressesController < ::AddressesController
81 | end
82 | end
83 |
84 | class FlashResponderTest < ActionController::TestCase
85 | tests AddressesController
86 |
87 | def setup
88 | Responders::FlashResponder.flash_keys = [ :success, :failure ]
89 | @controller.stubs(:polymorphic_url).returns("/")
90 | end
91 |
92 | def test_sets_success_flash_message_on_non_get_requests
93 | post :create
94 | assert_equal "Resource created with success", flash[:success]
95 | end
96 |
97 | def test_sets_failure_flash_message_on_not_get_requests
98 | post :create, params: { fail: true }
99 | assert_equal "Resource could not be created", flash[:failure]
100 | end
101 |
102 | def test_does_not_set_flash_message_on_get_requests
103 | get :new
104 | assert flash.empty?
105 | end
106 |
107 | def test_sets_flash_message_for_the_current_controller
108 | put :update, params: { fail: true }
109 | assert_equal "Oh no! We could not update your address!", flash[:failure]
110 | end
111 |
112 | def test_sets_flash_message_with_resource_name
113 | put :update
114 | assert_equal "Nice! Address was updated with success!", flash[:success]
115 | end
116 |
117 | def test_sets_flash_message_with_interpolation_options
118 | delete :destroy
119 | assert_equal "Successfully deleted the address at Ocean Avenue", flash[:success]
120 | end
121 |
122 | def test_does_not_set_flash_if_flash_false_is_given
123 | post :create, params: { flash: false }
124 | assert flash.empty?
125 | end
126 |
127 | def test_does_not_overwrite_the_flash_if_already_set
128 | post :create, params: { set_flash: true }
129 | assert_equal "Flash is set", flash[:success]
130 | end
131 |
132 | def test_sets_flash_message_even_if_block_is_given
133 | post :with_block
134 | assert_equal "Resource with block created with success", flash[:success]
135 | end
136 |
137 | def test_sets_now_flash_message_on_javascript_requests
138 | post :create, format: :js
139 | assert_equal "Resource created with success", flash[:success]
140 | assert_flash_now :success
141 | end
142 |
143 | def test_sets_flash_message_can_be_set_to_now
144 | post :create, params: { flash_now: true }
145 | assert_equal "Resource created with success", @controller.flash.now[:success]
146 | assert_flash_now :success
147 | end
148 |
149 | def test_sets_flash_message_can_be_set_to_now_only_on_success
150 | post :create, params: { flash_now: :on_success }
151 | assert_equal "Resource created with success", @controller.flash.now[:success]
152 | end
153 |
154 | def test_sets_flash_message_can_be_set_to_now_only_on_failure
155 | post :create, params: { flash_now: :on_failure }
156 | assert_not_flash_now :success
157 | end
158 |
159 | def test_sets_message_based_on_notice_key_with_custom_keys
160 | post :another
161 | assert_equal "Yes, notice this!", flash[:success]
162 | end
163 |
164 | def test_sets_message_based_on_alert_key_with_custom_keys
165 | post :another, params: { fail: true }
166 | assert_equal "Warning, warning!", flash[:failure]
167 | end
168 |
169 | def test_sets_message_based_on_notice_key
170 | Responders::FlashResponder.flash_keys = [ :notice, :alert ]
171 | post :another
172 | assert_equal "Yes, notice this!", flash[:notice]
173 | end
174 |
175 | def test_sets_message_based_on_alert_key
176 | Responders::FlashResponder.flash_keys = [ :notice, :alert ]
177 | post :another, params: { fail: true }
178 | assert_equal "Warning, warning!", flash[:alert]
179 | end
180 |
181 | def test_sets_html_using_controller_scope
182 | post :with_html
183 | assert_equal "Yay! You did it!", flash[:success]
184 | end
185 |
186 | def test_sets_html_using_actions_scope
187 | post :with_html, params: { fail: true }
188 | assert_equal "OH NOES! You did it wrong!", flash[:failure]
189 | end
190 |
191 | def test_escapes_html_interpolations
192 | Responders::FlashResponder.flash_keys = [ :xss, :xss ]
193 | post :with_html
194 | assert_equal "Yay! <script>alert(1)</script>", flash[:xss]
195 | end
196 |
197 | def test_sets_flash_now_on_failure_by_default
198 | post :another, params: { fail: true }
199 | assert_flash_now :failure
200 | end
201 |
202 | def test_does_not_set_flash_message_to_now_with_errors_and_redirect
203 | delete :with_html, params: { fail: true }
204 | assert_not_flash_now :failure
205 | assert_equal "OH NOES! You did it wrong!", flash[:failure]
206 | end
207 |
208 | def test_never_set_flash_now
209 | post :flexible, params: { fail: true, responder_options: { flash_now: false, alert: "Warning" } }
210 | assert_not_flash_now :failure
211 | end
212 |
213 | def assert_flash_now(k)
214 | assert_includes flash.used_keys, k.to_s, "Expected #{k} to be in flash.now, but it's not."
215 | end
216 |
217 | def assert_not_flash_now(k)
218 | assert flash[k], "Expected #{k} to be set"
219 | assert_not_includes flash.used_keys, k, "Expected #{k} to not be in flash.now, but it is."
220 | end
221 | end
222 |
223 | class NamespacedFlashResponderTest < ActionController::TestCase
224 | tests Admin::AddressesController
225 |
226 | def setup
227 | Responders::FlashResponder.flash_keys = [ :notice, :alert ]
228 | @controller.stubs(:polymorphic_url).returns("/")
229 | end
230 |
231 | def test_sets_the_flash_message_based_on_the_current_controller
232 | put :update
233 | assert_equal "Admin updated address with success", flash[:notice]
234 | end
235 |
236 | def test_sets_the_flash_message_based_on_namespace_actions
237 | Responders::FlashResponder.namespace_lookup = true
238 | post :create
239 | assert_equal "Admin created address with success", flash[:notice]
240 | ensure
241 | Responders::FlashResponder.namespace_lookup = false
242 | end
243 |
244 | def test_fallbacks_to_non_namespaced_controller_flash_message
245 | Responders::FlashResponder.namespace_lookup = true
246 | delete :destroy
247 | assert_equal "Successfully deleted the chosen address at Ocean Avenue", flash[:notice]
248 | ensure
249 | Responders::FlashResponder.namespace_lookup = false
250 | end
251 |
252 | def test_does_not_fallbacks_to_namespaced_actions_if_disabled
253 | post :create
254 | assert_equal "Address was successfully created.", flash[:notice]
255 | end
256 |
257 | def test_does_not_fallbacks_to_non_namespaced_controller_flash_message_if_disabled
258 | post :new
259 | assert_nil flash[:notice]
260 | end
261 | end
262 |
263 | class PolymorhicFlashResponderTest < ActionController::TestCase
264 | tests PolymorphicAddesssController
265 |
266 | def setup
267 | Responders::FlashResponder.flash_keys = [ :notice, :alert ]
268 | @controller.stubs(:polymorphic_url).returns("/")
269 | end
270 |
271 | def test_polymorhic_respond_with
272 | post :create
273 | assert_equal "Address was successfully created.", flash[:notice]
274 | end
275 | end
276 |
--------------------------------------------------------------------------------
/test/responders/http_cache_responder_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class HttpCacheResponder < ActionController::Responder
6 | include Responders::HttpCacheResponder
7 | end
8 |
9 | class HttpCacheController < ApplicationController
10 | self.responder = HttpCacheResponder
11 |
12 | HTTP_CACHE_PARAM_VALUES = { "false" => false }
13 |
14 | def single
15 | http_cache = HTTP_CACHE_PARAM_VALUES[params[:http_cache].to_s]
16 |
17 | response.last_modified = Time.utc(2008) if params[:last_modified]
18 | respond_with(Address.new(Time.utc(2009)), http_cache: http_cache)
19 | end
20 |
21 | def nested
22 | respond_with Address.new(Time.utc(2009)), Address.new(Time.utc(2008))
23 | end
24 |
25 | def collection
26 | respond_with [Address.new(Time.utc(2009)), Address.new(Time.utc(2008))]
27 | end
28 |
29 | def not_persisted
30 | model = Address.new(Time.utc(2009))
31 | model.persisted = false
32 | respond_with(model)
33 | end
34 |
35 | def empty
36 | respond_with []
37 | end
38 | end
39 |
40 | class HttpCacheResponderTest < ActionController::TestCase
41 | tests HttpCacheController
42 |
43 | def setup
44 | @request.accept = "application/xml"
45 | @controller.stubs(:polymorphic_url).returns("/")
46 | end
47 |
48 | def test_last_modified_at_is_set_with_single_resource_on_get
49 | get :single
50 | assert_equal Time.utc(2009).httpdate, @response.headers["Last-Modified"]
51 | assert_equal "", @response.body
52 | assert_equal 200, @response.status
53 | end
54 |
55 | def test_returns_not_modified_if_return_is_cache_is_still_valid
56 | @request.env["HTTP_IF_MODIFIED_SINCE"] = Time.utc(2009, 6).httpdate
57 | get :single
58 | assert_equal 304, @response.status
59 | assert_includes " ", @response.body
60 | end
61 |
62 | def test_refreshes_last_modified_if_cache_is_expired
63 | @request.env["HTTP_IF_MODIFIED_SINCE"] = Time.utc(2008, 6).httpdate
64 | get :single
65 | assert_equal Time.utc(2009).httpdate, @response.headers["Last-Modified"]
66 | assert_equal "", @response.body
67 | assert_equal 200, @response.status
68 | end
69 |
70 | def test_does_not_set_cache_unless_get_requests
71 | post :single
72 | assert_nil @response.headers["Last-Modified"]
73 | assert_equal 201, @response.status
74 | end
75 |
76 | def test_does_not_use_cache_unless_get_requests
77 | @request.env["HTTP_IF_MODIFIED_SINCE"] = Time.utc(2009, 6).httpdate
78 | post :single
79 | assert_equal 201, @response.status
80 | end
81 |
82 | def test_does_not_set_cache_if_http_cache_is_false
83 | get :single, params: { http_cache: false }
84 | assert_nil @response.headers["Last-Modified"]
85 | assert_equal 200, @response.status
86 | end
87 |
88 | def test_does_not_use_cache_if_http_cache_is_false
89 | @request.env["HTTP_IF_MODIFIED_SINCE"] = Time.utc(2009, 6).httpdate
90 | get :single, params: { http_cache: false }
91 | assert_equal 200, @response.status
92 | end
93 |
94 | def test_does_not_set_cache_for_collection
95 | get :collection
96 | assert_nil @response.headers["Last-Modified"]
97 | assert_equal 200, @response.status
98 | end
99 |
100 | def test_works_for_nested_resources
101 | get :nested
102 | assert_equal Time.utc(2009).httpdate, @response.headers["Last-Modified"]
103 | assert_match(/xml/, @response.body)
104 | assert_equal 200, @response.status
105 | end
106 |
107 | def test_work_with_an_empty_array
108 | get :empty
109 | assert_nil @response.headers["Last-Modified"]
110 | assert_match(/xml/, @response.body)
111 | assert_equal 200, @response.status
112 | end
113 |
114 | def test_it_does_not_set_body_etag_for_single_resource
115 | get :single
116 | assert_nil @response.headers["ETag"]
117 | end
118 |
119 | def test_does_not_set_cache_for_new_records
120 | get :not_persisted
121 | assert_nil @response.headers["Last-Modified"]
122 | assert_equal "", @response.body
123 | assert_equal 200, @response.status
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/test/support/models.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Customer < Struct.new(:name, :id)
4 | extend ActiveModel::Naming
5 | include ActiveModel::Conversion
6 |
7 | def to_json(*)
8 | to_h.to_json
9 | end
10 |
11 | def to_xml(options = {})
12 | if options[:builder]
13 | options[:builder].name name
14 | else
15 | "#{name}"
16 | end
17 | end
18 |
19 | def to_js(options = {})
20 | "name: #{name.inspect}"
21 | end
22 | alias :to_text :to_js
23 |
24 | def errors
25 | []
26 | end
27 |
28 | def persisted?
29 | id.present?
30 | end
31 | end
32 |
33 | module Quiz
34 | class Question < Struct.new(:name, :id)
35 | extend ActiveModel::Naming
36 | include ActiveModel::Conversion
37 |
38 | def persisted?
39 | id.present?
40 | end
41 | end
42 |
43 | class Store < Question
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/setup"
4 | require "minitest/autorun"
5 | require "mocha/minitest"
6 |
7 | # Configure Rails
8 | ENV["RAILS_ENV"] = "test"
9 |
10 | require "active_support"
11 | require "active_model"
12 | require "rails/engine"
13 | require "rails/railtie"
14 |
15 | $:.unshift File.expand_path("../../lib", __FILE__)
16 | require "responders"
17 |
18 | I18n.enforce_available_locales = true
19 | I18n.load_path << File.expand_path("../locales/en.yml", __FILE__)
20 | I18n.reload!
21 |
22 | Responders::Routes = ActionDispatch::Routing::RouteSet.new
23 | Responders::Routes.draw do
24 | resources :news
25 | (ActionDispatch.try(:deprecator) || ActiveSupport::Deprecation).silence do
26 | get "/admin/:action", controller: "admin/addresses"
27 | get "/:controller(/:action(/:id))"
28 | end
29 | end
30 |
31 | class ApplicationController < ActionController::Base
32 | include Responders::Routes.url_helpers
33 |
34 | self.view_paths = File.join(File.dirname(__FILE__), "views")
35 | respond_to :html, :xml
36 | end
37 |
38 | class ActiveSupport::TestCase
39 | self.test_order = :random
40 |
41 | setup do
42 | @routes = Responders::Routes
43 | end
44 | end
45 |
46 | require "rails-controller-testing"
47 |
48 | ActionController::TestCase.include Rails::Controller::Testing::TestProcess
49 | ActionController::TestCase.include Rails::Controller::Testing::TemplateAssertions
50 |
51 | module ActionDispatch
52 | class Flash
53 | class FlashHash
54 | def used_keys
55 | @discard
56 | end
57 | end
58 | end
59 | end
60 |
61 | class Model
62 | include ActiveModel::Conversion
63 | include ActiveModel::Validations
64 |
65 | attr_accessor :persisted, :updated_at
66 | alias :persisted? :persisted
67 |
68 | def persisted?
69 | @persisted
70 | end
71 |
72 | def to_xml(*args)
73 | ""
74 | end
75 |
76 | def initialize(updated_at = nil)
77 | @persisted = true
78 | self.updated_at = updated_at
79 | end
80 | end
81 |
82 | class Address < Model
83 | end
84 |
85 | class User < Model
86 | end
87 |
88 | class News < Model
89 | end
90 |
91 | module MyEngine
92 | class Business < Rails::Engine
93 | isolate_namespace MyEngine
94 | extend ActiveModel::Naming
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/test/views/addresses/create.js.erb:
--------------------------------------------------------------------------------
1 | create.js.erb
--------------------------------------------------------------------------------
/test/views/addresses/edit.html.erb:
--------------------------------------------------------------------------------
1 | edit.html.erb
--------------------------------------------------------------------------------
/test/views/addresses/new.html.erb:
--------------------------------------------------------------------------------
1 | new.html.erb
--------------------------------------------------------------------------------
/test/views/locations/new.html.erb:
--------------------------------------------------------------------------------
1 | new.html.erb
2 |
--------------------------------------------------------------------------------
/test/views/respond_with/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit world!
2 |
--------------------------------------------------------------------------------
/test/views/respond_with/new.html.erb:
--------------------------------------------------------------------------------
1 | New world!
2 |
--------------------------------------------------------------------------------
/test/views/respond_with/respond_with_additional_params.html.erb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heartcombo/responders/9bdc60dfbfa8001641c1c4df7bc73c3fc2a4cf41/test/views/respond_with/respond_with_additional_params.html.erb
--------------------------------------------------------------------------------
/test/views/respond_with/using_invalid_resource_with_template.xml.erb:
--------------------------------------------------------------------------------
1 | I should not be displayed
--------------------------------------------------------------------------------
/test/views/respond_with/using_options_with_template.xml.erb:
--------------------------------------------------------------------------------
1 | <%= @customer.name %>
--------------------------------------------------------------------------------
/test/views/respond_with/using_resource.js.erb:
--------------------------------------------------------------------------------
1 | alert("Hi");
--------------------------------------------------------------------------------
/test/views/respond_with/using_resource_with_block.html.erb:
--------------------------------------------------------------------------------
1 | Hello world!
--------------------------------------------------------------------------------