rescue_registry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 528eb94a1296262f6cb1d20d7b8209072147c72e392dabb468fdbb0d2e4d9f43
4
+ data.tar.gz: 296ea4a7f1a08fa3fbd981c1ec08e35e528befc17b4167a51b5cdcd480783229
5
+ SHA512:
6
+ metadata.gz: 076d737ba7c477015a3b984e34d2c3782b56688d039db35b27d9cf8d89b682755e1a01c0cae34996ab63ed1fdf2a6e20c8b945d8f1b84c1a5a196f002b060b6c
7
+ data.tar.gz: 46c7d35672ecb8d4f04b5d2cad6f4d9844ef3d6d5600c6ba2eaa090a638c2bce77f180163523c5bda7ca646db6355f172ce70da996bc9fbc5b9f6d9b4bcf7191
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ No releases means no changes...
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Peter Wagenet
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ # RescueRegistry
2
+
3
+ RescueRegistry improves error handling with Rails while still hewing as close to the defaults as possible.
4
+
5
+
6
+
7
+ ## Features
8
+
9
+ ### Better than `rescue_from`
10
+ `rescue_from` works fine, but it it's a big hammer. Using it completely bypasses Rail's built-in exception handling middlewares,
11
+ which actually do some nice things for us (e.g. automatically handling different data formats). RescueRegistry allows the built-in
12
+ middleware to handle the exceptions but with our custom handlers.
13
+
14
+ ### Better default exception handling
15
+ Rails also has some built-in support for assigning different exception classes to status types (See `config.action_dispatch.rescue_responses`).
16
+ Unfortunately, all this allows you to do is assign a status code. If you want more complex error handling, or to use different codes in
17
+ different controllers, you're out of luck. With RescueRegistry you can register and exception with a custom handler or with different status
18
+ codes in different controllers.
19
+
20
+ ## In Action
21
+
22
+ ### graphiti-rails
23
+ The [graphiti-rails gem](https://github.com/wagenet/graphiti-rails) uses RescueRegistry to facilitate better JSON:API error handling, since
24
+ Rails' out-of-the-box behavior doesn't adhere to the JSON:API spec.
25
+
26
+ ## Usage
27
+
28
+ Example usages:
29
+
30
+ ```ruby
31
+ class MyController < ActionController::Base
32
+ class CustomStatusError < StandardError; end
33
+ class CustomTitleError < StandardError; end
34
+ class DetailExceptionError < StandardError; end
35
+ class DetailProcError < StandardError; end
36
+ class DetailStringError < StandardError; end
37
+ class MetaProcError < StandardError; end
38
+ class LogFalseError < StandardError; end
39
+ class CustomHandlerError < StandardError; end
40
+ class RailsError < StandardError; end
41
+ class SubclassedError < CustomStatusError; end
42
+
43
+ class CustomErrorHandler < RescueRegistry::ExceptionHandler
44
+ def self.default_status
45
+ 302
46
+ end
47
+
48
+ def title
49
+ "Custom Title"
50
+ end
51
+ end
52
+
53
+ register_exception CustomStatusError, status: 401
54
+ register_exception CustomTitleError, title: "My Title"
55
+ register_exception DetailExceptionError, detail: :exception
56
+ register_exception DetailProcError, detail: -> (e) { e.class.name.upcase }
57
+ register_exception DetailStringError, detail: "Custom Detail"
58
+ register_exception MetaProcError, meta: -> (e) { { class_name: e.class.name.upcase } }
59
+ register_exception CustomHandlerError, handler: CustomErrorHandler
60
+ register_exception RailsError, status: 403, handler: RescueRegistry::RailsExceptionHandler
61
+ end
62
+ ```
63
+
64
+ ## Installation
65
+ Add this line to your application's Gemfile:
66
+
67
+ ```ruby
68
+ gem 'rescue_registry'
69
+ ```
70
+
71
+ ## Contributing
72
+ We'd love to have your help improving rescue_registry, send a PR!
73
+
74
+ ## License
75
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'yard'
8
+ require 'rspec/core/rake_task'
9
+ require 'bundler/gem_tasks'
10
+
11
+ YARD::Rake::YardocTask.new
12
+
13
+ RSpec::Core::RakeTask.new(:spec) do |t|
14
+ t.rspec_opts = "--order random"
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load "rails/tasks/engine.rake"
19
+
20
+ task default: :spec
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ module RescueRegistry
6
+ autoload :Controller, "rescue_registry/controller"
7
+ autoload :ExceptionsApp, "rescue_registry/exceptions_app"
8
+ autoload :ExceptionHandler, "rescue_registry/exception_handler"
9
+ autoload :RailsExceptionHandler, "rescue_registry/exception_handler"
10
+ autoload :Registry, "rescue_registry/registry"
11
+
12
+ class HandlerNotFound < StandardError; end
13
+
14
+ def self.context
15
+ Thread.current[:rescue_registry_context]
16
+ end
17
+
18
+ def self.context=(value)
19
+ Thread.current[:rescue_registry_context] = value
20
+ end
21
+
22
+ def self.with_context(value)
23
+ original = context
24
+ self.context = value
25
+ yield
26
+ ensure
27
+ self.context = original
28
+ end
29
+
30
+ REGISTRY_METHODS = %i[
31
+ handler_for_exception
32
+ handles_exception?
33
+ status_code_for_exception
34
+ response_for_debugging
35
+ response_for_public
36
+ ]
37
+
38
+ REGISTRY_METHODS.each do |method|
39
+ define_singleton_method(method) do |*args|
40
+ return unless context.respond_to?(:rescue_registry)
41
+ context.rescue_registry.public_send(method, *args)
42
+ end
43
+ end
44
+ end
45
+
46
+ ActiveSupport.on_load(:action_controller) do
47
+ include RescueRegistry::Controller
48
+ end
49
+
50
+ require 'rescue_registry/action_dispatch'
51
+ require 'rescue_registry/railtie'
@@ -0,0 +1,71 @@
1
+ require 'action_dispatch'
2
+
3
+ ActionDispatch::ExceptionWrapper.class_eval do
4
+ class << self
5
+ alias_method :status_code_for_exception_without_rescue_registry, :status_code_for_exception
6
+ def status_code_for_exception(class_name)
7
+ RescueRegistry.status_code_for_exception(class_name) ||
8
+ status_code_for_exception_without_rescue_registry(class_name)
9
+ end
10
+ end
11
+ end
12
+
13
+ # Since this is for debugging only it's less critical to make changes here. The main area that matters is
14
+ # returning the correct status code. Since all this code relies upon the ExceptionWrapper which we've monkeypatched,
15
+ # it should work correctly without changes. However, we can provide more details in some areas so we hook in for that.
16
+ ActionDispatch::DebugExceptions.class_eval do
17
+ private
18
+
19
+ # `#log_error`
20
+ # TODO: We may be able to add more information, though the details remain to be determined
21
+
22
+ # `#render_for_browser_request`
23
+ # TODO: We may be able to add more information, though the details remain to be determined
24
+
25
+ # This would work without changes, but the formatting would be incorrect. Since it's for debugging only,
26
+ # we could choose to ignore it, but the detailed information would definitely be useful.
27
+ alias_method :render_for_api_request_without_rescue_registry, :render_for_api_request
28
+ def render_for_api_request(content_type, wrapper)
29
+ response = nil
30
+
31
+ if RescueRegistry.handles_exception?(wrapper.exception)
32
+ # Ideally `render_for_api_request` would be split up so we could avoid some duplication in RescueRegistry
33
+ begin
34
+ response = RescueRegistry.response_for_debugging(content_type, wrapper.exception, traces: wrapper.traces)
35
+ rescue Exception => e
36
+ # Replace the original exception (still available via `cause`) and let it get handled with default handlers
37
+ wrapper = ActionDispatch::ExceptionWrapper.new(wrapper.backtrace_cleaner, e)
38
+ end
39
+ end
40
+
41
+ if response
42
+ render(*response)
43
+ else
44
+ # One of the following is true:
45
+ # - No handler for the exception
46
+ # - No response for content_type
47
+ # - An exception while generating the response
48
+ # In any case, we go with the default here.
49
+ render_for_api_request_without_rescue_registry(content_type, wrapper)
50
+ end
51
+ end
52
+ end
53
+
54
+ ActionDispatch::ShowExceptions.class_eval do
55
+ # @private
56
+ alias_method :initialize_without_rescue_registry, :initialize
57
+
58
+ # @private
59
+ def initialize(*args)
60
+ initialize_without_rescue_registry(*args)
61
+ @exceptions_app = RescueRegistry::ExceptionsApp.new(@exceptions_app)
62
+ end
63
+
64
+ alias_method :call_without_rescue_registry, :call
65
+ def call(*args)
66
+ warn "Didn't expect RescueRegistry context to be set in middleware" if RescueRegistry.context
67
+ call_without_rescue_registry(*args)
68
+ ensure
69
+ RescueRegistry.context = nil
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ module RescueRegistry
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ cattr_accessor :rescue_registry
7
+ self.rescue_registry = Registry.new(self)
8
+
9
+ class << self
10
+ delegate :register_exception, to: :rescue_registry
11
+ end
12
+ end
13
+
14
+ def rescue_registry
15
+ self.class.rescue_registry
16
+ end
17
+
18
+ def process_action(*args)
19
+ if RescueRegistry.context
20
+ # Controller logger isn't yet available
21
+ Rails.logger.warn "Didn't expect RescueRegistry context to be set in controller"
22
+ Rails.logger.debug caller.join("\n")
23
+ end
24
+
25
+ # Setting something globally is not very nice, but it allows us to access it without
26
+ # having to change a whole lot of internal Rails APIs. This especially matters when
27
+ # getting the status code via ExceptionWrapper.
28
+ # We don't unset here so that it is available to middleware
29
+ # Unsetting happens in ActionDispatch::ShowExceptions. This _should_ be ok since we shouldn't
30
+ # be processing multiple requests in the same thread.
31
+ RescueRegistry.context = self
32
+
33
+ super
34
+ end
35
+
36
+ class_methods do
37
+ def inherited(subklass)
38
+ super
39
+ subklass.rescue_registry = rescue_registry.dup
40
+ subklass.rescue_registry.owner = subklass
41
+ end
42
+
43
+ def default_exception_handler
44
+ ExceptionHandler
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,148 @@
1
+ module RescueRegistry
2
+ class ExceptionHandler
3
+ def self.default_status
4
+ 500
5
+ end
6
+
7
+ # Use a glob for options so that unknown values won't throw errors. Also, this list could get long...
8
+ def initialize(exception, **options)
9
+ @exception = exception
10
+
11
+ status = options[:status]
12
+ if status == :passthrough
13
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception_without_rescue_registry(exception.class.name)
14
+ end
15
+ @status = status
16
+
17
+ @title = options[:title]
18
+
19
+ @detail = options[:detail]
20
+ if options[:message] && @detail.nil?
21
+ # Deprecated, from GraphitiErrors
22
+ @detail = (options[:message] == true) ? :exception : options[:message]
23
+ end
24
+
25
+ @meta = options[:meta]
26
+
27
+ # TODO: Warn about unrecognized options
28
+ end
29
+
30
+ attr_reader :exception, :status
31
+ alias_method :status_code, :status
32
+
33
+ def error_code
34
+ error_code_from_status
35
+ end
36
+
37
+ def title
38
+ @title || title_from_status
39
+ end
40
+
41
+ def detail
42
+ detail =
43
+ case @detail
44
+ when :exception
45
+ exception.message
46
+ when Proc
47
+ @detail.call(exception)
48
+ else
49
+ @detail.try(:to_s)
50
+ end
51
+
52
+ detail.presence || default_detail_for_status
53
+ end
54
+
55
+ def meta
56
+ if @meta.is_a?(Proc)
57
+ @meta.call(exception)
58
+ else
59
+ { }
60
+ end
61
+ end
62
+
63
+ def build_payload(show_details: false, traces: nil)
64
+ payload_meta = meta
65
+
66
+ if show_details
67
+ payload_meta = payload_meta.merge(
68
+ __details__: {
69
+ exception: exception.inspect,
70
+ traces: traces || [exception.backtrace]
71
+ }
72
+ )
73
+ end
74
+
75
+ {
76
+ errors: [
77
+ code: error_code,
78
+ status: status_code.to_s,
79
+ title: title,
80
+ detail: detail,
81
+ meta: payload_meta
82
+ ]
83
+ }
84
+ end
85
+
86
+ def formatted_response(content_type, fallback: :json, **options)
87
+ body = build_payload(**options)
88
+
89
+ # TODO: Maybe make a helper to register these types?
90
+ to_format = content_type == :jsonapi ? "to_json" : "to_#{content_type.to_sym}"
91
+
92
+ if content_type && body.respond_to?(to_format)
93
+ formatted_body = body.public_send(to_format)
94
+ format = content_type
95
+ else
96
+ if fallback == :json
97
+ formatted_body = body.to_json
98
+ format = Mime[:json]
99
+ elsif fallback == :none
100
+ return nil
101
+ else
102
+ raise ArgumentError, "unknown fallback=#{fallback}"
103
+ end
104
+ end
105
+
106
+ [status_code, formatted_body, format]
107
+ end
108
+
109
+ private
110
+
111
+ def error_code_from_status
112
+ code_to_symbol = Rack::Utils::SYMBOL_TO_STATUS_CODE.invert
113
+ code_to_symbol.fetch(status_code, code_to_symbol[500])
114
+ end
115
+
116
+ def title_from_status
117
+ Rack::Utils::HTTP_STATUS_CODES.fetch(
118
+ status_code,
119
+ Rack::Utils::HTTP_STATUS_CODES[500]
120
+ )
121
+ end
122
+
123
+ def default_detail_for_status
124
+ if status_code >= 500
125
+ "We've notified our engineers and hope to address this issue shortly."
126
+ end
127
+ end
128
+ end
129
+
130
+ # Builds a payload in the style of the Rails default handling for compatibility
131
+ class RailsExceptionHandler < ExceptionHandler
132
+ def build_payload(show_details: false, traces: nil)
133
+ body = {
134
+ status: status_code,
135
+ error: title
136
+ }
137
+
138
+ if show_details
139
+ body[:exception] = exception.inspect
140
+ if traces
141
+ body[:traces] = traces
142
+ end
143
+ end
144
+
145
+ body
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,36 @@
1
+ module RescueRegistry
2
+ class ExceptionsApp
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ request = ActionDispatch::Request.new(env)
9
+ exception = request.get_header "action_dispatch.exception"
10
+
11
+ if RescueRegistry.handles_exception?(exception)
12
+ begin
13
+ content_type = request.formats.first
14
+ rescue Mime::Type::InvalidMimeType
15
+ content_type = Mime[:text]
16
+ end
17
+
18
+ if (response = RescueRegistry.response_for_public(content_type, exception))
19
+ status, body, format = response
20
+
21
+ if request.path_info != "/#{status}"
22
+ warn "status mismatch; path_info=#{request.path_info}; status=#{status}"
23
+ end
24
+
25
+ [status, { "Content-Type" => "#{format}; charset=#{ActionDispatch::Response.default_charset}",
26
+ "Content-Length" => body.bytesize.to_s }, [body]]
27
+ else
28
+ # If we have no response, it means we couldn't render for the content_type, use the default handler instead
29
+ @app.call(env)
30
+ end
31
+ else
32
+ @app.call(env)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module RescueRegistry
6
+ class Railtie < Rails::Railtie
7
+ end
8
+ end
@@ -0,0 +1,100 @@
1
+ module RescueRegistry
2
+ class Registry
3
+ attr_accessor :owner
4
+
5
+ def initialize(owner)
6
+ @owner = self
7
+ @handlers = { }
8
+ end
9
+
10
+ def initialize_dup(_other)
11
+ @handlers = @handlers.dup
12
+ end
13
+
14
+ # TODO: Support a shorthand for handler
15
+ def register_exception(exception_class, handler: nil, **options)
16
+ raise ArgumentError, "#{exception_class} is not an Exception" unless exception_class <= Exception
17
+
18
+ handler ||= owner.try(:default_exception_handler)
19
+ raise ArgumentError, "handler must be provided" unless handler
20
+
21
+ status = options[:status] ||= handler.default_status
22
+ raise ArgumentError, "status must be provided" unless status
23
+ raise ArgumentError, "invalid status: #{status}" unless status.is_a?(Integer) || status == :passthrough
24
+
25
+ # TODO: Validate options here
26
+
27
+ # We assign the status here as a default when looking up by class (and not instance)
28
+ @handlers[exception_class] = [handler, options]
29
+ end
30
+
31
+ def handler_for_exception(exception)
32
+ handler_info = handler_info_for_exception(exception)
33
+ raise HandlerNotFound, "no handler found for #{exception.class}" unless handler_info
34
+
35
+ handler_class, handler_options = handler_info
36
+ handler_class.new(exception, **handler_options)
37
+ end
38
+
39
+ def handles_exception?(exception)
40
+ handler_info_for_exception(exception).present?
41
+ end
42
+
43
+ def status_code_for_exception(exception)
44
+ _, options = handler_info_for_exception(exception)
45
+ return unless options
46
+
47
+ # Return no code for passthrough.
48
+ # Alternatively we could handle this explicitly in the ExceptionWrapper monkeypatch.
49
+ options[:status] == :passthrough ? nil : options[:status]
50
+ end
51
+
52
+ def build_response(content_type, exception, **options)
53
+ handler = handler_for_exception(exception)
54
+ handler.formatted_response(content_type, **options)
55
+ end
56
+
57
+ def response_for_debugging(content_type, exception, traces: nil)
58
+ build_response(content_type, exception, show_details: true, traces: traces)
59
+ end
60
+
61
+ def response_for_public(content_type, exception)
62
+ build_response(content_type, exception, fallback: :none)
63
+ end
64
+
65
+ private
66
+
67
+ def handler_info_for_exception(exception)
68
+ exception_class =
69
+ case exception
70
+ when String
71
+ exception.safe_constantize
72
+ when Class
73
+ exception
74
+ else
75
+ exception.class
76
+ end
77
+
78
+ return unless exception_class
79
+
80
+ raise ArgumentError, "#{exception_class} is not an Exception" unless exception_class <= Exception
81
+
82
+ # Reverse so most recently defined takes precedence
83
+ registry = @handlers.to_a.reverse
84
+
85
+ # Look for an exact class, then for the superclass and so on.
86
+ # There might be a more efficient way to do this, but this is pretty readable
87
+ match_class = exception_class
88
+ loop do
89
+ if (found = registry.find { |(klass, _)| klass == match_class })
90
+ return found.last
91
+ elsif match_class == Exception
92
+ # We've exhausted our options
93
+ return nil
94
+ end
95
+
96
+ match_class = match_class.superclass
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RescueRegistry
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :rescue_registry do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rescue_registry
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Wagenet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: appraisal
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.2'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: kramdown-parser-gfm
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rouge
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.3'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.3'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec-rails
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.8'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.8'
89
+ - !ruby/object:Gem::Dependency
90
+ name: sqlite3
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.3'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.3'
103
+ - !ruby/object:Gem::Dependency
104
+ name: yard
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.9'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.9'
117
+ description:
118
+ email:
119
+ - peter.wagenet@gmail.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - CHANGELOG.md
125
+ - MIT-LICENSE
126
+ - README.md
127
+ - Rakefile
128
+ - lib/rescue_registry.rb
129
+ - lib/rescue_registry/action_dispatch.rb
130
+ - lib/rescue_registry/controller.rb
131
+ - lib/rescue_registry/exception_handler.rb
132
+ - lib/rescue_registry/exceptions_app.rb
133
+ - lib/rescue_registry/railtie.rb
134
+ - lib/rescue_registry/registry.rb
135
+ - lib/rescue_registry/version.rb
136
+ - lib/tasks/rescue_registry_tasks.rake
137
+ homepage: https://github.com/wagenet/rescue_registry
138
+ licenses:
139
+ - MIT
140
+ metadata:
141
+ bug_tracker_uri: https://github.com/wagenet/rescue_registry/issues
142
+ changelog_uri: https://github.com/wagenet/rescue_registry/CHANGELOG.md
143
+ source_code_uri: https://github.com/wagenet/rescue_registry
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.4'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubygems_version: 3.0.3
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Registry for Rails Exceptions
163
+ test_files: []