kinetic_cafe_error 1.1 → 1.2
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 +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
|