kinetic_cafe_error 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>&nbsp;</td><td>%{status}</td>
16
+ </tr>
17
+ <tr>
18
+ <th>Code</th><td>&nbsp;</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>&nbsp;</td><td>%{status}</td>
16
+ </tr>
17
+ <tr>
18
+ <th>Code</th><td>&nbsp;</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>&nbsp;</td><td>%{status}</td>
16
+ </tr>
17
+ <tr>
18
+ <th>Code</th><td>&nbsp;</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