erratum 0.0.1

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