better_service 2.0.0 → 2.1.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/LICENSE +2 -0
- data/README.md +98 -45
- data/Rakefile +7 -209
- data/config/locales/better_service.en.yml +15 -0
- data/lib/better_service/cache_service.rb +4 -4
- data/lib/better_service/concerns/instrumentation.rb +59 -14
- data/lib/better_service/concerns/serviceable/authorizable.rb +1 -1
- data/lib/better_service/concerns/serviceable/messageable.rb +70 -1
- data/lib/better_service/concerns/serviceable/repository_aware.rb +8 -3
- data/lib/better_service/concerns/workflowable/callbacks.rb +27 -27
- data/lib/better_service/concerns/workflowable/step.rb +39 -5
- data/lib/better_service/errors/better_service_error.rb +4 -0
- data/lib/better_service/errors/runtime/authorization_error.rb +4 -1
- data/lib/better_service/errors/runtime/database_error.rb +4 -1
- data/lib/better_service/errors/runtime/execution_error.rb +4 -1
- data/lib/better_service/errors/runtime/invalid_result_error.rb +28 -0
- data/lib/better_service/errors/runtime/resource_not_found_error.rb +4 -1
- data/lib/better_service/errors/runtime/validation_error.rb +4 -1
- data/lib/better_service/repository/base_repository.rb +1 -1
- data/lib/better_service/result.rb +110 -0
- data/lib/better_service/services/base.rb +216 -57
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/branch_group.rb +1 -1
- data/lib/better_service.rb +1 -6
- data/lib/generators/serviceable/action_generator.rb +11 -0
- data/lib/generators/serviceable/base_generator.rb +109 -0
- data/lib/generators/serviceable/create_generator.rb +11 -0
- data/lib/generators/serviceable/destroy_generator.rb +11 -0
- data/lib/generators/serviceable/index_generator.rb +11 -0
- data/lib/generators/serviceable/scaffold_generator.rb +29 -7
- data/lib/generators/serviceable/show_generator.rb +11 -0
- data/lib/generators/serviceable/templates/action_service.rb.tt +8 -3
- data/lib/generators/serviceable/templates/base_locale.en.yml.tt +53 -0
- data/lib/generators/serviceable/templates/base_service.rb.tt +78 -0
- data/lib/generators/serviceable/templates/base_service_test.rb.tt +64 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +29 -18
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +16 -29
- data/lib/generators/serviceable/templates/index_service.rb.tt +16 -34
- data/lib/generators/serviceable/templates/repository.rb.tt +76 -0
- data/lib/generators/serviceable/templates/repository_test.rb.tt +124 -0
- data/lib/generators/serviceable/templates/show_service.rb.tt +10 -38
- data/lib/generators/serviceable/templates/update_service.rb.tt +24 -38
- data/lib/generators/serviceable/update_generator.rb +11 -0
- metadata +13 -12
- data/lib/better_service/concerns/serviceable/viewable.rb +0 -33
- data/lib/better_service/services/action_service.rb +0 -60
- data/lib/better_service/services/create_service.rb +0 -63
- data/lib/better_service/services/destroy_service.rb +0 -60
- data/lib/better_service/services/index_service.rb +0 -56
- data/lib/better_service/services/show_service.rb +0 -44
- data/lib/better_service/services/update_service.rb +0 -61
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a03149390ed14f18e00211a28f8549497048245e592f598a41612789a80b0889
|
|
4
|
+
data.tar.gz: f97b2063a3c2652942957b120a227b29cd92fb4b22a70b0162772c61125dee9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d157b4542bd163145c2a3db7eae5cc0154c40f412ba32287ebbbbe1c535b4ebf386763115d463104ad0816bde0fc218382a1382bfa981e839a538e7791d033c5
|
|
7
|
+
data.tar.gz: ad3756e28b89045810aee2e7014c483f821d7d0df396d967536f8f9e07f48f33235847e36d05439cd99022755d5860f62af44399d40346c4d74a7864949ca6d3
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# BetterService
|
|
4
4
|
|
|
5
5
|
### Clean, powerful Service Objects for Rails
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/better_service)
|
|
8
|
-
[](https://github.com/alessiobussolari/better_service/actions/workflows/ci.yml)
|
|
9
|
+
[](https://codecov.io/gh/alessiobussolari/better_service)
|
|
10
|
+
[](https://www.ruby-lang.org/)
|
|
11
|
+
[](https://rubyonrails.org/)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
|
|
14
|
+
[](https://rubygems.org/gems/better_service)
|
|
15
|
+
[](https://rubydoc.info/gems/better_service)
|
|
16
|
+
[](https://github.com/alessiobussolari/better_service/issues)
|
|
17
|
+
[](https://github.com/alessiobussolari/better_service/stargazers)
|
|
18
|
+
[](https://github.com/alessiobussolari/better_service/graphs/contributors)
|
|
9
19
|
|
|
10
20
|
[Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [Documentation](#-documentation) • [Usage](#-usage) • [Error Handling](#%EF%B8%8F-error-handling) • [Examples](#-examples)
|
|
11
21
|
|
|
@@ -15,22 +25,30 @@
|
|
|
15
25
|
|
|
16
26
|
## ✨ Features
|
|
17
27
|
|
|
18
|
-
BetterService is a comprehensive Service Objects framework for Rails that brings clean architecture and powerful features to your business logic layer
|
|
28
|
+
BetterService is a comprehensive Service Objects framework for Rails that brings clean architecture and powerful features to your business logic layer.
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
**Version 2.1.0** • 1,000+ tests passing (812 gem + 275 rails_app)
|
|
31
|
+
|
|
32
|
+
### Core Features
|
|
33
|
+
|
|
34
|
+
- 🎯 **4-Phase Flow Architecture**: Structured flow with validation → authorization → search → process → respond
|
|
35
|
+
- 📦 **Result Wrapper**: `BetterService::Result` with `.success?`, `.resource`, `.meta`, `.message` and destructuring support
|
|
36
|
+
- 🏛️ **Repository Pattern**: Clean data access with `RepositoryAware` concern and `repository :model_name` DSL
|
|
21
37
|
- ✅ **Mandatory Schema Validation**: Built-in [Dry::Schema](https://dry-rb.org/gems/dry-schema/) validation for all params
|
|
22
38
|
- 🔄 **Transaction Support**: Automatic database transaction wrapping with rollback
|
|
23
39
|
- 🔐 **Flexible Authorization**: `authorize_with` DSL that works with any auth system (Pundit, CanCanCan, custom)
|
|
24
40
|
- ⚠️ **Rich Error Handling**: Pure Exception Pattern with hierarchical errors, rich context, and detailed debugging info
|
|
41
|
+
|
|
42
|
+
### Advanced Features
|
|
43
|
+
|
|
25
44
|
- 💾 **Cache Management**: Built-in `CacheService` for invalidating cache by context, user, or globally with async support
|
|
26
45
|
- 🔄 **Auto-Invalidation**: Write operations (Create/Update/Destroy) automatically invalidate cache when configured
|
|
27
46
|
- 🌍 **I18n Support**: Built-in internationalization with `message()` helper, custom namespaces, and fallback chain
|
|
28
47
|
- 🎨 **Presenter System**: Optional data transformation layer with `BetterService::Presenter` base class
|
|
29
48
|
- 📊 **Metadata Tracking**: Automatic action metadata in all service responses
|
|
30
49
|
- 🔗 **Workflow Composition**: Chain multiple services into pipelines with conditional steps, rollback support, and lifecycle hooks
|
|
31
|
-
- 🌲 **Conditional Branching
|
|
32
|
-
- 🏗️ **Powerful Generators**:
|
|
33
|
-
- 📦 **6 Service Types**: Specialized services for different use cases
|
|
50
|
+
- 🌲 **Conditional Branching**: Multi-path workflow execution with `branch`/`on`/`otherwise` DSL for clean conditional logic
|
|
51
|
+
- 🏗️ **Powerful Generators**: 11 generators for rapid scaffolding (base, scaffold, CRUD services, action, workflow, locale, presenter)
|
|
34
52
|
- 🎨 **DSL-Based**: Clean, expressive DSL with `search_with`, `process_with`, `authorize_with`, etc.
|
|
35
53
|
|
|
36
54
|
---
|
|
@@ -59,19 +77,21 @@ gem install better_service
|
|
|
59
77
|
|
|
60
78
|
## 🚀 Quick Start
|
|
61
79
|
|
|
62
|
-
### 1. Generate
|
|
80
|
+
### 1. Generate Services
|
|
63
81
|
|
|
64
82
|
```bash
|
|
65
|
-
# Generate
|
|
66
|
-
rails generate serviceable:
|
|
83
|
+
# Generate BaseService + Repository + locale file
|
|
84
|
+
rails generate serviceable:base Product
|
|
85
|
+
|
|
86
|
+
# Generate all CRUD services inheriting from BaseService
|
|
87
|
+
rails generate serviceable:scaffold Product --base
|
|
67
88
|
|
|
68
89
|
# Or generate individual services
|
|
69
|
-
rails generate serviceable:create Product
|
|
70
|
-
rails generate serviceable:update Product
|
|
90
|
+
rails generate serviceable:create Product --base_class=Product::BaseService
|
|
71
91
|
rails generate serviceable:action Product publish
|
|
72
92
|
```
|
|
73
93
|
|
|
74
|
-
### 2. Use the Service
|
|
94
|
+
### 2. Use the Service with Result Wrapper
|
|
75
95
|
|
|
76
96
|
```ruby
|
|
77
97
|
# Create a product
|
|
@@ -80,15 +100,19 @@ result = Product::CreateService.new(current_user, params: {
|
|
|
80
100
|
price: 2499.99
|
|
81
101
|
}).call
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# => Product object
|
|
86
|
-
|
|
87
|
-
# => :created
|
|
103
|
+
# Check success with Result wrapper
|
|
104
|
+
if result.success?
|
|
105
|
+
product = result.resource # => Product object
|
|
106
|
+
message = result.message # => "Product created successfully"
|
|
107
|
+
action = result.meta[:action] # => :created
|
|
88
108
|
else
|
|
89
|
-
|
|
90
|
-
# =>
|
|
109
|
+
error_code = result.meta[:error_code] # => :unauthorized
|
|
110
|
+
message = result.message # => "Not authorized"
|
|
91
111
|
end
|
|
112
|
+
|
|
113
|
+
# Or use destructuring
|
|
114
|
+
product, meta = result
|
|
115
|
+
redirect_to product if meta[:success]
|
|
92
116
|
```
|
|
93
117
|
|
|
94
118
|
---
|
|
@@ -118,37 +142,57 @@ See **[Configuration Guide](docs/start/configuration.md)** for all options inclu
|
|
|
118
142
|
|
|
119
143
|
## 📚 Usage
|
|
120
144
|
|
|
121
|
-
### Service
|
|
145
|
+
### Service Architecture
|
|
122
146
|
|
|
123
|
-
All services
|
|
147
|
+
All services inherit from `BetterService::Services::Base` via a resource-specific BaseService:
|
|
124
148
|
|
|
125
149
|
```ruby
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
# 1. BaseService with Repository (generated with `rails g serviceable:base Product`)
|
|
151
|
+
class Product::BaseService < BetterService::Services::Base
|
|
152
|
+
include BetterService::Concerns::Serviceable::RepositoryAware
|
|
153
|
+
|
|
154
|
+
messages_namespace :products
|
|
155
|
+
cache_contexts [:products]
|
|
156
|
+
repository :product # Injects product_repository method
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 2. All services inherit from BaseService
|
|
160
|
+
class Product::CreateService < Product::BaseService
|
|
161
|
+
performed_action :created
|
|
162
|
+
with_transaction true
|
|
163
|
+
auto_invalidate_cache true
|
|
164
|
+
|
|
165
|
+
# Schema Validation (mandatory)
|
|
128
166
|
schema do
|
|
129
|
-
required(:name).filled(:string)
|
|
167
|
+
required(:name).filled(:string, min_size?: 2)
|
|
130
168
|
required(:price).filled(:decimal, gt?: 0)
|
|
131
169
|
end
|
|
132
170
|
|
|
133
|
-
#
|
|
171
|
+
# Authorization - IMPORTANT: use `next` not `return`
|
|
134
172
|
authorize_with do
|
|
135
|
-
user.admin?
|
|
173
|
+
next true if user.admin? # Admin bypass
|
|
174
|
+
user.seller?
|
|
136
175
|
end
|
|
137
176
|
|
|
138
|
-
#
|
|
177
|
+
# Search Phase - Load data
|
|
139
178
|
search_with do
|
|
140
|
-
{
|
|
179
|
+
{} # No data to load for create
|
|
141
180
|
end
|
|
142
181
|
|
|
143
|
-
#
|
|
144
|
-
process_with do |
|
|
145
|
-
product =
|
|
182
|
+
# Process Phase - Business logic
|
|
183
|
+
process_with do |_data|
|
|
184
|
+
product = product_repository.create!(
|
|
185
|
+
name: params[:name],
|
|
186
|
+
price: params[:price],
|
|
187
|
+
user: user
|
|
188
|
+
)
|
|
189
|
+
# IMPORTANT: Return { resource: ... } for proper extraction
|
|
146
190
|
{ resource: product }
|
|
147
191
|
end
|
|
148
192
|
|
|
149
|
-
#
|
|
193
|
+
# Respond Phase - Format response with Result wrapper
|
|
150
194
|
respond_with do |data|
|
|
151
|
-
success_result("
|
|
195
|
+
success_result(message("create.success", name: data[:resource].name), data)
|
|
152
196
|
end
|
|
153
197
|
end
|
|
154
198
|
```
|
|
@@ -282,11 +326,12 @@ class Product::DestroyService < BetterService::Services::DestroyService
|
|
|
282
326
|
end
|
|
283
327
|
```
|
|
284
328
|
|
|
285
|
-
#### 6. ⚡
|
|
329
|
+
#### 6. ⚡ Custom Action Services
|
|
286
330
|
|
|
287
331
|
```ruby
|
|
288
|
-
class Product::PublishService <
|
|
289
|
-
|
|
332
|
+
class Product::PublishService < Product::BaseService
|
|
333
|
+
# Action name for metadata
|
|
334
|
+
performed_action :publish
|
|
290
335
|
|
|
291
336
|
schema do
|
|
292
337
|
required(:id).filled(:integer)
|
|
@@ -297,7 +342,7 @@ class Product::PublishService < BetterService::Services::ActionService
|
|
|
297
342
|
end
|
|
298
343
|
|
|
299
344
|
search_with do
|
|
300
|
-
{ resource:
|
|
345
|
+
{ resource: product_repository.find(params[:id]) }
|
|
301
346
|
end
|
|
302
347
|
|
|
303
348
|
process_with do |data|
|
|
@@ -321,19 +366,27 @@ BetterService provides a flexible `authorize_with` DSL that works with **any** a
|
|
|
321
366
|
### Simple Role-Based Authorization
|
|
322
367
|
|
|
323
368
|
```ruby
|
|
324
|
-
class Product::CreateService <
|
|
369
|
+
class Product::CreateService < Product::BaseService
|
|
325
370
|
authorize_with do
|
|
326
|
-
|
|
371
|
+
# IMPORTANT: Use `next` not `return` (return causes LocalJumpError)
|
|
372
|
+
next true if user.admin?
|
|
373
|
+
user.seller?
|
|
327
374
|
end
|
|
328
375
|
end
|
|
329
376
|
```
|
|
330
377
|
|
|
331
|
-
### Resource Ownership Check
|
|
378
|
+
### Resource Ownership Check (Admin Bypass Pattern)
|
|
332
379
|
|
|
333
380
|
```ruby
|
|
334
|
-
class Product::UpdateService <
|
|
381
|
+
class Product::UpdateService < Product::BaseService
|
|
335
382
|
authorize_with do
|
|
336
|
-
product
|
|
383
|
+
# Admin can update any product (even non-existent - will get "not found" error)
|
|
384
|
+
next true if user.admin?
|
|
385
|
+
|
|
386
|
+
# For non-admin, check resource ownership
|
|
387
|
+
product = Product.find_by(id: params[:id])
|
|
388
|
+
next false unless product # Return unauthorized if product doesn't exist
|
|
389
|
+
|
|
337
390
|
product.user_id == user.id
|
|
338
391
|
end
|
|
339
392
|
end
|
|
@@ -1481,7 +1534,7 @@ BetterService includes comprehensive test coverage. Run tests with:
|
|
|
1481
1534
|
bundle exec rake
|
|
1482
1535
|
|
|
1483
1536
|
# Or
|
|
1484
|
-
bundle exec
|
|
1537
|
+
bundle exec rspec
|
|
1485
1538
|
```
|
|
1486
1539
|
|
|
1487
1540
|
### Manual Testing
|
|
@@ -1489,7 +1542,7 @@ bundle exec rake test
|
|
|
1489
1542
|
A manual test script is included for hands-on verification:
|
|
1490
1543
|
|
|
1491
1544
|
```bash
|
|
1492
|
-
cd
|
|
1545
|
+
cd spec/rails_app
|
|
1493
1546
|
rails console
|
|
1494
1547
|
load '../../manual_test.rb'
|
|
1495
1548
|
```
|
data/Rakefile
CHANGED
|
@@ -1,215 +1,13 @@
|
|
|
1
1
|
require "bundler/setup"
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
require "
|
|
5
|
-
require "fileutils"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
6
5
|
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Service file templates
|
|
12
|
-
SERVICE_TEMPLATES = {
|
|
13
|
-
"create_service.rb" => <<~RUBY,
|
|
14
|
-
# frozen_string_literal: true
|
|
15
|
-
|
|
16
|
-
class Product::CreateService < BetterService::Services::CreateService
|
|
17
|
-
# Schema for validating params
|
|
18
|
-
schema do
|
|
19
|
-
required(:name).filled(:string)
|
|
20
|
-
required(:price).filled(:decimal, gt?: 0)
|
|
21
|
-
optional(:published).filled(:bool)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Phase 1: Search - Prepare dependencies (optional)
|
|
25
|
-
search_with do
|
|
26
|
-
{}
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Phase 2: Process - Create the resource
|
|
30
|
-
process_with do |data|
|
|
31
|
-
product = user.products.create!(params)
|
|
32
|
-
{ resource: product }
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Phase 4: Respond - Format response (optional override)
|
|
36
|
-
respond_with do |data|
|
|
37
|
-
success_result("Product created successfully", data)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
RUBY
|
|
41
|
-
"index_service.rb" => <<~RUBY,
|
|
42
|
-
# frozen_string_literal: true
|
|
43
|
-
|
|
44
|
-
class Product::IndexService < BetterService::Services::IndexService
|
|
45
|
-
# Schema for validating params
|
|
46
|
-
schema do
|
|
47
|
-
optional(:page).filled(:integer, gteq?: 1)
|
|
48
|
-
optional(:per_page).filled(:integer, gteq?: 1, lteq?: 100)
|
|
49
|
-
optional(:search).maybe(:string)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Phase 1: Search - Load raw data
|
|
53
|
-
search_with do
|
|
54
|
-
products = user.products
|
|
55
|
-
products = products.where("name LIKE ?", "%\#{params[:search]}%") if params[:search].present?
|
|
56
|
-
|
|
57
|
-
{ items: products.to_a }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Phase 2: Process - Transform and aggregate data
|
|
61
|
-
process_with do |data|
|
|
62
|
-
{
|
|
63
|
-
items: data[:items],
|
|
64
|
-
metadata: {
|
|
65
|
-
stats: {
|
|
66
|
-
total: data[:items].count
|
|
67
|
-
},
|
|
68
|
-
pagination: {
|
|
69
|
-
page: params[:page] || 1,
|
|
70
|
-
per_page: params[:per_page] || 25
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Phase 4: Respond - Format response (optional override)
|
|
77
|
-
respond_with do |data|
|
|
78
|
-
success_result("Products loaded successfully", data)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
RUBY
|
|
82
|
-
"show_service.rb" => <<~RUBY,
|
|
83
|
-
# frozen_string_literal: true
|
|
84
|
-
|
|
85
|
-
class Product::ShowService < BetterService::Services::ShowService
|
|
86
|
-
# Schema for validating params
|
|
87
|
-
schema do
|
|
88
|
-
required(:id).filled
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Phase 1: Search - Load the resource
|
|
92
|
-
search_with do
|
|
93
|
-
{ resource: user.products.find(params[:id]) }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Phase 4: Respond - Format response (optional override)
|
|
97
|
-
respond_with do |data|
|
|
98
|
-
success_result("Product loaded successfully", data)
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
RUBY
|
|
102
|
-
"update_service.rb" => <<~RUBY,
|
|
103
|
-
# frozen_string_literal: true
|
|
104
|
-
|
|
105
|
-
class Product::UpdateService < BetterService::Services::UpdateService
|
|
106
|
-
# Schema for validating params
|
|
107
|
-
schema do
|
|
108
|
-
required(:id).filled
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Phase 1: Search - Load the resource
|
|
112
|
-
search_with do
|
|
113
|
-
{ resource: user.products.find(params[:id]) }
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Phase 2: Process - Update the resource
|
|
117
|
-
process_with do |data|
|
|
118
|
-
product = data[:resource]
|
|
119
|
-
product.update!(params.except(:id))
|
|
120
|
-
{ resource: product }
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Phase 4: Respond - Format response (optional override)
|
|
124
|
-
respond_with do |data|
|
|
125
|
-
success_result("Product updated successfully", data)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
RUBY
|
|
129
|
-
"destroy_service.rb" => <<~RUBY
|
|
130
|
-
# frozen_string_literal: true
|
|
131
|
-
|
|
132
|
-
class Product::DestroyService < BetterService::Services::DestroyService
|
|
133
|
-
# Schema for validating params
|
|
134
|
-
schema do
|
|
135
|
-
required(:id).filled
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Phase 1: Search - Load the resource
|
|
139
|
-
search_with do
|
|
140
|
-
{ resource: user.products.find(params[:id]) }
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Phase 2: Process - Delete the resource
|
|
144
|
-
process_with do |data|
|
|
145
|
-
product = data[:resource]
|
|
146
|
-
product.destroy!
|
|
147
|
-
{ resource: product }
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Phase 4: Respond - Format response (optional override)
|
|
151
|
-
respond_with do |data|
|
|
152
|
-
success_result("Product deleted successfully", data)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
RUBY
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
namespace :test do
|
|
159
|
-
desc "Setup test environment - create missing Product service files"
|
|
160
|
-
task :setup do
|
|
161
|
-
created_files = []
|
|
162
|
-
|
|
163
|
-
SERVICE_TEMPLATES.each do |filename, content|
|
|
164
|
-
filepath = File.join(PRODUCT_SERVICES_DIR, filename)
|
|
165
|
-
|
|
166
|
-
unless File.exist?(filepath)
|
|
167
|
-
puts "Creating temporary test file: #{filepath}"
|
|
168
|
-
File.write(filepath, content)
|
|
169
|
-
created_files << filepath
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Save list of created files
|
|
174
|
-
File.write(CREATED_FILES_MARKER, created_files.join("\n")) if created_files.any?
|
|
175
|
-
puts "Test setup complete (#{created_files.size} files created)" if created_files.any?
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
desc "Cleanup test environment - remove temporary Product service files"
|
|
179
|
-
task :cleanup do
|
|
180
|
-
if File.exist?(CREATED_FILES_MARKER)
|
|
181
|
-
created_files = File.read(CREATED_FILES_MARKER).split("\n")
|
|
182
|
-
|
|
183
|
-
created_files.each do |filepath|
|
|
184
|
-
if File.exist?(filepath)
|
|
185
|
-
puts "Removing temporary test file: #{filepath}"
|
|
186
|
-
File.delete(filepath)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
File.delete(CREATED_FILES_MARKER)
|
|
191
|
-
puts "Test cleanup complete (#{created_files.size} files removed)" if created_files.any?
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
Rake::TestTask.new(:test_only) do |t|
|
|
197
|
-
t.libs << "test"
|
|
198
|
-
t.test_files = FileList["test/**/*_test.rb"].exclude(
|
|
199
|
-
"test/dummy/**/*",
|
|
200
|
-
"test/generators/**/*" # Generator tests require Rails context - run manually with: bundle exec ruby -Itest test/generators/*_test.rb
|
|
201
|
-
)
|
|
202
|
-
t.verbose = false
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Main test task with automatic setup and cleanup
|
|
206
|
-
task :test do
|
|
207
|
-
begin
|
|
208
|
-
Rake::Task["test:setup"].invoke
|
|
209
|
-
Rake::Task["test_only"].invoke
|
|
210
|
-
ensure
|
|
211
|
-
Rake::Task["test:cleanup"].invoke
|
|
212
|
-
end
|
|
6
|
+
# Default task runs RSpec
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
8
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
9
|
+
t.exclude_pattern = "spec/rails_app/**/*"
|
|
213
10
|
end
|
|
214
11
|
|
|
215
|
-
task default: :
|
|
12
|
+
task default: :spec
|
|
13
|
+
task test: :spec
|
|
@@ -21,6 +21,7 @@ en:
|
|
|
21
21
|
services:
|
|
22
22
|
# Default success messages for standard CRUD operations
|
|
23
23
|
default:
|
|
24
|
+
# Success messages
|
|
24
25
|
created: "Resource created successfully"
|
|
25
26
|
updated: "Resource updated successfully"
|
|
26
27
|
deleted: "Resource deleted successfully"
|
|
@@ -28,6 +29,20 @@ en:
|
|
|
28
29
|
shown: "Resource retrieved successfully"
|
|
29
30
|
action_completed: "Action completed successfully"
|
|
30
31
|
|
|
32
|
+
# Structured success messages for new tuple format
|
|
33
|
+
index:
|
|
34
|
+
success: "Resources retrieved successfully"
|
|
35
|
+
show:
|
|
36
|
+
success: "Resource retrieved successfully"
|
|
37
|
+
create:
|
|
38
|
+
success: "Resource created successfully"
|
|
39
|
+
failure: "Failed to create resource"
|
|
40
|
+
update:
|
|
41
|
+
success: "Resource updated successfully"
|
|
42
|
+
failure: "Failed to update resource"
|
|
43
|
+
destroy:
|
|
44
|
+
success: "Resource deleted successfully"
|
|
45
|
+
|
|
31
46
|
# Default error messages
|
|
32
47
|
errors:
|
|
33
48
|
validation_failed: "Validation failed"
|
|
@@ -89,7 +89,7 @@ module BetterService
|
|
|
89
89
|
# contexts_for('unknown') # => ['unknown']
|
|
90
90
|
def contexts_to_invalidate(context)
|
|
91
91
|
context_str = context.to_s
|
|
92
|
-
(@invalidation_map || {})[context_str] || [context_str]
|
|
92
|
+
(@invalidation_map || {})[context_str] || [ context_str ]
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# Invalidate cache for a specific context and user
|
|
@@ -121,7 +121,7 @@ module BetterService
|
|
|
121
121
|
return 0 unless user && context && !context.to_s.strip.empty?
|
|
122
122
|
|
|
123
123
|
# Get all contexts to invalidate (cascading or single)
|
|
124
|
-
contexts = cascade ? contexts_to_invalidate(context) : [context.to_s]
|
|
124
|
+
contexts = cascade ? contexts_to_invalidate(context) : [ context.to_s ]
|
|
125
125
|
total_deleted = 0
|
|
126
126
|
|
|
127
127
|
contexts.each do |ctx|
|
|
@@ -164,7 +164,7 @@ module BetterService
|
|
|
164
164
|
return 0 unless context && !context.to_s.strip.empty?
|
|
165
165
|
|
|
166
166
|
# Get all contexts to invalidate (cascading or single)
|
|
167
|
-
contexts = cascade ? contexts_to_invalidate(context) : [context.to_s]
|
|
167
|
+
contexts = cascade ? contexts_to_invalidate(context) : [ context.to_s ]
|
|
168
168
|
total_deleted = 0
|
|
169
169
|
|
|
170
170
|
contexts.each do |ctx|
|
|
@@ -352,7 +352,7 @@ module BetterService
|
|
|
352
352
|
# Escape special regex characters except *
|
|
353
353
|
escaped = Regexp.escape(pattern.to_s)
|
|
354
354
|
# Replace escaped \* with .* for regex matching
|
|
355
|
-
regex_string = escaped.gsub('\*',
|
|
355
|
+
regex_string = escaped.gsub('\*', ".*")
|
|
356
356
|
Regexp.new(regex_string)
|
|
357
357
|
end
|
|
358
358
|
|
|
@@ -55,11 +55,27 @@ module BetterService
|
|
|
55
55
|
result = call_without_instrumentation
|
|
56
56
|
duration = ((Time.current - start_time) * 1000).round(2) # milliseconds
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
# Validate that result is a BetterService::Result object
|
|
59
|
+
unless result.is_a?(BetterService::Result)
|
|
60
|
+
raise BetterService::Errors::Runtime::InvalidResultError.new(
|
|
61
|
+
"Service #{service_name} must return BetterService::Result, got #{result.class}",
|
|
62
|
+
context: { service: service_name, result_class: result.class.name }
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if result.failure?
|
|
67
|
+
# Publish service.failed event for Result failures
|
|
68
|
+
failure_payload = build_result_failure_payload(
|
|
69
|
+
service_name, user_id, result, duration
|
|
70
|
+
)
|
|
71
|
+
ActiveSupport::Notifications.instrument("service.failed", failure_payload)
|
|
72
|
+
else
|
|
73
|
+
# Publish service.completed event
|
|
74
|
+
completion_payload = build_completion_payload(
|
|
75
|
+
service_name, user_id, result, duration
|
|
76
|
+
)
|
|
77
|
+
ActiveSupport::Notifications.instrument("service.completed", completion_payload)
|
|
78
|
+
end
|
|
63
79
|
|
|
64
80
|
result
|
|
65
81
|
rescue => error
|
|
@@ -130,7 +146,7 @@ module BetterService
|
|
|
130
146
|
#
|
|
131
147
|
# @param service_name [String] Name of service class
|
|
132
148
|
# @param user_id [Integer, String, nil] User ID
|
|
133
|
-
# @param result [
|
|
149
|
+
# @param result [BetterService::Result] Service result
|
|
134
150
|
# @param duration [Float] Execution duration in milliseconds
|
|
135
151
|
# @return [Hash] Event payload
|
|
136
152
|
def build_completion_payload(service_name, user_id, result, duration)
|
|
@@ -152,14 +168,43 @@ module BetterService
|
|
|
152
168
|
payload[:result] = result
|
|
153
169
|
end
|
|
154
170
|
|
|
155
|
-
# Include cache metadata if available
|
|
156
|
-
if result.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
# Include cache metadata if available from Result meta
|
|
172
|
+
if result.meta[:cache_hit]
|
|
173
|
+
payload[:cache_hit] = result.meta[:cache_hit]
|
|
174
|
+
end
|
|
175
|
+
if result.meta[:cache_key]
|
|
176
|
+
payload[:cache_key] = result.meta[:cache_key]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
payload
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Build payload for service.failed event from Result object
|
|
183
|
+
#
|
|
184
|
+
# @param service_name [String] Name of service class
|
|
185
|
+
# @param user_id [Integer, String, nil] User ID
|
|
186
|
+
# @param result [BetterService::Result] Result object with failure
|
|
187
|
+
# @param duration [Float] Execution duration in milliseconds
|
|
188
|
+
# @return [Hash] Event payload
|
|
189
|
+
def build_result_failure_payload(service_name, user_id, result, duration)
|
|
190
|
+
payload = {
|
|
191
|
+
service_name: service_name,
|
|
192
|
+
user_id: user_id,
|
|
193
|
+
duration: duration,
|
|
194
|
+
timestamp: Time.current.iso8601,
|
|
195
|
+
success: false,
|
|
196
|
+
error_class: result.meta[:error_code]&.to_s || "UnknownError",
|
|
197
|
+
error_message: result.message || "Service failed"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Include params if configured and available
|
|
201
|
+
if BetterService.configuration.instrumentation_include_args && respond_to?(:params, true)
|
|
202
|
+
payload[:params] = send(:params)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Include validation errors if present
|
|
206
|
+
if result.validation_errors
|
|
207
|
+
payload[:validation_errors] = result.validation_errors
|
|
163
208
|
end
|
|
164
209
|
|
|
165
210
|
payload
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module BetterService
|
|
4
4
|
module Concerns
|
|
5
5
|
module Serviceable
|
|
6
|
-
|
|
6
|
+
# Authorizable adds authorization support to services.
|
|
7
7
|
#
|
|
8
8
|
# Use the `authorize_with` DSL to define authorization logic that runs
|
|
9
9
|
# BEFORE the search phase (fail fast principle).
|