better_service 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +253 -16
- data/Rakefile +1 -1
- 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.rb +1 -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
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c796a9b8cd27afc4ae26e29b42decb8a606d88d0fdd6158b8599766cfa33df48
|
|
4
|
+
data.tar.gz: afdee95a722f85f08c919243b5ff8da37adb211b4d7b1dd3ba7a334e97d3b2f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fb831ec7c495b453b0915c33427080012faac9aa870919d7e0e30303251fd477c9309ccfb0e8c6e257d36fe45b9c03f7af7445290641f4af898d6f62ab5c800
|
|
7
|
+
data.tar.gz: f34a008657a3f9208c220d286f3c33201daadac7a7114c729d97e3a5734574907cce3ce0fc85c6ea6964ab8339266be0b31d9c6f67a5211d0eb1c7b7ff18a176
|
data/README.md
CHANGED
|
@@ -23,9 +23,12 @@ 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**:
|
|
31
|
+
- 🏗️ **Powerful Generators**: 10 generators for rapid scaffolding (scaffold, CRUD services, action, workflow, locale, presenter)
|
|
29
32
|
- 📦 **6 Service Types**: Specialized services for different use cases
|
|
30
33
|
- 🎨 **DSL-Based**: Clean, expressive DSL with `search_with`, `process_with`, `authorize_with`, etc.
|
|
31
34
|
|
|
@@ -95,9 +98,9 @@ Comprehensive guides and examples are available in the `/docs` directory:
|
|
|
95
98
|
|
|
96
99
|
### 🎓 Guides
|
|
97
100
|
|
|
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
|
|
101
|
+
- **[Getting Started](docs/start/getting-started.md)** - Installation, core concepts, your first service
|
|
102
|
+
- **[Service Types](docs/services/01_services_structure.md)** - Deep dive into all 6 service types (Index, Show, Create, Update, Destroy, Action)
|
|
103
|
+
- **[Concerns Reference](docs/concerns-reference.md)** - Complete reference for all 7 concerns (Validatable, Authorizable, Cacheable, etc.)
|
|
101
104
|
|
|
102
105
|
### 💡 Examples
|
|
103
106
|
|
|
@@ -105,7 +108,7 @@ Comprehensive guides and examples are available in the `/docs` directory:
|
|
|
105
108
|
|
|
106
109
|
### 🔧 Configuration
|
|
107
110
|
|
|
108
|
-
See
|
|
111
|
+
See **[Configuration Guide](docs/start/configuration.md)** for all options including:
|
|
109
112
|
- Instrumentation & Observability
|
|
110
113
|
- Built-in LogSubscriber and StatsSubscriber
|
|
111
114
|
- Cache configuration
|
|
@@ -868,9 +871,241 @@ The CacheService works with any Rails cache store, but pattern-based deletion (`
|
|
|
868
871
|
|
|
869
872
|
---
|
|
870
873
|
|
|
874
|
+
## 🔄 Auto-Invalidation Cache
|
|
875
|
+
|
|
876
|
+
Write operations (Create/Update/Destroy) can automatically invalidate cache after successful execution.
|
|
877
|
+
|
|
878
|
+
### How It Works
|
|
879
|
+
|
|
880
|
+
Auto-invalidation is **enabled by default** for Create, Update, and Destroy services when cache contexts are defined:
|
|
881
|
+
|
|
882
|
+
```ruby
|
|
883
|
+
class Products::CreateService < BetterService::Services::CreateService
|
|
884
|
+
cache_contexts :products, :homepage
|
|
885
|
+
|
|
886
|
+
# Cache is automatically invalidated for these contexts after create!
|
|
887
|
+
# No need to call invalidate_cache_for manually
|
|
888
|
+
end
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
When the service completes successfully:
|
|
892
|
+
1. The product is created/updated/deleted
|
|
893
|
+
2. Cache is automatically invalidated for all defined contexts
|
|
894
|
+
3. All cache keys matching the patterns are cleared
|
|
895
|
+
|
|
896
|
+
### Disabling Auto-Invalidation
|
|
897
|
+
|
|
898
|
+
Control auto-invalidation with the `auto_invalidate_cache` DSL:
|
|
899
|
+
|
|
900
|
+
```ruby
|
|
901
|
+
class Products::CreateService < BetterService::Services::CreateService
|
|
902
|
+
cache_contexts :products
|
|
903
|
+
auto_invalidate_cache false # Disable automatic invalidation
|
|
904
|
+
|
|
905
|
+
process_with do |data|
|
|
906
|
+
product = user.products.create!(params)
|
|
907
|
+
|
|
908
|
+
# Manual control: only invalidate for featured products
|
|
909
|
+
invalidate_cache_for(user) if product.featured?
|
|
910
|
+
|
|
911
|
+
{ resource: product }
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### Async Invalidation
|
|
917
|
+
|
|
918
|
+
Combine with async option for non-blocking cache invalidation:
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
class Products::CreateService < BetterService::Services::CreateService
|
|
922
|
+
cache_contexts :products, :homepage
|
|
923
|
+
|
|
924
|
+
# Auto-invalidation happens async via ActiveJob
|
|
925
|
+
cache_async true
|
|
926
|
+
end
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
**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.
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## 🌍 Internationalization (I18n)
|
|
934
|
+
|
|
935
|
+
BetterService includes built-in I18n support for service messages with automatic fallback.
|
|
936
|
+
|
|
937
|
+
### Using the message() Helper
|
|
938
|
+
|
|
939
|
+
All service templates use the `message()` helper for response messages:
|
|
940
|
+
|
|
941
|
+
```ruby
|
|
942
|
+
class Products::CreateService < BetterService::Services::CreateService
|
|
943
|
+
respond_with do |data|
|
|
944
|
+
success_result(message("create.success"), data)
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Default Messages
|
|
950
|
+
|
|
951
|
+
BetterService ships with English defaults in `config/locales/better_service.en.yml`:
|
|
952
|
+
|
|
953
|
+
```yaml
|
|
954
|
+
en:
|
|
955
|
+
better_service:
|
|
956
|
+
services:
|
|
957
|
+
default:
|
|
958
|
+
created: "Resource created successfully"
|
|
959
|
+
updated: "Resource updated successfully"
|
|
960
|
+
deleted: "Resource deleted successfully"
|
|
961
|
+
listed: "Resources retrieved successfully"
|
|
962
|
+
shown: "Resource retrieved successfully"
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### Custom Messages
|
|
966
|
+
|
|
967
|
+
Generate custom locale files for your services:
|
|
968
|
+
|
|
969
|
+
```bash
|
|
970
|
+
rails generate better_service:locale products
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
This creates `config/locales/products_services.en.yml`:
|
|
974
|
+
|
|
975
|
+
```yaml
|
|
976
|
+
en:
|
|
977
|
+
products:
|
|
978
|
+
services:
|
|
979
|
+
create:
|
|
980
|
+
success: "Product created and added to inventory"
|
|
981
|
+
update:
|
|
982
|
+
success: "Product updated successfully"
|
|
983
|
+
destroy:
|
|
984
|
+
success: "Product removed from catalog"
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
Then configure the namespace in your service:
|
|
988
|
+
|
|
989
|
+
```ruby
|
|
990
|
+
class Products::CreateService < BetterService::Services::CreateService
|
|
991
|
+
messages_namespace :products
|
|
992
|
+
|
|
993
|
+
respond_with do |data|
|
|
994
|
+
# Uses products.services.create.success
|
|
995
|
+
success_result(message("create.success"), data)
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### Fallback Chain
|
|
1001
|
+
|
|
1002
|
+
Messages follow a 3-level fallback:
|
|
1003
|
+
1. Custom namespace (e.g., `products.services.create.success`)
|
|
1004
|
+
2. BetterService defaults (e.g., `better_service.services.default.created`)
|
|
1005
|
+
3. Key itself (e.g., `"create.success"`)
|
|
1006
|
+
|
|
1007
|
+
### Message Interpolations
|
|
1008
|
+
|
|
1009
|
+
Pass dynamic values to messages:
|
|
1010
|
+
|
|
1011
|
+
```ruby
|
|
1012
|
+
respond_with do |data|
|
|
1013
|
+
success_result(
|
|
1014
|
+
message("create.success", product_name: data[:resource].name),
|
|
1015
|
+
data
|
|
1016
|
+
)
|
|
1017
|
+
end
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Locale file:**
|
|
1021
|
+
```yaml
|
|
1022
|
+
en:
|
|
1023
|
+
products:
|
|
1024
|
+
services:
|
|
1025
|
+
create:
|
|
1026
|
+
success: "Product '%{product_name}' created successfully"
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
## 🎨 Presenter System
|
|
1032
|
+
|
|
1033
|
+
BetterService includes an optional presenter layer for formatting data for API/view consumption.
|
|
1034
|
+
|
|
1035
|
+
### Creating Presenters
|
|
1036
|
+
|
|
1037
|
+
Generate a presenter class:
|
|
1038
|
+
|
|
1039
|
+
```bash
|
|
1040
|
+
rails generate better_service:presenter Product
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
This creates:
|
|
1044
|
+
- `app/presenters/product_presenter.rb`
|
|
1045
|
+
- `test/presenters/product_presenter_test.rb`
|
|
1046
|
+
|
|
1047
|
+
```ruby
|
|
1048
|
+
class ProductPresenter < BetterService::Presenter
|
|
1049
|
+
def as_json(opts = {})
|
|
1050
|
+
{
|
|
1051
|
+
id: object.id,
|
|
1052
|
+
name: object.name,
|
|
1053
|
+
price: object.price,
|
|
1054
|
+
display_name: "#{object.name} - $#{object.price}",
|
|
1055
|
+
|
|
1056
|
+
# Conditional fields based on user permissions
|
|
1057
|
+
**(admin_fields if current_user&.admin?)
|
|
1058
|
+
}
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
private
|
|
1062
|
+
|
|
1063
|
+
def admin_fields
|
|
1064
|
+
{
|
|
1065
|
+
cost: object.cost,
|
|
1066
|
+
margin: object.price - object.cost
|
|
1067
|
+
}
|
|
1068
|
+
end
|
|
1069
|
+
end
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### Using Presenters in Services
|
|
1073
|
+
|
|
1074
|
+
Configure presenters via the `presenter` DSL:
|
|
1075
|
+
|
|
1076
|
+
```ruby
|
|
1077
|
+
class Products::IndexService < BetterService::Services::IndexService
|
|
1078
|
+
presenter ProductPresenter
|
|
1079
|
+
|
|
1080
|
+
presenter_options do
|
|
1081
|
+
{ current_user: user }
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# Items are automatically formatted via ProductPresenter#as_json
|
|
1085
|
+
end
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### Presenter Features
|
|
1089
|
+
|
|
1090
|
+
**Available Methods:**
|
|
1091
|
+
- `object` - The resource being presented
|
|
1092
|
+
- `options` - Options hash passed via `presenter_options`
|
|
1093
|
+
- `current_user` - Shortcut for `options[:current_user]`
|
|
1094
|
+
- `as_json(opts)` - Format object as JSON
|
|
1095
|
+
- `to_json(opts)` - Serialize to JSON string
|
|
1096
|
+
- `to_h` - Alias for `as_json`
|
|
1097
|
+
|
|
1098
|
+
**Example with scaffold:**
|
|
1099
|
+
```bash
|
|
1100
|
+
# Generate services + presenter in one command
|
|
1101
|
+
rails generate serviceable:scaffold Product --presenter
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
---
|
|
1105
|
+
|
|
871
1106
|
## 🏗️ Generators
|
|
872
1107
|
|
|
873
|
-
BetterService includes
|
|
1108
|
+
BetterService includes 10 powerful generators:
|
|
874
1109
|
|
|
875
1110
|
### Scaffold Generator
|
|
876
1111
|
|
|
@@ -878,6 +1113,9 @@ Generates all 5 CRUD services at once:
|
|
|
878
1113
|
|
|
879
1114
|
```bash
|
|
880
1115
|
rails generate serviceable:scaffold Product
|
|
1116
|
+
|
|
1117
|
+
# With presenter
|
|
1118
|
+
rails generate serviceable:scaffold Product --presenter
|
|
881
1119
|
```
|
|
882
1120
|
|
|
883
1121
|
Creates:
|
|
@@ -886,23 +1124,16 @@ Creates:
|
|
|
886
1124
|
- `app/services/product/create_service.rb`
|
|
887
1125
|
- `app/services/product/update_service.rb`
|
|
888
1126
|
- `app/services/product/destroy_service.rb`
|
|
1127
|
+
- (Optional) `app/presenters/product_presenter.rb` with `--presenter`
|
|
889
1128
|
|
|
890
1129
|
### Individual Generators
|
|
891
1130
|
|
|
892
1131
|
```bash
|
|
893
|
-
#
|
|
1132
|
+
# CRUD Services
|
|
894
1133
|
rails generate serviceable:index Product
|
|
895
|
-
|
|
896
|
-
# Show service
|
|
897
1134
|
rails generate serviceable:show Product
|
|
898
|
-
|
|
899
|
-
# Create service
|
|
900
1135
|
rails generate serviceable:create Product
|
|
901
|
-
|
|
902
|
-
# Update service
|
|
903
1136
|
rails generate serviceable:update Product
|
|
904
|
-
|
|
905
|
-
# Destroy service
|
|
906
1137
|
rails generate serviceable:destroy Product
|
|
907
1138
|
|
|
908
1139
|
# Custom action service
|
|
@@ -910,6 +1141,12 @@ rails generate serviceable:action Product publish
|
|
|
910
1141
|
|
|
911
1142
|
# Workflow for composing services
|
|
912
1143
|
rails generate serviceable:workflow OrderPurchase --steps create_order charge_payment
|
|
1144
|
+
|
|
1145
|
+
# Presenter for data transformation
|
|
1146
|
+
rails generate better_service:presenter Product
|
|
1147
|
+
|
|
1148
|
+
# Custom locale file for I18n messages
|
|
1149
|
+
rails generate better_service:locale products
|
|
913
1150
|
```
|
|
914
1151
|
|
|
915
1152
|
---
|
|
@@ -1302,7 +1539,7 @@ ActiveSupport::Notifications.subscribe("service.completed") do |name, start, fin
|
|
|
1302
1539
|
end
|
|
1303
1540
|
```
|
|
1304
1541
|
|
|
1305
|
-
See [Configuration](docs/
|
|
1542
|
+
See [Configuration Guide](docs/start/configuration.md) for more details.
|
|
1306
1543
|
|
|
1307
1544
|
---
|
|
1308
1545
|
|
data/Rakefile
CHANGED
|
@@ -7,7 +7,7 @@ Rake::TestTask.new(:test) do |t|
|
|
|
7
7
|
t.libs << "test"
|
|
8
8
|
t.test_files = FileList["test/**/*_test.rb"].exclude(
|
|
9
9
|
"test/dummy/**/*",
|
|
10
|
-
"test/generators/**/*"
|
|
10
|
+
"test/generators/**/*" # Generator tests require Rails context - run manually with: bundle exec ruby -Itest test/generators/*_test.rb
|
|
11
11
|
)
|
|
12
12
|
t.verbose = false
|
|
13
13
|
end
|
|
@@ -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
|
|
@@ -10,7 +10,6 @@ module BetterService
|
|
|
10
10
|
|
|
11
11
|
included do
|
|
12
12
|
class_attribute :_schema, default: nil
|
|
13
|
-
attr_reader :validation_errors
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
class_methods do
|
|
@@ -23,10 +22,6 @@ module BetterService
|
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
def valid?
|
|
27
|
-
@validation_errors.empty?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
25
|
private
|
|
31
26
|
|
|
32
27
|
def validate_params!
|
|
@@ -20,22 +20,6 @@ module BetterService
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
# Override respond to add viewer phase
|
|
24
|
-
def respond(data)
|
|
25
|
-
# Get base result from parent respond
|
|
26
|
-
if self.class._respond_block
|
|
27
|
-
result = instance_exec(data, &self.class._respond_block)
|
|
28
|
-
else
|
|
29
|
-
result = success_result("Operation completed successfully", data)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Add viewer if enabled
|
|
33
|
-
return result unless viewer_enabled?
|
|
34
|
-
|
|
35
|
-
view_config = execute_viewer(data, data, result)
|
|
36
|
-
result.merge(view: view_config)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
23
|
def viewer_enabled?
|
|
40
24
|
self.class._viewer_enabled && self.class._viewer_block.present?
|
|
41
25
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
# Presenter - Base class for presenting service data
|
|
5
|
+
#
|
|
6
|
+
# Presenters transform raw model data into view-friendly formats.
|
|
7
|
+
# They are typically used with the Presentable concern's presenter DSL.
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
# class ProductPresenter < BetterService::Presenter
|
|
11
|
+
# def as_json(opts = {})
|
|
12
|
+
# {
|
|
13
|
+
# id: object.id,
|
|
14
|
+
# name: object.name,
|
|
15
|
+
# price_formatted: "$#{object.price}",
|
|
16
|
+
# available: object.stock > 0,
|
|
17
|
+
# # Conditional fields based on current user
|
|
18
|
+
# **(admin_fields if current_user&.admin?)
|
|
19
|
+
# }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# private
|
|
23
|
+
#
|
|
24
|
+
# def admin_fields
|
|
25
|
+
# {
|
|
26
|
+
# cost: object.cost,
|
|
27
|
+
# margin: object.price - object.cost
|
|
28
|
+
# }
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# Usage with services:
|
|
33
|
+
# class Products::IndexService < IndexService
|
|
34
|
+
# presenter ProductPresenter
|
|
35
|
+
#
|
|
36
|
+
# presenter_options do
|
|
37
|
+
# { current_user: user }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# search_with do
|
|
41
|
+
# { items: Product.all.to_a }
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
class Presenter
|
|
45
|
+
attr_reader :object, :options
|
|
46
|
+
|
|
47
|
+
# Initialize presenter
|
|
48
|
+
#
|
|
49
|
+
# @param object [Object] The object to present (e.g., ActiveRecord model)
|
|
50
|
+
# @param options [Hash] Additional options (e.g., current_user, permissions)
|
|
51
|
+
def initialize(object, **options)
|
|
52
|
+
@object = object
|
|
53
|
+
@options = options
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Override in subclass to define JSON representation
|
|
57
|
+
#
|
|
58
|
+
# @param opts [Hash] JSON serialization options
|
|
59
|
+
# @return [Hash] Hash representation of the object
|
|
60
|
+
def as_json(opts = {})
|
|
61
|
+
# Default implementation delegates to object
|
|
62
|
+
if object.respond_to?(:as_json)
|
|
63
|
+
object.as_json(opts)
|
|
64
|
+
else
|
|
65
|
+
{ data: object }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# JSON string representation
|
|
70
|
+
#
|
|
71
|
+
# @param opts [Hash] JSON serialization options
|
|
72
|
+
# @return [String] JSON string
|
|
73
|
+
def to_json(opts = {})
|
|
74
|
+
as_json(opts).to_json
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Hash representation (alias for as_json)
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash] Hash representation
|
|
80
|
+
def to_h
|
|
81
|
+
as_json
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Get current user from options
|
|
87
|
+
#
|
|
88
|
+
# @return [Object, nil] Current user if provided in options
|
|
89
|
+
def current_user
|
|
90
|
+
options[:current_user]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if a field should be included based on options
|
|
94
|
+
#
|
|
95
|
+
# Useful for selective field rendering based on client requests.
|
|
96
|
+
#
|
|
97
|
+
# @param field [Symbol, String] Field name to check
|
|
98
|
+
# @return [Boolean] Whether field should be included
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# # In service:
|
|
102
|
+
# presenter_options do
|
|
103
|
+
# { fields: params[:fields]&.split(',')&.map(&:to_sym) }
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# # In presenter:
|
|
107
|
+
# def as_json(opts = {})
|
|
108
|
+
# {
|
|
109
|
+
# id: object.id,
|
|
110
|
+
# name: object.name,
|
|
111
|
+
# **(expensive_data if include_field?(:details))
|
|
112
|
+
# }
|
|
113
|
+
# end
|
|
114
|
+
def include_field?(field)
|
|
115
|
+
return true unless options[:fields]
|
|
116
|
+
|
|
117
|
+
options[:fields].include?(field.to_sym)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if current user has a specific role/permission
|
|
121
|
+
#
|
|
122
|
+
# @param role [Symbol, String] Role to check
|
|
123
|
+
# @return [Boolean] Whether user has the role
|
|
124
|
+
def user_can?(role)
|
|
125
|
+
return false unless current_user
|
|
126
|
+
return false unless current_user.respond_to?(:has_role?)
|
|
127
|
+
|
|
128
|
+
current_user.has_role?(role)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
if defined?(Rails)
|
|
2
2
|
module BetterService
|
|
3
3
|
class Railtie < ::Rails::Railtie
|
|
4
|
+
# Initialize subscribers after Rails boots
|
|
5
|
+
#
|
|
6
|
+
# This hook runs after all initializers have been executed,
|
|
7
|
+
# ensuring BetterService.configuration is fully loaded.
|
|
8
|
+
config.after_initialize do
|
|
9
|
+
# Attach LogSubscriber if enabled in configuration
|
|
10
|
+
if BetterService.configuration.log_subscriber_enabled
|
|
11
|
+
BetterService::Subscribers::LogSubscriber.attach
|
|
12
|
+
Rails.logger.info "[BetterService] LogSubscriber attached" if Rails.logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Attach StatsSubscriber if enabled in configuration
|
|
16
|
+
if BetterService.configuration.stats_subscriber_enabled
|
|
17
|
+
BetterService::Subscribers::StatsSubscriber.attach
|
|
18
|
+
Rails.logger.info "[BetterService] StatsSubscriber attached" if Rails.logger
|
|
19
|
+
end
|
|
20
|
+
end
|
|
4
21
|
end
|
|
5
22
|
end
|
|
6
23
|
end
|