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.
- checksums.yaml +4 -4
- data/LICENSE +13 -0
- data/README.md +256 -18
- data/Rakefile +202 -2
- data/config/locales/better_service.en.yml +37 -0
- data/lib/better_service/concerns/serviceable/messageable.rb +45 -2
- data/lib/better_service/concerns/serviceable/validatable.rb +0 -5
- data/lib/better_service/concerns/serviceable/viewable.rb +0 -16
- data/lib/better_service/presenter.rb +131 -0
- data/lib/better_service/railtie.rb +17 -0
- data/lib/better_service/services/base.rb +78 -21
- data/lib/better_service/services/create_service.rb +3 -0
- data/lib/better_service/services/destroy_service.rb +3 -0
- data/lib/better_service/services/update_service.rb +3 -0
- data/lib/better_service/subscribers/log_subscriber.rb +25 -5
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/base.rb +1 -0
- data/lib/better_service/workflows/branch.rb +133 -0
- data/lib/better_service/workflows/branch_dsl.rb +151 -0
- data/lib/better_service/workflows/branch_group.rb +139 -0
- data/lib/better_service/workflows/dsl.rb +46 -0
- data/lib/better_service/workflows/execution.rb +35 -9
- data/lib/better_service/workflows/result_builder.rb +26 -17
- data/lib/better_service.rb +4 -0
- data/lib/generators/better_service/install_generator.rb +38 -0
- data/lib/generators/better_service/locale_generator.rb +54 -0
- data/lib/generators/better_service/presenter_generator.rb +60 -0
- data/lib/generators/better_service/templates/better_service_initializer.rb.tt +90 -0
- data/lib/generators/better_service/templates/locale.en.yml.tt +27 -0
- data/lib/generators/better_service/templates/presenter.rb.tt +53 -0
- data/lib/generators/better_service/templates/presenter_test.rb.tt +46 -0
- data/lib/generators/serviceable/scaffold_generator.rb +9 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +11 -1
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +11 -1
- data/lib/generators/serviceable/templates/index_service.rb.tt +19 -1
- data/lib/generators/serviceable/templates/show_service.rb.tt +19 -1
- data/lib/generators/serviceable/templates/update_service.rb.tt +11 -1
- data/lib/generators/workflowable/templates/workflow.rb.tt +22 -0
- metadata +16 -4
- data/MIT-LICENSE +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 622ac1a705ab117672d0e9c4896e65b66585c747023d1e00b34824e9c35606a0
|
|
4
|
+
data.tar.gz: f4c6f5b6ae7baaa3f61eeb123d4a2a493a7c8acc2ff835c6891396ed543efa1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://badge.fury.io/rb/better_service)
|
|
8
|
-
[](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
|
-
-
|
|
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/
|
|
100
|
-
- **[Concerns Reference](docs/concerns-reference.md)** - Complete reference for all
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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/
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|