kinetic_cafe_error 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|