├── .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 | [![Gem Version](https://fury-badge.herokuapp.com/rb/responders.svg)](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! --------------------------------------------------------------------------------