kinetic_cafe_error 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +35 -0
- data/.gemtest +1 -0
- data/.travis.yml +39 -0
- data/Contributing.rdoc +62 -0
- data/Gemfile +9 -0
- data/History.rdoc +6 -0
- data/Licence.rdoc +27 -0
- data/Manifest.txt +25 -0
- data/README.rdoc +32 -0
- data/Rakefile +67 -0
- data/app/controllers/concerns/kinetic_cafe/error_handler.rb +60 -0
- data/app/views/kinetic_cafe/error/page.html.erb +8 -0
- data/config/locales/kinetic_cafe_error.en.yml +21 -0
- data/config/locales/kinetic_cafe_error.en_ca.yml +21 -0
- data/config/locales/kinetic_cafe_error.en_uk.yml +21 -0
- data/config/locales/kinetic_cafe_error.en_us.yml +21 -0
- data/config/locales/kinetic_cafe_error.fr.yml +21 -0
- data/config/locales/kinetic_cafe_error.fr_ca.yml +21 -0
- data/lib/kinetic_cafe/error.rb +217 -0
- data/lib/kinetic_cafe/error_dsl.rb +165 -0
- data/lib/kinetic_cafe/error_engine.rb +6 -0
- data/lib/kinetic_cafe_error.rb +2 -0
- data/test/test_helper.rb +15 -0
- data/test/test_kinetic_cafe_error.rb +81 -0
- data/test/test_kinetic_cafe_error_dsl.rb +218 -0
- metadata +305 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
en-US:
|
2
|
+
kinetic_cafe_error:
|
3
|
+
page:
|
4
|
+
title: >-
|
5
|
+
An error occurred.
|
6
|
+
body_html: |
|
7
|
+
<p>The person responsible has been informed.</p>
|
8
|
+
<p>I’m not allowed to say anything else.</p>
|
9
|
+
<p>…</p>
|
10
|
+
<p>OK. Just for you, here’s a bit more:</p>
|
11
|
+
error_table_html: |
|
12
|
+
<table>
|
13
|
+
<tbody>
|
14
|
+
<tr>
|
15
|
+
<th>Status</th><td> </td><td>%{status}</td>
|
16
|
+
</tr>
|
17
|
+
<tr>
|
18
|
+
<th>Code</th><td> </td><td>%{code}</td>
|
19
|
+
</tr>
|
20
|
+
</tbody>
|
21
|
+
</table>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
fr-CA:
|
2
|
+
kinetic_cafe_error:
|
3
|
+
page:
|
4
|
+
title: >-
|
5
|
+
Il y avait une erreur.
|
6
|
+
body_html: |
|
7
|
+
<p>La personne qui a causé il a été informé.</p>
|
8
|
+
<p>Je ne suis pas autorisé à dire quoi que ce soit d’autre à ce sujet.</p>
|
9
|
+
<p>…</p>
|
10
|
+
<p>D’ACCORD. Juste pour vous, voici un peu plus:</p>
|
11
|
+
error_table_html: |
|
12
|
+
<table>
|
13
|
+
<tbody>
|
14
|
+
<tr>
|
15
|
+
<th>Condition</th><td> </td><td>%{status}</td>
|
16
|
+
</tr>
|
17
|
+
<tr>
|
18
|
+
<th>Code</th><td> </td><td>%{code}</td>
|
19
|
+
</tr>
|
20
|
+
</tbody>
|
21
|
+
</table>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
fr-CA:
|
2
|
+
kinetic_cafe_error:
|
3
|
+
page:
|
4
|
+
title: >-
|
5
|
+
Il y avait une erreur.
|
6
|
+
body_html: |
|
7
|
+
<p>La personne qui a causé il a été informé.</p>
|
8
|
+
<p>Je ne suis pas autorisé à dire quoi que ce soit d’autre à ce sujet.</p>
|
9
|
+
<p>…</p>
|
10
|
+
<p>D’ACCORD. Juste pour vous, voici un peu plus:</p>
|
11
|
+
error_table_html: |
|
12
|
+
<table>
|
13
|
+
<tbody>
|
14
|
+
<tr>
|
15
|
+
<th>Condition</th><td> </td><td>%{status}</td>
|
16
|
+
</tr>
|
17
|
+
<tr>
|
18
|
+
<th>Code</th><td> </td><td>%{code}</td>
|
19
|
+
</tr>
|
20
|
+
</tbody>
|
21
|
+
</table>
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module KineticCafe #:nodoc:
|
2
|
+
# A subclass of StandardError that can render itself as a descriptive JSON
|
3
|
+
# hash, or as a hash that can be passed to a Rails controller +render+
|
4
|
+
# method.
|
5
|
+
#
|
6
|
+
# This class is not expected to be used on its own, but is used as the parent
|
7
|
+
# (both in terms of base class and namespace) of an application error
|
8
|
+
# hierarchy.
|
9
|
+
#
|
10
|
+
# == Defining an Error Hierarchy
|
11
|
+
#
|
12
|
+
# An error hierarchy is defined by subclassing KineticCafe::Error, extending
|
13
|
+
# it with KineticCafe::ErrorDSL, and defining error subclasses with the DSL.
|
14
|
+
#
|
15
|
+
# class MyErrorBase < KineticCafe::Error
|
16
|
+
# extend KineticCafe::ErrorDSL
|
17
|
+
#
|
18
|
+
# not_found class: :user # => MyErrorBase::UserNotFound
|
19
|
+
# unauthorized class: :user # => MyErrorBase::UserUnauthorized
|
20
|
+
# forbidden class: :user # => MyErrorBase::UserForbidden
|
21
|
+
# conflict class: :user# => MyErrorBase::UserConflict
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# These errors then can be used and caught with a generic KineticCafe::Error
|
25
|
+
# rescue clause and handled there, as is shown in the included
|
26
|
+
# KineticCafe::ErrorHandler controller concern for Rails.
|
27
|
+
class Error < ::StandardError
|
28
|
+
VERSION = '1.0' # :nodoc:
|
29
|
+
|
30
|
+
# The HTTP status to be returned. If not provided in the constructor, uses
|
31
|
+
# #default_status.
|
32
|
+
attr_reader :status
|
33
|
+
# Extra data relevant to recipients of the exception, provided on
|
34
|
+
# construction.
|
35
|
+
attr_reader :extra
|
36
|
+
# The exception that caused this exception; provided on construction.
|
37
|
+
attr_reader :cause
|
38
|
+
|
39
|
+
# Create a new error with the given parameters.
|
40
|
+
#
|
41
|
+
# === Options
|
42
|
+
#
|
43
|
+
# +message+:: A message override. This may be provided either as the first
|
44
|
+
# parameter to the constructor or may be provided as an option.
|
45
|
+
# A value provided as the first parameter overrides any other
|
46
|
+
# value.
|
47
|
+
# +status+:: An override to the HTTP status code to be returned by this
|
48
|
+
# error by default.
|
49
|
+
# +i18n_params+:: The parameters to be sent to I18n.translate with the
|
50
|
+
# #i18n_key.
|
51
|
+
# +cause+:: The exception that caused this error. Used to wrap an earlier
|
52
|
+
# exception.
|
53
|
+
# +extra+:: Extra data to be returned in the API representation of this
|
54
|
+
# exception.
|
55
|
+
# +query+:: A hash of parameters, typically from Rails controller +params+
|
56
|
+
# or model +where+ query. This hash will be converted into a
|
57
|
+
# string value similar to ActiveSupport#to_query.
|
58
|
+
#
|
59
|
+
# Any unmatched options will be added transparently to +i18n_params+.
|
60
|
+
# Because of this, the following constructors are identical:
|
61
|
+
#
|
62
|
+
# KineticCafe::Error.new(i18n_params: { x: 1 })
|
63
|
+
# KineticCafe::Error.new(x: 1)
|
64
|
+
#
|
65
|
+
# :call-seq:
|
66
|
+
# new(message, options = {})
|
67
|
+
# new(options)
|
68
|
+
def initialize(*args)
|
69
|
+
options = args.last.kind_of?(Hash) ? args.pop.dup : {}
|
70
|
+
@message = args.shift
|
71
|
+
@message = options.delete(:message) if @message.nil? || @message.empty?
|
72
|
+
options.delete(:message)
|
73
|
+
|
74
|
+
@message && @message.freeze
|
75
|
+
|
76
|
+
@status = options.delete(:status) || default_status
|
77
|
+
@i18n_params = options.delete(:i18n_params) || {}
|
78
|
+
@extra = options.delete(:extra)
|
79
|
+
@cause = options.delete(:cause)
|
80
|
+
|
81
|
+
@i18n_params.update(cause: cause.message) if cause
|
82
|
+
|
83
|
+
query = options.delete(:query)
|
84
|
+
@i18n_params.merge!(query: stringify(query)) if query
|
85
|
+
@i18n_params.merge!(options)
|
86
|
+
@i18n_params.freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
# The message associated with this exception. If not provided, defaults to
|
90
|
+
# #i18n_message.
|
91
|
+
def message
|
92
|
+
@message || i18n_message
|
93
|
+
end
|
94
|
+
|
95
|
+
# The name of the error class.
|
96
|
+
def name
|
97
|
+
@name ||= KineticCafe::ErrorDSL.namify(self.class.name)
|
98
|
+
end
|
99
|
+
|
100
|
+
# The key used for I18n translation.
|
101
|
+
def i18n_key
|
102
|
+
@i18n_key ||= "#{self.class.i18n_key_base}.#{name}".freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
# Indicates that this error should *not* have its details rendered to the
|
106
|
+
# user, but should use the +head+ method.
|
107
|
+
def header_only?
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
# Indicates that this error should be rendered to the client, but clients
|
112
|
+
# are advised *not* to display the message to the user.
|
113
|
+
def internal?
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
# The I18n translation of the message. If I18n.translate is defined,
|
118
|
+
# returns #i18n_key and the I18n parameters.
|
119
|
+
def i18n_message
|
120
|
+
@i18n_message ||= if defined?(I18n.translate)
|
121
|
+
I18n.translate(i18n_key, @i18n_params).freeze
|
122
|
+
else
|
123
|
+
[ i18n_key, @i18n_params ].freeze
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# The details of this error as a hash. Values that are empty or nil are
|
128
|
+
# omitted.
|
129
|
+
def api_error(*)
|
130
|
+
{
|
131
|
+
message: @message,
|
132
|
+
status: status,
|
133
|
+
name: name,
|
134
|
+
internal: internal?,
|
135
|
+
i18n_message: i18n_message,
|
136
|
+
i18n_key: i18n_key,
|
137
|
+
i18n_params: @i18n_params,
|
138
|
+
cause: cause && cause.message,
|
139
|
+
extra: extra
|
140
|
+
}.delete_if { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
|
141
|
+
end
|
142
|
+
alias_method :as_json, :api_error
|
143
|
+
|
144
|
+
# An error result that can be passed as a response body.
|
145
|
+
def error_result
|
146
|
+
{ error: api_error, message: message }
|
147
|
+
end
|
148
|
+
|
149
|
+
# A hash that can be passed to the Rails +render+ method with +status+ of
|
150
|
+
# #status and +layout+ false. The +json+ field is rendered as a hash of
|
151
|
+
# +error+ (calling #api_error) and +message+ (calling #message).
|
152
|
+
def json_result
|
153
|
+
{ status: status, json: error_result, layout: false }
|
154
|
+
end
|
155
|
+
alias_method :render_json_for_rails, :json_result
|
156
|
+
|
157
|
+
# Nice debugging version of a KineticCafe::Error
|
158
|
+
def inspect
|
159
|
+
"#<#{self.class}: name=#{name} status=#{status} " \
|
160
|
+
"message=#{message.inspect} i18n_key=#{i18n_key} " \
|
161
|
+
"i18n_params=#{@i18n_params.inspect} extra=#{extra.inspect} " \
|
162
|
+
"cause=#{cause}>"
|
163
|
+
end
|
164
|
+
|
165
|
+
# The base for I18n key resolution.
|
166
|
+
def self.i18n_key_base
|
167
|
+
'kcerrors'.freeze
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def default_status
|
173
|
+
defined?(Rack::Utils) && :bad_request || 400
|
174
|
+
end
|
175
|
+
|
176
|
+
def stringify(object, namespace = nil)
|
177
|
+
case object
|
178
|
+
when Hash
|
179
|
+
stringify_hash(object, namespace).compact.sort.join('; ')
|
180
|
+
when Array
|
181
|
+
stringify_array(object, namespace)
|
182
|
+
else
|
183
|
+
stringify_value(namespace, object)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def stringify_hash(hash, namespace)
|
188
|
+
hash.collect do |key, value|
|
189
|
+
key = namespace ? "#{namespace}[#{key}]" : key
|
190
|
+
case value
|
191
|
+
when Hash
|
192
|
+
next if value.nil?
|
193
|
+
stringify(value, key)
|
194
|
+
when Array
|
195
|
+
stringify_array(key, value)
|
196
|
+
else
|
197
|
+
stringify_value(key, value)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def stringify_array(key, array)
|
203
|
+
key = "#{key}[]"
|
204
|
+
if array.empty?
|
205
|
+
stringify_value(key, [])
|
206
|
+
else
|
207
|
+
array.collect { |value| stringify(value, key) }.join(', ')
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def stringify_value(key, value)
|
212
|
+
"#{key}: #{value}"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
require_relative 'error_dsl'
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module KineticCafe
|
2
|
+
# Make defining new children of KineticCafe::Error easy. Adds the
|
3
|
+
# #define_error method.
|
4
|
+
#
|
5
|
+
# If using when Rack is present, useful variant methodss are provided
|
6
|
+
# matching Rack status symbol codes. These set the default status to the Rack
|
7
|
+
# status.
|
8
|
+
#
|
9
|
+
# conflict class: :user # => UserConflict, status: :conflict
|
10
|
+
# not_found class: :post # => PostNotFound, status: :not_found
|
11
|
+
module ErrorDSL
|
12
|
+
# Convert ThisName to this_name. Uses #underscore if +name+ responds to it.
|
13
|
+
# Otherwise, it uses a super naïve version.
|
14
|
+
def self.underscore(name)
|
15
|
+
name = name.to_s
|
16
|
+
if name.respond_to?(:underscore)
|
17
|
+
name.underscore.freeze
|
18
|
+
else
|
19
|
+
name.dup.tap { |n|
|
20
|
+
n.gsub!(/[[:upper:]]/) { "_#$&".downcase }
|
21
|
+
n.sub!(/^_/, '')
|
22
|
+
}.freeze
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Demodulizes This::Name to just Name. Uses name#demodulize if it is
|
27
|
+
# available, or a naïve version otherwise.
|
28
|
+
def self.demodulize(name)
|
29
|
+
name = name.to_s
|
30
|
+
if name.respond_to?(:demodulize)
|
31
|
+
name.demodulize.freeze
|
32
|
+
else
|
33
|
+
name.split(/::/)[-1].freeze
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Demodulizes and underscores the provided name.
|
38
|
+
def self.namify(name)
|
39
|
+
underscore(demodulize(name.to_s))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Convert this_name to ThisName. Uses #camelize if +name+ responds to it,
|
43
|
+
# or a naïve version otherwise.
|
44
|
+
def self.camelize(name)
|
45
|
+
name = name.to_s
|
46
|
+
if name.respond_to?(:camelize)
|
47
|
+
name.camelize.freeze
|
48
|
+
else
|
49
|
+
"_#{name}".gsub(/_([a-z])/i) { $1.upcase }.freeze
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Define a new error as a subclass of the exception hosting ErrorDSL.
|
54
|
+
# Options is a Hash supporting the following values:
|
55
|
+
#
|
56
|
+
# +status+:: A number or Ruby symbol representing the HTTP status code
|
57
|
+
# associated with this error. If not provided, defaults to
|
58
|
+
# :bad_request. Must be provided if +class+ is provided. HTTP
|
59
|
+
# status codes are defined in Rack::Utils.
|
60
|
+
# +key+:: The name of the error class to be created. Provide as a
|
61
|
+
# snake_case value that will be turned into a camelized class
|
62
|
+
# name. Mutually exclusive with +class+.
|
63
|
+
# +class+:: The name of the class the error is for. If present, +status+
|
64
|
+
# must be provided to create a complete error class. That is,
|
65
|
+
#
|
66
|
+
# define_error class: :object, status: :not_found
|
67
|
+
#
|
68
|
+
# will create an +ObjectNotFound+ error class.
|
69
|
+
#
|
70
|
+
# +header_only+:: Indicates that when this is caught, it should not be
|
71
|
+
# returned with full details, but shoudl instead be treated
|
72
|
+
# as a header-only API response.
|
73
|
+
def define_error(options)
|
74
|
+
fail ArgumentError, 'invalid options' unless options.kind_of?(Hash)
|
75
|
+
fail ArgumentError, 'define what error?' if options.empty?
|
76
|
+
|
77
|
+
options = options.dup
|
78
|
+
|
79
|
+
klass = options.delete(:class)
|
80
|
+
if klass
|
81
|
+
if options.key?(:key)
|
82
|
+
fail ArgumentError, ":key conflicts with class:#{klass}"
|
83
|
+
end
|
84
|
+
status = options[:status]
|
85
|
+
|
86
|
+
key = if status.kind_of?(Symbol) or status.kind_of?(String)
|
87
|
+
"#{klass}_#{KineticCafe::ErrorDSL.namify(status)}"
|
88
|
+
else
|
89
|
+
"#{klass}_#{KineticCafe::ErrorDSL.namify(self.name)}"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
status = options[:status]
|
93
|
+
key = options.fetch(:key) {
|
94
|
+
fail ArgumentError, 'one of :key or :class must be provided'
|
95
|
+
}.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
key.tap do |k|
|
99
|
+
k.squeeze!('_')
|
100
|
+
k.gsub!(/^_+/, '')
|
101
|
+
k.gsub!(/_+$/, '')
|
102
|
+
k.freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
error_name = KineticCafe::ErrorDSL.camelize(key)
|
106
|
+
i18n_key_base = respond_to?(:i18n_key_base) && self.i18n_key_base ||
|
107
|
+
'kcerrors'.freeze
|
108
|
+
i18n_key = "#{i18n_key_base}.#{key}".freeze
|
109
|
+
|
110
|
+
if const_defined?(error_name)
|
111
|
+
message = "key:#{key} already exists as #{error_name}"
|
112
|
+
message << " with class:#{klass}" if klass
|
113
|
+
fail ArgumentError, message
|
114
|
+
end
|
115
|
+
|
116
|
+
error = Class.new(self)
|
117
|
+
error.send :define_method, :name, -> { key }
|
118
|
+
error.send :define_method, :i18n_key, -> { i18n_key }
|
119
|
+
|
120
|
+
if options[:header_only]
|
121
|
+
error.send :define_method, :header_only?, -> { true }
|
122
|
+
end
|
123
|
+
|
124
|
+
if options[:internal]
|
125
|
+
error.send :define_method, :internal?, -> { true }
|
126
|
+
end
|
127
|
+
|
128
|
+
status ||= defined?(Rack::Utils) && :bad_request || 400
|
129
|
+
status.freeze
|
130
|
+
|
131
|
+
error.send :define_method, :default_status, -> { status } if status
|
132
|
+
error.send :private, :default_status
|
133
|
+
|
134
|
+
const_set(error_name, error)
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
#
|
139
|
+
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
##
|
144
|
+
def self.included(_mod)
|
145
|
+
fail "#{self} cannot be included"
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
def self.extended(base)
|
150
|
+
unless base < ::StandardError
|
151
|
+
fail "#{self} cannot extend #{base} (not a StandardError)"
|
152
|
+
end
|
153
|
+
|
154
|
+
if defined?(Rack::Utils)
|
155
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |name, value|
|
156
|
+
base.singleton_class.send :define_method, name do |options = {}|
|
157
|
+
define_error(options.merge(status: name))
|
158
|
+
end
|
159
|
+
|
160
|
+
base.send :define_error, status: name, key: name
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|