senro_usecaser 0.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +72 -0
- data/LICENSE +21 -0
- data/README.md +1069 -0
- data/Rakefile +12 -0
- data/Steepfile +24 -0
- data/examples/RBS_GENERATION.md +16 -0
- data/examples/namespace_demo.rb +751 -0
- data/examples/order_system.rb +1279 -0
- data/examples/sig/namespace_demo.rbs +279 -0
- data/examples/sig/order_system.rbs +685 -0
- data/lefthook.yml +31 -0
- data/lib/senro_usecaser/base.rb +660 -0
- data/lib/senro_usecaser/configuration.rb +149 -0
- data/lib/senro_usecaser/container.rb +315 -0
- data/lib/senro_usecaser/error.rb +88 -0
- data/lib/senro_usecaser/provider.rb +212 -0
- data/lib/senro_usecaser/result.rb +182 -0
- data/lib/senro_usecaser/version.rb +8 -0
- data/lib/senro_usecaser.rb +155 -0
- data/sig/generated/senro_usecaser/base.rbs +365 -0
- data/sig/generated/senro_usecaser/configuration.rbs +80 -0
- data/sig/generated/senro_usecaser/container.rbs +190 -0
- data/sig/generated/senro_usecaser/error.rbs +58 -0
- data/sig/generated/senro_usecaser/provider.rbs +153 -0
- data/sig/generated/senro_usecaser/result.rbs +109 -0
- data/sig/generated/senro_usecaser/version.rbs +6 -0
- data/sig/generated/senro_usecaser.rbs +113 -0
- data/sig/overrides.rbs +16 -0
- metadata +77 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module SenroUsecaser
|
|
6
|
+
# Configuration for SenroUsecaser
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# SenroUsecaser.configure do |config|
|
|
10
|
+
# config.providers = [CoreProvider, UserProvider]
|
|
11
|
+
# config.infer_namespace_from_module = true
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
class Configuration
|
|
15
|
+
# List of provider classes to boot
|
|
16
|
+
#: () -> Array[singleton(Provider)]
|
|
17
|
+
attr_accessor :providers
|
|
18
|
+
|
|
19
|
+
# Whether to infer namespace from module structure
|
|
20
|
+
#: () -> bool
|
|
21
|
+
attr_accessor :infer_namespace_from_module
|
|
22
|
+
|
|
23
|
+
#: () -> void
|
|
24
|
+
def initialize
|
|
25
|
+
@providers = []
|
|
26
|
+
@infer_namespace_from_module = false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Environment detection
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# SenroUsecaser.env.development?
|
|
34
|
+
# SenroUsecaser.env.production?
|
|
35
|
+
#
|
|
36
|
+
class Environment
|
|
37
|
+
#: (String) -> void
|
|
38
|
+
def initialize(name)
|
|
39
|
+
@name = name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#: () -> String
|
|
43
|
+
attr_reader :name
|
|
44
|
+
|
|
45
|
+
#: () -> bool
|
|
46
|
+
def development?
|
|
47
|
+
@name == "development"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: () -> bool
|
|
51
|
+
def test?
|
|
52
|
+
@name == "test"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: () -> bool
|
|
56
|
+
def production?
|
|
57
|
+
@name == "production"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: () -> String
|
|
61
|
+
def to_s
|
|
62
|
+
@name
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Provider boot manager
|
|
67
|
+
#
|
|
68
|
+
# Resolves provider dependencies and boots them in correct order.
|
|
69
|
+
#
|
|
70
|
+
class ProviderBooter
|
|
71
|
+
# Error raised when circular dependencies are detected
|
|
72
|
+
class CircularDependencyError < StandardError; end
|
|
73
|
+
|
|
74
|
+
#: (Array[singleton(Provider)], Container) -> void
|
|
75
|
+
def initialize(provider_classes, container)
|
|
76
|
+
@provider_classes = provider_classes
|
|
77
|
+
@container = container
|
|
78
|
+
@booted_providers = [] #: Array[singleton(Provider)]
|
|
79
|
+
@provider_instances = {} #: Hash[singleton(Provider), Provider]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Boots all providers in dependency order
|
|
83
|
+
#
|
|
84
|
+
#: () -> void
|
|
85
|
+
def boot!
|
|
86
|
+
sorted = topological_sort(@provider_classes)
|
|
87
|
+
|
|
88
|
+
sorted.each do |provider_class|
|
|
89
|
+
next unless provider_class.enabled?
|
|
90
|
+
|
|
91
|
+
instance = provider_class.new
|
|
92
|
+
@provider_instances[provider_class] = instance
|
|
93
|
+
instance.register_to(@container)
|
|
94
|
+
@booted_providers << provider_class
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Call after_boot on all providers
|
|
98
|
+
@booted_providers.each do |provider_class|
|
|
99
|
+
@provider_instances[provider_class].after_boot(@container)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Shuts down all providers in reverse order
|
|
104
|
+
#
|
|
105
|
+
#: () -> void
|
|
106
|
+
def shutdown!
|
|
107
|
+
@booted_providers.reverse_each do |provider_class|
|
|
108
|
+
@provider_instances[provider_class].shutdown(@container)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Topologically sorts providers based on dependencies
|
|
115
|
+
#
|
|
116
|
+
#: (Array[singleton(Provider)]) -> Array[singleton(Provider)]
|
|
117
|
+
def topological_sort(providers)
|
|
118
|
+
sorted = [] #: Array[singleton(Provider)]
|
|
119
|
+
visited = {} #: Hash[singleton(Provider), bool]
|
|
120
|
+
visiting = {} #: Hash[singleton(Provider), bool]
|
|
121
|
+
|
|
122
|
+
providers.each do |provider|
|
|
123
|
+
visit(provider, sorted, visited, visiting) unless visited[provider]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sorted
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
#: (singleton(Provider), Array[singleton(Provider)], untyped, untyped) -> void
|
|
130
|
+
def visit(provider, sorted, visited, visiting)
|
|
131
|
+
return if visited[provider]
|
|
132
|
+
|
|
133
|
+
if visiting[provider]
|
|
134
|
+
raise CircularDependencyError,
|
|
135
|
+
"Circular dependency detected involving #{provider.name}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
visiting[provider] = true
|
|
139
|
+
|
|
140
|
+
provider.provider_dependencies.each do |dep|
|
|
141
|
+
visit(dep, sorted, visited, visiting) unless visited[dep]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
visiting.delete(provider)
|
|
145
|
+
visited[provider] = true
|
|
146
|
+
sorted << provider
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module SenroUsecaser
|
|
6
|
+
# Wrapper for singleton registrations that caches the result
|
|
7
|
+
class SingletonRegistration
|
|
8
|
+
#: (^(Container) -> untyped) -> void
|
|
9
|
+
def initialize(block)
|
|
10
|
+
@block = block
|
|
11
|
+
@resolved = false
|
|
12
|
+
@value = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#: (Container) -> untyped
|
|
16
|
+
def call(container)
|
|
17
|
+
unless @resolved
|
|
18
|
+
@value = @block.call(container)
|
|
19
|
+
@resolved = true
|
|
20
|
+
end
|
|
21
|
+
@value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# DI Container with namespace support
|
|
26
|
+
#
|
|
27
|
+
# @example Basic usage
|
|
28
|
+
# container = SenroUsecaser::Container.new
|
|
29
|
+
# container.register(:logger, Logger.new)
|
|
30
|
+
# container.resolve(:logger) # => Logger instance
|
|
31
|
+
#
|
|
32
|
+
# @example With namespaces
|
|
33
|
+
# container = SenroUsecaser::Container.new
|
|
34
|
+
# container.register(:logger, Logger.new)
|
|
35
|
+
#
|
|
36
|
+
# container.namespace(:admin) do
|
|
37
|
+
# register(:user_repository, AdminUserRepository.new)
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # From admin namespace, can resolve both admin and root dependencies
|
|
41
|
+
# container.resolve_in(:admin, :user_repository) # => AdminUserRepository
|
|
42
|
+
# container.resolve_in(:admin, :logger) # => Logger (from root)
|
|
43
|
+
class Container
|
|
44
|
+
# Error raised when a dependency cannot be resolved
|
|
45
|
+
class ResolutionError < StandardError; end
|
|
46
|
+
|
|
47
|
+
# Error raised when a dependency is already registered
|
|
48
|
+
class DuplicateRegistrationError < StandardError; end
|
|
49
|
+
|
|
50
|
+
#: (?parent: Container?) -> void
|
|
51
|
+
def initialize(parent: nil)
|
|
52
|
+
@parent = parent
|
|
53
|
+
@registrations = {} #: Hash[String, untyped]
|
|
54
|
+
@current_namespace = [] #: Array[Symbol]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Creates a scoped child container that inherits from this container
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# scoped = container.scope do
|
|
61
|
+
# register(:current_user, current_user)
|
|
62
|
+
# end
|
|
63
|
+
# scoped.resolve(:current_user) # => current_user
|
|
64
|
+
# scoped.resolve(:logger) # => resolved from parent
|
|
65
|
+
#
|
|
66
|
+
#: () ?{ () -> void } -> Container
|
|
67
|
+
def scope(&block)
|
|
68
|
+
child = Container.new(parent: self)
|
|
69
|
+
child.instance_eval(&block) if block # steep:ignore BlockTypeMismatch
|
|
70
|
+
child
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Registers a dependency in the current namespace
|
|
74
|
+
#
|
|
75
|
+
# @example With value (returns same value every time)
|
|
76
|
+
# container.register(:logger, Logger.new)
|
|
77
|
+
#
|
|
78
|
+
# @example With block (lazy evaluation, called every time, receives container)
|
|
79
|
+
# container.register(:database) { |container| Database.connect }
|
|
80
|
+
#
|
|
81
|
+
#: (Symbol, ?untyped) ?{ (Container) -> untyped } -> void
|
|
82
|
+
def register(key, value = nil, &block)
|
|
83
|
+
raise ArgumentError, "Provide either a value or a block, not both" if value && block
|
|
84
|
+
raise ArgumentError, "Provide either a value or a block" if value.nil? && block.nil?
|
|
85
|
+
|
|
86
|
+
full_key = build_key(key)
|
|
87
|
+
check_duplicate_registration!(full_key)
|
|
88
|
+
|
|
89
|
+
@registrations[full_key] = block || ->(_) { value }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Registers a lazy dependency (block is called every time on resolve)
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# container.register_lazy(:connection) { |c| Database.connect }
|
|
96
|
+
#
|
|
97
|
+
# @example With dependency resolution
|
|
98
|
+
# container.register_lazy(:user_repository) do |container|
|
|
99
|
+
# UserRepository.new(current_user: container.resolve(:current_user))
|
|
100
|
+
# end
|
|
101
|
+
#
|
|
102
|
+
#: [T] (Symbol) { (Container) -> T } -> void
|
|
103
|
+
def register_lazy(key, &block)
|
|
104
|
+
raise ArgumentError, "Block is required for register_lazy" unless block
|
|
105
|
+
|
|
106
|
+
full_key = build_key(key)
|
|
107
|
+
check_duplicate_registration!(full_key)
|
|
108
|
+
|
|
109
|
+
@registrations[full_key] = block
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Registers a singleton dependency (block is called once and cached)
|
|
113
|
+
#
|
|
114
|
+
# @example
|
|
115
|
+
# container.register_singleton(:database) { |c| Database.connect }
|
|
116
|
+
# container.resolve(:database) # => same instance every time
|
|
117
|
+
#
|
|
118
|
+
# @example With dependency resolution
|
|
119
|
+
# container.register_singleton(:service) do |container|
|
|
120
|
+
# Service.new(logger: container.resolve(:logger))
|
|
121
|
+
# end
|
|
122
|
+
#
|
|
123
|
+
#: [T] (Symbol) { (Container) -> T } -> void
|
|
124
|
+
def register_singleton(key, &block)
|
|
125
|
+
raise ArgumentError, "Block is required for register_singleton" unless block
|
|
126
|
+
|
|
127
|
+
full_key = build_key(key)
|
|
128
|
+
check_duplicate_registration!(full_key)
|
|
129
|
+
|
|
130
|
+
@registrations[full_key] = SingletonRegistration.new(block)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Resolves a dependency from the current namespace or its ancestors
|
|
134
|
+
#
|
|
135
|
+
# @example
|
|
136
|
+
# container.resolve(:logger)
|
|
137
|
+
#
|
|
138
|
+
#: [T] (Symbol) -> T
|
|
139
|
+
def resolve(key)
|
|
140
|
+
resolve_in(current_namespace_path, key)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Resolves a dependency from a specific namespace or its ancestors
|
|
144
|
+
#
|
|
145
|
+
# @example
|
|
146
|
+
# container.resolve_in(:admin, :logger)
|
|
147
|
+
# container.resolve_in("admin::reports", :generator)
|
|
148
|
+
#
|
|
149
|
+
#: [T] ((Symbol | String | Array[Symbol]), Symbol) -> T
|
|
150
|
+
def resolve_in(namespace, key)
|
|
151
|
+
registration = find_registration(namespace, key)
|
|
152
|
+
|
|
153
|
+
unless registration
|
|
154
|
+
raise ResolutionError,
|
|
155
|
+
"Dependency #{key.inspect} not found in namespace #{namespace.inspect} or its ancestors"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Always invoke with self (the resolving container) for proper scoping
|
|
159
|
+
invoke_registration(registration)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Checks if a dependency is registered
|
|
163
|
+
#
|
|
164
|
+
#: (Symbol) -> bool
|
|
165
|
+
def registered?(key)
|
|
166
|
+
registered_in?(current_namespace_path, key)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Checks if a dependency is registered in a specific namespace or its ancestors
|
|
170
|
+
#
|
|
171
|
+
#: ((Symbol | String | Array[Symbol]), Symbol) -> bool
|
|
172
|
+
def registered_in?(namespace, key)
|
|
173
|
+
namespace_parts = normalize_namespace(namespace)
|
|
174
|
+
|
|
175
|
+
(namespace_parts.length + 1).times do |i|
|
|
176
|
+
current_parts = namespace_parts[0, namespace_parts.length - i] || []
|
|
177
|
+
full_key = build_key_with_namespace(current_parts, key)
|
|
178
|
+
return true if @registrations.key?(full_key)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Check parent container if available
|
|
182
|
+
return @parent.registered_in?(namespace, key) if @parent
|
|
183
|
+
|
|
184
|
+
false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Creates a namespace scope for registering dependencies
|
|
188
|
+
#
|
|
189
|
+
# @example
|
|
190
|
+
# container.namespace(:admin) do
|
|
191
|
+
# register(:user_repository, AdminUserRepository.new)
|
|
192
|
+
#
|
|
193
|
+
# namespace(:reports) do
|
|
194
|
+
# register(:generator, ReportGenerator.new)
|
|
195
|
+
# end
|
|
196
|
+
# end
|
|
197
|
+
#
|
|
198
|
+
#: ((Symbol | String)) { () -> void } -> void
|
|
199
|
+
def namespace(name, &)
|
|
200
|
+
@current_namespace.push(name.to_sym)
|
|
201
|
+
instance_eval(&) # steep:ignore BlockTypeMismatch
|
|
202
|
+
ensure
|
|
203
|
+
@current_namespace.pop
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns all registered keys (including parent keys)
|
|
207
|
+
#
|
|
208
|
+
#: () -> Array[String]
|
|
209
|
+
def keys
|
|
210
|
+
own_keys = @registrations.keys
|
|
211
|
+
return own_keys unless @parent
|
|
212
|
+
|
|
213
|
+
(own_keys + @parent.keys).uniq
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns only keys registered in this container (excluding parent)
|
|
217
|
+
#
|
|
218
|
+
#: () -> Array[String]
|
|
219
|
+
def own_keys
|
|
220
|
+
@registrations.keys
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns the parent container if any
|
|
224
|
+
#
|
|
225
|
+
#: () -> Container?
|
|
226
|
+
attr_reader :parent
|
|
227
|
+
|
|
228
|
+
# Clears all registrations
|
|
229
|
+
#
|
|
230
|
+
#: () -> void
|
|
231
|
+
def clear!
|
|
232
|
+
@registrations.clear
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
#: () -> String
|
|
238
|
+
def current_namespace_path
|
|
239
|
+
@current_namespace.join("::")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
#: (Symbol) -> String
|
|
243
|
+
def build_key(key)
|
|
244
|
+
build_key_with_namespace(@current_namespace, key)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
#: (Array[Symbol], Symbol) -> String
|
|
248
|
+
def build_key_with_namespace(namespace_parts, key)
|
|
249
|
+
if namespace_parts.empty?
|
|
250
|
+
key.to_s
|
|
251
|
+
else
|
|
252
|
+
"#{namespace_parts.join("::")}::#{key}"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
#: ((Symbol | String | Array[Symbol])) -> Array[Symbol]
|
|
257
|
+
def normalize_namespace(namespace)
|
|
258
|
+
case namespace
|
|
259
|
+
when Array then normalize_array_namespace(namespace)
|
|
260
|
+
when Symbol then normalize_symbol_namespace(namespace)
|
|
261
|
+
when String then normalize_string_namespace(namespace)
|
|
262
|
+
else raise ArgumentError, "Invalid namespace: #{namespace.inspect}"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
#: (Array[Symbol]) -> Array[Symbol]
|
|
267
|
+
def normalize_array_namespace(namespace)
|
|
268
|
+
namespace.map(&:to_sym)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
#: (Symbol) -> Array[Symbol]
|
|
272
|
+
def normalize_symbol_namespace(namespace)
|
|
273
|
+
namespace == :root ? [] : [namespace]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
#: (String) -> Array[Symbol]
|
|
277
|
+
def normalize_string_namespace(namespace)
|
|
278
|
+
namespace.empty? ? [] : namespace.split("::").map(&:to_sym)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
#: (String) -> void
|
|
282
|
+
def check_duplicate_registration!(full_key)
|
|
283
|
+
return unless @registrations.key?(full_key)
|
|
284
|
+
|
|
285
|
+
raise DuplicateRegistrationError, "Dependency #{full_key.inspect} is already registered"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Invokes a registration, passing the container for dependency resolution
|
|
289
|
+
#
|
|
290
|
+
#: (untyped) -> untyped
|
|
291
|
+
def invoke_registration(registration)
|
|
292
|
+
registration.call(self)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
protected
|
|
296
|
+
|
|
297
|
+
# Finds a registration in this container or its parent chain
|
|
298
|
+
#
|
|
299
|
+
#: ((Symbol | String | Array[Symbol]), Symbol) -> untyped
|
|
300
|
+
def find_registration(namespace, key)
|
|
301
|
+
namespace_parts = normalize_namespace(namespace)
|
|
302
|
+
|
|
303
|
+
# Try to find in the specified namespace and its ancestors
|
|
304
|
+
(namespace_parts.length + 1).times do |i|
|
|
305
|
+
current_parts = namespace_parts[0, namespace_parts.length - i] || []
|
|
306
|
+
full_key = build_key_with_namespace(current_parts, key)
|
|
307
|
+
|
|
308
|
+
return @registrations[full_key] if @registrations.key?(full_key)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Fall back to parent container if available
|
|
312
|
+
@parent&.find_registration(namespace, key)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module SenroUsecaser
|
|
6
|
+
# Represents an error in a Result
|
|
7
|
+
#
|
|
8
|
+
# @example Basic error
|
|
9
|
+
# error = SenroUsecaser::Error.new(
|
|
10
|
+
# code: :invalid_email,
|
|
11
|
+
# message: "Email format is invalid",
|
|
12
|
+
# field: :email
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# @example Error from exception
|
|
16
|
+
# begin
|
|
17
|
+
# # some code that raises
|
|
18
|
+
# rescue => e
|
|
19
|
+
# error = SenroUsecaser::Error.new(
|
|
20
|
+
# code: :unexpected_error,
|
|
21
|
+
# message: e.message,
|
|
22
|
+
# cause: e
|
|
23
|
+
# )
|
|
24
|
+
# end
|
|
25
|
+
class Error
|
|
26
|
+
#: Symbol
|
|
27
|
+
attr_reader :code
|
|
28
|
+
|
|
29
|
+
#: String
|
|
30
|
+
attr_reader :message
|
|
31
|
+
|
|
32
|
+
#: Symbol?
|
|
33
|
+
attr_reader :field
|
|
34
|
+
|
|
35
|
+
#: Exception?
|
|
36
|
+
attr_reader :cause
|
|
37
|
+
|
|
38
|
+
#: (code: Symbol, message: String, ?field: Symbol?, ?cause: Exception?) -> void
|
|
39
|
+
def initialize(code:, message:, field: nil, cause: nil)
|
|
40
|
+
@code = code
|
|
41
|
+
@message = message
|
|
42
|
+
@field = field
|
|
43
|
+
@cause = cause
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Creates an Error from an exception
|
|
47
|
+
#
|
|
48
|
+
#: (Exception, ?code: Symbol) -> Error
|
|
49
|
+
def self.from_exception(exception, code: :exception)
|
|
50
|
+
new(
|
|
51
|
+
code: code,
|
|
52
|
+
message: exception.message,
|
|
53
|
+
cause: exception
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns true if this error was caused by an exception
|
|
58
|
+
#
|
|
59
|
+
#: () -> bool
|
|
60
|
+
def caused_by_exception?
|
|
61
|
+
!cause.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: (Error) -> bool
|
|
65
|
+
def ==(other)
|
|
66
|
+
return false unless other.is_a?(Error)
|
|
67
|
+
|
|
68
|
+
code == other.code && message == other.message && field == other.field && cause == other.cause
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: () -> String
|
|
72
|
+
def to_s
|
|
73
|
+
base = field ? "[#{field}] #{message} (#{code})" : "#{message} (#{code})"
|
|
74
|
+
cause ? "#{base} caused by #{cause.class}" : base
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: () -> String
|
|
78
|
+
def inspect
|
|
79
|
+
parts = [
|
|
80
|
+
"code=#{code.inspect}",
|
|
81
|
+
"message=#{message.inspect}",
|
|
82
|
+
"field=#{field.inspect}"
|
|
83
|
+
]
|
|
84
|
+
parts << "cause=#{cause.class}" if cause
|
|
85
|
+
"#<#{self.class.name} #{parts.join(" ")}>"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|