servus 0.1.1 → 0.1.2

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: 69e20c0a6d4d7b3651839114f930d1befbc49701c15f9489f6a180e9384698c9
4
- data.tar.gz: 1267963f2b5957d73fc4fb39654df87104932fc831786541db48ba478fa53d4f
3
+ metadata.gz: 0bddb5c486e4996dff5183d6a9bd57ef086739d7007a12125479d320464a0018
4
+ data.tar.gz: 2d948b6202d0888664a75766cf4e9a3c607b4f819e7d9293ac0d0ff8f42c6c32
5
5
  SHA512:
6
- metadata.gz: 9ebcf136c2ce120b4489c4cf403c9a4b850ae83156429816acdc15776140b21a3d420cbe14fb24def19fde9c07fa7ef2fdac3cc2b62b0d2f6004a8dbd0898e8d
7
- data.tar.gz: b367bd7c82bf0f5e4606b0bf8cbee2c777171018a370478d3a9ef07a5dc7d24ea6e240c78bdaf34c5a0cd19491101ea88c865b15e9bfa589e3dad76672ad4868
6
+ metadata.gz: 18a6ffd8d72e5a452bda53b5d91a621d0b963872c08d72b8ee4a533160babfde16ed9690a1f9a0b2c5aa1a4c330f3e6209b9ada2ffdba3635bad9cf0bd351d8e
7
+ data.tar.gz: 59b87da3ae33ec0a586275892632b6fe84810b44d75759d9c6c2733fdb9224093d1ed9740467438908eb4a5640b41b6468f1c8381c65cc07adf843e23979fb80
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2025-10-10
4
+ - Added: Added `call_async` method to `Servus::Base` to enqueue a job for calling the service asynchronously
5
+ - Added: Added `Async::Job` to handle async enqueing with support for ActiveJob set options
6
+
3
7
  ## [0.1.1] - 2025-08-20
4
8
 
5
9
  - Added: Added `rescue_from` method to `Servus::Base` to rescue from standard errors and use custom error types.
data/READme.md CHANGED
@@ -119,6 +119,29 @@ end
119
119
 
120
120
  ```
121
121
 
122
+ Here’s a section you can add to your README for the new `.call_async` feature, matching the style of your existing `## Inheritance` section:
123
+
124
+ ---
125
+
126
+ ## **Asynchronous Execution**
127
+
128
+ You can asynchronously execute any service class that inherits from `Servus::Base` using `.call_async`. This uses `ActiveJob` under the hood and supports standard job options (`wait`, `queue`, `priority`, etc.). Only available in environments where `ActiveJob` is loaded (e.g., Rails apps)
129
+
130
+ ```ruby
131
+ # Good ✅
132
+ Services::NotifyUser::Service.call_async(
133
+ user_id: current_user.id,
134
+ wait: 5.minutes,
135
+ queue: :low_priority,
136
+ job_options: { tags: ['notifications'] }
137
+ )
138
+
139
+ # Bad ❌
140
+ Services::NotifyUser::Support::MessageBuilder.call_async(
141
+ # Invalid: support classes don't inherit from Servus::Base
142
+ )
143
+ ```
144
+
122
145
  ## **Inheritance**
123
146
 
124
147
  - Every main service class (`service.rb`) must inherit from `Servus::Base`
@@ -249,12 +272,12 @@ end
249
272
  class SomeController < AppController
250
273
  def controller_action
251
274
  result = SomeServiceObject::Service.call(arg: 1)
252
-
275
+
253
276
  return if result.success?
254
-
277
+
255
278
  # If you just want the error message
256
279
  bad_request(result.error.message)
257
-
280
+
258
281
  # If you want the API error
259
282
  service_object_error(result.error.api_error)
260
283
  end
@@ -271,9 +294,9 @@ class SomeServiceObject::Service < Servus::Base
271
294
  class SomethingGlitched < StandardError; end
272
295
 
273
296
  # Rescue from standard errors and use custom error
274
- rescue_from
275
- SomethingBroke,
276
- SomethingGlitched,
297
+ rescue_from
298
+ SomethingBroke,
299
+ SomethingGlitched,
277
300
  use: Servus::Support::Errors::ServiceUnavailableError # this is optional
278
301
 
279
302
  def call
@@ -312,8 +335,8 @@ Service objects can be called from controllers using the `run_service` and `rend
312
335
 
313
336
  ### run_service
314
337
 
315
- `run_service` calls the service object with the provided parameters and set's an instance variable `@result` to the
316
- result of the service object if the result is successful. If the result is not successful, it will pass the result
338
+ `run_service` calls the service object with the provided parameters and set's an instance variable `@result` to the
339
+ result of the service object if the result is successful. If the result is not successful, it will pass the result
317
340
  to error to the `render_service_object_error` helper. This allows for easy error handling in the controller for
318
341
  repetetive usecases.
319
342
 
@@ -335,7 +358,7 @@ end
335
358
 
336
359
  ### render_service_object_error
337
360
 
338
- `render_service_object_error` renders the error of a service object. It expects a hash with a `message` key and a `code` key from
361
+ `render_service_object_error` renders the error of a service object. It expects a hash with a `message` key and a `code` key from
339
362
  the api_error method of the service error. This is all setup by default for a JSON API response, thought the method can be
340
363
  overridden if needed to handle different usecases.
341
364
 
@@ -344,7 +367,7 @@ overridden if needed to handle different usecases.
344
367
  #
345
368
  # error = result.error.api_error
346
369
  # => { message: "Error message", code: 400 }
347
- #
370
+ #
348
371
  # render json: { message: error[:message], code: error[:code] }, status: error[:code]
349
372
 
350
373
  class SomeController < AppController
@@ -472,4 +495,4 @@ Both file-based and inline schemas are automatically cached:
472
495
 
473
496
  - First validation request loads and caches the schema
474
497
  - Subsequent validations use the cached version
475
- - Cache can be cleared using `SchemaValidation.clear_cache!`
498
+ - Cache can be cleared using `SchemaValidation.clear_cache!`
Binary file
data/lib/servus/config.rb CHANGED
@@ -9,11 +9,7 @@ module Servus
9
9
 
10
10
  def initialize
11
11
  # Default to Rails.root if available, otherwise use current working directory
12
- @schema_root = if defined?(Rails)
13
- Rails.root.join('app/schemas/services')
14
- else
15
- File.expand_path('../../../app/schemas/services', __dir__)
16
- end
12
+ @schema_root = File.join(root_path, 'app/schemas/services')
17
13
  end
18
14
 
19
15
  # Returns the path for a specific service's schema
@@ -32,6 +28,19 @@ module Servus
32
28
  def schema_dir_for(service_namespace)
33
29
  File.join(schema_root.to_s, service_namespace)
34
30
  end
31
+
32
+ private
33
+
34
+ # Sets the schema root directory
35
+ #
36
+ # @param path [String] the new schema root directory
37
+ def root_path
38
+ if defined?(Rails) && Rails.respond_to?(:root)
39
+ Rails.root
40
+ else
41
+ File.expand_path('../../..', __dir__)
42
+ end
43
+ end
35
44
  end
36
45
 
37
46
  # Singleton config instance
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ module Async
6
+ # Calls the service asynchronously using AsyncCallerJob.
7
+ #
8
+ # Supports all standard ActiveJob scheduling and routing options:
9
+ # - wait: <ActiveSupport::Duration> (e.g., 5.minutes)
10
+ # - wait_until: <Time> (e.g., 2.hours.from_now)
11
+ # - queue: <Symbol/String> (e.g., :critical, 'low_priority')
12
+ # - priority: <Integer> (depends on adapter support)
13
+ # - retry: <Boolean> (custom control for job retry)
14
+ # - job_options: <Hash> (extra options, merged in)
15
+ #
16
+ # Example:
17
+ # call_async(
18
+ # wait: 10.minutes,
19
+ # queue: :low_priority,
20
+ # priority: 20,
21
+ # job_options: { tags: ['user_graduation'] },
22
+ # user_id: current_user.id
23
+ # )
24
+ #
25
+ module Call
26
+ # @param args [Hash] The arguments to pass to the service and job options.
27
+ # @return [void]
28
+ def call_async(**args)
29
+ # Extract ActiveJob configuration options
30
+ job_options = args.slice(:wait, :wait_until, :queue, :priority)
31
+ job_options.merge!(args.delete(:job_options) || {}) # merge custom job options
32
+
33
+ # Remove special keys that shouldn't be passed to the service
34
+ args.except!(:wait, :wait_until, :queue, :priority, :job_options)
35
+
36
+ # Build job with optional delay, scheduling, or queue settings
37
+ job = job_options.any? ? Job.set(**job_options.compact) : Job
38
+
39
+ # Enqueue the job asynchronously
40
+ job.perform_later(name: name, args: args)
41
+ rescue StandardError => e
42
+ raise Errors::JobEnqueueError, "Failed to enqueue async job for #{self}: #{e.message}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ module Async
6
+ module Errors
7
+ # Base error class for async extensions
8
+ class AsyncError < StandardError; end
9
+
10
+ # Error raised when the job fails to enqueue
11
+ class JobEnqueueError < AsyncError; end
12
+
13
+ # Error raised when the service class cannot be found
14
+ class ServiceNotFoundError < AsyncError; end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ # Async extensions for Servus
6
+ module Async
7
+ require 'servus/extensions/async/errors'
8
+ require 'servus/extensions/async/job'
9
+ require 'servus/extensions/async/call'
10
+
11
+ # Module providing async extensions for Servus
12
+ module Ext; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ module Async
6
+ # Job to run a service class with given arguments.
7
+ #
8
+ # This job will be migrated to Servus once it's stable as a .call_async method.
9
+ # It takes the fully-qualified class name of the service as a string and any keyword arguments
10
+ # required by the service's .call method.
11
+ #
12
+ # Example usage:
13
+ # RunServiceJob.perform_later('SomeModule::SomeService', arg1: value1, arg2: value2)
14
+ #
15
+ # This will invoke SomeModule::SomeService.call(arg1: value1, arg2: value2) in a background job.
16
+ #
17
+ # Errors during service execution are logged.
18
+ class Job < ActiveJob::Base
19
+ queue_as :default
20
+
21
+ def perform(name:, args:)
22
+ constantize!(name).call(**args)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :klass
28
+
29
+ def constantize!(class_name)
30
+ class_name.safe_constantize || (raise Errors::ServiceNotFoundError,
31
+ "Service class '#{class_name}' not found.")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/servus/railtie.rb
4
3
  require 'rails/railtie'
5
4
 
6
5
  module Servus
@@ -11,5 +10,13 @@ module Servus
11
10
  include Servus::Helpers::ControllerHelpers
12
11
  end
13
12
  end
13
+
14
+ initializer 'servus.job_async' do
15
+ ActiveSupport.on_load(:active_job) do
16
+ require 'servus/extensions/async/ext'
17
+ # Extend the base service with the async call method
18
+ Servus::Base.extend Servus::Extensions::Async::Call
19
+ end
20
+ end
14
21
  end
15
22
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
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.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
@@ -79,6 +79,7 @@ files:
79
79
  - READme.md
80
80
  - Rakefile
81
81
  - builds/servus-0.0.1.gem
82
+ - builds/servus-0.1.1.gem
82
83
  - lib/generators/servus/service/service_generator.rb
83
84
  - lib/generators/servus/service/templates/arguments.json.erb
84
85
  - lib/generators/servus/service/templates/result.json.erb
@@ -87,6 +88,10 @@ files:
87
88
  - lib/servus.rb
88
89
  - lib/servus/base.rb
89
90
  - lib/servus/config.rb
91
+ - lib/servus/extensions/async/call.rb
92
+ - lib/servus/extensions/async/errors.rb
93
+ - lib/servus/extensions/async/ext.rb
94
+ - lib/servus/extensions/async/job.rb
90
95
  - lib/servus/helpers/controller_helpers.rb
91
96
  - lib/servus/railtie.rb
92
97
  - lib/servus/support/errors.rb