Class: Hoodoo::Errors

Inherits:
Object
  • Object
show all
Defined in:
lib/hoodoo/errors/errors.rb

Overview

During request processing, API service implementations create an Hoodoo::Errors instance and add error(s) to the collection as they arise using #add_error. That same instance can then be returned for the on-error handling path of whatever wider service framework is in use by the service code in question. Services should use new instances for each request.

Defined Under Namespace

Classes: MissingReferenceData, UnknownCode

Constant Summary collapse

DEFAULT_ERROR_DESCRIPTIONS =

Default Hoodoo::ErrorDescriptions instance, used if the instantiator provides no alternative.

Hoodoo::ErrorDescriptions.new()

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(descriptions = DEFAULT_ERROR_DESCRIPTIONS) ⇒ Errors

Create an instance.

descriptions

(Optional) Hoodoo::ErrorDescriptions instance with service-domain-specific error descriptions added, or omit for a default instance describing platform and generic error domains only.



71
72
73
74
75
76
77
# File 'lib/hoodoo/errors/errors.rb', line 71

def initialize( descriptions = DEFAULT_ERROR_DESCRIPTIONS )
  @uuid             = Hoodoo::UUID.generate()
  @descriptions     = descriptions
  @errors           = []
  @http_status_code = 200
  @created_at       = nil # See #persist!
end

Instance Attribute Details

#descriptionsObject (readonly)

The Hoodoo::ErrorDescriptions instance associated with this error collection. Only error codes that the instance’s Hoodoo::ErrorDescriptions#recognised? method says are recognised can be added to the error collection, else Hoodoo::Errors::UnknownCode will be raised.



62
63
64
# File 'lib/hoodoo/errors/errors.rb', line 62

def descriptions
  @descriptions
end

#errorsObject (readonly)

Array of error data - hashes with code, message and reference fields giving the error codes, human-readable messages and machine-readable reference data, where appropriate.



49
50
51
# File 'lib/hoodoo/errors/errors.rb', line 49

def errors
  @errors
end

#http_status_codeObject (readonly)

HTTP status code associated with the first error in the #errors array, _as an Integer_.



54
55
56
# File 'lib/hoodoo/errors/errors.rb', line 54

def http_status_code
  @http_status_code
end

#uuidObject (readonly)

Errors are manifestations of the Errors resource. They acquire a UUID when instantiated, even if the instance is never used or persisted.



43
44
45
# File 'lib/hoodoo/errors/errors.rb', line 43

def uuid
  @uuid
end

Instance Method Details

#add_error(code, options = nil) ⇒ Object

Add an error instance to this collection.

code

Error code in full, e.g. +generic.invalid_state’.

options

An options Hash, optional.

The options hash contains symbol keys named as follows, with values as described:

reference

Reference data Hash, optionality depending upon the error code and the reference data its error description mandates. Provide key/value pairs where (symbol) keys are names from the array of description requirements and values are strings. All values are concatenated into a single string, comma-separated. Commas within values are escaped with a backslash; backslash is itself escaped with a backslash.

You must provide that data at a minimum, but can provide additional keys too if you so wish. Required keys are always included first, in order of appearance in the requirements array of the error declaration, followed by any extra values in undefined order.

See also Hoodoo::ErrorDescriptions::DomainDescriptions#error

message

Optional human-readable for-developer message, en-nz locale. Default messages are provided for all errors, but if you think you can provide something more informative, you can do so through this parameter.

Example:

errors.add_error(
  'platform.not_found',
  :message => 'Optional custom message',
  :reference => { :entity_name => 'mandatory reference data' }
)

In the above example, the mandatory reference data entity_name comes from the description for the ‘platform.not_found’ message - see the Hoodoo::ErrorDescriptions#initialize implementation and Platform API.

Raises:



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/hoodoo/errors/errors.rb', line 121

def add_error( code, options = nil )

  options   = Hoodoo::Utilities.stringify( options || {} )
  reference = options[ 'reference' ] || {}
  message   = options[ 'message' ]

  # Make sure nobody uses an undeclared error code.

  raise UnknownCode, "In \#add_error: Unknown error code '#{code}'" unless @descriptions.recognised?( code )

  # If the error description specifies a list of required reference keys,
  # make sure all are present and complain if not.

  description = @descriptions.describe( code )

  required_keys = description[ 'reference' ] || []
  actual_keys   = reference.keys
  missing_keys  = required_keys - actual_keys

  unless missing_keys.empty?
    raise MissingReferenceData, "In \#add_error: Reference hash missing required keys: '#{ missing_keys.join( ', ' ) }'"
  end

  # All good!

  @http_status_code = ( description[ 'status' ] || 200 ).to_i if @errors.empty? # Use first in collection for overall HTTP status code

  error = {
    'code'    => code,
    'message' => message || description[ 'message' ] || code
  }

  ordered_keys   = required_keys + ( actual_keys - required_keys )
  ordered_values = ordered_keys.map { | key | escape_commas( reference[ key ].to_s ) }

  # See #unjoin_and_unescape_commas to undo the join below.

  error[ 'reference' ] = ordered_values.join( ',' ) unless ordered_values.empty?

  @errors << error
end

#add_precompiled_error(code, message, reference, http_status = 500) ⇒ Object

Add a precompiled error to the error collection. Pass error code, error message and reference data directly.

In most cases you should be calling #add_error instead, NOT here.

No validation is performed. You should only really call here if storing an error / errors from another, trusted source with assumed validity (e.g. another service called remotely with errors in the JSON response). It’s possible to store invalid error data using this call, which means counter-to-documentation results could be returned to API clients. That is Very Bad.

Pass optionally the HTTP status code to use if this happens to be the first stored error. If this is omitted, 500 is kept as the default.



178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/hoodoo/errors/errors.rb', line 178

def add_precompiled_error( code, message, reference, http_status = 500 )
  @http_status_code = http_status.to_i if @errors.empty?

  error = {
    'code'    => code,
    'message' => message
  }

  error[ 'reference' ] = reference unless reference.nil? || reference.empty?

  @errors << error
end

#clear_errorsObject

Clear (delete) all previously added errors (if any). After calling here, #has_errors? would always return false.



227
228
229
230
# File 'lib/hoodoo/errors/errors.rb', line 227

def clear_errors
  @errors           = []
  @http_status_code = 200
end

#has_errors?Boolean

Does this instance have any errors added? Returns true if so, else false.

Returns:

  • (Boolean)


220
221
222
# File 'lib/hoodoo/errors/errors.rb', line 220

def has_errors?
  ! @errors.empty?
end

#inspectObject

Make life easier for debugging on the console by having the object represent itself more concisely.



258
259
260
# File 'lib/hoodoo/errors/errors.rb', line 258

def inspect
  @errors.to_s
end

#merge!(source) ⇒ Object

Merge the contents of a source error object with this one, adding its errors to this collection. No checks are made for duplicates (in part because, depending on error code and source/target contexts, a duplicate may be a valid thing to have).

source

Hoodoo::Errors instance to merge into the error collection of ‘this’ target object.

Returns true if errors were merged, else false (the source collection was empty).



202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/hoodoo/errors/errors.rb', line 202

def merge!( source )
  source_errors = source.errors

  source_errors.each do | hash |
    add_precompiled_error(
      hash[ 'code'      ],
      hash[ 'message'   ],
      hash[ 'reference' ],
      source.http_status_code
    )
  end

  return ! source_errors.empty?
end

#render(interaction_id) ⇒ Object

Return a Hash rendered through the Hoodoo::Data::Resources::Errors collection representing the formalised resource.

interaction_id

Mandatory Interaction ID (UUID) to associate with the collection.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/hoodoo/errors/errors.rb', line 238

def render( interaction_id )
  unless Hoodoo::UUID.valid?( interaction_id )
    raise "Hoodoo::Errors\#render must be given a valid Interaction ID (got '#{ interaction_id.inspect }')"
  end

  @created_at ||= Time.now

  Hoodoo::Data::Resources::Errors.render(
    {
      'interaction_id' => interaction_id,
      'errors'         => @errors
    },
    @uuid,
    @created_at
  )
end

#unjoin_and_unescape_commas(str) ⇒ Object

When reference data is specified for errors, the reference values are concatenated together into a comma-separated string. Since reference values can themselves contain commas, comma is escaped with “\,” and “\” escaped with “\\”.

Call here with such a string; return an array of ‘unescaped’ values.

str

Value-escaped (“\\” / “\,”) comma-separated string. Unescaped commas separate individual values.



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/hoodoo/errors/errors.rb', line 278

def unjoin_and_unescape_commas( str )

  # In Ruby regular expressions, '(?<!pat)' is a negative lookbehind
  # assertion, making sure that the preceding characters do not match
  # 'pat'. To split the string joined on ',' to an array but not splitting
  # any escaped '\,', then, we can use this rather opaque split regexp:
  #
  #   error[ 'reference' ].split( /(?<!\\),/ )
  #
  # I.e. split on ',', provided it is not preceded by a '\' (escaped in the
  # regexp to '\\').

  ary = str.split( /(?<!\\),/ )
  ary.map { | entry | unescape_commas( entry ) }
end