better_service 1.0.0 → 1.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +13 -0
  3. data/README.md +256 -18
  4. data/Rakefile +202 -2
  5. data/config/locales/better_service.en.yml +37 -0
  6. data/lib/better_service/concerns/serviceable/messageable.rb +45 -2
  7. data/lib/better_service/concerns/serviceable/validatable.rb +0 -5
  8. data/lib/better_service/concerns/serviceable/viewable.rb +0 -16
  9. data/lib/better_service/presenter.rb +131 -0
  10. data/lib/better_service/railtie.rb +17 -0
  11. data/lib/better_service/services/base.rb +78 -21
  12. data/lib/better_service/services/create_service.rb +3 -0
  13. data/lib/better_service/services/destroy_service.rb +3 -0
  14. data/lib/better_service/services/update_service.rb +3 -0
  15. data/lib/better_service/subscribers/log_subscriber.rb +25 -5
  16. data/lib/better_service/version.rb +1 -1
  17. data/lib/better_service/workflows/base.rb +1 -0
  18. data/lib/better_service/workflows/branch.rb +133 -0
  19. data/lib/better_service/workflows/branch_dsl.rb +151 -0
  20. data/lib/better_service/workflows/branch_group.rb +139 -0
  21. data/lib/better_service/workflows/dsl.rb +46 -0
  22. data/lib/better_service/workflows/execution.rb +35 -9
  23. data/lib/better_service/workflows/result_builder.rb +26 -17
  24. data/lib/better_service.rb +4 -0
  25. data/lib/generators/better_service/install_generator.rb +38 -0
  26. data/lib/generators/better_service/locale_generator.rb +54 -0
  27. data/lib/generators/better_service/presenter_generator.rb +60 -0
  28. data/lib/generators/better_service/templates/better_service_initializer.rb.tt +90 -0
  29. data/lib/generators/better_service/templates/locale.en.yml.tt +27 -0
  30. data/lib/generators/better_service/templates/presenter.rb.tt +53 -0
  31. data/lib/generators/better_service/templates/presenter_test.rb.tt +46 -0
  32. data/lib/generators/serviceable/scaffold_generator.rb +9 -0
  33. data/lib/generators/serviceable/templates/create_service.rb.tt +11 -1
  34. data/lib/generators/serviceable/templates/destroy_service.rb.tt +11 -1
  35. data/lib/generators/serviceable/templates/index_service.rb.tt +19 -1
  36. data/lib/generators/serviceable/templates/show_service.rb.tt +19 -1
  37. data/lib/generators/serviceable/templates/update_service.rb.tt +11 -1
  38. data/lib/generators/workflowable/templates/workflow.rb.tt +22 -0
  39. metadata +16 -4
  40. data/MIT-LICENSE +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26d2fc11e6f3151030f88f3a82fbcac4e08fd056809099f82046a8e8f6a24bcd
4
- data.tar.gz: eca9dd6ea5cf3bc365eb25af7a46837906cc0fe67225742ef1d6cbaada77650c
3
+ metadata.gz: 622ac1a705ab117672d0e9c4896e65b66585c747023d1e00b34824e9c35606a0
4
+ data.tar.gz: f4c6f5b6ae7baaa3f61eeb123d4a2a493a7c8acc2ff835c6891396ed543efa1c
5
5
  SHA512:
6
- metadata.gz: 66f920ce24ec9b96e32a83ee4ec4a20652be82d92abd98434cfe8fce6e59017fc5192ed98ad34752af42f6465fa9e4d5aa3a5aa53195ed23307a88b915773767
7
- data.tar.gz: 645b218a1fc1166f35b60b829f4c417b31c82f41710dd89aa251f28f6337956f9e35b100438819486014482f0706a7c175b398d65926c32cbe0c90393f741d77
6
+ metadata.gz: d4f06ae88607c9d4b6eec865811abbba0f80403914fbcac556ad3949a395ff0b43e16e6663c652959cc950fcb89d64be427eb6e8c20b19ed0aa6b1771012e7b0
7
+ data.tar.gz: d1a69946471e0e2f5bd7067889c4f675d0ea94a198d69f9e877283fa5166664f49283a45fd52f282288b690670a6d095f562935f3503c86dce81633d03858e74
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2025 alessiobussolari <alessio@cosmic.tech>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
data/README.md CHANGED
@@ -5,7 +5,7 @@
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-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![License](https://img.shields.io/badge/license-WTFPL-blue.svg)](http://www.wtfpl.net/about/)
9
9
 
10
10
  [Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [Documentation](#-documentation) • [Usage](#-usage) • [Error Handling](#%EF%B8%8F-error-handling) • [Examples](#-examples)
11
11
 
@@ -23,9 +23,13 @@ BetterService is a comprehensive Service Objects framework for Rails that brings
23
23
  - 🔐 **Flexible Authorization**: `authorize_with` DSL that works with any auth system (Pundit, CanCanCan, custom)
24
24
  - ⚠️ **Rich Error Handling**: Pure Exception Pattern with hierarchical errors, rich context, and detailed debugging info
25
25
  - 💾 **Cache Management**: Built-in `CacheService` for invalidating cache by context, user, or globally with async support
26
+ - 🔄 **Auto-Invalidation**: Write operations (Create/Update/Destroy) automatically invalidate cache when configured
27
+ - 🌍 **I18n Support**: Built-in internationalization with `message()` helper, custom namespaces, and fallback chain
28
+ - 🎨 **Presenter System**: Optional data transformation layer with `BetterService::Presenter` base class
26
29
  - 📊 **Metadata Tracking**: Automatic action metadata in all service responses
27
30
  - 🔗 **Workflow Composition**: Chain multiple services into pipelines with conditional steps, rollback support, and lifecycle hooks
28
- - 🏗️ **Powerful Generators**: 8 generators for rapid scaffolding (scaffold, index, show, create, update, destroy, action, workflow)
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)
29
33
  - 📦 **6 Service Types**: Specialized services for different use cases
30
34
  - 🎨 **DSL-Based**: Clean, expressive DSL with `search_with`, `process_with`, `authorize_with`, etc.
31
35
 
@@ -95,9 +99,9 @@ Comprehensive guides and examples are available in the `/docs` directory:
95
99
 
96
100
  ### 🎓 Guides
97
101
 
98
- - **[Getting Started](docs/getting-started.md)** - Installation, core concepts, your first service
99
- - **[Service Types](docs/service-types.md)** - Deep dive into all 6 service types (Index, Show, Create, Update, Destroy, Action)
100
- - **[Concerns Reference](docs/concerns-reference.md)** - Complete reference for all 8 concerns (Validatable, Authorizable, Cacheable, etc.)
102
+ - **[Getting Started](docs/start/getting-started.md)** - Installation, core concepts, your first service
103
+ - **[Service Types](docs/services/01_services_structure.md)** - Deep dive into all 6 service types (Index, Show, Create, Update, Destroy, Action)
104
+ - **[Concerns Reference](docs/concerns-reference.md)** - Complete reference for all 7 concerns (Validatable, Authorizable, Cacheable, etc.)
101
105
 
102
106
  ### 💡 Examples
103
107
 
@@ -105,7 +109,7 @@ Comprehensive guides and examples are available in the `/docs` directory:
105
109
 
106
110
  ### 🔧 Configuration
107
111
 
108
- See `config/initializers/better_service.rb` for all configuration options including:
112
+ See **[Configuration Guide](docs/start/configuration.md)** for all options including:
109
113
  - Instrumentation & Observability
110
114
  - Built-in LogSubscriber and StatsSubscriber
111
115
  - Cache configuration
@@ -868,9 +872,241 @@ The CacheService works with any Rails cache store, but pattern-based deletion (`
868
872
 
869
873
  ---
870
874
 
875
+ ## 🔄 Auto-Invalidation Cache
876
+
877
+ Write operations (Create/Update/Destroy) can automatically invalidate cache after successful execution.
878
+
879
+ ### How It Works
880
+
881
+ Auto-invalidation is **enabled by default** for Create, Update, and Destroy services when cache contexts are defined:
882
+
883
+ ```ruby
884
+ class Products::CreateService < BetterService::Services::CreateService
885
+ cache_contexts :products, :homepage
886
+
887
+ # Cache is automatically invalidated for these contexts after create!
888
+ # No need to call invalidate_cache_for manually
889
+ end
890
+ ```
891
+
892
+ When the service completes successfully:
893
+ 1. The product is created/updated/deleted
894
+ 2. Cache is automatically invalidated for all defined contexts
895
+ 3. All cache keys matching the patterns are cleared
896
+
897
+ ### Disabling Auto-Invalidation
898
+
899
+ Control auto-invalidation with the `auto_invalidate_cache` DSL:
900
+
901
+ ```ruby
902
+ class Products::CreateService < BetterService::Services::CreateService
903
+ cache_contexts :products
904
+ auto_invalidate_cache false # Disable automatic invalidation
905
+
906
+ process_with do |data|
907
+ product = user.products.create!(params)
908
+
909
+ # Manual control: only invalidate for featured products
910
+ invalidate_cache_for(user) if product.featured?
911
+
912
+ { resource: product }
913
+ end
914
+ end
915
+ ```
916
+
917
+ ### Async Invalidation
918
+
919
+ Combine with async option for non-blocking cache invalidation:
920
+
921
+ ```ruby
922
+ class Products::CreateService < BetterService::Services::CreateService
923
+ cache_contexts :products, :homepage
924
+
925
+ # Auto-invalidation happens async via ActiveJob
926
+ cache_async true
927
+ end
928
+ ```
929
+
930
+ **Note**: Auto-invalidation only applies to Create, Update, and Destroy services. Index and Show services don't trigger cache invalidation since they're read-only operations.
931
+
932
+ ---
933
+
934
+ ## 🌍 Internationalization (I18n)
935
+
936
+ BetterService includes built-in I18n support for service messages with automatic fallback.
937
+
938
+ ### Using the message() Helper
939
+
940
+ All service templates use the `message()` helper for response messages:
941
+
942
+ ```ruby
943
+ class Products::CreateService < BetterService::Services::CreateService
944
+ respond_with do |data|
945
+ success_result(message("create.success"), data)
946
+ end
947
+ end
948
+ ```
949
+
950
+ ### Default Messages
951
+
952
+ BetterService ships with English defaults in `config/locales/better_service.en.yml`:
953
+
954
+ ```yaml
955
+ en:
956
+ better_service:
957
+ services:
958
+ default:
959
+ created: "Resource created successfully"
960
+ updated: "Resource updated successfully"
961
+ deleted: "Resource deleted successfully"
962
+ listed: "Resources retrieved successfully"
963
+ shown: "Resource retrieved successfully"
964
+ ```
965
+
966
+ ### Custom Messages
967
+
968
+ Generate custom locale files for your services:
969
+
970
+ ```bash
971
+ rails generate better_service:locale products
972
+ ```
973
+
974
+ This creates `config/locales/products_services.en.yml`:
975
+
976
+ ```yaml
977
+ en:
978
+ products:
979
+ services:
980
+ create:
981
+ success: "Product created and added to inventory"
982
+ update:
983
+ success: "Product updated successfully"
984
+ destroy:
985
+ success: "Product removed from catalog"
986
+ ```
987
+
988
+ Then configure the namespace in your service:
989
+
990
+ ```ruby
991
+ class Products::CreateService < BetterService::Services::CreateService
992
+ messages_namespace :products
993
+
994
+ respond_with do |data|
995
+ # Uses products.services.create.success
996
+ success_result(message("create.success"), data)
997
+ end
998
+ end
999
+ ```
1000
+
1001
+ ### Fallback Chain
1002
+
1003
+ Messages follow a 3-level fallback:
1004
+ 1. Custom namespace (e.g., `products.services.create.success`)
1005
+ 2. BetterService defaults (e.g., `better_service.services.default.created`)
1006
+ 3. Key itself (e.g., `"create.success"`)
1007
+
1008
+ ### Message Interpolations
1009
+
1010
+ Pass dynamic values to messages:
1011
+
1012
+ ```ruby
1013
+ respond_with do |data|
1014
+ success_result(
1015
+ message("create.success", product_name: data[:resource].name),
1016
+ data
1017
+ )
1018
+ end
1019
+ ```
1020
+
1021
+ **Locale file:**
1022
+ ```yaml
1023
+ en:
1024
+ products:
1025
+ services:
1026
+ create:
1027
+ success: "Product '%{product_name}' created successfully"
1028
+ ```
1029
+
1030
+ ---
1031
+
1032
+ ## 🎨 Presenter System
1033
+
1034
+ BetterService includes an optional presenter layer for formatting data for API/view consumption.
1035
+
1036
+ ### Creating Presenters
1037
+
1038
+ Generate a presenter class:
1039
+
1040
+ ```bash
1041
+ rails generate better_service:presenter Product
1042
+ ```
1043
+
1044
+ This creates:
1045
+ - `app/presenters/product_presenter.rb`
1046
+ - `test/presenters/product_presenter_test.rb`
1047
+
1048
+ ```ruby
1049
+ class ProductPresenter < BetterService::Presenter
1050
+ def as_json(opts = {})
1051
+ {
1052
+ id: object.id,
1053
+ name: object.name,
1054
+ price: object.price,
1055
+ display_name: "#{object.name} - $#{object.price}",
1056
+
1057
+ # Conditional fields based on user permissions
1058
+ **(admin_fields if current_user&.admin?)
1059
+ }
1060
+ end
1061
+
1062
+ private
1063
+
1064
+ def admin_fields
1065
+ {
1066
+ cost: object.cost,
1067
+ margin: object.price - object.cost
1068
+ }
1069
+ end
1070
+ end
1071
+ ```
1072
+
1073
+ ### Using Presenters in Services
1074
+
1075
+ Configure presenters via the `presenter` DSL:
1076
+
1077
+ ```ruby
1078
+ class Products::IndexService < BetterService::Services::IndexService
1079
+ presenter ProductPresenter
1080
+
1081
+ presenter_options do
1082
+ { current_user: user }
1083
+ end
1084
+
1085
+ # Items are automatically formatted via ProductPresenter#as_json
1086
+ end
1087
+ ```
1088
+
1089
+ ### Presenter Features
1090
+
1091
+ **Available Methods:**
1092
+ - `object` - The resource being presented
1093
+ - `options` - Options hash passed via `presenter_options`
1094
+ - `current_user` - Shortcut for `options[:current_user]`
1095
+ - `as_json(opts)` - Format object as JSON
1096
+ - `to_json(opts)` - Serialize to JSON string
1097
+ - `to_h` - Alias for `as_json`
1098
+
1099
+ **Example with scaffold:**
1100
+ ```bash
1101
+ # Generate services + presenter in one command
1102
+ rails generate serviceable:scaffold Product --presenter
1103
+ ```
1104
+
1105
+ ---
1106
+
871
1107
  ## 🏗️ Generators
872
1108
 
873
- BetterService includes 8 powerful generators:
1109
+ BetterService includes 10 powerful generators:
874
1110
 
875
1111
  ### Scaffold Generator
876
1112
 
@@ -878,6 +1114,9 @@ Generates all 5 CRUD services at once:
878
1114
 
879
1115
  ```bash
880
1116
  rails generate serviceable:scaffold Product
1117
+
1118
+ # With presenter
1119
+ rails generate serviceable:scaffold Product --presenter
881
1120
  ```
882
1121
 
883
1122
  Creates:
@@ -886,23 +1125,16 @@ Creates:
886
1125
  - `app/services/product/create_service.rb`
887
1126
  - `app/services/product/update_service.rb`
888
1127
  - `app/services/product/destroy_service.rb`
1128
+ - (Optional) `app/presenters/product_presenter.rb` with `--presenter`
889
1129
 
890
1130
  ### Individual Generators
891
1131
 
892
1132
  ```bash
893
- # Index service
1133
+ # CRUD Services
894
1134
  rails generate serviceable:index Product
895
-
896
- # Show service
897
1135
  rails generate serviceable:show Product
898
-
899
- # Create service
900
1136
  rails generate serviceable:create Product
901
-
902
- # Update service
903
1137
  rails generate serviceable:update Product
904
-
905
- # Destroy service
906
1138
  rails generate serviceable:destroy Product
907
1139
 
908
1140
  # Custom action service
@@ -910,6 +1142,12 @@ rails generate serviceable:action Product publish
910
1142
 
911
1143
  # Workflow for composing services
912
1144
  rails generate serviceable:workflow OrderPurchase --steps create_order charge_payment
1145
+
1146
+ # Presenter for data transformation
1147
+ rails generate better_service:presenter Product
1148
+
1149
+ # Custom locale file for I18n messages
1150
+ rails generate better_service:locale products
913
1151
  ```
914
1152
 
915
1153
  ---
@@ -1302,13 +1540,13 @@ ActiveSupport::Notifications.subscribe("service.completed") do |name, start, fin
1302
1540
  end
1303
1541
  ```
1304
1542
 
1305
- See [Configuration](docs/getting-started.md#configuration) for more details.
1543
+ See [Configuration Guide](docs/start/configuration.md) for more details.
1306
1544
 
1307
1545
  ---
1308
1546
 
1309
1547
  ## 📄 License
1310
1548
 
1311
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1549
+ The gem is available as open source under the terms of the [WTFPL License](http://www.wtfpl.net/about/).
1312
1550
 
1313
1551
  ---
1314
1552
 
data/Rakefile CHANGED
@@ -2,14 +2,214 @@ require "bundler/setup"
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "fileutils"
5
6
 
6
- Rake::TestTask.new(:test) do |t|
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|
7
197
  t.libs << "test"
8
198
  t.test_files = FileList["test/**/*_test.rb"].exclude(
9
199
  "test/dummy/**/*",
10
- "test/generators/**/*"
200
+ "test/generators/**/*" # Generator tests require Rails context - run manually with: bundle exec ruby -Itest test/generators/*_test.rb
11
201
  )
12
202
  t.verbose = false
13
203
  end
14
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
213
+ end
214
+
15
215
  task default: :test
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # BetterService Default Locale File (English)
4
+ #
5
+ # This file contains default messages for BetterService operations.
6
+ # These messages are used as fallbacks when services don't define
7
+ # their own custom messages via the message() helper.
8
+ #
9
+ # To override messages for a specific namespace:
10
+ # 1. Define a namespace with messages_namespace in your service
11
+ # 2. Create locale entries under that namespace
12
+ #
13
+ # Example:
14
+ # products:
15
+ # services:
16
+ # create:
17
+ # success: "Product created successfully"
18
+
19
+ en:
20
+ better_service:
21
+ services:
22
+ # Default success messages for standard CRUD operations
23
+ default:
24
+ created: "Resource created successfully"
25
+ updated: "Resource updated successfully"
26
+ deleted: "Resource deleted successfully"
27
+ listed: "Resources retrieved successfully"
28
+ shown: "Resource retrieved successfully"
29
+ action_completed: "Action completed successfully"
30
+
31
+ # Default error messages
32
+ errors:
33
+ validation_failed: "Validation failed"
34
+ unauthorized: "You are not authorized to perform this action"
35
+ not_found: "Resource not found"
36
+ database_error: "A database error occurred"
37
+ execution_error: "An error occurred while processing your request"
@@ -18,11 +18,54 @@ module BetterService
18
18
 
19
19
  private
20
20
 
21
+ # Get translated message with fallback support
22
+ #
23
+ # Lookup order:
24
+ # 1. Custom namespace: "#{namespace}.services.#{key_path}"
25
+ # 2. Default namespace: "better_service.services.default.#{action}"
26
+ # 3. Key itself as final fallback
27
+ #
28
+ # @param key_path [String] Message key path (e.g., "create.success")
29
+ # @param interpolations [Hash] Variables to interpolate
30
+ # @return [String] Translated message
21
31
  def message(key_path, interpolations = {})
22
- return key_path if self.class._messages_namespace.nil?
32
+ # If no namespace defined, use default BetterService messages
33
+ if self.class._messages_namespace.nil?
34
+ # Extract action from key_path (e.g., "create.success" -> "created")
35
+ action = extract_action_from_key(key_path)
36
+ default_key = "better_service.services.default.#{action}"
37
+ return I18n.t(default_key, default: key_path, **interpolations)
38
+ end
23
39
 
40
+ # Try custom namespace first, fallback to default, then to key itself
24
41
  full_key = "#{self.class._messages_namespace}.services.#{key_path}"
25
- I18n.t(full_key, **interpolations)
42
+ action = extract_action_from_key(key_path)
43
+ fallback_key = "better_service.services.default.#{action}"
44
+
45
+ # I18n supports array of fallback keys: try each in order
46
+ I18n.t(full_key, default: [fallback_key.to_sym, key_path], **interpolations)
47
+ end
48
+
49
+ # Extract action name from key path for fallback lookup
50
+ #
51
+ # Examples:
52
+ # "create.success" -> "created"
53
+ # "update.success" -> "updated"
54
+ # "destroy.success" -> "deleted"
55
+ # "custom_action" -> "action_completed"
56
+ #
57
+ # @param key_path [String] Full key path
58
+ # @return [String] Action name for default messages
59
+ def extract_action_from_key(key_path)
60
+ # Handle common patterns
61
+ case key_path
62
+ when /create/i then "created"
63
+ when /update/i then "updated"
64
+ when /destroy|delete/i then "deleted"
65
+ when /index|list/i then "listed"
66
+ when /show/i then "shown"
67
+ else "action_completed"
68
+ end
26
69
  end
27
70
  end
28
71
  end