servus 0.7.0 → 1.0.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: '0560049029e2b803e64323461fb8d5c8b56cdc1d69c5b5de83ca0e6a1858a65c'
4
- data.tar.gz: 0d1e6a460fbbf1b5c7c26811ee8fbb5eccccf151f2c4d8d94a8270c1f1d5ef41
3
+ metadata.gz: 8da8eeaf0d61216b5df16e50dbb6a7cf0fd36bc8a9a2c5d59f233a5ba6d399a8
4
+ data.tar.gz: ba37edced59e89519122e27dbee45334edadd1b35cda36b03f7966fe3452a0c9
5
5
  SHA512:
6
- metadata.gz: f59299fe30eb202c0a71858dc2c1491a29f4dd5a330e65ec471b412a1e060329bfca5c51eea2d7759fbddebbb2747fd65ccb79ecd048fc4d2dc07faba3f92420
7
- data.tar.gz: 81f9a1277db2e3d6b4d383c29a4be4f9c4cf92ba1bac10f680e687ea6cf4438067ec4b2b19d05ccd3909962e87e7eec738721a28cf1b65f281548bec7fbcb42e
6
+ metadata.gz: 5b7969e87edf801cc7c4e5e452db45a4bedb90f11b8bafb24e4750843a9babf80ca0b6d00d2a8abad6ce046192165cf17b50fe405b952d8324cdc3d04b131aed
7
+ data.tar.gz: 5485a32d587becac5ad967c3e552396c1b6994263dbe7a1c977e33d0a290c391dd748f02a3455a2e45307658932a1b66b51b9448ac6d8bcc92b96c0ce243ae55
@@ -11,23 +11,26 @@
11
11
  # <%= event_class_name %>.emit({ user_id: 123 })
12
12
  #
13
13
  # @example Invoke a service when this event fires
14
- # invoke SendEmail::Service, async: true do |payload|
14
+ # enqueue SendEmail::Service do |payload|
15
15
  # { user_id: payload[:user_id] }
16
16
  # end
17
17
  #
18
18
  # @example Pass full payload through (no mapper block)
19
- # invoke AuditLogger::Service, async: true
19
+ # enqueue AuditLogger::Service
20
20
  #
21
- # @example Conditional invocation
22
- # invoke GrantRewards::Service, if: ->(payload) { payload[:premium] } do |payload|
21
+ # @example Conditional
22
+ # enqueue GrantRewards::Service, if: ->(payload) { payload[:premium] } do |payload|
23
23
  # { user_id: payload[:user_id] }
24
24
  # end
25
25
  #
26
- # Available options for `invoke`:
27
- # - async: true - Invoke service asynchronously via ActiveJob
28
- # - queue: :queue_name - Specify ActiveJob queue (requires async: true)
29
- # - if: ->(payload) {} - Condition that must be true to invoke
30
- # - unless: ->(payload) {} - Condition that must be false to invoke
26
+ # Services are always enqueued via ActiveJob, never run inline.
27
+ #
28
+ # Available options for `enqueue`:
29
+ # - queue: :queue_name - Route the job to a queue
30
+ # - wait: 5.minutes - Delay before the job runs
31
+ # - priority: 10 - Job priority (adapter-dependent)
32
+ # - if: ->(payload) {} - Condition that must be true to enqueue
33
+ # - unless: ->(payload) {} - Condition that must be false to enqueue
31
34
  #
32
35
  # @see Servus::Event
33
36
  # @see Servus::Events::Bus
@@ -38,7 +41,7 @@ class <%= event_class_name %> < Servus::Event
38
41
  description: '<%= event_class_name %> event payload',
39
42
  }
40
43
 
41
- # invoke YourService, async: true do |payload|
44
+ # enqueue YourService do |payload|
42
45
  # { example_arg: payload[:example_field] }
43
46
  # end
44
47
  end
@@ -7,7 +7,6 @@ module Servus
7
7
  # Generates a complete service structure including:
8
8
  # - Service class file
9
9
  # - RSpec test file
10
- # - JSON schema files for arguments and results
11
10
  #
12
11
  # @example Generate a service
13
12
  # rails g servus:service namespace/do_something_helpful user amount
@@ -15,8 +14,6 @@ module Servus
15
14
  # @example Generated files
16
15
  # app/services/namespace/do_something_helpful/service.rb
17
16
  # spec/services/namespace/do_something_helpful/service_spec.rb
18
- # app/schemas/services/namespace/do_something_helpful/arguments.json
19
- # app/schemas/services/namespace/do_something_helpful/result.json
20
17
  #
21
18
  # @see https://guides.rubyonrails.org/generators.html
22
19
  class ServiceGenerator < Rails::Generators::NamedBase
@@ -30,16 +27,15 @@ module Servus
30
27
 
31
28
  # Creates all service-related files.
32
29
  #
33
- # Generates the service class, spec file, and schema files from templates.
30
+ # Generates the service class and spec file from templates.
31
+ #
32
+ # Schemas are declared inline with the +schema+ DSL, so there are no
33
+ # schema files to generate — the service template scaffolds them in place.
34
34
  #
35
35
  # @return [void]
36
36
  def create_service_file
37
37
  template 'service.rb.erb', service_path
38
38
  template 'service_spec.rb.erb', service_path_spec
39
-
40
- # Template json schemas
41
- template 'result.json.erb', service_result_schema_path
42
- template 'arguments.json.erb', service_arguments_shecma_path
43
39
  end
44
40
 
45
41
  private
@@ -49,7 +45,7 @@ module Servus
49
45
  # @return [String] service file path
50
46
  # @api private
51
47
  def service_path
52
- "app/services/#{file_path}/service.rb"
48
+ File.join(Servus.config.services_dir, file_path, 'service.rb')
53
49
  end
54
50
 
55
51
  # Returns the path for the service spec file.
@@ -60,22 +56,6 @@ module Servus
60
56
  "#{Servus.config.tests_dir}/services/#{file_path}/service_spec.rb"
61
57
  end
62
58
 
63
- # Returns the path for the result schema file.
64
- #
65
- # @return [String] result schema path
66
- # @api private
67
- def service_result_schema_path
68
- "app/schemas/services/#{file_path}/result.json"
69
- end
70
-
71
- # Returns the path for the arguments schema file.
72
- #
73
- # @return [String] arguments schema path
74
- # @api private
75
- def service_arguments_shecma_path
76
- "app/schemas/services/#{file_path}/arguments.json"
77
- end
78
-
79
59
  # Returns the service class name with ::Service appended.
80
60
  #
81
61
  # @return [String] service class name
@@ -108,10 +88,10 @@ module Servus
108
88
  #
109
89
  # @return [String] multi-line instance variable assignments
110
90
  # @example
111
- # initialize_params # => "@user = user\n @amount = amount"
91
+ # initialize_params # => "@user = user\n @amount = amount"
112
92
  # @api private
113
93
  def initialize_params
114
- parameters.map { |param| "@#{param} = #{param}" }.join("\n ")
94
+ parameters.map { |param| "@#{param} = #{param}" }.join("\n ")
115
95
  end
116
96
 
117
97
  # Generates attr_reader declarations for parameters.
@@ -41,34 +41,27 @@
41
41
  module <%= class_name %>
42
42
  class Service < Servus::Base
43
43
  <%- unless options[:no_docs] -%>
44
- # TODO: Define argument validation schema (optional but recommended)
45
- # schema arguments: {
46
- # type: 'object',
47
- <%- if parameters.any? -%>
48
- # required: [<%= parameters.map { |p| "'#{p}'" }.join(', ') %>],
49
- <%- else -%>
50
- # required: [],
44
+ # TODO: give each property a type. An empty property schema accepts anything,
45
+ # so until these are filled in only presence is enforced.
46
+ # Shared shapes can be referenced instead: { '$ref' => '#/core/$defs/amount' }
51
47
  <%- end -%>
52
- # properties: {
48
+ schema(
49
+ arguments: {
50
+ type: 'object',
51
+ required: <%= parameters.empty? ? '[]' : "%w[#{parameters.join(' ')}]" %>,
52
+ properties: {
53
53
  <%- parameters.each do |param| -%>
54
- # <%= param %>: { type: 'string' }<%= param == parameters.last ? '' : ',' %>
54
+ <%= param %>: {}<%= param == parameters.last ? '' : ',' %>
55
55
  <%- end -%>
56
- <%- if parameters.empty? -%>
57
- # # property_name: { type: 'string' }
58
- <%- end -%>
59
- # }
60
- # }
56
+ }
57
+ },
58
+ result: {
59
+ type: 'object',
60
+ required: [],
61
+ properties: {}
62
+ }
63
+ )
61
64
 
62
- # TODO: Define result validation schema (optional)
63
- # schema result: {
64
- # type: 'object',
65
- # required: [],
66
- # properties: {
67
- # # result_field: { type: 'string' }
68
- # }
69
- # }
70
-
71
- <%- end -%>
72
65
  <%- unless options[:no_docs] -%>
73
66
  # Initializes the service with required parameters.
74
67
  #
data/lib/servus/base.rb CHANGED
@@ -52,6 +52,63 @@ module Servus
52
52
  include Servus::Events::Emitter
53
53
  include Servus::Guards
54
54
 
55
+ extend Servus::Schema::Declaration
56
+
57
+ # @!method self.schema(arguments: nil, result: nil, failure: nil)
58
+ # Declares the JSON schemas used to validate this service.
59
+ #
60
+ # Arguments are validated before +call+ runs, so the body can trust the
61
+ # shape of its inputs. Result data is validated after it returns, so a
62
+ # service that stops honouring its own contract fails loudly rather than
63
+ # shipping the wrong shape to its callers.
64
+ #
65
+ # Schemas may reference shared fragments registered with
66
+ # {Servus::Schema.register}; refs are resolved on first read.
67
+ #
68
+ # Omitting a keyword leaves any schema declared earlier — or by a
69
+ # superclass — in place. Passing one explicitly as +nil+ raises.
70
+ #
71
+ # @param arguments [Hash] JSON schema for the service's arguments
72
+ # @param result [Hash] JSON schema for successful result data
73
+ # @param failure [Hash] JSON schema for failure response data
74
+ # @return [void]
75
+ # @raise [ArgumentError] on an unknown keyword or an explicit nil
76
+ #
77
+ # @example Declaring arguments and result schemas
78
+ # class ProcessPayment::Service < Servus::Base
79
+ # schema(
80
+ # arguments: {
81
+ # type: 'object',
82
+ # required: ['user_id', 'amount'],
83
+ # properties: {
84
+ # user_id: { type: 'integer' },
85
+ # amount: { type: 'number', minimum: 0.01 }
86
+ # }
87
+ # },
88
+ # result: {
89
+ # type: 'object',
90
+ # required: ['transaction_id'],
91
+ # properties: { transaction_id: { type: 'string' } }
92
+ # }
93
+ # )
94
+ # end
95
+ #
96
+ # @example Referencing a shared fragment
97
+ # schema arguments: {
98
+ # type: 'object',
99
+ # properties: { amount: { '$ref' => '#/core/$defs/amount' } }
100
+ # }
101
+ #
102
+ # @see Servus::Schema
103
+ #
104
+ # @!method self.arguments_schema
105
+ # @return [Hash, nil] the compiled arguments schema
106
+ # @!method self.result_schema
107
+ # @return [Hash, nil] the compiled result schema
108
+ # @!method self.failure_schema
109
+ # @return [Hash, nil] the compiled failure schema
110
+ declare_schemas :arguments, :result, :failure
111
+
55
112
  # Support class aliases
56
113
  Logger = Servus::Support::Logger
57
114
  Emitter = Servus::Events::Emitter
@@ -158,46 +215,6 @@ module Servus
158
215
  raise type, message
159
216
  end
160
217
 
161
- # Invokes another service from within this service's {#call} and returns its
162
- # data on success. On failure, halts the outer service with the sub-service's
163
- # failure Response — the outer service's caller receives that Response
164
- # unchanged (same error object, message, code, http_status).
165
- #
166
- # Sugar over:
167
- #
168
- # result = SubService.call(**params)
169
- # return result unless result.success?
170
- # data = result.data
171
- #
172
- # Only call from within a service's `#call` (or helpers reachable from
173
- # it); the throw is caught by {Servus::Base.call}.
174
- #
175
- # @example Composing services
176
- # class SendDigitalCash::Service < Servus::Base
177
- # def call
178
- # data1 = call!(Accounts::Lookup::Service, id: account_id)
179
- # data2 = call!(Ledger::RecordTransfer::Service, account:, amount:)
180
- # success(ref: data2.ref)
181
- # end
182
- # end
183
- #
184
- # For invoking a service from *outside* a service context (controllers,
185
- # rake tasks, jobs, consoles), see
186
- # {Servus::Helpers::ControllerHelpers#run_service!}.
187
- #
188
- # @param service_class [Class<Servus::Base>] the sub-service to invoke
189
- # @param params [Hash] keyword arguments to pass to the sub-service
190
- # @return [Servus::Support::DataObject, Object] the sub-service's data on success
191
- # @throw [:guard_failure, Servus::Support::Response] the failure Response, otherwise
192
- #
193
- # @see Servus::Helpers::ControllerHelpers#run_service!
194
- def call!(service_class, **params)
195
- result = service_class.call(**params)
196
- return result.data if result.success?
197
-
198
- throw(:guard_failure, result)
199
- end
200
-
201
218
  class << self
202
219
  # Executes the service with automatic validation, logging, and benchmarking.
203
220
  #
@@ -229,7 +246,7 @@ module Servus
229
246
  # @see #initialize
230
247
  # @see #call
231
248
  #
232
- # rubocop:disable Metrics/MethodLength
249
+ # rubocop:disable-next Metrics/MethodLength
233
250
  def call(**args)
234
251
  before_call(args)
235
252
 
@@ -255,70 +272,6 @@ module Servus
255
272
  Logger.log_exception(self, e)
256
273
  raise e
257
274
  end
258
- # rubocop:enable Metrics/MethodLength
259
-
260
- # Defines schema validation rules for the service's arguments, result, and/or failure data.
261
- #
262
- # This method provides a clean DSL for specifying JSON schemas that will be used
263
- # to validate service inputs and outputs. Schemas defined via this method take
264
- # precedence over ARGUMENTS_SCHEMA, RESULT_SCHEMA, and FAILURE_SCHEMA constants.
265
- # The next major version will deprecate those constants in favor of this DSL.
266
- #
267
- # @param arguments [Hash, nil] JSON schema for validating service arguments
268
- # @param result [Hash, nil] JSON schema for validating service result data
269
- # @param failure [Hash, nil] JSON schema for validating failure response data
270
- # @return [void]
271
- #
272
- # @example Defining both arguments and result schemas
273
- # class ProcessPayment::Service < Servus::Base
274
- # schema(
275
- # arguments: {
276
- # type: 'object',
277
- # required: ['user_id', 'amount'],
278
- # properties: {
279
- # user_id: { type: 'integer' },
280
- # amount: { type: 'number', minimum: 0.01 }
281
- # }
282
- # },
283
- # result: {
284
- # type: 'object',
285
- # required: ['transaction_id'],
286
- # properties: {
287
- # transaction_id: { type: 'string' }
288
- # }
289
- # }
290
- # )
291
- # end
292
- #
293
- # @example Defining only arguments schema
294
- # class SendEmail::Service < Servus::Base
295
- # schema arguments: { type: 'object', required: ['email', 'subject'] }
296
- # end
297
- #
298
- # @see Servus::Support::Validator
299
- def schema(arguments: nil, result: nil, failure: nil)
300
- @arguments_schema = arguments.with_indifferent_access if arguments
301
- @result_schema = result.with_indifferent_access if result
302
- @failure_schema = failure.with_indifferent_access if failure
303
- end
304
-
305
- # Returns the arguments schema defined via the schema DSL method.
306
- #
307
- # @return [Hash, nil] the arguments schema or nil if not defined
308
- # @api private
309
- attr_reader :arguments_schema
310
-
311
- # Returns the result schema defined via the schema DSL method.
312
- #
313
- # @return [Hash, nil] the result schema or nil if not defined
314
- # @api private
315
- attr_reader :result_schema
316
-
317
- # Returns the failure schema defined via the schema DSL method.
318
- #
319
- # @return [Hash, nil] the failure schema or nil if not defined
320
- # @api private
321
- attr_reader :failure_schema
322
275
 
323
276
  # Executes pre-call hooks including logging and argument validation.
324
277
  #
data/lib/servus/config.rb CHANGED
@@ -4,24 +4,18 @@
4
4
  module Servus
5
5
  # Configuration settings for the Servus gem.
6
6
  #
7
- # Manages global configuration options including schema file locations.
8
- # Access the configuration via {Servus.config} or modify via {Servus.configure}.
7
+ # Manages global configuration options for services, events, guards, and
8
+ # logging. Access the configuration via {Servus.config} or
9
+ # modify via {Servus.configure}.
9
10
  #
10
- # @example Customizing schema location
11
+ # @example Configuring Servus
11
12
  # Servus.configure do |config|
12
- # config.schema_root = Rails.root.join('lib/schemas')
13
+ # config.require_service_arguments_schema = true
13
14
  # end
14
15
  #
15
16
  # @see Servus.config
16
17
  # @see Servus.configure
17
18
  class Config
18
- # The directory where JSON schema files are located.
19
- #
20
- # Defaults to `Rails.root/app/schemas/services` in Rails applications.
21
- #
22
- # @return [String] the schemas directory path
23
- attr_accessor :schemas_dir
24
-
25
19
  # The directory where Event classes are located.
26
20
  #
27
21
  # Defaults to `Rails.root/app/events` in Rails applications.
@@ -171,49 +165,9 @@ module Servus
171
165
  def set_default_directories
172
166
  @guards_dir = 'app/guards'
173
167
  @events_dir = 'app/events'
174
- @schemas_dir = 'app/schemas'
175
168
  @services_dir = 'app/services'
176
169
  @tests_dir = 'spec'
177
170
  end
178
-
179
- # Returns the full path to a service's schema file.
180
- #
181
- # @param service_namespace [String] underscored service namespace (e.g., "process_payment")
182
- # @param type [String] schema type ("arguments" or "result")
183
- # @return [String] full path to the schema JSON file
184
- #
185
- # @example
186
- # config.schema_path_for("process_payment", "arguments")
187
- # # => "/full/path/app/schemas/process_payment/arguments.json"
188
- def schema_path_for(service_namespace, type)
189
- File.join(root_path, schemas_dir, service_namespace, "#{type}.json")
190
- end
191
-
192
- # Returns the directory containing a service's schema files.
193
- #
194
- # @param service_namespace [String] underscored service namespace
195
- # @return [String] directory path for the service's schemas
196
- #
197
- # @example
198
- # config.schema_dir_for("process_payment")
199
- # # => "/full/path/app/schemas/process_payment"
200
- def schema_dir_for(service_namespace)
201
- File.join(root_path, schemas_dir, service_namespace)
202
- end
203
-
204
- private
205
-
206
- # Determines the application root path.
207
- #
208
- # @return [String] Rails.root in Rails apps, or gem's root directory otherwise
209
- # @api private
210
- def root_path
211
- if defined?(Rails) && Rails.respond_to?(:root)
212
- Rails.root
213
- else
214
- File.expand_path('../../..', __dir__)
215
- end
216
- end
217
171
  end
218
172
 
219
173
  # Returns the singleton configuration instance.
@@ -221,8 +175,8 @@ module Servus
221
175
  # @return [Servus::Config] the global configuration object
222
176
  #
223
177
  # @example
224
- # Servus.config.schema_root
225
- # # => "/app/app/schemas/services"
178
+ # Servus.config.services_dir
179
+ # # => "app/services"
226
180
  def self.config
227
181
  @config ||= Config.new
228
182
  end
@@ -234,7 +188,7 @@ module Servus
234
188
  #
235
189
  # @example
236
190
  # Servus.configure do |config|
237
- # config.schema_root = Rails.root.join('custom/schemas')
191
+ # config.require_service_result_schema = true
238
192
  # end
239
193
  def self.configure
240
194
  yield(config)