servus 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +73 -10
- data/docs/core/1_overview.md +6 -2
- data/docs/core/3_service_objects.md +33 -0
- data/docs/features/1_schema_validation.md +43 -1
- data/docs/features/2_error_handling.md +9 -1
- data/docs/features/7_lazy_resolvers.md +238 -0
- data/docs/guides/2_migration_guide.md +51 -1
- data/docs/integration/2_testing.md +18 -1
- data/lib/servus/base.rb +21 -6
- data/lib/servus/event_handler.rb +27 -12
- data/lib/servus/extensions/lazily/call.rb +82 -0
- data/lib/servus/extensions/lazily/errors.rb +37 -0
- data/lib/servus/extensions/lazily/ext.rb +23 -0
- data/lib/servus/extensions/lazily/resolver.rb +32 -0
- data/lib/servus/railtie.rb +7 -1
- data/lib/servus/support/data_object.rb +80 -0
- data/lib/servus/support/errors.rb +291 -23
- data/lib/servus/support/response.rb +12 -1
- data/lib/servus/support/validator.rb +28 -14
- data/lib/servus/testing/example_builders.rb +22 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +1 -0
- metadata +12 -6
|
@@ -54,6 +54,39 @@ module Servus
|
|
|
54
54
|
def api_error = { code: http_status, message: message }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Guard validation failure with custom code.
|
|
58
|
+
#
|
|
59
|
+
# Guards define their own error code and HTTP status via the DSL.
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# GuardError.new("Amount must be positive", code: 'invalid_amount', http_status: 422)
|
|
63
|
+
class GuardError < ServiceError
|
|
64
|
+
DEFAULT_MESSAGE = 'Guard validation failed'
|
|
65
|
+
|
|
66
|
+
# @return [String] application-specific error code
|
|
67
|
+
attr_reader :code
|
|
68
|
+
|
|
69
|
+
# @return [Symbol, Integer] HTTP status code
|
|
70
|
+
attr_reader :http_status
|
|
71
|
+
|
|
72
|
+
# Creates a new guard error with metadata.
|
|
73
|
+
#
|
|
74
|
+
# @param message [String, nil] error message
|
|
75
|
+
# @param code [String] error code for API responses (default: 'guard_failed')
|
|
76
|
+
# @param http_status [Symbol, Integer] HTTP status (default: :unprocessable_entity)
|
|
77
|
+
def initialize(message = nil, code: 'guard_failed', http_status: :unprocessable_entity)
|
|
78
|
+
super(message)
|
|
79
|
+
@code = code
|
|
80
|
+
@http_status = http_status
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def api_error = { code: code, message: message }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------
|
|
87
|
+
# Standard HTTP error classes
|
|
88
|
+
# --------------------------------------------------------
|
|
89
|
+
|
|
57
90
|
# 400 Bad Request - malformed or invalid request data.
|
|
58
91
|
class BadRequestError < ServiceError
|
|
59
92
|
DEFAULT_MESSAGE = 'Bad request'
|
|
@@ -91,6 +124,126 @@ module Servus
|
|
|
91
124
|
def api_error = { code: http_status, message: message }
|
|
92
125
|
end
|
|
93
126
|
|
|
127
|
+
# 405 Method Not Allowed - HTTP method not supported for this resource.
|
|
128
|
+
class MethodNotAllowedError < ServiceError
|
|
129
|
+
DEFAULT_MESSAGE = 'Method not allowed'
|
|
130
|
+
|
|
131
|
+
def http_status = :method_not_allowed
|
|
132
|
+
def api_error = { code: http_status, message: message }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# 406 Not Acceptable - requested content type cannot be provided.
|
|
136
|
+
class NotAcceptableError < ServiceError
|
|
137
|
+
DEFAULT_MESSAGE = 'Not acceptable'
|
|
138
|
+
|
|
139
|
+
def http_status = :not_acceptable
|
|
140
|
+
def api_error = { code: http_status, message: message }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# 407 Proxy Authentication Required - proxy credentials required.
|
|
144
|
+
class ProxyAuthenticationRequiredError < ServiceError
|
|
145
|
+
DEFAULT_MESSAGE = 'Proxy authentication required'
|
|
146
|
+
|
|
147
|
+
def http_status = :proxy_authentication_required
|
|
148
|
+
def api_error = { code: http_status, message: message }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# 408 Request Timeout - client did not produce a request in time.
|
|
152
|
+
class RequestTimeoutError < ServiceError
|
|
153
|
+
DEFAULT_MESSAGE = 'Request timeout'
|
|
154
|
+
|
|
155
|
+
def http_status = :request_timeout
|
|
156
|
+
def api_error = { code: http_status, message: message }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 409 Conflict - request conflicts with current state of the resource.
|
|
160
|
+
class ConflictError < ServiceError
|
|
161
|
+
DEFAULT_MESSAGE = 'Conflict'
|
|
162
|
+
|
|
163
|
+
def http_status = :conflict
|
|
164
|
+
def api_error = { code: http_status, message: message }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# 410 Gone - resource is no longer available and will not be available again.
|
|
168
|
+
class GoneError < ServiceError
|
|
169
|
+
DEFAULT_MESSAGE = 'Gone'
|
|
170
|
+
|
|
171
|
+
def http_status = :gone
|
|
172
|
+
def api_error = { code: http_status, message: message }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 411 Length Required - Content-Length header is required.
|
|
176
|
+
class LengthRequiredError < ServiceError
|
|
177
|
+
DEFAULT_MESSAGE = 'Length required'
|
|
178
|
+
|
|
179
|
+
def http_status = :length_required
|
|
180
|
+
def api_error = { code: http_status, message: message }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# 412 Precondition Failed - precondition in headers evaluated to false.
|
|
184
|
+
class PreconditionFailedError < ServiceError
|
|
185
|
+
DEFAULT_MESSAGE = 'Precondition failed'
|
|
186
|
+
|
|
187
|
+
def http_status = :precondition_failed
|
|
188
|
+
def api_error = { code: http_status, message: message }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# 413 Payload Too Large - request entity is larger than server limits.
|
|
192
|
+
class PayloadTooLargeError < ServiceError
|
|
193
|
+
DEFAULT_MESSAGE = 'Payload too large'
|
|
194
|
+
|
|
195
|
+
def http_status = :payload_too_large
|
|
196
|
+
def api_error = { code: http_status, message: message }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# 414 URI Too Long - URI is too long for the server to process.
|
|
200
|
+
class UriTooLongError < ServiceError
|
|
201
|
+
DEFAULT_MESSAGE = 'URI too long'
|
|
202
|
+
|
|
203
|
+
def http_status = :uri_too_long
|
|
204
|
+
def api_error = { code: http_status, message: message }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# 415 Unsupported Media Type - request entity has unsupported media type.
|
|
208
|
+
class UnsupportedMediaTypeError < ServiceError
|
|
209
|
+
DEFAULT_MESSAGE = 'Unsupported media type'
|
|
210
|
+
|
|
211
|
+
def http_status = :unsupported_media_type
|
|
212
|
+
def api_error = { code: http_status, message: message }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# 416 Range Not Satisfiable - client requested a portion that cannot be supplied.
|
|
216
|
+
class RangeNotSatisfiableError < ServiceError
|
|
217
|
+
DEFAULT_MESSAGE = 'Range not satisfiable'
|
|
218
|
+
|
|
219
|
+
def http_status = :range_not_satisfiable
|
|
220
|
+
def api_error = { code: http_status, message: message }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# 417 Expectation Failed - server cannot meet Expect header requirements.
|
|
224
|
+
class ExpectationFailedError < ServiceError
|
|
225
|
+
DEFAULT_MESSAGE = 'Expectation failed'
|
|
226
|
+
|
|
227
|
+
def http_status = :expectation_failed
|
|
228
|
+
def api_error = { code: http_status, message: message }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# 418 I'm a Teapot - server refuses to brew coffee because it is a teapot.
|
|
232
|
+
class ImATeapotError < ServiceError
|
|
233
|
+
DEFAULT_MESSAGE = "I'm a teapot"
|
|
234
|
+
|
|
235
|
+
def http_status = :im_a_teapot
|
|
236
|
+
def api_error = { code: http_status, message: message }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# 421 Misdirected Request - request was directed at a server unable to respond.
|
|
240
|
+
class MisdirectedRequestError < ServiceError
|
|
241
|
+
DEFAULT_MESSAGE = 'Misdirected request'
|
|
242
|
+
|
|
243
|
+
def http_status = :misdirected_request
|
|
244
|
+
def api_error = { code: http_status, message: message }
|
|
245
|
+
end
|
|
246
|
+
|
|
94
247
|
# 422 Unprocessable Entity - semantic errors in request.
|
|
95
248
|
class UnprocessableEntityError < ServiceError
|
|
96
249
|
DEFAULT_MESSAGE = 'Unprocessable entity'
|
|
@@ -99,6 +252,14 @@ module Servus
|
|
|
99
252
|
def api_error = { code: http_status, message: message }
|
|
100
253
|
end
|
|
101
254
|
|
|
255
|
+
# 422 Unprocessable Content - content could not be processed.
|
|
256
|
+
class UnprocessableContentError < UnprocessableEntityError
|
|
257
|
+
DEFAULT_MESSAGE = 'Unprocessable content'
|
|
258
|
+
|
|
259
|
+
def http_status = :unprocessable_content
|
|
260
|
+
def api_error = { code: http_status, message: message }
|
|
261
|
+
end
|
|
262
|
+
|
|
102
263
|
# 422 Validation Error - schema or business validation failed.
|
|
103
264
|
class ValidationError < UnprocessableEntityError
|
|
104
265
|
DEFAULT_MESSAGE = 'Validation failed'
|
|
@@ -106,33 +267,68 @@ module Servus
|
|
|
106
267
|
def api_error = { code: http_status, message: message }
|
|
107
268
|
end
|
|
108
269
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
-
# @example
|
|
114
|
-
# GuardError.new("Amount must be positive", code: 'invalid_amount', http_status: 422)
|
|
115
|
-
class GuardError < ServiceError
|
|
116
|
-
DEFAULT_MESSAGE = 'Guard validation failed'
|
|
270
|
+
# 423 Locked - resource is locked.
|
|
271
|
+
class LockedError < ServiceError
|
|
272
|
+
DEFAULT_MESSAGE = 'Locked'
|
|
117
273
|
|
|
118
|
-
|
|
119
|
-
|
|
274
|
+
def http_status = :locked
|
|
275
|
+
def api_error = { code: http_status, message: message }
|
|
276
|
+
end
|
|
120
277
|
|
|
121
|
-
|
|
122
|
-
|
|
278
|
+
# 424 Failed Dependency - request failed due to failure of a previous request.
|
|
279
|
+
class FailedDependencyError < ServiceError
|
|
280
|
+
DEFAULT_MESSAGE = 'Failed dependency'
|
|
123
281
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# @param code [String] error code for API responses (default: 'guard_failed')
|
|
128
|
-
# @param http_status [Symbol, Integer] HTTP status (default: :unprocessable_entity)
|
|
129
|
-
def initialize(message = nil, code: 'guard_failed', http_status: :unprocessable_entity)
|
|
130
|
-
super(message)
|
|
131
|
-
@code = code
|
|
132
|
-
@http_status = http_status
|
|
133
|
-
end
|
|
282
|
+
def http_status = :failed_dependency
|
|
283
|
+
def api_error = { code: http_status, message: message }
|
|
284
|
+
end
|
|
134
285
|
|
|
135
|
-
|
|
286
|
+
# 425 Too Early - server unwilling to process request that might be replayed.
|
|
287
|
+
class TooEarlyError < ServiceError
|
|
288
|
+
DEFAULT_MESSAGE = 'Too early'
|
|
289
|
+
|
|
290
|
+
def http_status = :too_early
|
|
291
|
+
def api_error = { code: http_status, message: message }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# 426 Upgrade Required - client should switch to a different protocol.
|
|
295
|
+
class UpgradeRequiredError < ServiceError
|
|
296
|
+
DEFAULT_MESSAGE = 'Upgrade required'
|
|
297
|
+
|
|
298
|
+
def http_status = :upgrade_required
|
|
299
|
+
def api_error = { code: http_status, message: message }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# 428 Precondition Required - origin server requires the request to be conditional.
|
|
303
|
+
class PreconditionRequiredError < ServiceError
|
|
304
|
+
DEFAULT_MESSAGE = 'Precondition required'
|
|
305
|
+
|
|
306
|
+
def http_status = :precondition_required
|
|
307
|
+
def api_error = { code: http_status, message: message }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# 429 Too Many Requests - user has sent too many requests in a given time.
|
|
311
|
+
class TooManyRequestsError < ServiceError
|
|
312
|
+
DEFAULT_MESSAGE = 'Too many requests'
|
|
313
|
+
|
|
314
|
+
def http_status = :too_many_requests
|
|
315
|
+
def api_error = { code: http_status, message: message }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# 431 Request Header Fields Too Large - server unwilling to process due to header size.
|
|
319
|
+
class RequestHeaderFieldsTooLargeError < ServiceError
|
|
320
|
+
DEFAULT_MESSAGE = 'Request header fields too large'
|
|
321
|
+
|
|
322
|
+
def http_status = :request_header_fields_too_large
|
|
323
|
+
def api_error = { code: http_status, message: message }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# 451 Unavailable For Legal Reasons - resource unavailable due to legal demands.
|
|
327
|
+
class UnavailableForLegalReasonsError < ServiceError
|
|
328
|
+
DEFAULT_MESSAGE = 'Unavailable for legal reasons'
|
|
329
|
+
|
|
330
|
+
def http_status = :unavailable_for_legal_reasons
|
|
331
|
+
def api_error = { code: http_status, message: message }
|
|
136
332
|
end
|
|
137
333
|
|
|
138
334
|
# 500 Internal Server Error - unexpected server-side failure.
|
|
@@ -143,6 +339,22 @@ module Servus
|
|
|
143
339
|
def api_error = { code: http_status, message: message }
|
|
144
340
|
end
|
|
145
341
|
|
|
342
|
+
# 501 Not Implemented - server does not support the functionality required.
|
|
343
|
+
class NotImplementedError < ServiceError
|
|
344
|
+
DEFAULT_MESSAGE = 'Not implemented'
|
|
345
|
+
|
|
346
|
+
def http_status = :not_implemented
|
|
347
|
+
def api_error = { code: http_status, message: message }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# 502 Bad Gateway - server received an invalid response from upstream.
|
|
351
|
+
class BadGatewayError < ServiceError
|
|
352
|
+
DEFAULT_MESSAGE = 'Bad gateway'
|
|
353
|
+
|
|
354
|
+
def http_status = :bad_gateway
|
|
355
|
+
def api_error = { code: http_status, message: message }
|
|
356
|
+
end
|
|
357
|
+
|
|
146
358
|
# 503 Service Unavailable - dependency temporarily unavailable.
|
|
147
359
|
class ServiceUnavailableError < ServiceError
|
|
148
360
|
DEFAULT_MESSAGE = 'Service unavailable'
|
|
@@ -150,6 +362,62 @@ module Servus
|
|
|
150
362
|
def http_status = :service_unavailable
|
|
151
363
|
def api_error = { code: http_status, message: message }
|
|
152
364
|
end
|
|
365
|
+
|
|
366
|
+
# 504 Gateway Timeout - upstream server did not respond in time.
|
|
367
|
+
class GatewayTimeoutError < ServiceError
|
|
368
|
+
DEFAULT_MESSAGE = 'Gateway timeout'
|
|
369
|
+
|
|
370
|
+
def http_status = :gateway_timeout
|
|
371
|
+
def api_error = { code: http_status, message: message }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# 505 HTTP Version Not Supported - server does not support the HTTP version.
|
|
375
|
+
class HttpVersionNotSupportedError < ServiceError
|
|
376
|
+
DEFAULT_MESSAGE = 'HTTP version not supported'
|
|
377
|
+
|
|
378
|
+
def http_status = :http_version_not_supported
|
|
379
|
+
def api_error = { code: http_status, message: message }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# 506 Variant Also Negotiates - transparent content negotiation error.
|
|
383
|
+
class VariantAlsoNegotiatesError < ServiceError
|
|
384
|
+
DEFAULT_MESSAGE = 'Variant also negotiates'
|
|
385
|
+
|
|
386
|
+
def http_status = :variant_also_negotiates
|
|
387
|
+
def api_error = { code: http_status, message: message }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# 507 Insufficient Storage - server unable to store the representation.
|
|
391
|
+
class InsufficientStorageError < ServiceError
|
|
392
|
+
DEFAULT_MESSAGE = 'Insufficient storage'
|
|
393
|
+
|
|
394
|
+
def http_status = :insufficient_storage
|
|
395
|
+
def api_error = { code: http_status, message: message }
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# 508 Loop Detected - server detected an infinite loop while processing.
|
|
399
|
+
class LoopDetectedError < ServiceError
|
|
400
|
+
DEFAULT_MESSAGE = 'Loop detected'
|
|
401
|
+
|
|
402
|
+
def http_status = :loop_detected
|
|
403
|
+
def api_error = { code: http_status, message: message }
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# 510 Not Extended - further extensions to the request are required.
|
|
407
|
+
class NotExtendedError < ServiceError
|
|
408
|
+
DEFAULT_MESSAGE = 'Not extended'
|
|
409
|
+
|
|
410
|
+
def http_status = :not_extended
|
|
411
|
+
def api_error = { code: http_status, message: message }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# 511 Network Authentication Required - client needs to authenticate for network access.
|
|
415
|
+
class NetworkAuthenticationRequiredError < ServiceError
|
|
416
|
+
DEFAULT_MESSAGE = 'Network authentication required'
|
|
417
|
+
|
|
418
|
+
def http_status = :network_authentication_required
|
|
419
|
+
def api_error = { code: http_status, message: message }
|
|
420
|
+
end
|
|
153
421
|
end
|
|
154
422
|
end
|
|
155
423
|
end
|
|
@@ -51,7 +51,7 @@ module Servus
|
|
|
51
51
|
# @api private
|
|
52
52
|
def initialize(success, data, error)
|
|
53
53
|
@success = success
|
|
54
|
-
@data = data
|
|
54
|
+
@data = DataObject.wrap(data)
|
|
55
55
|
@error = error
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -69,6 +69,17 @@ module Servus
|
|
|
69
69
|
def success?
|
|
70
70
|
@success
|
|
71
71
|
end
|
|
72
|
+
|
|
73
|
+
# Checks if the service execution failed.
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean] true if the service failed, false if it succeeded
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# result = MyService.call(params)
|
|
79
|
+
# return render_error(result.error.message) if result.failure?
|
|
80
|
+
def failure?
|
|
81
|
+
!@success
|
|
82
|
+
end
|
|
72
83
|
end
|
|
73
84
|
end
|
|
74
85
|
end
|
|
@@ -58,11 +58,11 @@ module Servus
|
|
|
58
58
|
true
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# Validates service result data against the
|
|
61
|
+
# Validates service result data against the appropriate schema.
|
|
62
62
|
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
63
|
+
# For successful responses, validates against the +result+ schema.
|
|
64
|
+
# For failure responses with data, validates against the +failure+ schema.
|
|
65
|
+
# Failure responses without data are skipped.
|
|
66
66
|
#
|
|
67
67
|
# @param service_class [Class] the service class being validated
|
|
68
68
|
# @param result [Servus::Support::Response] the response object to validate
|
|
@@ -74,16 +74,15 @@ module Servus
|
|
|
74
74
|
#
|
|
75
75
|
# @api private
|
|
76
76
|
def self.validate_result!(service_class, result)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
schema = load_schema(service_class, 'result')
|
|
80
|
-
return result unless schema # Skip validation if no schema exists
|
|
77
|
+
schema = result_schema_for(service_class, result)
|
|
78
|
+
return result unless schema
|
|
81
79
|
|
|
82
80
|
serialized_result = result.data.as_json
|
|
83
81
|
validation_errors = JSON::Validator.fully_validate(schema, serialized_result)
|
|
84
82
|
|
|
85
83
|
if validation_errors.any?
|
|
86
|
-
|
|
84
|
+
schema_type = result.success? ? 'result' : 'failure'
|
|
85
|
+
error_message = "Invalid #{schema_type} structure from #{service_class.name}: #{validation_errors.join(', ')}"
|
|
87
86
|
raise Servus::Base::ValidationError, error_message
|
|
88
87
|
end
|
|
89
88
|
|
|
@@ -127,7 +126,7 @@ module Servus
|
|
|
127
126
|
# Schemas are cached after first load for performance.
|
|
128
127
|
#
|
|
129
128
|
# @param service_class [Class] the service class
|
|
130
|
-
# @param type [String] schema type ("arguments" or "
|
|
129
|
+
# @param type [String] schema type ("arguments", "result", or "failure")
|
|
131
130
|
# @return [Hash, nil] the schema hash, or nil if no schema found
|
|
132
131
|
#
|
|
133
132
|
# @api private
|
|
@@ -141,10 +140,10 @@ module Servus
|
|
|
141
140
|
return @schema_cache[schema_path] if @schema_cache.key?(schema_path)
|
|
142
141
|
|
|
143
142
|
# Check for DSL-defined schema first
|
|
144
|
-
dsl_schema =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
dsl_schema = case type
|
|
144
|
+
when 'arguments' then service_class.arguments_schema
|
|
145
|
+
when 'result' then service_class.result_schema
|
|
146
|
+
when 'failure' then service_class.failure_schema
|
|
148
147
|
end
|
|
149
148
|
|
|
150
149
|
inline_schema_constant_name = "#{service_class}::#{type.upcase}_SCHEMA"
|
|
@@ -180,6 +179,21 @@ module Servus
|
|
|
180
179
|
@schema_cache
|
|
181
180
|
end
|
|
182
181
|
|
|
182
|
+
# Resolves the appropriate schema for a result based on its success/failure state.
|
|
183
|
+
#
|
|
184
|
+
# @param service_class [Class] the service class
|
|
185
|
+
# @param result [Servus::Support::Response] the response to resolve schema for
|
|
186
|
+
# @return [Hash, nil] the schema, or nil if none applies
|
|
187
|
+
#
|
|
188
|
+
# @api private
|
|
189
|
+
def self.result_schema_for(service_class, result)
|
|
190
|
+
if result.success?
|
|
191
|
+
load_schema(service_class, 'result')
|
|
192
|
+
elsif result.data
|
|
193
|
+
load_schema(service_class, 'failure')
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
183
197
|
# Fetches schema from DSL, inline constant, or file.
|
|
184
198
|
#
|
|
185
199
|
# Implements the schema resolution precedence:
|
|
@@ -116,6 +116,28 @@ module Servus
|
|
|
116
116
|
Servus::Support::Response.new(true, example, nil)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Extracts example failure data values from a service's schema.
|
|
120
|
+
#
|
|
121
|
+
# Looks for `example` or `examples` keywords in the service's failure schema
|
|
122
|
+
# and returns them wrapped in a failure Response. Useful for validating failure
|
|
123
|
+
# response structure in tests.
|
|
124
|
+
#
|
|
125
|
+
# @param service_class [Class] The service class to extract examples from
|
|
126
|
+
# @param overrides [Hash] Optional values to override the schema examples
|
|
127
|
+
# @return [Servus::Support::Response] Failure response object with example data
|
|
128
|
+
#
|
|
129
|
+
# @example Basic usage
|
|
130
|
+
# expected = servus_failure_example(ProcessPayment::Service)
|
|
131
|
+
# # => Servus::Support::Response with failure? == true, data:
|
|
132
|
+
# # { reason: 'card_declined', decline_code: 'insufficient_funds' }
|
|
133
|
+
#
|
|
134
|
+
# @note Override keys can be strings or symbols; they'll be converted to symbols
|
|
135
|
+
# @note Returns empty hash if service has no failure schema defined
|
|
136
|
+
def servus_failure_example(service_class, overrides = {})
|
|
137
|
+
example = extract_example_from(service_class, :failure, overrides)
|
|
138
|
+
Servus::Support::Response.new(false, example, Servus::Support::Errors::ServiceError.new)
|
|
139
|
+
end
|
|
140
|
+
|
|
119
141
|
private
|
|
120
142
|
|
|
121
143
|
# Helper method to extract and merge examples from schema
|
data/lib/servus/version.rb
CHANGED
data/lib/servus.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative 'servus/config'
|
|
|
20
20
|
|
|
21
21
|
# Support
|
|
22
22
|
require_relative 'servus/support/logger'
|
|
23
|
+
require_relative 'servus/support/data_object'
|
|
23
24
|
require_relative 'servus/support/response'
|
|
24
25
|
require_relative 'servus/support/validator'
|
|
25
26
|
require_relative 'servus/support/errors'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: servus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Scholl
|
|
@@ -27,14 +27,14 @@ dependencies:
|
|
|
27
27
|
name: activesupport
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '8.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '8.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
@@ -55,14 +55,14 @@ dependencies:
|
|
|
55
55
|
name: actionpack
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
|
-
- - "
|
|
58
|
+
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
60
|
version: '8.0'
|
|
61
61
|
type: :development
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
|
-
- - "
|
|
65
|
+
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '8.0'
|
|
68
68
|
description: Servus is a Ruby gem that provides a structured way to create and manage
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- docs/features/4_logging.md
|
|
98
98
|
- docs/features/5_event_bus.md
|
|
99
99
|
- docs/features/6_guards.md
|
|
100
|
+
- docs/features/7_lazy_resolvers.md
|
|
100
101
|
- docs/features/guards_naming_convention.md
|
|
101
102
|
- docs/guides/1_common_patterns.md
|
|
102
103
|
- docs/guides/2_migration_guide.md
|
|
@@ -203,6 +204,10 @@ files:
|
|
|
203
204
|
- lib/servus/extensions/async/errors.rb
|
|
204
205
|
- lib/servus/extensions/async/ext.rb
|
|
205
206
|
- lib/servus/extensions/async/job.rb
|
|
207
|
+
- lib/servus/extensions/lazily/call.rb
|
|
208
|
+
- lib/servus/extensions/lazily/errors.rb
|
|
209
|
+
- lib/servus/extensions/lazily/ext.rb
|
|
210
|
+
- lib/servus/extensions/lazily/resolver.rb
|
|
206
211
|
- lib/servus/guard.rb
|
|
207
212
|
- lib/servus/guards.rb
|
|
208
213
|
- lib/servus/guards/falsey_guard.rb
|
|
@@ -211,6 +216,7 @@ files:
|
|
|
211
216
|
- lib/servus/guards/truthy_guard.rb
|
|
212
217
|
- lib/servus/helpers/controller_helpers.rb
|
|
213
218
|
- lib/servus/railtie.rb
|
|
219
|
+
- lib/servus/support/data_object.rb
|
|
214
220
|
- lib/servus/support/errors.rb
|
|
215
221
|
- lib/servus/support/logger.rb
|
|
216
222
|
- lib/servus/support/message_resolver.rb
|
|
@@ -245,7 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
245
251
|
- !ruby/object:Gem::Version
|
|
246
252
|
version: '0'
|
|
247
253
|
requirements: []
|
|
248
|
-
rubygems_version:
|
|
254
|
+
rubygems_version: 4.0.6
|
|
249
255
|
specification_version: 4
|
|
250
256
|
summary: A gem for managing service objects.
|
|
251
257
|
test_files: []
|