zappb-exceptions 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +457 -0
- data/config/locales/en.yml +22 -0
- data/config/locales/es.yml +22 -0
- data/lib/zappb/exceptions.rb +3 -0
- data/lib/zappb_exception/base_catalog.rb +34 -0
- data/lib/zappb_exception/class_factory.rb +73 -0
- data/lib/zappb_exception/configuration.rb +86 -0
- data/lib/zappb_exception/definition.rb +101 -0
- data/lib/zappb_exception/describer.rb +33 -0
- data/lib/zappb_exception/detail_sanitizer.rb +18 -0
- data/lib/zappb_exception/envelope.rb +117 -0
- data/lib/zappb_exception/errors.rb +95 -0
- data/lib/zappb_exception/handler.rb +30 -0
- data/lib/zappb_exception/nats.rb +49 -0
- data/lib/zappb_exception/notifier.rb +75 -0
- data/lib/zappb_exception/rack.rb +15 -0
- data/lib/zappb_exception/railtie.rb +11 -0
- data/lib/zappb_exception/registry.rb +191 -0
- data/lib/zappb_exception/remote_error.rb +133 -0
- data/lib/zappb_exception/resolver.rb +17 -0
- data/lib/zappb_exception/rspec.rb +102 -0
- data/lib/zappb_exception/testing.rb +41 -0
- data/lib/zappb_exception/version.rb +5 -0
- data/lib/zappb_exception.rb +96 -0
- metadata +83 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZappbException
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(view: nil)
|
|
6
|
+
@definitions = {}
|
|
7
|
+
@aliases = {}
|
|
8
|
+
@class_aliases = {}
|
|
9
|
+
@frozen = false
|
|
10
|
+
@view = view
|
|
11
|
+
@index = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_base
|
|
15
|
+
BASE_CATALOG.each { |entry| register(entry[:name], **entry.except(:name), base: true) }
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register(name, aliases: [], override: false, **attrs)
|
|
20
|
+
raise FrozenRegistryError, frozen_message(name) if @frozen
|
|
21
|
+
|
|
22
|
+
definition = Definition.new(name: name, **attrs)
|
|
23
|
+
alias_names = Array(aliases).map { |a| validate_alias!(a, definition) }
|
|
24
|
+
check_name!(definition, override)
|
|
25
|
+
check_numbering!(definition)
|
|
26
|
+
|
|
27
|
+
ClassFactory.call(definition, alias_names)
|
|
28
|
+
@definitions[definition.name] = definition
|
|
29
|
+
alias_names.each { |a| @aliases[a] = definition.name }
|
|
30
|
+
changed!
|
|
31
|
+
definition
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def alias_external(class_name, to:)
|
|
35
|
+
raise FrozenRegistryError, frozen_message(class_name) if @frozen
|
|
36
|
+
|
|
37
|
+
class_name = validate_class_alias!(class_name)
|
|
38
|
+
target = lookup(to)
|
|
39
|
+
raise ConfigurationError, "alias_external '#{class_name}': target entry '#{to}' does not exist" unless target
|
|
40
|
+
|
|
41
|
+
@class_aliases[class_name] = target.name
|
|
42
|
+
changed!
|
|
43
|
+
target
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def freeze!
|
|
47
|
+
@frozen = true
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def frozen?
|
|
52
|
+
@frozen
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def definitions
|
|
56
|
+
@definitions.values
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def aliases
|
|
60
|
+
@aliases.dup
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def class_aliases
|
|
64
|
+
@class_aliases.dup
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def lookup(name)
|
|
68
|
+
name = name.to_s.delete_prefix("ZappbException::")
|
|
69
|
+
@definitions[name] || (@aliases[name] && @definitions[@aliases[name]]) ||
|
|
70
|
+
(@class_aliases[name] && @definitions[@class_aliases[name]])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def definition_for_class(klass)
|
|
74
|
+
name = klass.name
|
|
75
|
+
found = index[name] if name
|
|
76
|
+
return found if found
|
|
77
|
+
|
|
78
|
+
@definitions.each_value do |definition|
|
|
79
|
+
return definition if definition.klass && definition.resolve_klass.equal?(klass)
|
|
80
|
+
end
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_exceptions
|
|
85
|
+
@definitions.transform_values do |d|
|
|
86
|
+
entry = { status: d.status, error_code: d.number, message: d.message }
|
|
87
|
+
entry[:code] = d.code if d.code
|
|
88
|
+
entry[:external] = true if d.external
|
|
89
|
+
entry[:payload] = d.payload if d.payload
|
|
90
|
+
entry.freeze
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def changed!
|
|
97
|
+
@index = nil
|
|
98
|
+
@view&.replace(to_exceptions)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def frozen_message(name)
|
|
102
|
+
"Cannot register '#{name}': the catalog is already frozen (it is frozen in after_initialize " \
|
|
103
|
+
"or by ZappbException.finalize!). Register errors in config/initializers."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_alias!(alias_name, definition)
|
|
107
|
+
alias_name = alias_name.to_s
|
|
108
|
+
if alias_name.start_with?("ZappbException::")
|
|
109
|
+
raise ConfigurationError, "The alias '#{alias_name}' must not have the 'ZappbException::' prefix"
|
|
110
|
+
end
|
|
111
|
+
if alias_name == definition.name
|
|
112
|
+
raise ConfigurationError, "The alias '#{alias_name}' matches the definition name"
|
|
113
|
+
end
|
|
114
|
+
if @definitions.key?(alias_name) || (@aliases.key?(alias_name) && @aliases[alias_name] != definition.name)
|
|
115
|
+
raise ConfigurationError, "The alias '#{alias_name}' is already registered"
|
|
116
|
+
end
|
|
117
|
+
unless alias_name.include?("::") || alias_name.match?(/\A[A-Z]\w*\z/)
|
|
118
|
+
raise ConfigurationError, "#{alias_name}: invalid alias name"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
alias_name
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_class_alias!(class_name)
|
|
125
|
+
class_name = class_name.to_s
|
|
126
|
+
if class_name.start_with?("ZappbException::")
|
|
127
|
+
raise ConfigurationError, "The alias '#{class_name}' must not have the 'ZappbException::' prefix"
|
|
128
|
+
end
|
|
129
|
+
unless class_name.split("::", -1).all? { |segment| segment.match?(NAME_SEGMENT) }
|
|
130
|
+
raise ConfigurationError, "#{class_name}: invalid class name"
|
|
131
|
+
end
|
|
132
|
+
if @definitions.key?(class_name) || @aliases.key?(class_name) || @class_aliases.key?(class_name)
|
|
133
|
+
raise ConfigurationError, "The alias '#{class_name}' is already registered"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class_name
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def check_name!(definition, override)
|
|
140
|
+
if @class_aliases.key?(definition.name)
|
|
141
|
+
raise ConfigurationError, "'#{definition.name}' is already registered as an alias of #{@class_aliases[definition.name]}"
|
|
142
|
+
end
|
|
143
|
+
existing = @definitions[definition.name]
|
|
144
|
+
if @aliases.key?(definition.name)
|
|
145
|
+
raise ConfigurationError, "'#{definition.name}' is already registered as an alias of #{@aliases[definition.name]}"
|
|
146
|
+
end
|
|
147
|
+
return unless existing
|
|
148
|
+
return if override
|
|
149
|
+
|
|
150
|
+
origin = existing.base ? "a base entry" : "a registered entry"
|
|
151
|
+
raise ConfigurationError, "'#{definition.name}' already exists as #{origin}; use override: true to redefine it"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def check_numbering!(definition)
|
|
155
|
+
return if definition.code
|
|
156
|
+
|
|
157
|
+
clash = @definitions.values.find do |other|
|
|
158
|
+
other.name != definition.name && other.code.nil? &&
|
|
159
|
+
other.status == definition.status && other.number == definition.number
|
|
160
|
+
end
|
|
161
|
+
return unless clash
|
|
162
|
+
|
|
163
|
+
raise ConfigurationError, "#{definition.name}: #{definition.status}-#{definition.number} is already used by " \
|
|
164
|
+
"#{clash.name}; use another number or define a code:"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def index
|
|
168
|
+
@index ||= build_index
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def build_index
|
|
172
|
+
idx = {}
|
|
173
|
+
@definitions.each_value do |d|
|
|
174
|
+
next if d.klass
|
|
175
|
+
|
|
176
|
+
idx[d.external ? d.name : "ZappbException::#{d.name}"] = d
|
|
177
|
+
idx[d.name] = d if !d.external && d.name != "Exception" && !d.name.include?("::") && core_class?(d.name)
|
|
178
|
+
end
|
|
179
|
+
@class_aliases.each { |class_name, target| idx[class_name] = @definitions.fetch(target) }
|
|
180
|
+
@aliases.each do |alias_name, target|
|
|
181
|
+
idx[alias_name.include?("::") ? alias_name : "ZappbException::#{alias_name}"] = @definitions.fetch(target)
|
|
182
|
+
end
|
|
183
|
+
idx
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def core_class?(name)
|
|
187
|
+
::Object.const_defined?(name, false) && ::Object.const_get(name, false).is_a?(::Class) &&
|
|
188
|
+
::Object.const_source_location(name) == []
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZappbException
|
|
4
|
+
class RemoteError
|
|
5
|
+
PREFIX = "ZappbException::"
|
|
6
|
+
LABEL = /\A\[[^\]]*\]\s*/
|
|
7
|
+
|
|
8
|
+
attr_reader :raw
|
|
9
|
+
|
|
10
|
+
def self.from(response)
|
|
11
|
+
response.is_a?(::Hash) ? new(response) : NoResponse.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.read(hash, key)
|
|
15
|
+
return unless hash.is_a?(::Hash)
|
|
16
|
+
|
|
17
|
+
hash.key?(key.to_s) ? hash[key.to_s] : hash[key.to_sym]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.normalize_name(name)
|
|
21
|
+
text = name.to_s
|
|
22
|
+
text.start_with?(PREFIX) ? text.delete_prefix(PREFIX) : text
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(raw)
|
|
26
|
+
@raw = raw
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def no_response? = false
|
|
30
|
+
|
|
31
|
+
def error? = Nats.error_response?(raw)
|
|
32
|
+
|
|
33
|
+
def ok? = !error?
|
|
34
|
+
|
|
35
|
+
def success?
|
|
36
|
+
return false unless raw.is_a?(::Hash)
|
|
37
|
+
|
|
38
|
+
[true, "true"].include?(self.class.read(raw, :success))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def status
|
|
42
|
+
Integer(self.class.read(raw, :status), exception: false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def code
|
|
46
|
+
string_or_nil(self.class.read(raw, :code))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def message
|
|
50
|
+
string_or_nil(self.class.read(raw, :message))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exception_name
|
|
54
|
+
name = string_or_nil(error_hash_value(:exception))
|
|
55
|
+
name && self.class.normalize_name(name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def kind?(*names)
|
|
59
|
+
current = exception_name
|
|
60
|
+
return false if current.nil?
|
|
61
|
+
|
|
62
|
+
names.flatten.any? { |name| self.class.normalize_name(name) == current }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detail(strip_label: false)
|
|
66
|
+
text = string_or_nil(error_hash_value(:detail))
|
|
67
|
+
strip_label && text ? text.sub(LABEL, "") : text
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def meta
|
|
71
|
+
value = self.class.read(raw, :meta)
|
|
72
|
+
value.is_a?(::Hash) ? value : {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extra
|
|
76
|
+
error = self.class.read(raw, :error)
|
|
77
|
+
return {} unless error.is_a?(::Hash)
|
|
78
|
+
|
|
79
|
+
error.each_with_object({}) do |(key, value), acc|
|
|
80
|
+
name = key.to_sym
|
|
81
|
+
acc[name] = value unless %i[exception detail].include?(name)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_forwarded_error
|
|
86
|
+
ForwardedError.new(raw)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def error_hash_value(key)
|
|
92
|
+
self.class.read(self.class.read(raw, :error), key)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def string_or_nil(value)
|
|
96
|
+
value.nil? ? nil : value.to_s
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class NoResponse < RemoteError
|
|
100
|
+
def initialize(_raw = nil)
|
|
101
|
+
super(nil)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def no_response? = true
|
|
105
|
+
|
|
106
|
+
def error? = false
|
|
107
|
+
|
|
108
|
+
def ok? = false
|
|
109
|
+
|
|
110
|
+
def success? = false
|
|
111
|
+
|
|
112
|
+
def extra = {}
|
|
113
|
+
|
|
114
|
+
def status = nil
|
|
115
|
+
|
|
116
|
+
def code = nil
|
|
117
|
+
|
|
118
|
+
def message = nil
|
|
119
|
+
|
|
120
|
+
def exception_name = nil
|
|
121
|
+
|
|
122
|
+
def kind?(*) = false
|
|
123
|
+
|
|
124
|
+
def meta = {}
|
|
125
|
+
|
|
126
|
+
def detail(strip_label: false) = nil # rubocop:disable Lint/UnusedMethodArgument
|
|
127
|
+
|
|
128
|
+
def to_forwarded_error
|
|
129
|
+
GatewayTimeout.new
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZappbException
|
|
4
|
+
module Resolver
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(exception, registry: ZappbException.registry)
|
|
8
|
+
exception.class.ancestors.each do |ancestor|
|
|
9
|
+
next unless ancestor.is_a?(::Class)
|
|
10
|
+
|
|
11
|
+
definition = registry.definition_for_class(ancestor)
|
|
12
|
+
return definition if definition
|
|
13
|
+
end
|
|
14
|
+
Definition::UNHANDLED
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rspec/expectations"
|
|
5
|
+
require "zappb_exception/testing"
|
|
6
|
+
|
|
7
|
+
module ZappbException
|
|
8
|
+
module RSpecHelpers
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def resolved_name(exception)
|
|
12
|
+
ZappbException::Resolver.call(exception).name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def envelope_hash(actual)
|
|
16
|
+
actual = JSON.parse(actual) if actual.is_a?(::String)
|
|
17
|
+
actual.is_a?(::Hash) ? actual.deep_symbolize_keys : {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deep_symbolize(value)
|
|
21
|
+
value.is_a?(::Hash) ? value.deep_symbolize_keys : value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def expected_fields(code:, status:, message:, exception:, detail:, meta:)
|
|
25
|
+
{ code: code, status: status, message: message,
|
|
26
|
+
exception: exception && ZappbException::RemoteError.normalize_name(exception),
|
|
27
|
+
detail: detail, meta: meta && deep_symbolize(meta) }.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def actual_fields(hash)
|
|
31
|
+
error = hash[:error].is_a?(::Hash) ? hash[:error] : {}
|
|
32
|
+
meta = hash[:meta].is_a?(::Hash) ? hash[:meta] : {}
|
|
33
|
+
exception = error[:exception] && ZappbException::RemoteError.normalize_name(error[:exception])
|
|
34
|
+
status = Integer(hash[:status], exception: false)
|
|
35
|
+
{ code: hash[:code], status: status, message: hash[:message], exception: exception,
|
|
36
|
+
detail: error[:detail], meta: meta }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
RSpec::Matchers.define :be_zappb_error do |name = nil|
|
|
42
|
+
match do |actual|
|
|
43
|
+
if name.nil?
|
|
44
|
+
actual.is_a?(ZappbException::BaseError)
|
|
45
|
+
else
|
|
46
|
+
actual.is_a?(::StandardError) &&
|
|
47
|
+
ZappbException::RSpecHelpers.resolved_name(actual) == ZappbException::RemoteError.normalize_name(name)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
failure_message do |actual|
|
|
52
|
+
got = actual.is_a?(::StandardError) ? ZappbException::RSpecHelpers.resolved_name(actual) : actual.inspect
|
|
53
|
+
"expected #{actual.class} to resolve to #{name.inspect}, got #{got}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
description { "be a ZappbException error#{" #{name}" if name}" }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
RSpec::Matchers.define :raise_zappb_error do |name, message = nil|
|
|
60
|
+
supports_block_expectations
|
|
61
|
+
|
|
62
|
+
match do |block|
|
|
63
|
+
@error = nil
|
|
64
|
+
begin
|
|
65
|
+
block.call
|
|
66
|
+
rescue ::StandardError => e
|
|
67
|
+
@error = e
|
|
68
|
+
end
|
|
69
|
+
@error &&
|
|
70
|
+
ZappbException::RSpecHelpers.resolved_name(@error) == ZappbException::RemoteError.normalize_name(name) &&
|
|
71
|
+
(message.nil? || values_match?(message, @error.message))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
failure_message do
|
|
75
|
+
if @error
|
|
76
|
+
"expected a #{name} error#{" with message #{message.inspect}" if message}, " \
|
|
77
|
+
"got #{@error.class}: #{@error.message}"
|
|
78
|
+
else
|
|
79
|
+
"expected a #{name} error, but nothing was raised"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
description { "raise a ZappbException #{name} error" }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
RSpec::Matchers.define :match_error_envelope do |code: nil, status: nil, message: nil, exception: nil,
|
|
87
|
+
detail: nil, meta: nil|
|
|
88
|
+
match do |actual|
|
|
89
|
+
helpers = ZappbException::RSpecHelpers
|
|
90
|
+
@expected = helpers.expected_fields(code: code, status: status, message: message, exception: exception,
|
|
91
|
+
detail: detail, meta: meta)
|
|
92
|
+
@actual = helpers.actual_fields(helpers.envelope_hash(actual))
|
|
93
|
+
@expected.all? { |key, value| values_match?(value, @actual[key]) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
failure_message do
|
|
97
|
+
diff = @expected.reject { |key, value| values_match?(value, @actual[key]) }
|
|
98
|
+
"expected error envelope to match #{diff.inspect}, got #{@actual.slice(*diff.keys).inspect}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
description { "match error envelope #{@expected.inspect}" }
|
|
102
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zappb_exception"
|
|
4
|
+
|
|
5
|
+
module ZappbException
|
|
6
|
+
module Testing
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def error_response(name, detail: nil, meta: {}, extra: {}, config: ZappbException.configuration)
|
|
10
|
+
key = RemoteError.normalize_name(name)
|
|
11
|
+
definition = ZappbException.registry.lookup(key)
|
|
12
|
+
raise ::ArgumentError, "Unregistered error: #{key}" unless definition
|
|
13
|
+
|
|
14
|
+
text = detail || Envelope.message_for(definition, config: config)
|
|
15
|
+
label = config.service_label
|
|
16
|
+
body = {
|
|
17
|
+
success: false,
|
|
18
|
+
status: definition.status,
|
|
19
|
+
code: definition.error_code(config.code_prefix),
|
|
20
|
+
message: Envelope.message_for(definition, config: config),
|
|
21
|
+
error: {
|
|
22
|
+
exception: exception_name(definition),
|
|
23
|
+
detail: label.nil? || label.to_s.empty? ? text : "[#{label}] #{text}"
|
|
24
|
+
}.merge(extra.to_h.transform_keys(&:to_sym).except(*Envelope::RESERVED_ERROR_KEYS))
|
|
25
|
+
}
|
|
26
|
+
meta.is_a?(::Hash) && !meta.empty? ? body.merge(meta: meta) : body
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def exception_name(definition)
|
|
30
|
+
return definition.resolve_klass&.name || definition.name if definition.klass
|
|
31
|
+
return definition.name unless definition.internal?
|
|
32
|
+
|
|
33
|
+
"#{RemoteError::PREFIX}#{definition.name}"
|
|
34
|
+
end
|
|
35
|
+
private_class_method :exception_name
|
|
36
|
+
|
|
37
|
+
def success_response(data = {})
|
|
38
|
+
{ success: true, status: 200, message: nil, data: data, meta: {} }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/core_ext/hash/keys"
|
|
5
|
+
require "active_support/i18n"
|
|
6
|
+
|
|
7
|
+
require "zappb_exception/version"
|
|
8
|
+
require "zappb_exception/errors"
|
|
9
|
+
require "zappb_exception/definition"
|
|
10
|
+
require "zappb_exception/base_catalog"
|
|
11
|
+
require "zappb_exception/class_factory"
|
|
12
|
+
require "zappb_exception/registry"
|
|
13
|
+
require "zappb_exception/configuration"
|
|
14
|
+
require "zappb_exception/resolver"
|
|
15
|
+
require "zappb_exception/detail_sanitizer"
|
|
16
|
+
require "zappb_exception/envelope"
|
|
17
|
+
require "zappb_exception/describer"
|
|
18
|
+
require "zappb_exception/notifier"
|
|
19
|
+
require "zappb_exception/remote_error"
|
|
20
|
+
require "zappb_exception/nats"
|
|
21
|
+
require "zappb_exception/handler"
|
|
22
|
+
require "zappb_exception/rack"
|
|
23
|
+
|
|
24
|
+
module ZappbException
|
|
25
|
+
LOCALE_FILES = Dir[File.expand_path("../config/locales/*.yml", __dir__)].freeze
|
|
26
|
+
|
|
27
|
+
EXCEPTIONS = {}
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def registry
|
|
31
|
+
@registry ||= Registry.new(view: EXCEPTIONS).load_base
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configuration
|
|
35
|
+
@configuration ||= Configuration.new(registry)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configure
|
|
39
|
+
yield configuration
|
|
40
|
+
configuration
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def finalize!
|
|
44
|
+
registry.freeze!
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset!
|
|
49
|
+
@registry = nil
|
|
50
|
+
@configuration = nil
|
|
51
|
+
EXCEPTIONS.clear
|
|
52
|
+
registry
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def error_code_for(entry)
|
|
57
|
+
prefix = configuration.code_prefix
|
|
58
|
+
return entry.error_code(prefix) if entry.is_a?(Definition)
|
|
59
|
+
|
|
60
|
+
entry[:code] || "#{entry[:status]}-#{prefix}#{format("%02d", entry[:error_code])}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def generate_json(exception, name = exception.class.name, meta: {})
|
|
64
|
+
definition = unless name == exception.class.name
|
|
65
|
+
registry.lookup(name) || Definition::UNHANDLED
|
|
66
|
+
end
|
|
67
|
+
Envelope.call(exception, meta: meta, definition: definition).body
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def describe(exception, config: configuration)
|
|
71
|
+
Describer.call(exception, config: config)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def log_level(exception, config: configuration)
|
|
75
|
+
Describer.level(exception, config: config)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def define_exceptions!
|
|
79
|
+
EXCEPTIONS
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def const_missing(name)
|
|
83
|
+
definition = registry.lookup(name.to_s)
|
|
84
|
+
klass = definition&.resolve_klass
|
|
85
|
+
return const_set(name, klass) if klass
|
|
86
|
+
|
|
87
|
+
super
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
I18n.load_path |= LOCALE_FILES
|
|
92
|
+
|
|
93
|
+
registry
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
require "zappb_exception/railtie" if defined?(::Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zappb-exceptions
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- BPO Advisors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
description: Exception handling for microservices in an MSA architecture that communicate
|
|
27
|
+
through messaging. Each service declares its own error catalog; errors travel between
|
|
28
|
+
services and to clients as a stable JSON contract. Includes capture for Rails controllers,
|
|
29
|
+
Rack middlewares and NATS listeners, plus instrumentation events.
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- config/locales/en.yml
|
|
38
|
+
- config/locales/es.yml
|
|
39
|
+
- lib/zappb/exceptions.rb
|
|
40
|
+
- lib/zappb_exception.rb
|
|
41
|
+
- lib/zappb_exception/base_catalog.rb
|
|
42
|
+
- lib/zappb_exception/class_factory.rb
|
|
43
|
+
- lib/zappb_exception/configuration.rb
|
|
44
|
+
- lib/zappb_exception/definition.rb
|
|
45
|
+
- lib/zappb_exception/describer.rb
|
|
46
|
+
- lib/zappb_exception/detail_sanitizer.rb
|
|
47
|
+
- lib/zappb_exception/envelope.rb
|
|
48
|
+
- lib/zappb_exception/errors.rb
|
|
49
|
+
- lib/zappb_exception/handler.rb
|
|
50
|
+
- lib/zappb_exception/nats.rb
|
|
51
|
+
- lib/zappb_exception/notifier.rb
|
|
52
|
+
- lib/zappb_exception/rack.rb
|
|
53
|
+
- lib/zappb_exception/railtie.rb
|
|
54
|
+
- lib/zappb_exception/registry.rb
|
|
55
|
+
- lib/zappb_exception/remote_error.rb
|
|
56
|
+
- lib/zappb_exception/resolver.rb
|
|
57
|
+
- lib/zappb_exception/rspec.rb
|
|
58
|
+
- lib/zappb_exception/testing.rb
|
|
59
|
+
- lib/zappb_exception/version.rb
|
|
60
|
+
homepage: https://rubygems.org/gems/zappb-exceptions
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.2'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.14
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: 'Exception handling for messaging-based microservices: per-service error
|
|
82
|
+
catalog and a stable JSON contract.'
|
|
83
|
+
test_files: []
|