light-services 3.1.1 → 3.2.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/.cursor/rules/services/RULE.md +265 -0
- data/.cursor/rules/services-rspec/RULE.md +354 -0
- data/CHANGELOG.md +23 -1
- data/Gemfile.lock +2 -2
- data/docs/arguments.md +3 -3
- data/docs/configuration.md +22 -7
- data/docs/errors.md +25 -4
- data/docs/generators.md +1 -1
- data/docs/outputs.md +3 -3
- data/docs/pundit-authorization.md +1 -1
- data/docs/rubocop.md +46 -0
- data/docs/steps.md +52 -0
- data/docs/testing.md +8 -8
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +1 -1
- data/lib/light/services/base.rb +20 -0
- data/lib/light/services/concerns/execution.rb +3 -0
- data/lib/light/services/config.rb +20 -5
- data/lib/light/services/dsl/validation.rb +16 -5
- data/lib/light/services/exceptions.rb +4 -0
- data/lib/light/services/rubocop/cop/light_services/prefer_fail_method.rb +113 -0
- data/lib/light/services/rubocop.rb +1 -0
- data/lib/light/services/version.rb +1 -1
- metadata +4 -1
data/docs/configuration.md
CHANGED
|
@@ -8,8 +8,9 @@ Configure Light Services globally using an initializer. For Rails applications,
|
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
10
|
Light::Services.configure do |config|
|
|
11
|
-
# Type enforcement
|
|
12
|
-
config.
|
|
11
|
+
# Type enforcement
|
|
12
|
+
config.require_arg_type = true # Require type option for all arguments
|
|
13
|
+
config.require_output_type = true # Require type option for all outputs
|
|
13
14
|
|
|
14
15
|
# Transaction settings
|
|
15
16
|
config.use_transactions = true # Wrap each service in a database transaction
|
|
@@ -32,7 +33,8 @@ end
|
|
|
32
33
|
|
|
33
34
|
| Option | Default | Description |
|
|
34
35
|
|--------|---------|-------------|
|
|
35
|
-
| `
|
|
36
|
+
| `require_arg_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments without a `type` option |
|
|
37
|
+
| `require_output_type` | `true` | Raises `Light::Services::MissingTypeError` when defining outputs without a `type` option |
|
|
36
38
|
| `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
|
|
37
39
|
| `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
|
|
38
40
|
| `break_on_error` | `true` | Stops executing remaining steps when an error is added |
|
|
@@ -159,7 +161,8 @@ To disable type enforcement globally (not recommended):
|
|
|
159
161
|
|
|
160
162
|
```ruby
|
|
161
163
|
Light::Services.configure do |config|
|
|
162
|
-
config.
|
|
164
|
+
config.require_arg_type = false # Disable for arguments
|
|
165
|
+
config.require_output_type = false # Disable for outputs
|
|
163
166
|
end
|
|
164
167
|
```
|
|
165
168
|
|
|
@@ -167,10 +170,22 @@ Or disable for specific services:
|
|
|
167
170
|
|
|
168
171
|
```ruby
|
|
169
172
|
class LegacyService < ApplicationService
|
|
170
|
-
config
|
|
173
|
+
config require_arg_type: false, require_output_type: false
|
|
171
174
|
|
|
172
|
-
arg :data # Allowed when
|
|
173
|
-
output :result # Allowed when
|
|
175
|
+
arg :data # Allowed when require_arg_type is disabled
|
|
176
|
+
output :result # Allowed when require_output_type is disabled
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
You can also control them independently:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
class StrictInputService < ApplicationService
|
|
184
|
+
# Require types for arguments but not outputs
|
|
185
|
+
config require_arg_type: true, require_output_type: false
|
|
186
|
+
|
|
187
|
+
arg :data, type: Hash # Type required
|
|
188
|
+
output :result # Type not required
|
|
174
189
|
end
|
|
175
190
|
```
|
|
176
191
|
|
data/docs/errors.md
CHANGED
|
@@ -44,6 +44,24 @@ class ParsePage < ApplicationService
|
|
|
44
44
|
end
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Quick Error with `fail!`
|
|
48
|
+
|
|
49
|
+
The `fail!` method is a shortcut for adding an error to the `:base` key:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class ParsePage < ApplicationService
|
|
53
|
+
def validate
|
|
54
|
+
fail!("URL is required") if url.blank?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is equivalent to:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
errors.add(:base, "URL is required")
|
|
63
|
+
```
|
|
64
|
+
|
|
47
65
|
## Reading Errors
|
|
48
66
|
|
|
49
67
|
To check if a service has errors, you can use the `#failed?` method. You can also use methods like `errors.any?` to inspect errors.
|
|
@@ -205,11 +223,13 @@ Light Services defines several exception classes for different error scenarios:
|
|
|
205
223
|
| `Light::Services::ReservedNameError` | Raised when using a reserved name for arguments, outputs, or steps |
|
|
206
224
|
| `Light::Services::InvalidNameError` | Raised when using an invalid name format |
|
|
207
225
|
| `Light::Services::NoStepsError` | Raised when a service has no steps defined and no `run` method |
|
|
208
|
-
| `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `
|
|
226
|
+
| `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `require_arg_type` or `require_output_type` is enabled |
|
|
227
|
+
| `Light::Services::StopExecution` | Control flow exception raised by `stop_immediately!` to halt execution without rollback |
|
|
228
|
+
| `Light::Services::FailExecution` | Control flow exception raised by `fail_immediately!` to halt execution and rollback transactions |
|
|
209
229
|
|
|
210
230
|
### MissingTypeError
|
|
211
231
|
|
|
212
|
-
This exception is raised when you define an argument or output without a `type` option. Since `
|
|
232
|
+
This exception is raised when you define an argument or output without a `type` option. Since `require_arg_type` and `require_output_type` are enabled by default, all arguments and outputs must have a type.
|
|
213
233
|
|
|
214
234
|
```ruby
|
|
215
235
|
class MyService < ApplicationService
|
|
@@ -230,9 +250,10 @@ If you need to disable type enforcement for legacy services, you can use the `co
|
|
|
230
250
|
|
|
231
251
|
```ruby
|
|
232
252
|
class LegacyService < ApplicationService
|
|
233
|
-
config
|
|
253
|
+
config require_arg_type: false, require_output_type: false
|
|
234
254
|
|
|
235
|
-
arg :data # Allowed when
|
|
255
|
+
arg :data # Allowed when require_arg_type is disabled
|
|
256
|
+
output :result # Allowed when require_output_type is disabled
|
|
236
257
|
end
|
|
237
258
|
```
|
|
238
259
|
|
data/docs/generators.md
CHANGED
data/docs/outputs.md
CHANGED
|
@@ -89,13 +89,13 @@ class MyService < ApplicationService
|
|
|
89
89
|
end
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
To disable type enforcement for a specific service:
|
|
92
|
+
To disable type enforcement for outputs in a specific service:
|
|
93
93
|
|
|
94
94
|
```ruby
|
|
95
95
|
class LegacyService < ApplicationService
|
|
96
|
-
config
|
|
96
|
+
config require_output_type: false
|
|
97
97
|
|
|
98
|
-
output :data # Allowed when
|
|
98
|
+
output :data # Allowed when require_output_type is disabled
|
|
99
99
|
end
|
|
100
100
|
```
|
|
101
101
|
|
data/docs/rubocop.md
CHANGED
|
@@ -232,6 +232,47 @@ LightServices/DeprecatedMethods:
|
|
|
232
232
|
ServicePattern: 'Service$' # default: matches classes ending with "Service"
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
+
### LightServices/PreferFailMethod
|
|
236
|
+
|
|
237
|
+
Detects `errors.add(:base, "message")` calls and suggests using the `fail!("message")` helper instead. Includes autocorrection.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# bad
|
|
241
|
+
class MyService < ApplicationService
|
|
242
|
+
step :process
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def process
|
|
247
|
+
errors.add(:base, "user is required")
|
|
248
|
+
errors.add(:base, "invalid input", rollback: false)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# good
|
|
253
|
+
class MyService < ApplicationService
|
|
254
|
+
step :process
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def process
|
|
259
|
+
fail!("user is required")
|
|
260
|
+
fail!("invalid input", rollback: false)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The cop only detects `errors.add(:base, ...)` calls. It does not flag `errors.add(:field_name, ...)` calls for specific fields, as those should not use `fail!`.
|
|
266
|
+
|
|
267
|
+
**Configuration:** Customize the base service classes to check:
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
LightServices/PreferFailMethod:
|
|
271
|
+
BaseServiceClasses:
|
|
272
|
+
- ApplicationService
|
|
273
|
+
- BaseCreator
|
|
274
|
+
```
|
|
275
|
+
|
|
235
276
|
## Configuration
|
|
236
277
|
|
|
237
278
|
Full configuration example:
|
|
@@ -267,6 +308,11 @@ LightServices/NoDirectInstantiation:
|
|
|
267
308
|
LightServices/DeprecatedMethods:
|
|
268
309
|
Enabled: true
|
|
269
310
|
ServicePattern: 'Service$'
|
|
311
|
+
|
|
312
|
+
LightServices/PreferFailMethod:
|
|
313
|
+
Enabled: true
|
|
314
|
+
BaseServiceClasses:
|
|
315
|
+
- ApplicationService
|
|
270
316
|
```
|
|
271
317
|
|
|
272
318
|
To disable a cop for specific files:
|
data/docs/steps.md
CHANGED
|
@@ -292,6 +292,58 @@ end
|
|
|
292
292
|
**Database Transactions:** Calling `stop_immediately!` does NOT rollback database transactions. All database changes made before `stop_immediately!` was called will be committed.
|
|
293
293
|
{% endhint %}
|
|
294
294
|
|
|
295
|
+
## Immediate Failure with `fail_immediately!`
|
|
296
|
+
|
|
297
|
+
Use `fail_immediately!` when you need to halt execution immediately AND rollback any database transactions. Unlike `stop_immediately!`, this method adds an error and causes transaction rollback.
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
class Payment::Process < ApplicationService
|
|
301
|
+
arg :amount, type: Integer
|
|
302
|
+
arg :card_token, type: String
|
|
303
|
+
|
|
304
|
+
step :validate_card
|
|
305
|
+
step :charge_card
|
|
306
|
+
step :send_receipt
|
|
307
|
+
|
|
308
|
+
output :transaction_id, type: String
|
|
309
|
+
|
|
310
|
+
private
|
|
311
|
+
|
|
312
|
+
def validate_card
|
|
313
|
+
unless valid_card?(card_token)
|
|
314
|
+
fail_immediately!("Card validation failed")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# This code won't run if card is invalid
|
|
318
|
+
log_validation_success
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def charge_card
|
|
322
|
+
# This step won't run if fail_immediately! was called
|
|
323
|
+
self.transaction_id = PaymentGateway.charge(amount, card_token)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
{% hint style="info" %}
|
|
329
|
+
`fail_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will still run when `fail_immediately!` is called, allowing for cleanup operations.
|
|
330
|
+
{% endhint %}
|
|
331
|
+
|
|
332
|
+
{% hint style="danger" %}
|
|
333
|
+
**Database Transactions:** Calling `fail_immediately!` DOES rollback database transactions. All database changes made before `fail_immediately!` was called will be rolled back.
|
|
334
|
+
{% endhint %}
|
|
335
|
+
|
|
336
|
+
### Comparison Table
|
|
337
|
+
|
|
338
|
+
| Method | Adds Error | Stops Execution | Transaction Rollback |
|
|
339
|
+
|--------|------------|-----------------|---------------------|
|
|
340
|
+
| `stop!` | No | After current step | No |
|
|
341
|
+
| `stop_immediately!` | No | Immediately | No |
|
|
342
|
+
| `fail!(msg)` | Yes (:base) | After current step* | No |
|
|
343
|
+
| `fail_immediately!(msg)` | Yes (:base) | Immediately | Yes |
|
|
344
|
+
|
|
345
|
+
*By default, adding an error stops subsequent steps from running due to `break_on_add` configuration.
|
|
346
|
+
|
|
295
347
|
## Removing Inherited Steps
|
|
296
348
|
|
|
297
349
|
When inheriting from a parent service, you can remove steps using `remove_step`:
|
data/docs/testing.md
CHANGED
|
@@ -30,7 +30,7 @@ RSpec.describe GreetService do
|
|
|
30
30
|
it "returns a greeting message" do
|
|
31
31
|
service = described_class.run(name: "John")
|
|
32
32
|
|
|
33
|
-
expect(service).to
|
|
33
|
+
expect(service).to be_successful
|
|
34
34
|
expect(service.message).to eq("Hello, John!")
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -48,7 +48,7 @@ RSpec.describe User::Create do
|
|
|
48
48
|
it "creates a user" do
|
|
49
49
|
service = described_class.run(attributes: attributes)
|
|
50
50
|
|
|
51
|
-
expect(service).to
|
|
51
|
+
expect(service).to be_successful
|
|
52
52
|
expect(service.user).to be_persisted
|
|
53
53
|
expect(service.user.email).to eq("test@example.com")
|
|
54
54
|
end
|
|
@@ -86,7 +86,7 @@ RSpec.describe Comment::Create do
|
|
|
86
86
|
text: "Great post!"
|
|
87
87
|
)
|
|
88
88
|
|
|
89
|
-
expect(service).to
|
|
89
|
+
expect(service).to be_successful
|
|
90
90
|
expect(service.comment.user).to eq(current_user)
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -122,7 +122,7 @@ RSpec.describe Order::Create do
|
|
|
122
122
|
items: [{ product_id: product.id, quantity: 2 }]
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
expect(service).to
|
|
125
|
+
expect(service).to be_successful
|
|
126
126
|
expect(service.order.order_items.count).to eq(1)
|
|
127
127
|
expect(service.order.total).to eq(200)
|
|
128
128
|
end
|
|
@@ -252,7 +252,7 @@ RSpec.describe DataImport do
|
|
|
252
252
|
it "completes with warnings for skipped records" do
|
|
253
253
|
service = described_class.run(data: mixed_valid_invalid_data)
|
|
254
254
|
|
|
255
|
-
expect(service).to
|
|
255
|
+
expect(service).to be_successful # Warnings don't fail the service
|
|
256
256
|
expect(service.warnings?).to be true
|
|
257
257
|
expect(service.warnings[:skipped]).to include("Row 3: invalid format")
|
|
258
258
|
end
|
|
@@ -273,7 +273,7 @@ RSpec.describe Payment::Charge do
|
|
|
273
273
|
it "processes payment successfully" do
|
|
274
274
|
service = described_class.run(amount: 1000, card_token: "tok_visa")
|
|
275
275
|
|
|
276
|
-
expect(service).to
|
|
276
|
+
expect(service).to be_successful
|
|
277
277
|
expect(service.payment_intent_id).to eq("pi_123")
|
|
278
278
|
end
|
|
279
279
|
|
|
@@ -312,7 +312,7 @@ RSpec.describe MyService do
|
|
|
312
312
|
|
|
313
313
|
it "accepts optional arguments as nil" do
|
|
314
314
|
service = described_class.run(name: "John", nickname: nil)
|
|
315
|
-
expect(service).to
|
|
315
|
+
expect(service).to be_successful
|
|
316
316
|
end
|
|
317
317
|
end
|
|
318
318
|
end
|
|
@@ -353,7 +353,7 @@ Create a helper module for common service testing patterns:
|
|
|
353
353
|
# spec/support/service_helpers.rb
|
|
354
354
|
module ServiceHelpers
|
|
355
355
|
def expect_service_success(service)
|
|
356
|
-
expect(service).to
|
|
356
|
+
expect(service).to be_successful, -> { "Expected success but got errors: #{service.errors.to_h}" }
|
|
357
357
|
end
|
|
358
358
|
|
|
359
359
|
def expect_service_failure(service, key = nil)
|
data/lib/light/services/base.rb
CHANGED
|
@@ -89,6 +89,7 @@ module Light
|
|
|
89
89
|
def success?
|
|
90
90
|
!errors?
|
|
91
91
|
end
|
|
92
|
+
alias successful? success?
|
|
92
93
|
|
|
93
94
|
# Check if the service completed with errors.
|
|
94
95
|
#
|
|
@@ -136,6 +137,25 @@ module Light
|
|
|
136
137
|
raise Light::Services::StopExecution
|
|
137
138
|
end
|
|
138
139
|
|
|
140
|
+
# Add an error to the :base key.
|
|
141
|
+
#
|
|
142
|
+
# @param message [String] the error message
|
|
143
|
+
# @return [void]
|
|
144
|
+
def fail!(message)
|
|
145
|
+
errors.add(:base, message)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Add an error and stop execution immediately, causing transaction rollback.
|
|
149
|
+
# Steps marked with `always: true` will still run after this method is called.
|
|
150
|
+
#
|
|
151
|
+
# @param message [String] the error message
|
|
152
|
+
# @raise [FailExecution] always raises to halt execution and rollback
|
|
153
|
+
# @return [void]
|
|
154
|
+
def fail_immediately!(message)
|
|
155
|
+
errors.add(:base, message, rollback: false)
|
|
156
|
+
raise Light::Services::FailExecution
|
|
157
|
+
end
|
|
158
|
+
|
|
139
159
|
# Execute the service steps.
|
|
140
160
|
#
|
|
141
161
|
# @return [void]
|
|
@@ -44,6 +44,9 @@ module Light
|
|
|
44
44
|
# Gracefully handle stop_immediately! inside transaction to prevent rollback
|
|
45
45
|
@stopped = true
|
|
46
46
|
end
|
|
47
|
+
rescue Light::Services::FailExecution
|
|
48
|
+
# FailExecution bubbles out of transaction (causing rollback) but is caught here
|
|
49
|
+
nil
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
# Run steps with parameter `always` if they weren't launched because of errors/warnings
|
|
@@ -10,7 +10,8 @@ module Light
|
|
|
10
10
|
#
|
|
11
11
|
# @example
|
|
12
12
|
# Light::Services.configure do |config|
|
|
13
|
-
# config.
|
|
13
|
+
# config.require_arg_type = true
|
|
14
|
+
# config.require_output_type = true
|
|
14
15
|
# config.use_transactions = false
|
|
15
16
|
# end
|
|
16
17
|
def configure
|
|
@@ -28,13 +29,16 @@ module Light
|
|
|
28
29
|
# Configuration class for Light::Services global settings.
|
|
29
30
|
#
|
|
30
31
|
# @example Accessing configuration
|
|
31
|
-
# Light::Services.config.
|
|
32
|
+
# Light::Services.config.require_arg_type # => true
|
|
32
33
|
#
|
|
33
34
|
# @example Modifying configuration
|
|
34
35
|
# Light::Services.config.use_transactions = false
|
|
35
36
|
class Config
|
|
36
|
-
# @return [Boolean] whether arguments
|
|
37
|
-
attr_reader :
|
|
37
|
+
# @return [Boolean] whether arguments must have a type specified
|
|
38
|
+
attr_reader :require_arg_type
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] whether outputs must have a type specified
|
|
41
|
+
attr_reader :require_output_type
|
|
38
42
|
|
|
39
43
|
# @return [Boolean] whether to wrap service execution in a database transaction
|
|
40
44
|
attr_reader :use_transactions
|
|
@@ -69,7 +73,8 @@ module Light
|
|
|
69
73
|
attr_reader :ruby_lsp_type_mappings
|
|
70
74
|
|
|
71
75
|
DEFAULTS = {
|
|
72
|
-
|
|
76
|
+
require_arg_type: true,
|
|
77
|
+
require_output_type: true,
|
|
73
78
|
use_transactions: true,
|
|
74
79
|
|
|
75
80
|
load_errors: true,
|
|
@@ -92,6 +97,16 @@ module Light
|
|
|
92
97
|
end
|
|
93
98
|
end
|
|
94
99
|
|
|
100
|
+
# Convenience setter for backward compatibility.
|
|
101
|
+
# Sets both require_arg_type and require_output_type.
|
|
102
|
+
#
|
|
103
|
+
# @param value [Boolean] whether to require types for arguments and outputs
|
|
104
|
+
# @return [void]
|
|
105
|
+
def require_type=(value)
|
|
106
|
+
self.require_arg_type = value
|
|
107
|
+
self.require_output_type = value
|
|
108
|
+
end
|
|
109
|
+
|
|
95
110
|
# Initialize configuration with default values.
|
|
96
111
|
def initialize
|
|
97
112
|
reset_to_defaults!
|
|
@@ -135,26 +135,37 @@ module Light
|
|
|
135
135
|
# @param opts [Hash] the options hash to check for type
|
|
136
136
|
def self.validate_type_required!(name, field_type, service_class, opts)
|
|
137
137
|
return if opts.key?(:type)
|
|
138
|
-
return unless
|
|
138
|
+
return unless require_type_enabled_for?(field_type, service_class)
|
|
139
139
|
|
|
140
|
+
config_name = field_type == :argument ? "require_arg_type" : "require_output_type"
|
|
140
141
|
raise Light::Services::MissingTypeError,
|
|
141
142
|
"#{field_type.to_s.capitalize} `#{name}` in #{service_class} must have a type specified " \
|
|
142
|
-
"(
|
|
143
|
+
"(#{config_name} is enabled)"
|
|
143
144
|
end
|
|
144
145
|
|
|
145
|
-
# Check if require_type is enabled for the service class
|
|
146
|
-
|
|
146
|
+
# Check if require_type is enabled for the given field type and service class
|
|
147
|
+
#
|
|
148
|
+
# @param field_type [Symbol] the type of field (:argument, :output)
|
|
149
|
+
# @param service_class [Class] the service class to check
|
|
150
|
+
# @return [Boolean] whether type is required for the field type
|
|
151
|
+
def self.require_type_enabled_for?(field_type, service_class)
|
|
152
|
+
config_key = field_type == :argument ? :require_arg_type : :require_output_type
|
|
153
|
+
|
|
147
154
|
# Check class-level config in the inheritance chain, then fall back to global config
|
|
148
155
|
klass = service_class
|
|
149
156
|
while klass.respond_to?(:class_config)
|
|
150
157
|
class_config = klass.class_config
|
|
151
158
|
|
|
159
|
+
# Check specific config first (require_arg_type or require_output_type)
|
|
160
|
+
return class_config[config_key] if class_config&.key?(config_key)
|
|
161
|
+
|
|
162
|
+
# Check convenience config (require_type) for backward compatibility
|
|
152
163
|
return class_config[:require_type] if class_config&.key?(:require_type)
|
|
153
164
|
|
|
154
165
|
klass = klass.superclass
|
|
155
166
|
end
|
|
156
167
|
|
|
157
|
-
Light::Services.config.
|
|
168
|
+
Light::Services.config.public_send(config_key)
|
|
158
169
|
end
|
|
159
170
|
end
|
|
160
171
|
end
|
|
@@ -24,6 +24,10 @@ module Light
|
|
|
24
24
|
# Not an error - used to halt execution gracefully.
|
|
25
25
|
class StopExecution < StandardError; end
|
|
26
26
|
|
|
27
|
+
# Control flow exception for fail_immediately!
|
|
28
|
+
# Unlike StopExecution, this exception causes transaction rollback.
|
|
29
|
+
class FailExecution < StandardError; end
|
|
30
|
+
|
|
27
31
|
# @deprecated Use {Error} instead
|
|
28
32
|
NoStepError = Error
|
|
29
33
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module LightServices
|
|
6
|
+
# Detects `errors.add(:base, "message")` and suggests using `fail!("message")` instead.
|
|
7
|
+
#
|
|
8
|
+
# This cop checks calls inside service classes that inherit from
|
|
9
|
+
# Light::Services::Base or any configured base service classes.
|
|
10
|
+
#
|
|
11
|
+
# @safety
|
|
12
|
+
# This cop's autocorrection is safe as `fail!` is a wrapper method
|
|
13
|
+
# that calls `errors.add(:base, message)` internally.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# class User::Create < ApplicationService
|
|
18
|
+
# step :process
|
|
19
|
+
#
|
|
20
|
+
# private
|
|
21
|
+
#
|
|
22
|
+
# def process
|
|
23
|
+
# errors.add(:base, "user is required")
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # good
|
|
28
|
+
# class User::Create < ApplicationService
|
|
29
|
+
# step :process
|
|
30
|
+
#
|
|
31
|
+
# private
|
|
32
|
+
#
|
|
33
|
+
# def process
|
|
34
|
+
# fail!("user is required")
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
class PreferFailMethod < Base
|
|
39
|
+
extend AutoCorrector
|
|
40
|
+
|
|
41
|
+
MSG = "Use `fail!(...)` instead of `errors.add(:base, ...)`."
|
|
42
|
+
|
|
43
|
+
def_node_matcher :errors_add_base?, <<~PATTERN
|
|
44
|
+
(send
|
|
45
|
+
(send nil? :errors) :add
|
|
46
|
+
(sym :base)
|
|
47
|
+
...)
|
|
48
|
+
PATTERN
|
|
49
|
+
|
|
50
|
+
DEFAULT_BASE_CLASSES = ["ApplicationService"].freeze
|
|
51
|
+
|
|
52
|
+
def on_class(node)
|
|
53
|
+
@in_service_class = service_class?(node)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def after_class(_node)
|
|
57
|
+
@in_service_class = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def on_send(node)
|
|
61
|
+
return unless @in_service_class
|
|
62
|
+
return unless node.method_name == :add
|
|
63
|
+
return unless node.receiver&.method_name == :errors
|
|
64
|
+
|
|
65
|
+
return unless errors_add_base?(node)
|
|
66
|
+
|
|
67
|
+
# Only flag if there's a message argument after :base
|
|
68
|
+
# errors.add(:base) without a message is invalid Light Services syntax
|
|
69
|
+
message_args = node.arguments[1..]
|
|
70
|
+
return if message_args.empty?
|
|
71
|
+
|
|
72
|
+
add_offense(node, message: MSG) do |corrector|
|
|
73
|
+
autocorrect(corrector, node, message_args)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def autocorrect(corrector, node, message_args)
|
|
80
|
+
# Get the source code for all arguments after the :base symbol
|
|
81
|
+
args_source = message_args.map(&:source).join(", ")
|
|
82
|
+
replacement = "fail!(#{args_source})"
|
|
83
|
+
|
|
84
|
+
corrector.replace(node, replacement)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def service_class?(node)
|
|
88
|
+
return false unless node.parent_class
|
|
89
|
+
|
|
90
|
+
parent_class_name = extract_class_name(node.parent_class)
|
|
91
|
+
return false unless parent_class_name
|
|
92
|
+
|
|
93
|
+
# Check for direct Light::Services::Base inheritance
|
|
94
|
+
return true if parent_class_name == "Light::Services::Base"
|
|
95
|
+
|
|
96
|
+
# Check against configured base service classes
|
|
97
|
+
base_classes = cop_config.fetch("BaseServiceClasses", DEFAULT_BASE_CLASSES)
|
|
98
|
+
base_classes.include?(parent_class_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def extract_class_name(node)
|
|
102
|
+
case node.type
|
|
103
|
+
when :const
|
|
104
|
+
node.const_name
|
|
105
|
+
when :send
|
|
106
|
+
# For namespaced constants like Light::Services::Base
|
|
107
|
+
node.source
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -9,4 +9,5 @@ require_relative "rubocop/cop/light_services/dsl_order"
|
|
|
9
9
|
require_relative "rubocop/cop/light_services/missing_private_keyword"
|
|
10
10
|
require_relative "rubocop/cop/light_services/no_direct_instantiation"
|
|
11
11
|
require_relative "rubocop/cop/light_services/output_type_required"
|
|
12
|
+
require_relative "rubocop/cop/light_services/prefer_fail_method"
|
|
12
13
|
require_relative "rubocop/cop/light_services/step_method_exists"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: light-services
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kodkod
|
|
@@ -17,6 +17,8 @@ executables: []
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
+
- ".cursor/rules/services-rspec/RULE.md"
|
|
21
|
+
- ".cursor/rules/services/RULE.md"
|
|
20
22
|
- ".github/config/rubocop_linter_action.yml"
|
|
21
23
|
- ".github/dependabot.yml"
|
|
22
24
|
- ".github/workflows/ci.yml"
|
|
@@ -96,6 +98,7 @@ files:
|
|
|
96
98
|
- lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb
|
|
97
99
|
- lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb
|
|
98
100
|
- lib/light/services/rubocop/cop/light_services/output_type_required.rb
|
|
101
|
+
- lib/light/services/rubocop/cop/light_services/prefer_fail_method.rb
|
|
99
102
|
- lib/light/services/rubocop/cop/light_services/step_method_exists.rb
|
|
100
103
|
- lib/light/services/settings/field.rb
|
|
101
104
|
- lib/light/services/settings/step.rb
|