servus 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f63266b1efb1e782745c67d24957ce1126b1031592667bd78fe80f5ed2c93ac7
4
- data.tar.gz: 64d263185dc9214707aafc7eaaf740343e5c24311eec49db6bbccff25fa413a9
3
+ metadata.gz: 6f20bb1c0b41657377b94a29aef1915dc7a0c4c65442d21b9f6a60c75537dd8a
4
+ data.tar.gz: 9273a9d2e3efa50c3ce2d80935e7538a1fa89e0cc3195acfde974621f7960277
5
5
  SHA512:
6
- metadata.gz: a6d3ff24ec58b33b926acca83447ede76c1feb54daab1ade3b4a744f66e70a87785b68d6e235934b6b20f6477e7518441486bb6d405f34cd8bfb2334c095122e
7
- data.tar.gz: 5f562d3cac4a258387fab0936d8fecb41e9e42803556014d91c5802f0b27f6b5cd2a085f76b4d0d486c609f2589c286d5fe13fbd4be34270f4ecc104d2b478a1
6
+ metadata.gz: e2e4ec7e3390784c35acbe238b3d809c4e07859c39ccd159a1618daf3439d04d7bb4eb3c847ea30e1a3b94a1d4dfac2508601ce263160ddee1d7e0a9e20ed13b
7
+ data.tar.gz: 4f135793c078f9230436a6ff3f9508847469a198dfa818afadcbace146b7b85fdb78844a864538b96bded6cb3eed910002c0fdc8b6802ad4cae3189e06349a54
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.2.1] - 2025-12-20
2
+
3
+ ### Added
4
+
5
+ - **EventHandler Scheduling Options**: Extended the `invoke` DSL to support ActiveJob scheduling options
6
+ - `:wait` - delay execution (e.g., `5.minutes`)
7
+ - `:wait_until` - schedule for specific time
8
+ - `:priority` - job priority
9
+ - `:job_options` - additional ActiveJob options
10
+ - Options are passed through to `call_async`, enabling delayed and scheduled event handling
11
+
12
+ - **Custom HTTP Error Classes**: Added granular error classes for HTTP status handling
13
+ - Error classes for common HTTP statuses (400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504)
14
+ - Each error class has appropriate `http_status` and default `code`/`message`
15
+ - Enables more precise error handling and cleaner rescue blocks
16
+
1
17
  ## [0.2.0] - 2025-12-16
2
18
 
3
19
  ### Added
@@ -207,9 +207,9 @@ module Servus
207
207
  def collect_emitted_events
208
208
  events = Set.new
209
209
 
210
- ObjectSpace.each_object(Class)
211
- .select { |klass| klass < Servus::Base }
212
- .each do |service_class|
210
+ services = ObjectSpace.each_object(Class).select { |klass| klass < Servus::Base }
211
+
212
+ services.each do |service_class|
213
213
  service_class.event_emissions.each_value do |emissions|
214
214
  emissions.each { |emission| events << emission[:event_name] }
215
215
  end
@@ -226,9 +226,9 @@ module Servus
226
226
  def find_orphaned_handlers(emitted_events)
227
227
  orphaned = []
228
228
 
229
- ObjectSpace.each_object(Class)
230
- .select { |klass| klass < Servus::EventHandler && klass != Servus::EventHandler }
231
- .each do |handler_class|
229
+ handlers = ObjectSpace.each_object(Class).select { _1 < Servus::EventHandler && _1 != Servus::EventHandler }
230
+
231
+ handlers.each do |handler_class|
232
232
  next unless handler_class.event_name
233
233
  next if emitted_events.include?(handler_class.event_name)
234
234
 
@@ -245,15 +245,13 @@ module Servus
245
245
  # @return [Servus::Support::Response] the service result
246
246
  # @api private
247
247
  def invoke_service(invocation, payload)
248
- service_kwargs = invocation[:mapper].call(payload)
249
-
250
- async = invocation.dig(:options, :async) || false
251
- queue = invocation.dig(:options, :queue) || nil
248
+ use_async = invocation.dig(:options, :async) || false
252
249
 
253
- if async
254
- service_kwargs = service_kwargs.merge(queue: queue) if queue
250
+ if use_async
251
+ service_kwargs = prepare_call_sync_args(invocation, payload)
255
252
  invocation[:service_class].call_async(**service_kwargs)
256
253
  else
254
+ service_kwargs = invocation[:mapper].call(payload)
257
255
  invocation[:service_class].call(**service_kwargs)
258
256
  end
259
257
  end
@@ -270,6 +268,23 @@ module Servus
270
268
 
271
269
  true
272
270
  end
271
+
272
+ # Prepares the service arguments by merging event payload with job options.
273
+ #
274
+ # @param invocation [Hash] the invocation configuration
275
+ # @param payload [Hash] the event payload
276
+ # @return [Hash] combined service arguments
277
+ def prepare_call_sync_args(invocation, payload)
278
+ mapper = invocation[:mapper]
279
+ options = invocation[:options]
280
+
281
+ # Extract service arguments and merge with job options
282
+ job_opts = options.slice(:queue, :wait, :wait_until, :priority, :job_options).compact
283
+
284
+ mapper
285
+ .call(payload)
286
+ .merge(job_opts)
287
+ end
273
288
  end
274
289
  end
275
290
  end
@@ -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
- # Guard validation failure with custom code.
110
- #
111
- # Guards define their own error code and HTTP status via the DSL.
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
- # @return [String] application-specific error code
119
- attr_reader :code
274
+ def http_status = :locked
275
+ def api_error = { code: http_status, message: message }
276
+ end
120
277
 
121
- # @return [Symbol, Integer] HTTP status code
122
- attr_reader :http_status
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
- # Creates a new guard error with metadata.
125
- #
126
- # @param message [String, nil] error message
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
- def api_error = { code: code, message: message }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.1'
5
5
  end
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.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl