steroids 1.0.0 → 1.6.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/README.md +854 -0
- data/Rakefile +27 -0
- data/app/jobs/steroids/async_service_job.rb +10 -0
- data/app/serializers/steroids/error_serializer.rb +35 -0
- data/lib/resources/quotes.yml +114 -0
- data/lib/steroids/controllers/methods.rb +18 -0
- data/lib/{concerns/controller.rb → steroids/controllers/responders_helper.rb} +20 -21
- data/lib/steroids/controllers/serializers_helper.rb +15 -0
- data/lib/steroids/engine.rb +6 -0
- data/lib/steroids/errors/base.rb +57 -0
- data/lib/steroids/errors/context.rb +70 -0
- data/lib/steroids/errors/quotes.rb +29 -0
- data/lib/steroids/errors.rb +89 -0
- data/lib/steroids/extensions/array_extension.rb +25 -0
- data/lib/steroids/extensions/class_extension.rb +141 -0
- data/lib/steroids/extensions/hash_extension.rb +14 -0
- data/lib/steroids/extensions/method_extension.rb +63 -0
- data/lib/steroids/extensions/module_extension.rb +32 -0
- data/lib/steroids/extensions/object_extension.rb +122 -0
- data/lib/steroids/extensions/proc_extension.rb +9 -0
- data/lib/steroids/logger.rb +162 -0
- data/lib/steroids/railtie.rb +60 -0
- data/lib/steroids/serializers/base.rb +7 -0
- data/lib/{concerns/serializer.rb → steroids/serializers/methods.rb} +3 -3
- data/lib/steroids/services/base.rb +181 -0
- data/lib/steroids/support/magic_class.rb +17 -0
- data/lib/steroids/support/noticable_methods.rb +134 -0
- data/lib/steroids/support/servicable_methods.rb +34 -0
- data/lib/{base/type.rb → steroids/types/base.rb} +3 -3
- data/lib/{base/model.rb → steroids/types/serializable_type.rb} +2 -2
- data/lib/steroids/version.rb +4 -0
- data/lib/steroids.rb +12 -0
- metadata +75 -34
- data/lib/base/class.rb +0 -15
- data/lib/base/error.rb +0 -87
- data/lib/base/hash.rb +0 -49
- data/lib/base/list.rb +0 -51
- data/lib/base/service.rb +0 -104
- data/lib/concern.rb +0 -130
- data/lib/concerns/error.rb +0 -20
- data/lib/concerns/model.rb +0 -9
- data/lib/errors/bad_request_error.rb +0 -15
- data/lib/errors/conflict_error.rb +0 -15
- data/lib/errors/forbidden_error.rb +0 -15
- data/lib/errors/generic_error.rb +0 -14
- data/lib/errors/internal_server_error.rb +0 -15
- data/lib/errors/not_found_error.rb +0 -15
- data/lib/errors/not_implemented_error.rb +0 -15
- data/lib/errors/unauthorized_error.rb +0 -15
- data/lib/errors/unprocessable_entity_error.rb +0 -15
data/README.md
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
# Steroids
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/steroids)
|
|
4
|
+
[](https://rubyonrails.org/)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](https://github.com/somelibs/steroids)
|
|
7
|
+
[](LICENSE.md)
|
|
8
|
+
|
|
9
|
+
**Steroids** supercharges your Rails applications with powerful service objects, enhanced error handling, and useful Ruby extensions. Build maintainable, testable business logic with a battle-tested service layer pattern.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Getting Started](#getting-started)
|
|
14
|
+
- [Service Objects](#service-objects)
|
|
15
|
+
- [Error Handling](#error-handling)
|
|
16
|
+
- [Controller Integration](#controller-integration)
|
|
17
|
+
- [Async Services](#async-services)
|
|
18
|
+
- [Serializers (Deprecated)](#serializers-deprecated)
|
|
19
|
+
- [Error Classes](#error-classes)
|
|
20
|
+
- [Logger](#logger)
|
|
21
|
+
- [Extensions](#extensions)
|
|
22
|
+
- [Testing](#testing)
|
|
23
|
+
- [Configuration](#configuration)
|
|
24
|
+
- [Contributing](#contributing)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
### Requirements
|
|
30
|
+
|
|
31
|
+
- Ruby 3.0+
|
|
32
|
+
- Rails 7.1+
|
|
33
|
+
- Sidekiq (optional, for async services)
|
|
34
|
+
|
|
35
|
+
### Installation
|
|
36
|
+
|
|
37
|
+
Add Steroids to your application's Gemfile:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# From GitHub (recommended during active development)
|
|
41
|
+
gem 'steroids', git: 'git@github.com:somelibs/steroids.git', branch: 'master'
|
|
42
|
+
|
|
43
|
+
# Or from RubyGems (when published)
|
|
44
|
+
gem 'steroids'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
And then execute:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
$ bundle install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Service Objects
|
|
54
|
+
|
|
55
|
+
Steroids provides a powerful service object pattern for encapsulating business logic.
|
|
56
|
+
|
|
57
|
+
### Basic Service
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
class CreateUserService < Steroids::Services::Base
|
|
61
|
+
success_notice "User created successfully"
|
|
62
|
+
|
|
63
|
+
def initialize(name:, email:, role: 'user')
|
|
64
|
+
@name = name
|
|
65
|
+
@email = email
|
|
66
|
+
@role = role
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def process
|
|
70
|
+
user = User.create!(
|
|
71
|
+
name: @name,
|
|
72
|
+
email: @email,
|
|
73
|
+
role: @role
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
UserMailer.welcome(user).deliver_later
|
|
77
|
+
user # Return value becomes the service call result
|
|
78
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
79
|
+
errors.add("Failed to create user: #{e.message}", e)
|
|
80
|
+
nil # Return nil on failure
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Usage Patterns
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Method 1: Direct call with block (RECOMMENDED for controllers)
|
|
89
|
+
CreateUserService.call(name: "John", email: "john@example.com") do |service|
|
|
90
|
+
if service.success?
|
|
91
|
+
redirect_to users_path, notice: service.notice
|
|
92
|
+
else
|
|
93
|
+
flash.now[:alert] = service.errors.full_messages
|
|
94
|
+
render :new
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Method 2: Get return value directly
|
|
99
|
+
user = CreateUserService.call(name: "John", email: "john@example.com")
|
|
100
|
+
# user is the return value from process method (User object or nil)
|
|
101
|
+
|
|
102
|
+
# Method 3: Check service instance
|
|
103
|
+
service = CreateUserService.new(name: "John", email: "john@example.com")
|
|
104
|
+
result = service.call
|
|
105
|
+
|
|
106
|
+
if service.success?
|
|
107
|
+
puts service.notice # => "User created successfully"
|
|
108
|
+
# result contains the User object
|
|
109
|
+
else
|
|
110
|
+
puts service.errors.full_messages
|
|
111
|
+
# result is nil
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Important Behaviors
|
|
116
|
+
|
|
117
|
+
**Block Parameters**: When using blocks, the service instance is passed as the first parameter:
|
|
118
|
+
```ruby
|
|
119
|
+
CreateUserService.call(name: "John") do |service|
|
|
120
|
+
# service contains the service instance with noticable methods
|
|
121
|
+
if service.success?
|
|
122
|
+
# handle success
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Return Values**:
|
|
128
|
+
- Without a block: `call` returns the result of the `process` method
|
|
129
|
+
- With a block: `call` returns the result of the `process` method, and yields the service instance to the block
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Without block - returns process result directly
|
|
133
|
+
user = CreateUserService.call(name: "John", email: "john@example.com")
|
|
134
|
+
# user is the User object (or nil if failed)
|
|
135
|
+
|
|
136
|
+
# With block - still returns process result, but yields service for status checking
|
|
137
|
+
user = CreateUserService.call(name: "John", email: "john@example.com") do |service|
|
|
138
|
+
if service.errors?
|
|
139
|
+
# Handle errors using service.errors
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
# user is still the User object (or nil if failed)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Service with Validations
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class UpdateProfileService < Steroids::Services::Base
|
|
149
|
+
success_notice "Profile updated"
|
|
150
|
+
|
|
151
|
+
def initialize(user:, params:)
|
|
152
|
+
@user = user
|
|
153
|
+
@params = params
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def process
|
|
159
|
+
validate_params!
|
|
160
|
+
@user.update!(@params)
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
errors.add("Update failed: #{e.message}", e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def validate_params!
|
|
166
|
+
if @params[:email].blank?
|
|
167
|
+
errors.add("Email cannot be blank")
|
|
168
|
+
drop! # Halts execution
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Service Callbacks
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class ProcessPaymentService < Steroids::Services::Base
|
|
178
|
+
before_process :validate_payment
|
|
179
|
+
after_process :send_receipt
|
|
180
|
+
|
|
181
|
+
def initialize(order:, payment_method:)
|
|
182
|
+
@order = order
|
|
183
|
+
@payment_method = payment_method
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def process
|
|
187
|
+
@payment = Payment.create!(
|
|
188
|
+
order: @order,
|
|
189
|
+
amount: @order.total,
|
|
190
|
+
method: @payment_method
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def validate_payment
|
|
197
|
+
drop!("Invalid payment amount") if @order.total <= 0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def send_receipt(payment)
|
|
201
|
+
PaymentMailer.receipt(payment).deliver_later
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
**⚠️ IMPORTANT:** Steroids uses a different error handling pattern than ActiveRecord.
|
|
209
|
+
|
|
210
|
+
### Correct Usage
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# ✅ CORRECT - Steroids pattern
|
|
214
|
+
errors.add("Something went wrong")
|
|
215
|
+
errors.add("Operation failed", exception)
|
|
216
|
+
notices.add("Processing started")
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Incorrect Usage
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# ❌ WRONG - ActiveRecord pattern (will NOT work)
|
|
223
|
+
errors.add(:base, "Something went wrong")
|
|
224
|
+
errors.add(:field, "is invalid")
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Error Flow Control
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class ComplexService < Steroids::Services::Base
|
|
231
|
+
def process
|
|
232
|
+
# Method 1: Add error and return
|
|
233
|
+
if condition_failed?
|
|
234
|
+
errors.add("Condition not met")
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Method 2: Drop with message (halts execution)
|
|
239
|
+
drop!("Critical failure") if critical_error?
|
|
240
|
+
|
|
241
|
+
# Method 3: Automatic drop on errors
|
|
242
|
+
validate_something # adds errors
|
|
243
|
+
# Service automatically drops if errors.any? is true
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def rescue!(exception)
|
|
247
|
+
# Handle any uncaught exceptions
|
|
248
|
+
logger.error "Service failed: #{exception.message}"
|
|
249
|
+
errors.add("An unexpected error occurred")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def ensure!
|
|
253
|
+
# Always runs, even on failure
|
|
254
|
+
cleanup_resources
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Controller Integration
|
|
260
|
+
|
|
261
|
+
### Using the Service Macro
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
class UsersController < ApplicationController
|
|
265
|
+
# Define service with custom class
|
|
266
|
+
service :create_user, class_name: "Users::CreateService"
|
|
267
|
+
service :update_user, class_name: "Users::UpdateService"
|
|
268
|
+
|
|
269
|
+
def create
|
|
270
|
+
create_user(user_params) do |service|
|
|
271
|
+
if service.success?
|
|
272
|
+
redirect_to users_path, notice: service.notice
|
|
273
|
+
else
|
|
274
|
+
@user = User.new(user_params)
|
|
275
|
+
flash.now[:alert] = service.errors.full_messages
|
|
276
|
+
render :new
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def update
|
|
282
|
+
update_user(user: @user, params: user_params) do |service|
|
|
283
|
+
if service.success?
|
|
284
|
+
redirect_to @user, notice: service.notice
|
|
285
|
+
else
|
|
286
|
+
flash.now[:alert] = service.errors.full_messages
|
|
287
|
+
render :edit
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
def user_params
|
|
295
|
+
params.require(:user).permit(:name, :email, :role)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Direct Service Call
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
class OrdersController < ApplicationController
|
|
304
|
+
def complete
|
|
305
|
+
service = CompleteOrderService.call(order: @order, payment_id: params[:payment_id])
|
|
306
|
+
|
|
307
|
+
respond_to do |format|
|
|
308
|
+
if service.success?
|
|
309
|
+
format.html { redirect_to @order, notice: service.notice }
|
|
310
|
+
format.json { render json: { message: service.notice }, status: :ok }
|
|
311
|
+
else
|
|
312
|
+
format.html { redirect_to @order, alert: service.errors.full_messages }
|
|
313
|
+
format.json { render json: { errors: service.errors.to_a }, status: :unprocessable_entity }
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Async Services
|
|
321
|
+
|
|
322
|
+
Services can run asynchronously using Sidekiq. **Important:** In development, test environments, and Rails console, async services automatically run synchronously for easier debugging.
|
|
323
|
+
|
|
324
|
+
### Defining an Async Service
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
class SendNewsletterService < Steroids::Services::Base
|
|
328
|
+
success_notice "Newsletter sent to all subscribers"
|
|
329
|
+
|
|
330
|
+
def initialize(subject:, content:)
|
|
331
|
+
@subject = subject
|
|
332
|
+
@content = content
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Use async_process instead of process
|
|
336
|
+
def async_process
|
|
337
|
+
User.subscribed.find_each do |user|
|
|
338
|
+
NewsletterMailer.weekly(user, @subject, @content).deliver_now
|
|
339
|
+
end
|
|
340
|
+
rescue StandardError => e
|
|
341
|
+
errors.add("Newsletter delivery failed", e)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Behavior varies by environment:
|
|
346
|
+
# - Production with Sidekiq running: Runs in background
|
|
347
|
+
# - Development/Test/Console: Runs synchronously (immediate execution)
|
|
348
|
+
SendNewsletterService.call(subject: "Weekly Update", content: "...")
|
|
349
|
+
|
|
350
|
+
# Force synchronous execution in any environment
|
|
351
|
+
SendNewsletterService.call(subject: "Test", content: "...", async: false)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Async Execution Logic
|
|
355
|
+
|
|
356
|
+
The service automatically determines execution mode based on:
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# Runs async when ALL conditions are met:
|
|
360
|
+
# 1. Sidekiq is running (workers available)
|
|
361
|
+
# 2. NOT in Rails console
|
|
362
|
+
# 3. NOT in development (unless Sidekiq is running)
|
|
363
|
+
# 4. async: true (default)
|
|
364
|
+
|
|
365
|
+
# Otherwise runs synchronously for easier debugging
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Important Notes for Async Services
|
|
369
|
+
|
|
370
|
+
1. **Parameters must be serializable** (strings, numbers, hashes, arrays)
|
|
371
|
+
2. **Don't pass ActiveRecord objects** - pass IDs instead
|
|
372
|
+
3. **Use `async_process` method** instead of `process`
|
|
373
|
+
4. **Runs via `AsyncServiceJob`** with Sidekiq in production
|
|
374
|
+
5. **Auto-synchronous in dev/test** for easier debugging
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
# ❌ WRONG - AR object won't serialize
|
|
378
|
+
AsyncService.call(user: current_user)
|
|
379
|
+
|
|
380
|
+
# ✅ CORRECT - Pass serializable data
|
|
381
|
+
AsyncService.call(user_id: current_user.id)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Serializers (Deprecated)
|
|
385
|
+
|
|
386
|
+
> **⚠️ DEPRECATION WARNING:** The Serializers module will be removed in the next major version. Consider using [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers) or [Blueprinter](https://github.com/procore/blueprinter) directly.
|
|
387
|
+
|
|
388
|
+
Steroids provides a thin wrapper around ActiveModel::Serializer:
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
class UserSerializer < Steroids::Serializers::Base
|
|
392
|
+
attributes :id, :name, :email, :role
|
|
393
|
+
has_many :posts
|
|
394
|
+
|
|
395
|
+
def custom_attribute
|
|
396
|
+
object.some_computed_value
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Usage
|
|
401
|
+
serializer = UserSerializer.new(user)
|
|
402
|
+
serializer.to_json
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Error Classes
|
|
406
|
+
|
|
407
|
+
Steroids provides a comprehensive error hierarchy with HTTP status codes and logging capabilities.
|
|
408
|
+
|
|
409
|
+
### Base Error Class
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
class CustomError < Steroids::Errors::Base
|
|
413
|
+
self.default_message = "Something went wrong"
|
|
414
|
+
self.default_status = :internal_server_error
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Usage with various options
|
|
418
|
+
raise CustomError.new("Specific error message")
|
|
419
|
+
raise CustomError.new(
|
|
420
|
+
message: "Error occurred",
|
|
421
|
+
status: :bad_request,
|
|
422
|
+
code: "ERR_001",
|
|
423
|
+
cause: original_exception,
|
|
424
|
+
context: { user_id: 123 },
|
|
425
|
+
log: true # Automatically log the error
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Access error properties
|
|
429
|
+
begin
|
|
430
|
+
# some code
|
|
431
|
+
rescue CustomError => e
|
|
432
|
+
e.message # Error message
|
|
433
|
+
e.status # HTTP status symbol
|
|
434
|
+
e.code # Custom error code
|
|
435
|
+
e.cause # Original exception if any
|
|
436
|
+
e.context # Additional context
|
|
437
|
+
e.timestamp # When the error occurred
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Pre-defined HTTP Error Classes
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
# 400 Bad Request
|
|
445
|
+
raise Steroids::Errors::BadRequestError.new("Invalid parameters")
|
|
446
|
+
|
|
447
|
+
# 401 Unauthorized
|
|
448
|
+
raise Steroids::Errors::UnauthorizedError.new("Please login")
|
|
449
|
+
|
|
450
|
+
# 403 Forbidden
|
|
451
|
+
raise Steroids::Errors::ForbiddenError.new("Access denied")
|
|
452
|
+
|
|
453
|
+
# 404 Not Found
|
|
454
|
+
raise Steroids::Errors::NotFoundError.new("Resource not found")
|
|
455
|
+
|
|
456
|
+
# 409 Conflict
|
|
457
|
+
raise Steroids::Errors::ConflictError.new("Resource already exists")
|
|
458
|
+
|
|
459
|
+
# 422 Unprocessable Entity
|
|
460
|
+
raise Steroids::Errors::UnprocessableEntityError.new("Validation failed")
|
|
461
|
+
|
|
462
|
+
# 500 Internal Server Error
|
|
463
|
+
raise Steroids::Errors::InternalServerError.new("Server error")
|
|
464
|
+
|
|
465
|
+
# 501 Not Implemented
|
|
466
|
+
raise Steroids::Errors::NotImplementedError.new("Feature coming soon")
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Error Serialization
|
|
470
|
+
|
|
471
|
+
Errors can be serialized for API responses:
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
class ApiController < ApplicationController
|
|
475
|
+
rescue_from Steroids::Errors::Base do |error|
|
|
476
|
+
render json: error.to_json, status: error.status
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Error Context and Logging
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
# Add context for debugging
|
|
485
|
+
error = Steroids::Errors::BadRequestError.new(
|
|
486
|
+
"Invalid input",
|
|
487
|
+
context: {
|
|
488
|
+
user_id: current_user.id,
|
|
489
|
+
params: params.to_unsafe_h,
|
|
490
|
+
timestamp: Time.current
|
|
491
|
+
},
|
|
492
|
+
log: true # Will automatically log with Steroids::Logger
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Manual logging
|
|
496
|
+
error.log! # Logs the error with full backtrace
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Logger
|
|
500
|
+
|
|
501
|
+
Steroids provides an enhanced logger with colored output, backtrace formatting, and error notification support.
|
|
502
|
+
|
|
503
|
+
### Basic Usage
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
# Simple logging
|
|
507
|
+
Steroids::Logger.print("Operation completed")
|
|
508
|
+
Steroids::Logger.print("Warning message", verbosity: :concise)
|
|
509
|
+
|
|
510
|
+
# Logging exceptions
|
|
511
|
+
begin
|
|
512
|
+
risky_operation
|
|
513
|
+
rescue => e
|
|
514
|
+
Steroids::Logger.print(e) # Automatically detects error level
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Verbosity Levels
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
# Full backtrace (default for exceptions)
|
|
522
|
+
Steroids::Logger.print(exception, verbosity: :full)
|
|
523
|
+
|
|
524
|
+
# Concise backtrace (app code only)
|
|
525
|
+
Steroids::Logger.print(exception, verbosity: :concise)
|
|
526
|
+
|
|
527
|
+
# No backtrace
|
|
528
|
+
Steroids::Logger.print(exception, verbosity: :none)
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Format Options
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
# Decorated output with colors (default)
|
|
535
|
+
Steroids::Logger.print("Message", format: :decorated)
|
|
536
|
+
|
|
537
|
+
# Raw output without colors
|
|
538
|
+
Steroids::Logger.print("Message", format: :raw)
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Automatic Log Levels
|
|
542
|
+
|
|
543
|
+
The logger automatically determines the appropriate log level:
|
|
544
|
+
|
|
545
|
+
- **`:error`** - For `StandardError`, `InternalServerError`, `GenericError`
|
|
546
|
+
- **`:warn`** - For other `Steroids::Errors::Base` subclasses
|
|
547
|
+
- **`:info`** - For regular messages
|
|
548
|
+
|
|
549
|
+
### Error Notifications
|
|
550
|
+
|
|
551
|
+
Configure a notifier to receive alerts for errors:
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# In an initializer
|
|
555
|
+
Steroids::Logger.notifier = lambda do |error|
|
|
556
|
+
# Send to error tracking service
|
|
557
|
+
Bugsnag.notify(error)
|
|
558
|
+
# Or send to Slack
|
|
559
|
+
SlackNotifier.alert(error.message)
|
|
560
|
+
end
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Colored Output
|
|
564
|
+
|
|
565
|
+
The logger uses Rainbow for colored terminal output:
|
|
566
|
+
|
|
567
|
+
- 🔴 **Red** - Errors
|
|
568
|
+
- 🟡 **Yellow** - Warnings
|
|
569
|
+
- 🟢 **Green** - Info messages
|
|
570
|
+
- 🟣 **Magenta** - Error class names and quiet logs
|
|
571
|
+
|
|
572
|
+
### Integration with Services
|
|
573
|
+
|
|
574
|
+
Services automatically use the logger for error handling:
|
|
575
|
+
|
|
576
|
+
```ruby
|
|
577
|
+
class MyService < Steroids::Services::Base
|
|
578
|
+
def process
|
|
579
|
+
Steroids::Logger.print("Starting process")
|
|
580
|
+
|
|
581
|
+
perform_operation
|
|
582
|
+
|
|
583
|
+
Steroids::Logger.print("Process completed")
|
|
584
|
+
rescue => e
|
|
585
|
+
Steroids::Logger.print(e) # Full error logging with backtrace
|
|
586
|
+
errors.add("Process failed", e)
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## Extensions
|
|
592
|
+
|
|
593
|
+
Steroids provides useful extensions to Ruby core classes.
|
|
594
|
+
|
|
595
|
+
### Type Checking
|
|
596
|
+
|
|
597
|
+
```ruby
|
|
598
|
+
# Ensure type at runtime
|
|
599
|
+
def process_name(name)
|
|
600
|
+
name.typed!(String) # Raises TypeError if not a String
|
|
601
|
+
name.upcase
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Type casting with enums
|
|
605
|
+
STATUSES = %i[draft published archived]
|
|
606
|
+
status = STATUSES.cast(:published) # Returns :published
|
|
607
|
+
status = STATUSES.cast(:invalid) # Raises error
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Hash Extensions
|
|
611
|
+
|
|
612
|
+
```ruby
|
|
613
|
+
# Check if hash is serializable
|
|
614
|
+
params.serializable? # => true/false
|
|
615
|
+
|
|
616
|
+
# Deep serialize for storage
|
|
617
|
+
data = { user: { name: "John", tags: ["ruby", "rails"] } }
|
|
618
|
+
serialized = data.deep_serialize
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Safe Method Calls
|
|
622
|
+
|
|
623
|
+
```ruby
|
|
624
|
+
# Safe send with fallback
|
|
625
|
+
object.send_apply(:optional_method, arg1, arg2)
|
|
626
|
+
|
|
627
|
+
# Try to get method object
|
|
628
|
+
method_obj = object.try_method(:method_name)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
## Testing
|
|
632
|
+
|
|
633
|
+
### RSpec Examples
|
|
634
|
+
|
|
635
|
+
```ruby
|
|
636
|
+
RSpec.describe CreateUserService do
|
|
637
|
+
describe "#call" do
|
|
638
|
+
context "with valid params" do
|
|
639
|
+
subject { described_class.call(name: "John", email: "john@test.com") }
|
|
640
|
+
|
|
641
|
+
it "succeeds" do
|
|
642
|
+
expect(subject).to be_success
|
|
643
|
+
expect(subject.errors).not_to be_any
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
it "creates a user" do
|
|
647
|
+
expect { subject }.to change(User, :count).by(1)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
it "returns success notice" do
|
|
651
|
+
expect(subject.notice).to eq("User created successfully")
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
context "with invalid params" do
|
|
656
|
+
subject { described_class.call(name: "", email: "invalid") }
|
|
657
|
+
|
|
658
|
+
it "fails" do
|
|
659
|
+
expect(subject).to be_errors
|
|
660
|
+
expect(subject).not_to be_success
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it "returns error messages" do
|
|
664
|
+
expect(subject.errors.full_messages).to include(/failed/i)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Testing Async Services
|
|
672
|
+
|
|
673
|
+
```ruby
|
|
674
|
+
RSpec.describe AsyncNewsletterService do
|
|
675
|
+
it "enqueues job" do
|
|
676
|
+
expect {
|
|
677
|
+
described_class.call(subject: "Test", content: "Content")
|
|
678
|
+
}.to have_enqueued_job(AsyncServiceJob)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
it "processes synchronously when forced" do
|
|
682
|
+
service = described_class.call(subject: "Test", content: "Content", async: false)
|
|
683
|
+
expect(service).to be_success
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Configuration
|
|
689
|
+
|
|
690
|
+
### Transaction Wrapping
|
|
691
|
+
|
|
692
|
+
Services are wrapped in database transactions by default:
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
class MyService < Steroids::Services::Base
|
|
696
|
+
# Disable transaction wrapping for this service
|
|
697
|
+
self.wrap_in_transaction = false
|
|
698
|
+
|
|
699
|
+
def process
|
|
700
|
+
# Not wrapped in transaction
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Callback Configuration
|
|
706
|
+
|
|
707
|
+
```ruby
|
|
708
|
+
class MyService < Steroids::Services::Base
|
|
709
|
+
# Skip all callbacks
|
|
710
|
+
self.skip_callbacks = true
|
|
711
|
+
|
|
712
|
+
# Or skip per invocation
|
|
713
|
+
def process
|
|
714
|
+
MyService.call(data: data, skip_callbacks: true)
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
## Development
|
|
720
|
+
|
|
721
|
+
### Local Development
|
|
722
|
+
|
|
723
|
+
When developing Steroids locally alongside a Rails application, you can use Bundler's local gem override:
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
# Point Bundler to your local Steroids repository
|
|
727
|
+
$ bundle config local.steroids /path/to/local/steroids
|
|
728
|
+
|
|
729
|
+
# Example:
|
|
730
|
+
$ bundle config local.steroids ~/Projects/steroids
|
|
731
|
+
|
|
732
|
+
# Verify the configuration
|
|
733
|
+
$ bundle config
|
|
734
|
+
# Should show: local.steroids => "/path/to/local/steroids"
|
|
735
|
+
|
|
736
|
+
# Install/update dependencies
|
|
737
|
+
$ bundle install
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Now your Rails app will use the local version of Steroids. Any changes you make to the gem will be reflected immediately (after restarting Rails).
|
|
741
|
+
|
|
742
|
+
To remove the local override:
|
|
743
|
+
|
|
744
|
+
```bash
|
|
745
|
+
$ bundle config --delete local.steroids
|
|
746
|
+
$ bundle install
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Running Tests
|
|
750
|
+
|
|
751
|
+
Steroids uses Minitest for testing. The test suite includes comprehensive coverage of:
|
|
752
|
+
- Service objects and lifecycle
|
|
753
|
+
- Noticable methods (error/notice handling)
|
|
754
|
+
- Controller integration (servicable methods)
|
|
755
|
+
- Error classes and logging
|
|
756
|
+
- Async services
|
|
757
|
+
|
|
758
|
+
#### Run All Tests
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
# Using Rake (recommended)
|
|
762
|
+
$ bundle exec rake test
|
|
763
|
+
|
|
764
|
+
# With verbose output
|
|
765
|
+
$ bundle exec rake test TESTOPTS="--verbose"
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
#### Run Specific Test Files
|
|
769
|
+
|
|
770
|
+
```bash
|
|
771
|
+
# Test services
|
|
772
|
+
$ bundle exec rake test TEST=test/services/base_service_test.rb
|
|
773
|
+
$ bundle exec rake test TEST=test/services/async_service_test.rb
|
|
774
|
+
|
|
775
|
+
# Test support modules
|
|
776
|
+
$ bundle exec rake test TEST=test/support/noticable_methods_test.rb
|
|
777
|
+
$ bundle exec rake test TEST=test/support/servicable_methods_test.rb
|
|
778
|
+
|
|
779
|
+
# Test errors
|
|
780
|
+
$ bundle exec rake test TEST=test/errors/base_error_test.rb
|
|
781
|
+
|
|
782
|
+
# Main module test
|
|
783
|
+
$ bundle exec rake test TEST=test/steroids_test.rb
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
#### Run Tests by Pattern
|
|
787
|
+
|
|
788
|
+
```bash
|
|
789
|
+
# Run all service tests
|
|
790
|
+
$ bundle exec rake test TEST="test/services/*"
|
|
791
|
+
|
|
792
|
+
# Run multiple specific tests
|
|
793
|
+
$ bundle exec rake test TEST="test/services/base_service_test.rb,test/support/noticable_methods_test.rb"
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
#### Test Coverage
|
|
797
|
+
|
|
798
|
+
To check test coverage (requires simplecov gem):
|
|
799
|
+
|
|
800
|
+
```bash
|
|
801
|
+
# Add to Gemfile (test group)
|
|
802
|
+
gem 'simplecov', require: false
|
|
803
|
+
|
|
804
|
+
# Add to test_helper.rb (at the top)
|
|
805
|
+
require 'simplecov'
|
|
806
|
+
SimpleCov.start 'rails'
|
|
807
|
+
|
|
808
|
+
# Run tests and generate coverage report
|
|
809
|
+
$ bundle exec rake test
|
|
810
|
+
# Coverage report will be in coverage/index.html
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
## Troubleshooting
|
|
814
|
+
|
|
815
|
+
### Common Issues
|
|
816
|
+
|
|
817
|
+
**Issue:** `TypeError: Expected String instance`
|
|
818
|
+
**Solution:** Ensure you're using `errors.add("message")` not `errors.add(:symbol, "message")`
|
|
819
|
+
|
|
820
|
+
**Issue:** Async service not running
|
|
821
|
+
**Solution:** Ensure Sidekiq is running and parameters are serializable
|
|
822
|
+
|
|
823
|
+
**Issue:** Transaction rollback not working
|
|
824
|
+
**Solution:** Ensure `wrap_in_transaction` is not disabled
|
|
825
|
+
|
|
826
|
+
**Issue:** `force` flag not preventing service from dropping
|
|
827
|
+
**Solution:** The `force: true` option may not work as expected in all cases. Currently, the force flag behavior is being reviewed.
|
|
828
|
+
|
|
829
|
+
**Issue:** `skip_callbacks` option not working properly
|
|
830
|
+
**Solution:** The `skip_callbacks: true` option may not skip all callbacks as expected. This is a known limitation being addressed.
|
|
831
|
+
|
|
832
|
+
## Roadmap
|
|
833
|
+
|
|
834
|
+
- [ ] Standalone testing with dummy Rails app
|
|
835
|
+
- [ ] Generator for service objects
|
|
836
|
+
- [ ] Built-in metrics and instrumentation
|
|
837
|
+
- [ ] Service composition patterns
|
|
838
|
+
- [ ] Enhanced async job features
|
|
839
|
+
|
|
840
|
+
## Contributing
|
|
841
|
+
|
|
842
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/somelibs/steroids.
|
|
843
|
+
|
|
844
|
+
## Disclaimer
|
|
845
|
+
|
|
846
|
+
This gem is under active development and may not strictly follow SemVer. Use at your own risk in production environments.
|
|
847
|
+
|
|
848
|
+
## Credits
|
|
849
|
+
|
|
850
|
+
Created and maintained by Paul R.
|
|
851
|
+
|
|
852
|
+
## License
|
|
853
|
+
|
|
854
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.md).
|