diff options
Diffstat (limited to 'activesupport')
-rw-r--r-- | activesupport/CHANGELOG.md | 32 | ||||
-rw-r--r-- | activesupport/lib/active_support.rb | 1 | ||||
-rw-r--r-- | activesupport/lib/active_support/actionable_error.rb | 48 | ||||
-rw-r--r-- | activesupport/test/actionable_error_test.rb | 47 |
4 files changed, 128 insertions, 0 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 301b0c8822..4c7b134c35 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,35 @@ +* Introduce `ActiveSupport::ActionableError`. + + Actionable errors let's you dispatch actions from Rails' error pages. This + can help you save time if you have a clear action for the resolution of + common development errors. + + The de-facto example are pending migrations. Every time pending migrations + are found, a middleware raises an error. With actionable errors, you can + run the migrations right from the error page. Other examples include Rails + plugins that need to run a rake task to setup themselves. They can now + raise actionable errors to run the setup straight from the error pages. + + Here is how to define an actionable error: + + ```ruby + class PendingMigrationError < MigrationError #:nodoc: + include ActiveSupport::ActionableError + + action "Run pending migrations" do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + ``` + + To make an error actionable, include the `ActiveSupport::ActionableError` + module and invoke the `action` class macro to define the action. An action + needs a name and a procedure to execute. The name is shown as the name of a + button on the error pages. Once clicked, it will invoke the given + procedure. + + *Vipul A M*, *Yao Jie*, *Genadi Samokovarov* + * Preserve `html_safe?` status on `ActiveSupport::SafeBuffer#*`. Before: diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 5589c71281..9e242ddeaa 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -34,6 +34,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :ActionableError autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker diff --git a/activesupport/lib/active_support/actionable_error.rb b/activesupport/lib/active_support/actionable_error.rb new file mode 100644 index 0000000000..7db14cd178 --- /dev/null +++ b/activesupport/lib/active_support/actionable_error.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module ActiveSupport + # Actionable errors let's you define actions to resolve an error. + # + # To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt> + # module and invoke the +action+ class macro to define the action. An action + # needs a name and a block to execute. + module ActionableError + extend Concern + + class NonActionable < StandardError; end + + included do + class_attribute :_actions, default: {} + end + + def self.actions(error) # :nodoc: + case error + when ActionableError, -> it { Class === it && it < ActionableError } + error._actions + else + {} + end + end + + def self.dispatch(error, name) # :nodoc: + actions(error).fetch(name).call + rescue KeyError + raise NonActionable, "Cannot find action \"#{name}\"" + end + + module ClassMethods + # Defines an action that can resolve the error. + # + # class PendingMigrationError < MigrationError + # include ActiveSupport::ActionableError + # + # action "Run pending migrations" do + # ActiveRecord::Tasks::DatabaseTasks.migrate + # end + # end + def action(name, &block) + _actions[name] = block + end + end + end +end diff --git a/activesupport/test/actionable_error_test.rb b/activesupport/test/actionable_error_test.rb new file mode 100644 index 0000000000..63046b937c --- /dev/null +++ b/activesupport/test/actionable_error_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/actionable_error" + +class ActionableErrorTest < ActiveSupport::TestCase + NonActionableError = Class.new(StandardError) + + class DispatchableError < StandardError + include ActiveSupport::ActionableError + + class_attribute :flip1, default: false + class_attribute :flip2, default: false + + action "Flip 1" do + self.flip1 = true + end + + action "Flip 2" do + self.flip2 = true + end + end + + test "returns all action of an actionable error" do + assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError).keys + assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError.new).keys + end + + test "returns no actions for non-actionable errors" do + assert ActiveSupport::ActionableError.actions(Exception).empty? + assert ActiveSupport::ActionableError.actions(Exception.new).empty? + end + + test "dispatches actions from error and name" do + assert_changes "DispatchableError.flip1", from: false, to: true do + ActiveSupport::ActionableError.dispatch DispatchableError, "Flip 1" + end + end + + test "cannot dispatch missing actions" do + err = assert_raises ActiveSupport::ActionableError::NonActionable do + ActiveSupport::ActionableError.dispatch NonActionableError, "action" + end + + assert_equal 'Cannot find action "action"', err.to_s + end +end |