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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -0
  3. data/README.md +98 -45
  4. data/Rakefile +7 -209
  5. data/config/locales/better_service.en.yml +15 -0
  6. data/lib/better_service/cache_service.rb +4 -4
  7. data/lib/better_service/concerns/instrumentation.rb +59 -14
  8. data/lib/better_service/concerns/serviceable/authorizable.rb +1 -1
  9. data/lib/better_service/concerns/serviceable/messageable.rb +70 -1
  10. data/lib/better_service/concerns/serviceable/repository_aware.rb +8 -3
  11. data/lib/better_service/concerns/workflowable/callbacks.rb +27 -27
  12. data/lib/better_service/concerns/workflowable/step.rb +39 -5
  13. data/lib/better_service/errors/better_service_error.rb +4 -0
  14. data/lib/better_service/errors/runtime/authorization_error.rb +4 -1
  15. data/lib/better_service/errors/runtime/database_error.rb +4 -1
  16. data/lib/better_service/errors/runtime/execution_error.rb +4 -1
  17. data/lib/better_service/errors/runtime/invalid_result_error.rb +28 -0
  18. data/lib/better_service/errors/runtime/resource_not_found_error.rb +4 -1
  19. data/lib/better_service/errors/runtime/validation_error.rb +4 -1
  20. data/lib/better_service/repository/base_repository.rb +1 -1
  21. data/lib/better_service/result.rb +110 -0
  22. data/lib/better_service/services/base.rb +216 -57
  23. data/lib/better_service/version.rb +1 -1
  24. data/lib/better_service/workflows/branch_group.rb +1 -1
  25. data/lib/better_service.rb +1 -6
  26. data/lib/generators/serviceable/action_generator.rb +11 -0
  27. data/lib/generators/serviceable/base_generator.rb +109 -0
  28. data/lib/generators/serviceable/create_generator.rb +11 -0
  29. data/lib/generators/serviceable/destroy_generator.rb +11 -0
  30. data/lib/generators/serviceable/index_generator.rb +11 -0
  31. data/lib/generators/serviceable/scaffold_generator.rb +29 -7
  32. data/lib/generators/serviceable/show_generator.rb +11 -0
  33. data/lib/generators/serviceable/templates/action_service.rb.tt +8 -3
  34. data/lib/generators/serviceable/templates/base_locale.en.yml.tt +53 -0
  35. data/lib/generators/serviceable/templates/base_service.rb.tt +78 -0
  36. data/lib/generators/serviceable/templates/base_service_test.rb.tt +64 -0
  37. data/lib/generators/serviceable/templates/create_service.rb.tt +29 -18
  38. data/lib/generators/serviceable/templates/destroy_service.rb.tt +16 -29
  39. data/lib/generators/serviceable/templates/index_service.rb.tt +16 -34
  40. data/lib/generators/serviceable/templates/repository.rb.tt +76 -0
  41. data/lib/generators/serviceable/templates/repository_test.rb.tt +124 -0
  42. data/lib/generators/serviceable/templates/show_service.rb.tt +10 -38
  43. data/lib/generators/serviceable/templates/update_service.rb.tt +24 -38
  44. data/lib/generators/serviceable/update_generator.rb +11 -0
  45. metadata +13 -12
  46. data/lib/better_service/concerns/serviceable/viewable.rb +0 -33
  47. data/lib/better_service/services/action_service.rb +0 -60
  48. data/lib/better_service/services/create_service.rb +0 -63
  49. data/lib/better_service/services/destroy_service.rb +0 -60
  50. data/lib/better_service/services/index_service.rb +0 -56
  51. data/lib/better_service/services/show_service.rb +0 -44
  52. 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: 6ce89074c072a76a05a669dc079d3d4fbf1ba99a7f758f4ae45c9640b335c4c4
4
- data.tar.gz: 34bcc32228b8c358f21f4713e783ef3f17e7e38047ab0116abad5a6e0d2a1aec
3
+ metadata.gz: a03149390ed14f18e00211a28f8549497048245e592f598a41612789a80b0889
4
+ data.tar.gz: f97b2063a3c2652942957b120a227b29cd92fb4b22a70b0162772c61125dee9e
5
5
  SHA512:
6
- metadata.gz: 1479b3e94a12463614adb2022aa8a8d725939224715c5f5ae876be036f96da3c3a63171d6ca1cda4dc10ac3c6e97f98a615a45b952a9441066f24f064161521d
7
- data.tar.gz: 2a985c1abfdbb8dc352fdc34351439f35bf0f09ed8784593e04e113a3bd2a38c1d172a3fb1f69836ab24cfd25881b59b52a5c54e5a4a3a6c8b8eacb1fcdb31f8
6
+ metadata.gz: d157b4542bd163145c2a3db7eae5cc0154c40f412ba32287ebbbbe1c535b4ebf386763115d463104ad0816bde0fc218382a1382bfa981e839a538e7791d033c5
7
+ data.tar.gz: ad3756e28b89045810aee2e7014c483f821d7d0df396d967536f8f9e07f48f33235847e36d05439cd99022755d5860f62af44399d40346c4d74a7864949ca6d3
data/LICENSE CHANGED
@@ -11,3 +11,5 @@ as the name is changed.
11
11
  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
12
 
13
13
  0. You just DO WHAT THE FUCK YOU WANT TO.
14
+
15
+ "public_key": "pk_B9nH6LdAbbONpo8kXlCON",
data/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  <div align="center">
2
2
 
3
- # 💎 BetterService
3
+ # BetterService
4
4
 
5
5
  ### Clean, powerful Service Objects for Rails
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/better_service.svg)](https://badge.fury.io/rb/better_service)
8
- [![License](https://img.shields.io/badge/license-WTFPL-blue.svg)](http://www.wtfpl.net/about/)
8
+ [![CI](https://github.com/alessiobussolari/better_service/actions/workflows/ci.yml/badge.svg)](https://github.com/alessiobussolari/better_service/actions/workflows/ci.yml)
9
+ [![Codecov](https://codecov.io/gh/alessiobussolari/better_service/branch/main/graph/badge.svg)](https://codecov.io/gh/alessiobussolari/better_service)
10
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-ruby.svg)](https://www.ruby-lang.org/)
11
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%207.0-CC0000.svg)](https://rubyonrails.org/)
12
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
13
+
14
+ [![Downloads](https://img.shields.io/gem/dt/better_service.svg)](https://rubygems.org/gems/better_service)
15
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/better_service)
16
+ [![GitHub issues](https://img.shields.io/github/issues/alessiobussolari/better_service.svg)](https://github.com/alessiobussolari/better_service/issues)
17
+ [![GitHub stars](https://img.shields.io/github/stars/alessiobussolari/better_service.svg)](https://github.com/alessiobussolari/better_service/stargazers)
18
+ [![Contributors](https://img.shields.io/github/contributors/alessiobussolari/better_service.svg)](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
- - 🎯 **5-Phase Flow Architecture**: Structured flow with search process → transform → respond → viewer phases
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** (v1.1.0+): Multi-path workflow execution with `branch`/`on`/`otherwise` DSL for clean conditional logic
32
- - 🏗️ **Powerful Generators**: 10 generators for rapid scaffolding (scaffold, CRUD services, action, workflow, locale, presenter)
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 a Service
80
+ ### 1. Generate Services
63
81
 
64
82
  ```bash
65
- # Generate a complete CRUD scaffold
66
- rails generate serviceable:scaffold Product
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
- if result[:success]
84
- product = result[:resource]
85
- # => Product object
86
- action = result[:metadata][:action]
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
- errors = result[:errors]
90
- # => { name: ["can't be blank"], price: ["must be greater than 0"] }
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 Structure
145
+ ### Service Architecture
122
146
 
123
- All services follow a 5-phase flow:
147
+ All services inherit from `BetterService::Services::Base` via a resource-specific BaseService:
124
148
 
125
149
  ```ruby
126
- class Product::CreateService < BetterService::Services::CreateService
127
- # 1. Schema Validation (mandatory)
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
- # 2. Authorization (optional)
171
+ # Authorization - IMPORTANT: use `next` not `return`
134
172
  authorize_with do
135
- user.admin? || user.can_create_products?
173
+ next true if user.admin? # Admin bypass
174
+ user.seller?
136
175
  end
137
176
 
138
- # 3. Search Phase - Load data
177
+ # Search Phase - Load data
139
178
  search_with do
140
- { category: Category.find_by(id: params[:category_id]) }
179
+ {} # No data to load for create
141
180
  end
142
181
 
143
- # 4. Process Phase - Business logic
144
- process_with do |data|
145
- product = user.products.create!(params)
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
- # 5. Respond Phase - Format response
193
+ # Respond Phase - Format response with Result wrapper
150
194
  respond_with do |data|
151
- success_result("Product created successfully", data)
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. ⚡ ActionService - Custom Actions
329
+ #### 6. ⚡ Custom Action Services
286
330
 
287
331
  ```ruby
288
- class Product::PublishService < BetterService::Services::ActionService
289
- action_name :publish
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: user.products.find(params[:id]) }
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 < BetterService::Services::CreateService
369
+ class Product::CreateService < Product::BaseService
325
370
  authorize_with do
326
- user.admin?
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 < BetterService::Services::UpdateService
381
+ class Product::UpdateService < Product::BaseService
335
382
  authorize_with do
336
- product = Product.find(params[:id])
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 rake test
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 test/dummy
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 "rake/testtask"
5
- require "fileutils"
4
+ require "rspec/core/rake_task"
6
5
 
7
- # Track files created during test setup
8
- CREATED_FILES_MARKER = ".test_created_files"
9
- PRODUCT_SERVICES_DIR = "test/dummy/app/services/product"
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: :test
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
- # Publish service.completed event
59
- completion_payload = build_completion_payload(
60
- service_name, user_id, result, duration
61
- )
62
- ActiveSupport::Notifications.instrument("service.completed", completion_payload)
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 [Object] Service 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.is_a?(Hash)
157
- if result.key?(:cache_hit)
158
- payload[:cache_hit] = result[:cache_hit]
159
- end
160
- if result.key?(:cache_key)
161
- payload[:cache_key] = result[:cache_key]
162
- end
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
- # Authorizable adds authorization support to services.
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).