kinetic_cafe_error 1.1 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.rdoc +33 -0
- data/Manifest.txt +3 -0
- data/README.rdoc +97 -10
- data/Rakefile +2 -0
- data/app/controllers/concerns/kinetic_cafe/error_handler.rb +13 -2
- data/config/locales/kinetic_cafe_error.en-CA.yml +2 -0
- data/config/locales/kinetic_cafe_error.en-UK.yml +2 -0
- data/config/locales/kinetic_cafe_error.en-US.yml +2 -0
- data/config/locales/kinetic_cafe_error.en.yml +2 -0
- data/config/locales/kinetic_cafe_error.fr-CA.yml +2 -0
- data/config/locales/kinetic_cafe_error.fr.yml +2 -0
- data/lib/kinetic_cafe/error.rb +136 -135
- data/lib/kinetic_cafe/error/minitest.rb +27 -2
- data/lib/kinetic_cafe/error_dsl.rb +39 -19
- data/lib/kinetic_cafe/error_engine.rb +3 -0
- data/lib/kinetic_cafe/error_module.rb +192 -0
- data/lib/kinetic_cafe/error_rspec.rb +17 -0
- data/lib/kinetic_cafe/error_tasks.rake +59 -0
- data/test/test_helper.rb +2 -1
- data/test/test_kinetic_cafe_error.rb +21 -2
- data/test/test_kinetic_cafe_error_dsl.rb +83 -2
- data/test/test_kinetic_cafe_error_hierarchy.rb +147 -0
- metadata +11 -3
@@ -7,6 +7,9 @@ module Minitest #:nodoc:
|
|
7
7
|
def assert_kc_error expected, actual, params = {}, msg = nil
|
8
8
|
msg, params = params, {} if msg.nil? && params.kind_of?(String)
|
9
9
|
|
10
|
+
assert_kind_of KineticCafe::ErrorDSL, actual,
|
11
|
+
msg || "Expected #{actual} to be #{expected}, but it was not."
|
12
|
+
|
10
13
|
assert_kind_of expected, actual,
|
11
14
|
msg || "Expected #{actual} to be #{expected}, but it was not."
|
12
15
|
|
@@ -34,14 +37,36 @@ module Minitest #:nodoc:
|
|
34
37
|
end
|
35
38
|
|
36
39
|
# Assert that a reponse body (<tt>@response.body</tt>, useful from
|
37
|
-
# ActionController::TestCase) is
|
40
|
+
# ActionController::TestCase) is HTML for the expected error.
|
38
41
|
# convenience wrapper around #assert_kc_error_json.
|
42
|
+
def assert_response_kc_error_html expected, params = {}, msg = nil
|
43
|
+
msg, params = params, {} if msg.nil? && params.kind_of?(String)
|
44
|
+
|
45
|
+
msg ||= "Expected #{actual} to be HTML for #{expected}, but it was not."
|
46
|
+
|
47
|
+
assert_template 'kinetic_cafe_error/page', msg
|
48
|
+
assert_template 'kinetic_cafe_error/_table', msg
|
49
|
+
|
50
|
+
assert_match(/#{expected.i18n_key}/, @response.body, msg)
|
51
|
+
assert_response expected.new.status, msg
|
52
|
+
end
|
53
|
+
|
54
|
+
# Assert that a reponse body (<tt>@response.body</tt>, useful from
|
55
|
+
# ActionController::TestCase) is JSON for the expected error. This is a
|
56
|
+
# convenience wrapper around #assert_kc_error_json or
|
57
|
+
# #assert_kc_error_html, depending on whether or not the response is HTML
|
58
|
+
# or not.
|
39
59
|
def assert_response_kc_error expected, params = {}, msg = nil
|
40
60
|
if msg.nil? && params.kind_of?(String)
|
41
61
|
msg, params = params, {}
|
42
62
|
end
|
43
63
|
msg ||= "Expected response to be #{expected}, but was not."
|
44
|
-
|
64
|
+
|
65
|
+
if @request.format.html?
|
66
|
+
assert_response_kc_error_html expected, params, msg
|
67
|
+
else
|
68
|
+
assert_kc_error_json expected, @response.body, params, msg
|
69
|
+
end
|
45
70
|
end
|
46
71
|
|
47
72
|
Minitest::Test.send(:include, self)
|
@@ -66,22 +66,26 @@ module KineticCafe
|
|
66
66
|
# define_error class: :object, status: :not_found
|
67
67
|
#
|
68
68
|
# will create an +ObjectNotFound+ error class.
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
69
|
+
# +header+:: Indicates that when this is caught, it should not be returned
|
70
|
+
# with full details, but should instead be treated as a
|
71
|
+
# header-only API response. Also available as +header_only+.
|
72
|
+
# +internal+:: Generates a response that indicates to clients that the
|
73
|
+
# error should not be shown to users.
|
74
|
+
# +i18n_params+:: An array of parameter names that are expected to be
|
75
|
+
# provided for translation. This helps document the
|
76
|
+
# expected translations.
|
73
77
|
def define_error(options)
|
74
78
|
fail ArgumentError, 'invalid options' unless options.kind_of?(Hash)
|
75
79
|
fail ArgumentError, 'define what error?' if options.empty?
|
76
80
|
|
77
81
|
options = options.dup
|
82
|
+
status = options[:status]
|
78
83
|
|
79
84
|
klass = options.delete(:class)
|
80
85
|
if klass
|
81
86
|
if options.key?(:key)
|
82
87
|
fail ArgumentError, ":key conflicts with class:#{klass}"
|
83
88
|
end
|
84
|
-
status = options[:status]
|
85
89
|
|
86
90
|
key = if status.kind_of?(Symbol) or status.kind_of?(String)
|
87
91
|
"#{klass}_#{KineticCafe::ErrorDSL.namify(status)}"
|
@@ -89,8 +93,7 @@ module KineticCafe
|
|
89
93
|
"#{klass}_#{KineticCafe::ErrorDSL.namify(self.name)}"
|
90
94
|
end
|
91
95
|
else
|
92
|
-
|
93
|
-
key = options.fetch(:key) {
|
96
|
+
key = options.fetch(:key) {
|
94
97
|
fail ArgumentError, 'one of :key or :class must be provided'
|
95
98
|
}.to_s
|
96
99
|
end
|
@@ -116,13 +119,19 @@ module KineticCafe
|
|
116
119
|
error = Class.new(self)
|
117
120
|
error.send :define_method, :name, -> { key }
|
118
121
|
error.send :define_method, :i18n_key, -> { i18n_key }
|
122
|
+
error.send :define_singleton_method, :i18n_key, -> { i18n_key }
|
119
123
|
|
120
|
-
if options[:header_only]
|
121
|
-
error.send :define_method, :
|
124
|
+
if options[:header] || options[:header_only]
|
125
|
+
error.send :define_method, :header?, -> { true }
|
126
|
+
error.send :alias_method, :header_only?, :header?
|
122
127
|
end
|
123
128
|
|
124
|
-
if options[:internal]
|
125
|
-
|
129
|
+
error.send :define_method, :internal?, -> { true } if options[:internal]
|
130
|
+
|
131
|
+
i18n_params = options[:i18n_params]
|
132
|
+
if i18n_params || !error.respond_to?(:i18n_params)
|
133
|
+
i18n_params = Array(i18n_params).freeze
|
134
|
+
error.send :define_singleton_method, :i18n_params, -> { i18n_params }
|
126
135
|
end
|
127
136
|
|
128
137
|
status ||= defined?(Rack::Utils) && :bad_request || 400
|
@@ -134,10 +143,6 @@ module KineticCafe
|
|
134
143
|
const_set(error_name, error)
|
135
144
|
end
|
136
145
|
|
137
|
-
##
|
138
|
-
#
|
139
|
-
|
140
|
-
|
141
146
|
private
|
142
147
|
|
143
148
|
##
|
@@ -151,13 +156,28 @@ module KineticCafe
|
|
151
156
|
fail "#{self} cannot extend #{base} (not a StandardError)"
|
152
157
|
end
|
153
158
|
|
154
|
-
if
|
159
|
+
rack_status = base.__rack_status if base.respond_to?(:__rack_status)
|
160
|
+
|
161
|
+
case rack_status
|
162
|
+
when Hash
|
163
|
+
rack_status = { methods: true, errors: true }.merge(rack_status)
|
164
|
+
when true, nil
|
165
|
+
rack_status = {}.freeze
|
166
|
+
when false
|
167
|
+
rack_status = { methods: false, errors: false }.freeze
|
168
|
+
end
|
169
|
+
|
170
|
+
if defined?(Rack::Utils) && rack_status
|
155
171
|
Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |name, value|
|
156
|
-
|
157
|
-
|
172
|
+
if rack_status.fetch(:methods, true)
|
173
|
+
base.singleton_class.send :define_method, name do |options = {}|
|
174
|
+
define_error(options.merge(status: name))
|
175
|
+
end
|
158
176
|
end
|
159
177
|
|
160
|
-
|
178
|
+
if rack_status.fetch(:errors, true)
|
179
|
+
base.send :define_error, status: name, key: name
|
180
|
+
end
|
161
181
|
end
|
162
182
|
end
|
163
183
|
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module KineticCafe # :nodoc:
|
2
|
+
# The core functionality provided by a KineticCafe::Error, extracted to a
|
3
|
+
# module to ensure that exceptions that are made hosts of error hierarchies
|
4
|
+
# have expected functionality.
|
5
|
+
module ErrorModule
|
6
|
+
# The HTTP status to be returned. If not provided in the constructor, uses
|
7
|
+
# #default_status.
|
8
|
+
attr_reader :status
|
9
|
+
# Extra data relevant to recipients of the exception, provided on
|
10
|
+
# construction.
|
11
|
+
attr_reader :extra
|
12
|
+
# The exception that caused this exception; provided on construction.
|
13
|
+
attr_reader :cause
|
14
|
+
|
15
|
+
# Create a new error with the given parameters.
|
16
|
+
#
|
17
|
+
# === Options
|
18
|
+
#
|
19
|
+
# +message+:: A message override. This may be provided either as the first
|
20
|
+
# parameter to the constructor or may be provided as an option.
|
21
|
+
# A value provided as the first parameter overrides any other
|
22
|
+
# value.
|
23
|
+
# +status+:: An override to the HTTP status code to be returned by this
|
24
|
+
# error by default.
|
25
|
+
# +i18n_params+:: The parameters to be sent to I18n.translate with the
|
26
|
+
# #i18n_key.
|
27
|
+
# +cause+:: The exception that caused this error. Used to wrap an earlier
|
28
|
+
# exception.
|
29
|
+
# +extra+:: Extra data to be returned in the API representation of this
|
30
|
+
# exception.
|
31
|
+
# +query+:: A hash of parameters added to +i18n_params+, typically from
|
32
|
+
# Rails controller +params+ or model +where+ query. This hash
|
33
|
+
# will be converted into a string value similar to
|
34
|
+
# ActiveSupport#to_query.
|
35
|
+
#
|
36
|
+
# Any unmatched options will be added to +i18n_params+. Because of this,
|
37
|
+
# the following constructors are identical:
|
38
|
+
#
|
39
|
+
# KineticCafe::Error.new(i18n_params: { x: 1 })
|
40
|
+
# KineticCafe::Error.new(x: 1)
|
41
|
+
#
|
42
|
+
# :call-seq:
|
43
|
+
# new(message, options = {})
|
44
|
+
# new(options)
|
45
|
+
def initialize(*args)
|
46
|
+
options = args.last.kind_of?(Hash) ? args.pop.dup : {}
|
47
|
+
@message = args.shift
|
48
|
+
@message = options.delete(:message) if @message.nil? || @message.empty?
|
49
|
+
options.delete(:message)
|
50
|
+
|
51
|
+
@message && @message.freeze
|
52
|
+
|
53
|
+
@status = options.delete(:status) || default_status
|
54
|
+
@i18n_params = options.delete(:i18n_params) || {}
|
55
|
+
@extra = options.delete(:extra)
|
56
|
+
@cause = options.delete(:cause)
|
57
|
+
|
58
|
+
@i18n_params.update(cause: cause.message) if cause
|
59
|
+
|
60
|
+
query = options.delete(:query)
|
61
|
+
@i18n_params.merge!(query: stringify(query)) if query
|
62
|
+
@i18n_params.merge!(options)
|
63
|
+
@i18n_params.freeze
|
64
|
+
end
|
65
|
+
|
66
|
+
# The message associated with this exception. If not provided, defaults to
|
67
|
+
# #i18n_message.
|
68
|
+
def message
|
69
|
+
@message || i18n_message
|
70
|
+
end
|
71
|
+
|
72
|
+
# The name of the error class.
|
73
|
+
def name
|
74
|
+
@name ||= KineticCafe::ErrorDSL.namify(self.class.name)
|
75
|
+
end
|
76
|
+
|
77
|
+
# The key used for I18n translation.
|
78
|
+
def i18n_key
|
79
|
+
@i18n_key ||= if self.class.respond_to? :i18n_key
|
80
|
+
self.class.i18n_key
|
81
|
+
else
|
82
|
+
[
|
83
|
+
i18n_key_base, (name)
|
84
|
+
].join('.').freeze
|
85
|
+
end
|
86
|
+
end
|
87
|
+
alias_method :code, :i18n_key
|
88
|
+
|
89
|
+
# Indicates that this error should *not* have its details rendered to the
|
90
|
+
# user, but should use the +head+ method.
|
91
|
+
def header?
|
92
|
+
false
|
93
|
+
end
|
94
|
+
alias_method :header_only?, :header?
|
95
|
+
|
96
|
+
# Indicates that this error should be rendered to the client, but clients
|
97
|
+
# are advised *not* to display the message to the user.
|
98
|
+
def internal?
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
# The I18n translation of the message. If I18n.translate is defined,
|
103
|
+
# returns #i18n_key and the I18n parameters.
|
104
|
+
def i18n_message
|
105
|
+
@i18n_message ||= if defined?(I18n.translate)
|
106
|
+
I18n.translate(i18n_key, @i18n_params).freeze
|
107
|
+
else
|
108
|
+
[ i18n_key, @i18n_params ].freeze
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# The details of this error as a hash. Values that are empty or nil are
|
113
|
+
# omitted.
|
114
|
+
def api_error(*)
|
115
|
+
{
|
116
|
+
message: @message,
|
117
|
+
status: status,
|
118
|
+
name: name,
|
119
|
+
internal: internal?,
|
120
|
+
i18n_message: i18n_message,
|
121
|
+
i18n_key: i18n_key,
|
122
|
+
i18n_params: @i18n_params,
|
123
|
+
cause: cause && cause.message,
|
124
|
+
extra: extra
|
125
|
+
}.delete_if { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
|
126
|
+
end
|
127
|
+
alias_method :as_json, :api_error
|
128
|
+
|
129
|
+
# An error result that can be passed as a response body.
|
130
|
+
def error_result
|
131
|
+
{ error: api_error, message: message }
|
132
|
+
end
|
133
|
+
|
134
|
+
# A hash that can be passed to the Rails +render+ method with +status+ of
|
135
|
+
# #status and +layout+ false. The +json+ field is rendered as a hash of
|
136
|
+
# +error+ (calling #api_error) and +message+ (calling #message).
|
137
|
+
def json_result
|
138
|
+
{ status: status, json: error_result, layout: false }
|
139
|
+
end
|
140
|
+
alias_method :render_json_for_rails, :json_result
|
141
|
+
|
142
|
+
# Nice debugging version of a KineticCafe::Error
|
143
|
+
def inspect
|
144
|
+
"#<#{self.class}: name=#{name} status=#{status} " \
|
145
|
+
"message=#{message.inspect} i18n_key=#{i18n_key} " \
|
146
|
+
"i18n_params=#{@i18n_params.inspect} extra=#{extra.inspect} " \
|
147
|
+
"cause=#{cause}>"
|
148
|
+
end
|
149
|
+
|
150
|
+
class << self
|
151
|
+
##
|
152
|
+
# The base for I18n key resolution. Defaults to 'kcerrors'.
|
153
|
+
#
|
154
|
+
# :method: i18n_key_base
|
155
|
+
|
156
|
+
##
|
157
|
+
# The names of the expected parameters for this error. Defaults to [].
|
158
|
+
#
|
159
|
+
# :method: i18n_params
|
160
|
+
|
161
|
+
##
|
162
|
+
# The i18n_key for the parameter. Defaults to the combination of
|
163
|
+
# i18n_key_base and the namified version of the class (see
|
164
|
+
# KineticCafe::ErrorDSL.namify).
|
165
|
+
#
|
166
|
+
# :method: i18n_key
|
167
|
+
|
168
|
+
##
|
169
|
+
def included(mod)
|
170
|
+
unless mod.respond_to?(:i18n_key_base)
|
171
|
+
mod.send :define_singleton_method, :i18n_key_base do
|
172
|
+
'kcerrors'.freeze
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
unless mod.respond_to?(:i18n_params)
|
177
|
+
mod.send :define_singleton_method, :i18n_params do
|
178
|
+
[].freeze
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
unless mod.respond_to?(:i18n_key)
|
183
|
+
mod.send :define_singleton_method, :i18n_key do
|
184
|
+
@i18n_key ||= [
|
185
|
+
i18n_key_base, KineticCafe::ErrorDSL.namify(name)
|
186
|
+
].join('.').freeze
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -12,6 +12,14 @@ module KineticCafe
|
|
12
12
|
# +be_json_for+:: Verifies that the expected value is a JSON representation
|
13
13
|
# of the actual value. If the actual value responds to
|
14
14
|
# #body, the actual value is replaced with +actual.body+.
|
15
|
+
#
|
16
|
+
# +be_kc_error+:: Verifies that the expected value is a KineticCafe::Error.
|
17
|
+
#
|
18
|
+
# +be_kc_error_json+:: Verifies that the JSON value matches the output of
|
19
|
+
# KineticCafe::Error.
|
20
|
+
#
|
21
|
+
# +be_kc_error_html+:: Verifies that the rendered HTML matches the output of
|
22
|
+
# KineticCafe::Error.
|
15
23
|
module ErrorRspec
|
16
24
|
extend ::Rspec::Matchers::DSL
|
17
25
|
|
@@ -30,6 +38,7 @@ module KineticCafe
|
|
30
38
|
|
31
39
|
matcher :be_kc_error do |expected, params = {}|
|
32
40
|
match do |actual|
|
41
|
+
expect(actual).to be_kind_of(KineticCafe::ErrorModule)
|
33
42
|
expect(actual).to be_kind_of(expected)
|
34
43
|
expect(actual).to eq(expected.new(params))
|
35
44
|
end
|
@@ -45,5 +54,13 @@ module KineticCafe
|
|
45
54
|
|
46
55
|
diffable
|
47
56
|
end
|
57
|
+
|
58
|
+
matcher :be_kc_error_html do |expected, params = {}|
|
59
|
+
match do |actual|
|
60
|
+
expect(actual).to render_template('kinetic_cafe_error/page')
|
61
|
+
expect(actual).to render_template('kinetic_cafe_error/_table')
|
62
|
+
expect(actual).to include(expected.i18n_key)
|
63
|
+
end
|
64
|
+
end
|
48
65
|
end
|
49
66
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
namespace :kcerror do
|
2
|
+
desc 'Show defined errors.'
|
3
|
+
task defined: 'kcerror:find' do
|
4
|
+
display = ->(root, prefix = '') {
|
5
|
+
puts "#{prefix}- #{root}"
|
6
|
+
|
7
|
+
if @descendants[root]
|
8
|
+
sorted = @descendants[root].sort_by(&:to_s)
|
9
|
+
sorted.each do |child|
|
10
|
+
s = (child == sorted.last) ? '`' : '|'
|
11
|
+
display.(child, "#{prefix.tr('|`', ' ')} #{s}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
@descendants[StandardError].sort_by(&:to_s).each { |d| display.(d) }
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Generate a sample translation key file.'
|
20
|
+
task :translations, [ :output ] => 'kcerror:find' do |_, args|
|
21
|
+
translations = {}
|
22
|
+
traverse = ->(root) {
|
23
|
+
translation = (translations[root.i18n_key_base] ||= {})
|
24
|
+
name = KineticCafe::ErrorDSL.namify(root)
|
25
|
+
|
26
|
+
params = root.i18n_params.map { |param| "%{#{param}}" }.join(' ')
|
27
|
+
|
28
|
+
if params.empty?
|
29
|
+
translation[name] = %Q(Translation for #{name} with no params.)
|
30
|
+
else
|
31
|
+
translation[name] = %Q(Translation for #{name} with #{params}.)
|
32
|
+
end
|
33
|
+
|
34
|
+
if @descendants[root]
|
35
|
+
@descendants[root].sort_by(&:to_s).each { |child| traverse.(child) }
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
@descendants[StandardError].sort_by(&:to_s).each { |d| traverse.(d) }
|
40
|
+
|
41
|
+
require 'yaml'
|
42
|
+
translations = YAML.dump({ 'en' => translations })
|
43
|
+
|
44
|
+
if args.output
|
45
|
+
File.open(args.output, 'w') { |f| f.write translations }
|
46
|
+
else
|
47
|
+
puts translations
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
task :find do
|
52
|
+
@descendants = {}
|
53
|
+
ObjectSpace.each_object(Class) do |k|
|
54
|
+
next unless k.singleton_class < KineticCafe::ErrorDSL
|
55
|
+
|
56
|
+
(@descendants[k.superclass] ||= []) << k
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|