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.
@@ -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