erratum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.txt +19 -0
  4. data/README.md +2 -0
  5. data/Rakefile +2 -0
  6. data/lib/erratum/configuration.rb +44 -0
  7. data/lib/erratum/error.rb +110 -0
  8. data/lib/erratum/errors/authentication_error.rb +9 -0
  9. data/lib/erratum/errors/authentication_errors/duplicate_authentication_error.rb +37 -0
  10. data/lib/erratum/errors/authentication_errors/invalid_token_error.rb +30 -0
  11. data/lib/erratum/errors/authentication_errors/invalid_username_or_password_error.rb +30 -0
  12. data/lib/erratum/errors/authorization_error.rb +9 -0
  13. data/lib/erratum/errors/authorization_errors/forbidden_error.rb +48 -0
  14. data/lib/erratum/errors/crud_error.rb +24 -0
  15. data/lib/erratum/errors/crud_errors/association_error.rb +54 -0
  16. data/lib/erratum/errors/crud_errors/resource_not_found_error.rb +52 -0
  17. data/lib/erratum/errors/crud_errors/resource_persistence_error.rb +50 -0
  18. data/lib/erratum/errors/request_errors/parameter_missing_error.rb +43 -0
  19. data/lib/erratum/errors/request_errors/unpermitted_parameters_error.rb +49 -0
  20. data/lib/erratum/rescuable_resource.rb +29 -0
  21. data/lib/erratum/resource_naming.rb +31 -0
  22. data/lib/erratum/utilities/string.rb +18 -0
  23. data/lib/erratum/verifiable_resource.rb +23 -0
  24. data/lib/erratum/version.rb +4 -0
  25. data/lib/erratum.rb +48 -0
  26. data/spec/lib/erratum/configuration_spec.rb +27 -0
  27. data/spec/lib/erratum/error_spec.rb +189 -0
  28. data/spec/lib/erratum/errors/authentication_errors/duplicate_authentication_error_spec.rb +43 -0
  29. data/spec/lib/erratum/errors/authentication_errors/invalid_token_error_spec.rb +33 -0
  30. data/spec/lib/erratum/errors/authentication_errors/invalid_username_or_password_error_spec.rb +35 -0
  31. data/spec/lib/erratum/errors/authorization_errors/forbidden_error_spec.rb +52 -0
  32. data/spec/lib/erratum/errors/crud_errors/association_error_spec.rb +69 -0
  33. data/spec/lib/erratum/errors/crud_errors/resource_not_found_error_spec.rb +89 -0
  34. data/spec/lib/erratum/errors/crud_errors/resource_persistence_error_spec.rb +84 -0
  35. data/spec/lib/erratum/errors/request_errors/parameter_missing_error_spec.rb +58 -0
  36. data/spec/lib/erratum/errors/request_errors/unpermitted_parameters_error_spec.rb +67 -0
  37. data/spec/lib/erratum/rescuable_resource_spec.rb +8 -0
  38. data/spec/lib/human_error_spec.rb +20 -0
  39. data.tar.gz.sig +0 -0
  40. metadata +171 -0
  41. metadata.gz.sig +0 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c87aeb2c02c44b93d2d11f4c3c253fe229b72c44
4
+ data.tar.gz: d596655b0085b9e0a22e1725235282a2c1f87964
5
+ SHA512:
6
+ metadata.gz: 9afbb9199a4a7972ed0b0fea7ec56611ccb412bf504835eb58b1a102bcd3407a15add805c1bfbe1277dc9aa308256b2b1466eddbd9f00daf43aaef537e46966f
7
+ data.tar.gz: 7daab0272eeb312e81e663c792199d799e9511f596f9cde1f304c21659331b7d50b92acc38993d726f4c59507faffe414fc7192af72abbe3db4a1396c8c2aa52
checksums.yaml.gz.sig ADDED
Binary file
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2016 The Grand Design
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ Erratum
2
+ ================================================================================
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ class Erratum
5
+ class Configuration
6
+ include Singleton
7
+
8
+ attr_accessor :url_mappings
9
+
10
+ def external_documentation_urls
11
+ @external_documentation_urls ||= url_mappings['external_documentation_urls']
12
+ end
13
+
14
+ def developer_documentation_urls
15
+ @developer_documentation_urls ||= url_mappings['developer_documentation_urls']
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ external_documentation_urls: external_documentation_urls,
21
+ developer_documentation_urls: developer_documentation_urls,
22
+ }
23
+ end
24
+
25
+ def url_mappings
26
+ @url_mappings ||= {
27
+ 'external_documentation_urls' => {},
28
+ 'developer_documentation_urls' => {},
29
+ }
30
+ end
31
+ end
32
+
33
+ def configuration
34
+ Configuration.instance
35
+ end
36
+
37
+ def self.configure
38
+ yield configuration
39
+ end
40
+
41
+ def self.configuration
42
+ Configuration.instance
43
+ end
44
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ require 'erratum/configuration'
4
+ require 'erratum/utilities/string'
5
+
6
+ class Erratum
7
+ module Error
8
+ module ClassMethods
9
+ def wrap(other)
10
+ wrapped_error = new message: "#{other.class.name}: #{other.message}"
11
+ wrapped_error.set_backtrace other.backtrace
12
+ wrapped_error
13
+ end
14
+ end
15
+
16
+ attr_accessor :id,
17
+ :external_documentation_url,
18
+ :developer_documentation_url,
19
+ :http_status,
20
+ :code,
21
+ :title,
22
+ :detail,
23
+ :source,
24
+ :message
25
+
26
+ def initialize(**args)
27
+ args.each do |variable, value|
28
+ public_send("#{variable}=", value)
29
+ end
30
+ end
31
+
32
+ def as_json(_options = {})
33
+ {
34
+ errors: [
35
+ {
36
+ id: id,
37
+ links: {
38
+ about: external_documentation_url,
39
+ documentation: developer_documentation_url,
40
+ },
41
+ status: http_status,
42
+ code: code,
43
+ title: title,
44
+ detail: detail,
45
+ source: source,
46
+ },
47
+ ],
48
+ }
49
+ end
50
+
51
+ def to_json(_options = {})
52
+ JSON.dump(as_json)
53
+ end
54
+
55
+ def id
56
+ @id ||= SecureRandom.uuid
57
+ end
58
+
59
+ def external_documentation_url
60
+ @external_documentation_url ||= configuration.external_documentation_urls[code]
61
+ end
62
+
63
+ def developer_documentation_url
64
+ @developer_documentation_url ||= configuration.developer_documentation_urls[code]
65
+ end
66
+
67
+ def http_status
68
+ @http_status ||= 500
69
+ end
70
+
71
+ alias status http_status
72
+
73
+ def code
74
+ @code ||= Erratum::Utilities::String.
75
+ underscore(self.class.name).
76
+ gsub(%r{\A[^/]+/}, '').
77
+ gsub(%r{/}, '.')
78
+ end
79
+
80
+ def title
81
+ @title ||= self.class.name
82
+ end
83
+
84
+ def detail
85
+ @detail ||= 'The server encountered an error.'
86
+ end
87
+
88
+ def source
89
+ @source ||= {}
90
+ end
91
+
92
+ def message
93
+ to_s
94
+ end
95
+
96
+ def to_s
97
+ @message || detail
98
+ end
99
+
100
+ def self.included(base)
101
+ base.extend ClassMethods
102
+ end
103
+
104
+ private
105
+
106
+ def configuration
107
+ Erratum.configuration
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ module AuthenticationError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/authentication_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class DuplicateAuthenticationError < RuntimeError
7
+ include Error
8
+ include AuthenticationError
9
+
10
+ attr_accessor :provider,
11
+ :provider_user_id,
12
+ :user_id
13
+
14
+ def http_status
15
+ 409
16
+ end
17
+
18
+ def title
19
+ 'Duplicate Authentication'
20
+ end
21
+
22
+ def detail
23
+ 'The authentication you attempted to register has already been registered by ' \
24
+ 'another user. We do not currently support allowing multiple users to be connected ' \
25
+ 'to the same authentication.'
26
+ end
27
+
28
+ def source
29
+ {
30
+ 'provider' => provider,
31
+ 'provider_user_id' => provider_user_id,
32
+ 'user_id' => user_id,
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/authentication_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class InvalidTokenError < RuntimeError
7
+ include Error
8
+ include AuthenticationError
9
+
10
+ attr_accessor :authentication_token
11
+
12
+ def http_status
13
+ 401
14
+ end
15
+
16
+ def title
17
+ 'Invalid Token'
18
+ end
19
+
20
+ def detail
21
+ 'The token you attempted to use for this request is invalid for this resource. ' \
22
+ 'Please double-check and try again.'
23
+ end
24
+
25
+ def source
26
+ { token: '[FILTERED]' }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/authentication_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class InvalidUsernameOrPasswordError < RuntimeError
7
+ include Error
8
+ include AuthenticationError
9
+
10
+ attr_accessor :username
11
+
12
+ def http_status
13
+ 401
14
+ end
15
+
16
+ def title
17
+ 'Invalid Username/Password'
18
+ end
19
+
20
+ def detail
21
+ 'Either the username or password passed in or this request is invalid. Please ' \
22
+ 'double-check and try again.'
23
+ end
24
+
25
+ def source
26
+ { username: username, password: '[FILTERED]' }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ module AuthorizationError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/authorization_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class ForbiddenError < RuntimeError
7
+ include Error
8
+ include AuthorizationError
9
+
10
+ NON_SPECIFIC_RESOURCE_ACTIONS = %w{index create}.freeze
11
+
12
+ attr_accessor :resource_name,
13
+ :resource_id,
14
+ :action
15
+
16
+ def http_status
17
+ 403
18
+ end
19
+
20
+ def title
21
+ 'Forbidden'
22
+ end
23
+
24
+ def detail
25
+ detail_quantity = if NON_SPECIFIC_RESOURCE_ACTIONS.include? action.to_s
26
+ "#{action} a #{resource_name}"
27
+ else
28
+ "#{action} the #{resource_name} with ID #{resource_id}"
29
+ end
30
+
31
+ "You do not have access to #{detail_quantity}. Providing a different set of " \
32
+ "credentials may potentially allow you access to this resource."
33
+ end
34
+
35
+ def source
36
+ @source ||= {
37
+ 'resource_name' => resource_name,
38
+ 'resource_id' => resource_id,
39
+ 'action' => @action,
40
+ }
41
+ end
42
+
43
+ def action
44
+ @action || 'access'
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ class Erratum
3
+ module Errors
4
+ module CrudError
5
+ attr_accessor :resource_name,
6
+ :action,
7
+ :resource_id
8
+
9
+ def initialize(resource_name: nil, action: nil, resource_id: nil, **args)
10
+ self.resource_name = resource_name
11
+ self.action = action || 'persist'
12
+ self.resource_id = resource_id
13
+
14
+ super(**args)
15
+ end
16
+
17
+ private
18
+
19
+ def resource_name_underscored
20
+ @resource_name_underscored ||= resource_name.gsub(/\s/, '_')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/crud_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class AssociationError < RuntimeError
7
+ include Error
8
+ include CrudError
9
+
10
+ attr_accessor :association_name,
11
+ :association_id,
12
+ :attributes
13
+
14
+ def self.convert(original_error, overrides = {})
15
+ initialization_parameters = {}
16
+
17
+ case original_error.class.name
18
+ when 'ActiveRecord::InvalidForeignKey'
19
+ message_info_pattern = /DETAIL: Key \((.*)_id\)=\(([a-f0-9\-]+)\)/
20
+ message_info = original_error.
21
+ message.
22
+ match(message_info_pattern)[1..-1]
23
+
24
+ initialization_parameters = {
25
+ association_name: message_info[0],
26
+ association_id: message_info[1],
27
+ }
28
+ end
29
+
30
+ new(initialization_parameters.merge(overrides))
31
+ end
32
+
33
+ def http_status
34
+ 422
35
+ end
36
+
37
+ def title
38
+ 'Association Error'
39
+ end
40
+
41
+ def detail
42
+ "The #{association_name} that you attempted to associate with " \
43
+ "the #{resource_name} was not valid."
44
+ end
45
+
46
+ def source
47
+ {
48
+ resource_name => attributes,
49
+ "#{association_name} id" => association_id,
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/crud_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class ResourceNotFoundError < RuntimeError
7
+ include Error
8
+ include CrudError
9
+
10
+ def self.convert(original_error, overrides = {})
11
+ initialization_parameters = {}
12
+
13
+ case original_error.class.name
14
+ when 'ActiveRecord::RecordNotFound'
15
+ initialization_parameters = {
16
+ resource_id: case original_error.message
17
+ when /\ACouldn't find .* without an ID\z/
18
+ []
19
+ when /\ACouldn't find .* with \'.*\'=([a-f0-9\-]+)/
20
+ [Regexp.last_match(1)]
21
+ when /\ACouldn't find all .* with \'.*\': ((?:[a-f0-9\-]+(?:, )?)+)/
22
+ Array(Regexp.last_match(1).split(', '))
23
+ end,
24
+ }
25
+ end
26
+
27
+ new(initialization_parameters.merge(overrides))
28
+ end
29
+
30
+ def http_status
31
+ 404
32
+ end
33
+
34
+ def title
35
+ 'Resource Not Found'
36
+ end
37
+
38
+ def detail
39
+ "The #{resource_name} you attempted to #{action} for this request is either " \
40
+ "not authorized for the authenticated user or does not exist."
41
+ end
42
+
43
+ def source
44
+ { "#{resource_name_underscored}_id" => resource_id }
45
+ end
46
+
47
+ def action
48
+ @action || 'access'
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/errors/crud_error'
3
+
4
+ class Erratum
5
+ module Errors
6
+ class ResourcePersistenceError < RuntimeError
7
+ include Error
8
+ include CrudError
9
+
10
+ attr_accessor :errors,
11
+ :attributes
12
+
13
+ def self.convert(original_error, overrides = {})
14
+ initialization_parameters = {}
15
+
16
+ case original_error.class.name
17
+ when 'ActiveRecord::RecordInvalid',
18
+ 'ActiveRecord::RecordNotSaved'
19
+
20
+ initialization_parameters = {
21
+ attributes: original_error.record.attributes,
22
+ errors: original_error.record.errors.full_messages,
23
+ }
24
+ end
25
+
26
+ new(initialization_parameters.merge(overrides))
27
+ end
28
+
29
+ def http_status
30
+ 422
31
+ end
32
+
33
+ def title
34
+ 'Resource Persistence Error'
35
+ end
36
+
37
+ def detail
38
+ "One or more of the attributes on the #{resource_name} you attempted " \
39
+ "to #{action} is invalid."
40
+ end
41
+
42
+ def source
43
+ {
44
+ 'errors' => errors,
45
+ 'attributes' => attributes,
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ class Erratum
3
+ module Errors
4
+ class ParameterMissingError < RuntimeError
5
+ include Error
6
+ include CrudError
7
+
8
+ attr_accessor :parameter
9
+
10
+ def self.convert(original_error, overrides = {})
11
+ initialization_parameters = {}
12
+
13
+ case original_error.class.name
14
+ when 'ActionController::ParameterMissing'
15
+ initialization_parameters = {
16
+ parameter: original_error.param,
17
+ }
18
+ end
19
+
20
+ new(initialization_parameters.merge(overrides))
21
+ end
22
+
23
+ def http_status
24
+ 400
25
+ end
26
+
27
+ def title
28
+ 'Missing Parameter'
29
+ end
30
+
31
+ def detail
32
+ "When attempting to #{action} a #{resource_name}, '#{parameter}' is a " \
33
+ "required parameter."
34
+ end
35
+
36
+ def source
37
+ {
38
+ 'required_parameter' => parameter,
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ class Erratum
3
+ module Errors
4
+ class UnpermittedParametersError < RuntimeError
5
+ include Error
6
+ include CrudError
7
+
8
+ attr_accessor :parameters
9
+
10
+ def self.convert(original_error, overrides = {})
11
+ initialization_parameters = {}
12
+
13
+ case original_error.class.name
14
+ when 'ActionController::UnpermittedParameters'
15
+ initialization_parameters = {
16
+ parameters: Array(original_error.params),
17
+ }
18
+ end
19
+
20
+ new(initialization_parameters.merge(overrides))
21
+ end
22
+
23
+ def initialize(**attrs)
24
+ self.parameters = Array(attrs.delete(:parameters))
25
+
26
+ super(**attrs)
27
+ end
28
+
29
+ def http_status
30
+ 400
31
+ end
32
+
33
+ def title
34
+ 'Unpermitted Parameters'
35
+ end
36
+
37
+ def detail
38
+ "Attempting to #{action} a #{resource_name} with the following parameters is " \
39
+ "not allowed: #{parameters.join(', ')}"
40
+ end
41
+
42
+ def source
43
+ {
44
+ 'unpermitted_parameters' => parameters,
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/resource_naming'
3
+
4
+ class Erratum
5
+ module RescuableResource
6
+ def self.included(base)
7
+ base.include ResourceNaming
8
+
9
+ base.rescue_from 'ActiveRecord::RecordInvalid',
10
+ 'ActiveRecord::RecordNotSaved',
11
+ 'ActiveRecord::RecordNotFound',
12
+ 'ActiveRecord::InvalidForeignKey',
13
+ 'ActionController::ParameterMissing',
14
+ 'ActionController::UnpermittedParameters' do |exception|
15
+ erratum = Erratum.convert(exception,
16
+ resource_name: self.class.singular_resource_name,
17
+ action: action_name)
18
+
19
+ render json: erratum,
20
+ status: erratum.http_status
21
+ end
22
+
23
+ base.rescue_from 'Erratum::Error' do |exception|
24
+ render json: exception,
25
+ status: exception.http_status
26
+ end
27
+ end
28
+ end
29
+ end