servus 0.3.0 → 0.4.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/lib/generators/servus/event_handler/event_handler_generator.rb +1 -1
- data/lib/generators/servus/guard/guard_generator.rb +1 -1
- data/lib/generators/servus/guard/templates/guard.rb.erb +5 -3
- data/lib/generators/servus/service/service_generator.rb +1 -1
- data/lib/servus/base.rb +46 -3
- data/lib/servus/config.rb +71 -3
- data/lib/servus/events/bus.rb +29 -0
- data/lib/servus/events/emitter.rb +15 -0
- data/lib/servus/guard.rb +7 -6
- data/lib/servus/guards/falsey_guard.rb +3 -3
- data/lib/servus/guards/presence_guard.rb +4 -4
- data/lib/servus/guards/state_guard.rb +4 -5
- data/lib/servus/guards/truthy_guard.rb +3 -3
- data/lib/servus/helpers/controller_helpers.rb +40 -0
- data/lib/servus/support/errors.rb +16 -0
- data/lib/servus/support/lockdown.rb +94 -0
- data/lib/servus/support/logger.rb +16 -0
- data/lib/servus/support/validator.rb +65 -34
- data/lib/servus/testing/example_builders.rb +52 -0
- data/lib/servus/testing/matchers.rb +99 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +1 -0
- metadata +7 -111
- data/.claude/commands/check-docs.md +0 -1
- data/.claude/commands/consistency-check.md +0 -1
- data/.claude/commands/fine-tooth-comb.md +0 -1
- data/.claude/commands/red-green-refactor.md +0 -5
- data/.claude/settings.json +0 -24
- data/.rspec +0 -3
- data/.rubocop.yml +0 -27
- data/.yardopts +0 -6
- data/CHANGELOG.md +0 -169
- data/CLAUDE.md +0 -10
- data/IDEAS.md +0 -5
- data/LICENSE.txt +0 -21
- data/READme.md +0 -856
- data/Rakefile +0 -45
- data/docs/core/1_overview.md +0 -81
- data/docs/core/2_architecture.md +0 -120
- data/docs/core/3_service_objects.md +0 -154
- data/docs/features/1_schema_validation.md +0 -161
- data/docs/features/2_error_handling.md +0 -129
- data/docs/features/3_async_execution.md +0 -81
- data/docs/features/4_logging.md +0 -64
- data/docs/features/5_event_bus.md +0 -244
- data/docs/features/6_guards.md +0 -356
- data/docs/features/7_lazy_resolvers.md +0 -238
- data/docs/features/guards_naming_convention.md +0 -540
- data/docs/guides/1_common_patterns.md +0 -90
- data/docs/guides/2_migration_guide.md +0 -225
- data/docs/integration/1_configuration.md +0 -154
- data/docs/integration/2_testing.md +0 -304
- data/docs/integration/3_rails_integration.md +0 -99
- data/docs/yard/Servus/Base.html +0 -1645
- data/docs/yard/Servus/Config.html +0 -582
- data/docs/yard/Servus/Extensions/Async/Call.html +0 -400
- data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +0 -140
- data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +0 -154
- data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +0 -154
- data/docs/yard/Servus/Extensions/Async/Errors.html +0 -128
- data/docs/yard/Servus/Extensions/Async/Ext.html +0 -119
- data/docs/yard/Servus/Extensions/Async/Job.html +0 -310
- data/docs/yard/Servus/Extensions/Async.html +0 -141
- data/docs/yard/Servus/Extensions.html +0 -117
- data/docs/yard/Servus/Generators/ServiceGenerator.html +0 -261
- data/docs/yard/Servus/Generators.html +0 -115
- data/docs/yard/Servus/Helpers/ControllerHelpers.html +0 -457
- data/docs/yard/Servus/Helpers.html +0 -115
- data/docs/yard/Servus/Railtie.html +0 -134
- data/docs/yard/Servus/Support/Errors/AuthenticationError.html +0 -287
- data/docs/yard/Servus/Support/Errors/BadRequestError.html +0 -283
- data/docs/yard/Servus/Support/Errors/ForbiddenError.html +0 -284
- data/docs/yard/Servus/Support/Errors/InternalServerError.html +0 -283
- data/docs/yard/Servus/Support/Errors/NotFoundError.html +0 -284
- data/docs/yard/Servus/Support/Errors/ServiceError.html +0 -489
- data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +0 -290
- data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +0 -200
- data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +0 -288
- data/docs/yard/Servus/Support/Errors/ValidationError.html +0 -200
- data/docs/yard/Servus/Support/Errors.html +0 -140
- data/docs/yard/Servus/Support/Logger.html +0 -856
- data/docs/yard/Servus/Support/Rescuer/BlockContext.html +0 -585
- data/docs/yard/Servus/Support/Rescuer/CallOverride.html +0 -257
- data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +0 -343
- data/docs/yard/Servus/Support/Rescuer.html +0 -267
- data/docs/yard/Servus/Support/Response.html +0 -574
- data/docs/yard/Servus/Support/Validator.html +0 -1150
- data/docs/yard/Servus/Support.html +0 -119
- data/docs/yard/Servus/Testing/ExampleBuilders.html +0 -523
- data/docs/yard/Servus/Testing/ExampleExtractor.html +0 -578
- data/docs/yard/Servus/Testing.html +0 -142
- data/docs/yard/Servus.html +0 -343
- data/docs/yard/_index.html +0 -535
- data/docs/yard/class_list.html +0 -54
- data/docs/yard/css/common.css +0 -1
- data/docs/yard/css/full_list.css +0 -58
- data/docs/yard/css/style.css +0 -503
- data/docs/yard/file.1_common_patterns.html +0 -154
- data/docs/yard/file.1_configuration.html +0 -115
- data/docs/yard/file.1_overview.html +0 -142
- data/docs/yard/file.1_schema_validation.html +0 -188
- data/docs/yard/file.2_architecture.html +0 -157
- data/docs/yard/file.2_error_handling.html +0 -190
- data/docs/yard/file.2_migration_guide.html +0 -242
- data/docs/yard/file.2_testing.html +0 -227
- data/docs/yard/file.3_async_execution.html +0 -145
- data/docs/yard/file.3_rails_integration.html +0 -160
- data/docs/yard/file.3_service_objects.html +0 -191
- data/docs/yard/file.4_logging.html +0 -135
- data/docs/yard/file.ErrorHandling.html +0 -190
- data/docs/yard/file.READme.html +0 -674
- data/docs/yard/file.architecture.html +0 -157
- data/docs/yard/file.async_execution.html +0 -145
- data/docs/yard/file.common_patterns.html +0 -154
- data/docs/yard/file.configuration.html +0 -115
- data/docs/yard/file.error_handling.html +0 -190
- data/docs/yard/file.logging.html +0 -135
- data/docs/yard/file.migration_guide.html +0 -242
- data/docs/yard/file.overview.html +0 -142
- data/docs/yard/file.rails_integration.html +0 -160
- data/docs/yard/file.schema_validation.html +0 -188
- data/docs/yard/file.service_objects.html +0 -191
- data/docs/yard/file.testing.html +0 -227
- data/docs/yard/file_list.html +0 -119
- data/docs/yard/frames.html +0 -22
- data/docs/yard/index.html +0 -674
- data/docs/yard/js/app.js +0 -344
- data/docs/yard/js/full_list.js +0 -242
- data/docs/yard/js/jquery.js +0 -4
- data/docs/yard/method_list.html +0 -542
- data/docs/yard/top-level-namespace.html +0 -110
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e468137fc40c9d2f18214dea55b9c3fc6211e5479e73f5b0917ed168d116a2f
|
|
4
|
+
data.tar.gz: 5390b065cb3478d837cbd90047a7f8facaaf33273b0e2aa4d459167343354d8d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20e5b65a0cd82207a0c494b76e21d0562ce41ca27394b418e7d6921b841c4f47d4f8384f157146195eea713f1af3d28d09b81c210fd7f8a5041b13f8464d66a2
|
|
7
|
+
data.tar.gz: 52f7ca40905dc890fa6850cf87b8394471a94607749457aaeb4784c4524315fd769e40daa4322aae12b5bd84eb9282524a90836bab8a31ab9abfe01d021211a7
|
|
@@ -44,7 +44,7 @@ module Servus
|
|
|
44
44
|
# @return [String] spec file path
|
|
45
45
|
# @api private
|
|
46
46
|
def handler_spec_path
|
|
47
|
-
File.join(
|
|
47
|
+
File.join(Servus.config.tests_dir, Servus.config.events_dir, "#{file_name}_handler_spec.rb")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Returns the handler class name.
|
|
@@ -44,7 +44,7 @@ module Servus
|
|
|
44
44
|
# @return [String] spec file path
|
|
45
45
|
# @api private
|
|
46
46
|
def guard_spec_path
|
|
47
|
-
File.join(
|
|
47
|
+
File.join(Servus.config.tests_dir, Servus.config.guards_dir, "#{file_name}_guard_spec.rb")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Returns the guard class name.
|
|
@@ -34,14 +34,16 @@ class <%= guard_class_name %> < Servus::Guard
|
|
|
34
34
|
|
|
35
35
|
# Tests whether the <%= file_name.humanize.downcase %> condition is met.
|
|
36
36
|
#
|
|
37
|
-
#
|
|
37
|
+
# Arguments passed to `enforce_*!` / `check_*?` are stored as `@kwargs`
|
|
38
|
+
# and exposed via `method_missing`, so read them directly off the instance.
|
|
39
|
+
#
|
|
38
40
|
# @return [Boolean] true if validation passes
|
|
39
|
-
def test
|
|
41
|
+
def test
|
|
40
42
|
# TODO: Implement validation logic
|
|
41
43
|
# Return true if the condition is met, false otherwise
|
|
42
44
|
#
|
|
43
45
|
# Example:
|
|
44
|
-
# def test
|
|
46
|
+
# def test
|
|
45
47
|
# account.balance >= amount
|
|
46
48
|
# end
|
|
47
49
|
true
|
|
@@ -57,7 +57,7 @@ module Servus
|
|
|
57
57
|
# @return [String] spec file path
|
|
58
58
|
# @api private
|
|
59
59
|
def service_path_spec
|
|
60
|
-
"
|
|
60
|
+
"#{Servus.config.tests_dir}/services/#{file_path}/service_spec.rb"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Returns the path for the result schema file.
|
data/lib/servus/base.rb
CHANGED
|
@@ -48,6 +48,7 @@ module Servus
|
|
|
48
48
|
class Base
|
|
49
49
|
include Servus::Support::Errors
|
|
50
50
|
include Servus::Support::Rescuer
|
|
51
|
+
include Servus::Support::Lockdown
|
|
51
52
|
include Servus::Events::Emitter
|
|
52
53
|
include Servus::Guards
|
|
53
54
|
|
|
@@ -157,6 +158,46 @@ module Servus
|
|
|
157
158
|
raise type, message
|
|
158
159
|
end
|
|
159
160
|
|
|
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
|
+
|
|
160
201
|
class << self
|
|
161
202
|
# Executes the service with automatic validation, logging, and benchmarking.
|
|
162
203
|
#
|
|
@@ -196,11 +237,13 @@ module Servus
|
|
|
196
237
|
|
|
197
238
|
# Wrap execution in catch block to handle guard failures
|
|
198
239
|
result = catch(:guard_failure) do
|
|
199
|
-
benchmark(**args) { instance.call }
|
|
240
|
+
benchmark(**args) { instance.send(:call) }
|
|
200
241
|
end
|
|
201
242
|
|
|
202
|
-
|
|
203
|
-
|
|
243
|
+
if result.is_a?(Servus::Support::Errors::GuardError)
|
|
244
|
+
Logger.log_guard_failure(self, result)
|
|
245
|
+
result = Response.new(false, nil, result)
|
|
246
|
+
end
|
|
204
247
|
|
|
205
248
|
after_call(result, instance)
|
|
206
249
|
|
data/lib/servus/config.rb
CHANGED
|
@@ -51,22 +51,90 @@ module Servus
|
|
|
51
51
|
# @return [String] the guards directory path
|
|
52
52
|
attr_accessor :guards_dir
|
|
53
53
|
|
|
54
|
+
# The directory where generated spec/test files are placed.
|
|
55
|
+
#
|
|
56
|
+
# Defaults to `"spec"`. Projects using Minitest or a custom test layout
|
|
57
|
+
# can override this (e.g., `"test"`) so generators write files into the
|
|
58
|
+
# correct location.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] the tests directory path
|
|
61
|
+
attr_accessor :tests_dir
|
|
62
|
+
|
|
54
63
|
# Whether to include the default built-in guards (EnsurePresent, EnsurePositive).
|
|
55
64
|
#
|
|
56
65
|
# @return [Boolean] true to include default guards, false to exclude them
|
|
57
66
|
attr_accessor :include_default_guards
|
|
58
67
|
|
|
68
|
+
# Whether to require all services to define an arguments schema.
|
|
69
|
+
#
|
|
70
|
+
# When enabled, raises {Servus::Support::Errors::SchemaRequiredError} when
|
|
71
|
+
# a service is called without an arguments schema defined.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean] true to require arguments schemas, false to allow schema-less services
|
|
74
|
+
attr_accessor :require_service_arguments_schema
|
|
75
|
+
|
|
76
|
+
# Whether to require all services to define a result schema.
|
|
77
|
+
#
|
|
78
|
+
# When enabled, raises {Servus::Support::Errors::SchemaRequiredError} when
|
|
79
|
+
# a service returns a successful response without a result schema defined.
|
|
80
|
+
# Failure schemas remain optional regardless of this setting.
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean] true to require result schemas, false to allow schema-less services
|
|
83
|
+
attr_accessor :require_service_result_schema
|
|
84
|
+
|
|
85
|
+
# Whether to require all event handlers to define a payload schema.
|
|
86
|
+
#
|
|
87
|
+
# When enabled, raises {Servus::Support::Errors::SchemaRequiredError} when
|
|
88
|
+
# an event handler validates a payload without a payload schema defined.
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean] true to require payload schemas, false to allow schema-less handlers
|
|
91
|
+
attr_accessor :require_event_payload_schema
|
|
92
|
+
|
|
93
|
+
# Whether external instantiation of services is blocked and instance
|
|
94
|
+
# `#call` methods are automatically privatized.
|
|
95
|
+
#
|
|
96
|
+
# When enabled (default), callers must invoke services via the class
|
|
97
|
+
# method {Servus::Base.call}, which runs argument validation, logging,
|
|
98
|
+
# benchmarking, guards, result validation, and event emission. Calling
|
|
99
|
+
# `MyService.new` or `instance.call` directly raises `NoMethodError`.
|
|
100
|
+
#
|
|
101
|
+
# Disable this if you have existing code that instantiates services
|
|
102
|
+
# directly or otherwise prefer to opt out of the enforcement.
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean] true to enforce lockdown (default), false to allow
|
|
105
|
+
# direct instantiation and public instance `#call`
|
|
106
|
+
# @see Servus::Support::Lockdown
|
|
107
|
+
attr_reader :lockdown_enabled
|
|
108
|
+
|
|
109
|
+
# Sets whether lockdown is enforced, immediately re-applying the
|
|
110
|
+
# resulting `.new` visibility to {Servus::Base}.
|
|
111
|
+
#
|
|
112
|
+
# @param value [Boolean] the new lockdown setting
|
|
113
|
+
# @return [Boolean] the new value
|
|
114
|
+
def lockdown_enabled=(value)
|
|
115
|
+
@lockdown_enabled = value
|
|
116
|
+
Servus::Base.apply_lockdown!
|
|
117
|
+
end
|
|
118
|
+
|
|
59
119
|
# Initializes a new configuration with default values.
|
|
60
120
|
#
|
|
61
121
|
# @api private
|
|
62
122
|
def initialize
|
|
123
|
+
set_default_directories
|
|
124
|
+
@strict_event_validation = true
|
|
125
|
+
@include_default_guards = true
|
|
126
|
+
@lockdown_enabled = true
|
|
127
|
+
@require_service_arguments_schema = false
|
|
128
|
+
@require_service_result_schema = false
|
|
129
|
+
@require_event_payload_schema = false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def set_default_directories
|
|
63
133
|
@guards_dir = 'app/guards'
|
|
64
134
|
@events_dir = 'app/events'
|
|
65
135
|
@schemas_dir = 'app/schemas'
|
|
66
136
|
@services_dir = 'app/services'
|
|
67
|
-
|
|
68
|
-
@strict_event_validation = true
|
|
69
|
-
@include_default_guards = true
|
|
137
|
+
@tests_dir = 'spec'
|
|
70
138
|
end
|
|
71
139
|
|
|
72
140
|
# Returns the full path to a service's schema file.
|
data/lib/servus/events/bus.rb
CHANGED
|
@@ -91,6 +91,35 @@ module Servus
|
|
|
91
91
|
ActiveSupport::Notifications.instrument(notification_name(event_name), payload)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
# Subscribes to all Servus event emissions.
|
|
95
|
+
#
|
|
96
|
+
# Yields the clean event name and payload as positional args, plus
|
|
97
|
+
# +started_at+, +finished_at+, and +id+ as keyword args.
|
|
98
|
+
# Returns the subscription for manual unsubscribe.
|
|
99
|
+
#
|
|
100
|
+
# @yield [event_name, payload, started_at:, finished_at:, id:]
|
|
101
|
+
# @yieldparam event_name [Symbol] the event name
|
|
102
|
+
# @yieldparam payload [Hash] the event payload
|
|
103
|
+
# @yieldparam started_at [Time] when the event was emitted
|
|
104
|
+
# @yieldparam finished_at [Time] when the instrumented block completed
|
|
105
|
+
# @yieldparam id [String] unique notification ID
|
|
106
|
+
# @return [Object] the ActiveSupport::Notifications subscription
|
|
107
|
+
#
|
|
108
|
+
# @example Forward all events to an external system
|
|
109
|
+
# Servus::Events::Bus.subscribe_all do |event_name, payload, started_at:, **|
|
|
110
|
+
# EventusForwardJob.perform_later(
|
|
111
|
+
# event: event_name.to_s,
|
|
112
|
+
# payload: payload.as_json,
|
|
113
|
+
# occurred_at: started_at.utc.iso8601(6)
|
|
114
|
+
# )
|
|
115
|
+
# end
|
|
116
|
+
def subscribe_all(&block)
|
|
117
|
+
ActiveSupport::Notifications.subscribe(/^servus\.events\./) do |name, started, finished, id, payload|
|
|
118
|
+
event_name = name.delete_prefix('servus.events.').to_sym
|
|
119
|
+
block.call(event_name, payload, started_at: started, finished_at: finished, id: id)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
94
123
|
# Clears all registered handlers and unsubscribes from notifications.
|
|
95
124
|
#
|
|
96
125
|
# Useful for testing and development mode reloading.
|
|
@@ -127,6 +127,8 @@ module Servus
|
|
|
127
127
|
def emit_events_for(trigger, result)
|
|
128
128
|
self.class.emissions_for(trigger).each do |emission|
|
|
129
129
|
payload = build_event_payload(emission, result)
|
|
130
|
+
validate_event_payload!(emission[:event_name], payload)
|
|
131
|
+
Servus::Support::Logger.log_event(emission[:event_name], payload)
|
|
130
132
|
Servus::Events::Bus.emit(emission[:event_name], payload)
|
|
131
133
|
end
|
|
132
134
|
end
|
|
@@ -134,6 +136,19 @@ module Servus
|
|
|
134
136
|
# Instance methods for emitting events during service execution
|
|
135
137
|
private
|
|
136
138
|
|
|
139
|
+
# Validates the payload against all handler schemas registered for the event.
|
|
140
|
+
#
|
|
141
|
+
# @param event_name [Symbol] the event name
|
|
142
|
+
# @param payload [Hash] the event payload
|
|
143
|
+
# @return [void]
|
|
144
|
+
# @raise [Servus::Support::Errors::ValidationError] if payload fails validation
|
|
145
|
+
# @api private
|
|
146
|
+
def validate_event_payload!(event_name, payload)
|
|
147
|
+
Servus::Events::Bus.handlers_for(event_name).each do |handler_class|
|
|
148
|
+
Servus::Support::Validator.validate_event_payload!(handler_class, payload)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
137
152
|
# Builds the event payload using the configured payload builder or defaults.
|
|
138
153
|
#
|
|
139
154
|
# @param emission [Hash] the emission configuration
|
data/lib/servus/guard.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Servus
|
|
|
19
19
|
# }
|
|
20
20
|
# end
|
|
21
21
|
#
|
|
22
|
-
# def test
|
|
22
|
+
# def test
|
|
23
23
|
# account.balance >= amount
|
|
24
24
|
# end
|
|
25
25
|
# end
|
|
@@ -52,7 +52,7 @@ module Servus
|
|
|
52
52
|
# Servus::Guard.execute!(EnsurePositive, amount: -10) # throws :guard_failure
|
|
53
53
|
def execute!(guard_class, **)
|
|
54
54
|
guard = guard_class.new(**)
|
|
55
|
-
return if guard.test
|
|
55
|
+
return if guard.test
|
|
56
56
|
|
|
57
57
|
throw(:guard_failure, guard.error)
|
|
58
58
|
end
|
|
@@ -69,7 +69,7 @@ module Servus
|
|
|
69
69
|
# Servus::Guard.execute?(EnsurePositive, amount: 100) # => true
|
|
70
70
|
# Servus::Guard.execute?(EnsurePositive, amount: -10) # => false
|
|
71
71
|
def execute?(guard_class, **)
|
|
72
|
-
guard_class.new(**).test
|
|
72
|
+
guard_class.new(**).test
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# Declares the HTTP status code for API responses.
|
|
@@ -218,14 +218,15 @@ module Servus
|
|
|
218
218
|
|
|
219
219
|
# Tests whether the guard passes.
|
|
220
220
|
#
|
|
221
|
-
# Subclasses must implement this method
|
|
222
|
-
#
|
|
221
|
+
# Subclasses must implement this zero-argument method. Arguments passed
|
|
222
|
+
# to `new` are stored as `@kwargs` and exposed via `method_missing`, so
|
|
223
|
+
# `test` reads them directly off the instance.
|
|
223
224
|
#
|
|
224
225
|
# @return [Boolean] true if the guard passes, false otherwise
|
|
225
226
|
# @raise [NotImplementedError] if not implemented by subclass
|
|
226
227
|
#
|
|
227
228
|
# @example
|
|
228
|
-
# def test
|
|
229
|
+
# def test
|
|
229
230
|
# account.balance >= amount
|
|
230
231
|
# end
|
|
231
232
|
def test
|
|
@@ -24,10 +24,10 @@ module Servus
|
|
|
24
24
|
|
|
25
25
|
# Tests whether all specified attributes are falsey.
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
27
|
+
# Reads `on` and `check` from the kwargs stored at initialization.
|
|
28
|
+
#
|
|
29
29
|
# @return [Boolean] true if all attributes are falsey
|
|
30
|
-
def test
|
|
30
|
+
def test
|
|
31
31
|
Array(check).all? { |attr| !on.public_send(attr) }
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -36,12 +36,12 @@ module Servus
|
|
|
36
36
|
# Tests whether all provided values are present.
|
|
37
37
|
#
|
|
38
38
|
# A value is considered present if it is not nil and not empty
|
|
39
|
-
# (for values that respond to empty?).
|
|
39
|
+
# (for values that respond to empty?). Reads all values from the
|
|
40
|
+
# kwargs stored at initialization.
|
|
40
41
|
#
|
|
41
|
-
# @param values [Hash] keyword arguments to validate
|
|
42
42
|
# @return [Boolean] true if all values are present
|
|
43
|
-
def test
|
|
44
|
-
|
|
43
|
+
def test
|
|
44
|
+
kwargs.all? { |_, value| present?(value) }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
private
|
|
@@ -24,12 +24,11 @@ module Servus
|
|
|
24
24
|
|
|
25
25
|
# Tests whether the attribute matches the expected value(s).
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# @param is [Object, Array] expected value(s) - passes if attribute matches any
|
|
27
|
+
# Reads `on`, `check`, and `is` from the kwargs stored at initialization.
|
|
28
|
+
#
|
|
30
29
|
# @return [Boolean] true if attribute matches expected value(s)
|
|
31
|
-
def test
|
|
32
|
-
Array(is).include?(on.public_send(check))
|
|
30
|
+
def test
|
|
31
|
+
Array(kwargs[:is]).include?(on.public_send(check))
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
private
|
|
@@ -24,10 +24,10 @@ module Servus
|
|
|
24
24
|
|
|
25
25
|
# Tests whether all specified attributes are truthy.
|
|
26
26
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
27
|
+
# Reads `on` and `check` from the kwargs stored at initialization.
|
|
28
|
+
#
|
|
29
29
|
# @return [Boolean] true if all attributes are truthy
|
|
30
|
-
def test
|
|
30
|
+
def test
|
|
31
31
|
Array(check).all? { |attr| !!on.public_send(attr) }
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -39,6 +39,46 @@ module Servus
|
|
|
39
39
|
render_service_error(@result.error) unless @result.success?
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Executes a service and returns its data on success, raising the
|
|
43
|
+
# failure's error otherwise.
|
|
44
|
+
#
|
|
45
|
+
# The bang counterpart to {#run_service}. Use it outside a standard
|
|
46
|
+
# controller render flow — inside background logic, callbacks, or any
|
|
47
|
+
# place where a failure should propagate as an exception rather than be
|
|
48
|
+
# rendered as JSON.
|
|
49
|
+
#
|
|
50
|
+
# Inside a service's `#call` method, use {Servus::Base#call!} instead —
|
|
51
|
+
# it preserves the failure Response for the outer service's caller rather
|
|
52
|
+
# than raising.
|
|
53
|
+
#
|
|
54
|
+
# Mirrors {#run_service}: stores the full Response in @result so views
|
|
55
|
+
# and downstream helpers can reach for it the same way, then returns the
|
|
56
|
+
# data on success or raises on failure. The only behavioural difference
|
|
57
|
+
# between the two is raise-vs-render on failure.
|
|
58
|
+
#
|
|
59
|
+
# Sugar over:
|
|
60
|
+
#
|
|
61
|
+
# @result = Service.call(**params)
|
|
62
|
+
# raise @result.error unless @result.success?
|
|
63
|
+
# data = @result.data
|
|
64
|
+
#
|
|
65
|
+
# @example From a rake task
|
|
66
|
+
# data = run_service!(Treasury::Reconcile::Service, date: Date.current)
|
|
67
|
+
#
|
|
68
|
+
# @param klass [Class<Servus::Base>] service class to execute
|
|
69
|
+
# @param params [Hash] keyword arguments to pass to the service
|
|
70
|
+
# @return [Servus::Support::DataObject, Object] the service's data on success
|
|
71
|
+
# @raise [Servus::Support::Errors::ServiceError] the failure's error otherwise
|
|
72
|
+
#
|
|
73
|
+
# @see #run_service
|
|
74
|
+
# @see Servus::Base#call!
|
|
75
|
+
def run_service!(klass, **params)
|
|
76
|
+
@result = klass.call(**params)
|
|
77
|
+
return @result.data if @result.success?
|
|
78
|
+
|
|
79
|
+
raise @result.error
|
|
80
|
+
end
|
|
81
|
+
|
|
42
82
|
# Renders a service error as a JSON response.
|
|
43
83
|
#
|
|
44
84
|
# Uses error.http_status for the response status code and
|
|
@@ -267,6 +267,22 @@ module Servus
|
|
|
267
267
|
def api_error = { code: http_status, message: message }
|
|
268
268
|
end
|
|
269
269
|
|
|
270
|
+
# Raised when a service or event handler is invoked without a required schema.
|
|
271
|
+
#
|
|
272
|
+
# Triggered by the +require_service_arguments_schema+,
|
|
273
|
+
# +require_service_result_schema+, or +require_event_payload_schema+
|
|
274
|
+
# configuration flags.
|
|
275
|
+
#
|
|
276
|
+
# @see Servus::Config#require_service_arguments_schema
|
|
277
|
+
# @see Servus::Config#require_service_result_schema
|
|
278
|
+
# @see Servus::Config#require_event_payload_schema
|
|
279
|
+
class SchemaRequiredError < ServiceError
|
|
280
|
+
DEFAULT_MESSAGE = 'Schema is required but not defined'
|
|
281
|
+
|
|
282
|
+
def http_status = :unprocessable_entity
|
|
283
|
+
def api_error = { code: :schema_required, message: message }
|
|
284
|
+
end
|
|
285
|
+
|
|
270
286
|
# 423 Locked - resource is locked.
|
|
271
287
|
class LockedError < ServiceError
|
|
272
288
|
DEFAULT_MESSAGE = 'Locked'
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servus
|
|
4
|
+
module Support
|
|
5
|
+
# Enforces that services are invoked through {Servus::Base.call} rather
|
|
6
|
+
# than by instantiating a service and calling its instance `#call`
|
|
7
|
+
# directly. The class-level `.call` runs argument validation, logging,
|
|
8
|
+
# benchmarking, guard handling, result validation, and event emission;
|
|
9
|
+
# calling the instance method directly would silently skip all of that.
|
|
10
|
+
#
|
|
11
|
+
# When included in {Servus::Base}, this module:
|
|
12
|
+
# - Privatizes `.new` on the base class (and, by inheritance, on every
|
|
13
|
+
# descendant) so `MyService.new` from outside the class raises
|
|
14
|
+
# `NoMethodError`.
|
|
15
|
+
# - Installs a `method_added` hook on every descendant that privatizes
|
|
16
|
+
# any instance-level `#call` at definition time.
|
|
17
|
+
#
|
|
18
|
+
# Controlled by {Servus::Config#lockdown_enabled} (default `true`). Set
|
|
19
|
+
# it to `false` to allow direct instantiation and public instance
|
|
20
|
+
# `#call` — useful if you have existing code that relies on those entry
|
|
21
|
+
# points, or if you prefer to opt out of this enforcement entirely.
|
|
22
|
+
#
|
|
23
|
+
# @example Opting out
|
|
24
|
+
# Servus.configure do |config|
|
|
25
|
+
# config.lockdown_enabled = false
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @see Servus::Config#lockdown_enabled
|
|
29
|
+
module Lockdown
|
|
30
|
+
# Wires the lockdown hooks into the including class.
|
|
31
|
+
#
|
|
32
|
+
# Extends the base with {ClassMethods} (for {ClassMethods#apply_lockdown!}),
|
|
33
|
+
# prepends {Inherited} so subclasses receive the `method_added` hook,
|
|
34
|
+
# and applies the current config value to `.new`'s visibility.
|
|
35
|
+
#
|
|
36
|
+
# @param base [Class] the class including this module (expected to be {Servus::Base})
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @api private
|
|
39
|
+
def self.included(base)
|
|
40
|
+
base.extend(ClassMethods)
|
|
41
|
+
base.singleton_class.prepend(Inherited)
|
|
42
|
+
base.apply_lockdown!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Prepended onto the base class's singleton so that every subclass of
|
|
46
|
+
# {Servus::Base} is automatically extended with {PrivateCall} at
|
|
47
|
+
# class-definition time.
|
|
48
|
+
#
|
|
49
|
+
# @api private
|
|
50
|
+
module Inherited
|
|
51
|
+
# Ensures each subclass has the `method_added` hook installed.
|
|
52
|
+
#
|
|
53
|
+
# @param subclass [Class] the newly defined subclass
|
|
54
|
+
# @return [void]
|
|
55
|
+
def inherited(subclass)
|
|
56
|
+
super
|
|
57
|
+
subclass.extend(PrivateCall)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Extended onto every {Servus::Base} subclass. Privatizes any
|
|
62
|
+
# instance-level `#call` as soon as it is defined, provided lockdown
|
|
63
|
+
# is enabled in config at definition time.
|
|
64
|
+
#
|
|
65
|
+
# @api private
|
|
66
|
+
module PrivateCall
|
|
67
|
+
# @param name [Symbol] the name of the newly added method
|
|
68
|
+
# @return [void]
|
|
69
|
+
def method_added(name)
|
|
70
|
+
super
|
|
71
|
+
return unless Servus.config.lockdown_enabled
|
|
72
|
+
|
|
73
|
+
private :call if name == :call && public_method_defined?(:call)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Class-level helpers installed on {Servus::Base}.
|
|
78
|
+
module ClassMethods
|
|
79
|
+
# Applies {Servus::Config#lockdown_enabled} to `.new`'s visibility.
|
|
80
|
+
# Called on include and re-called whenever the config flag changes.
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
83
|
+
# @api private
|
|
84
|
+
def apply_lockdown!
|
|
85
|
+
if Servus.config.lockdown_enabled
|
|
86
|
+
singleton_class.send(:private, :new)
|
|
87
|
+
else
|
|
88
|
+
singleton_class.send(:public, :new)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -55,6 +55,22 @@ module Servus
|
|
|
55
55
|
logger.warn("#{service_class.name} failed in #{duration.round(3)}s with error: #{error}")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Logs a guard failure from a service
|
|
59
|
+
#
|
|
60
|
+
# @param service_class [Class] The service class
|
|
61
|
+
# @param error [Servus::Support::Errors::GuardError] The guard error
|
|
62
|
+
def self.log_guard_failure(service_class, error)
|
|
63
|
+
logger.warn("#{service_class.name} guard failed: #{error.message}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Logs an event emission
|
|
67
|
+
#
|
|
68
|
+
# @param event_name [Symbol] The event name
|
|
69
|
+
# @param payload [Hash] The event payload
|
|
70
|
+
def self.log_event(event_name, payload)
|
|
71
|
+
logger.info("Event :#{event_name} emitted with payload: #{payload.inspect}")
|
|
72
|
+
end
|
|
73
|
+
|
|
58
74
|
# Logs a validation error from a service
|
|
59
75
|
#
|
|
60
76
|
# @param service_class [Class] The service class
|