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
|
@@ -19,6 +19,7 @@ module BetterService
|
|
|
19
19
|
class_attribute :_process_block, default: nil
|
|
20
20
|
class_attribute :_transform_block, default: nil
|
|
21
21
|
class_attribute :_respond_block, default: nil
|
|
22
|
+
class_attribute :_auto_invalidate_cache, default: false
|
|
22
23
|
|
|
23
24
|
include Concerns::Serviceable::Messageable
|
|
24
25
|
include Concerns::Serviceable::Validatable
|
|
@@ -41,11 +42,29 @@ module BetterService
|
|
|
41
42
|
validate_schema_presence!
|
|
42
43
|
@user = user
|
|
43
44
|
@params = safe_params_to_hash(params)
|
|
44
|
-
@validation_errors = {}
|
|
45
45
|
validate_params!
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# DSL methods to define phase blocks
|
|
49
|
+
|
|
50
|
+
# Configure whether this service allows nil user
|
|
51
|
+
#
|
|
52
|
+
# @param value [Boolean] Whether to allow nil user (default: true)
|
|
53
|
+
# @return [void]
|
|
54
|
+
#
|
|
55
|
+
# @example Allow nil user
|
|
56
|
+
# class PublicService < BetterService::Services::Base
|
|
57
|
+
# allow_nil_user true
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Require user (default)
|
|
61
|
+
# class PrivateService < BetterService::Services::Base
|
|
62
|
+
# allow_nil_user false
|
|
63
|
+
# end
|
|
64
|
+
def self.allow_nil_user(value = true)
|
|
65
|
+
self._allow_nil_user = value
|
|
66
|
+
end
|
|
67
|
+
|
|
49
68
|
def self.search_with(&block)
|
|
50
69
|
self._search_block = block
|
|
51
70
|
end
|
|
@@ -62,6 +81,31 @@ module BetterService
|
|
|
62
81
|
self._respond_block = block
|
|
63
82
|
end
|
|
64
83
|
|
|
84
|
+
# Configure automatic cache invalidation after write operations
|
|
85
|
+
#
|
|
86
|
+
# @param enabled [Boolean] Whether to automatically invalidate cache (default: true)
|
|
87
|
+
# @return [void]
|
|
88
|
+
#
|
|
89
|
+
# @example Enable automatic cache invalidation (default for Create/Update/Destroy)
|
|
90
|
+
# class Products::CreateService < CreateService
|
|
91
|
+
# cache_contexts :products, :category_products
|
|
92
|
+
# # Cache is automatically invalidated after successful create
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# @example Disable automatic cache invalidation
|
|
96
|
+
# class Products::CreateService < CreateService
|
|
97
|
+
# auto_invalidate_cache false
|
|
98
|
+
#
|
|
99
|
+
# process_with do |data|
|
|
100
|
+
# product = Product.create!(params)
|
|
101
|
+
# invalidate_cache_for(user) if should_invalidate? # Manual control
|
|
102
|
+
# { resource: product }
|
|
103
|
+
# end
|
|
104
|
+
# end
|
|
105
|
+
def self.auto_invalidate_cache(enabled = true)
|
|
106
|
+
self._auto_invalidate_cache = enabled
|
|
107
|
+
end
|
|
108
|
+
|
|
65
109
|
# Main entry point - executes the 5-phase flow
|
|
66
110
|
def call
|
|
67
111
|
# Validation already raises ValidationError in initialize
|
|
@@ -70,6 +114,12 @@ module BetterService
|
|
|
70
114
|
|
|
71
115
|
data = search
|
|
72
116
|
processed = process(data)
|
|
117
|
+
|
|
118
|
+
# Auto-invalidate cache after write operations if configured
|
|
119
|
+
if should_auto_invalidate_cache?
|
|
120
|
+
invalidate_cache_for(user)
|
|
121
|
+
end
|
|
122
|
+
|
|
73
123
|
transformed = transform(processed)
|
|
74
124
|
result = respond(transformed)
|
|
75
125
|
|
|
@@ -117,7 +167,33 @@ module BetterService
|
|
|
117
167
|
|
|
118
168
|
# Phase 3: Transform - Handled by Presentable concern
|
|
119
169
|
|
|
120
|
-
# Phase 4: Respond -
|
|
170
|
+
# Phase 4: Respond - Format response (override in service types or with respond_with block)
|
|
171
|
+
def respond(data)
|
|
172
|
+
if self.class._respond_block
|
|
173
|
+
instance_exec(data, &self.class._respond_block)
|
|
174
|
+
else
|
|
175
|
+
success_result("Operation completed successfully", data)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check if cache should be automatically invalidated
|
|
180
|
+
#
|
|
181
|
+
# Auto-invalidation happens when:
|
|
182
|
+
# 1. auto_invalidate_cache is enabled (true)
|
|
183
|
+
# 2. cache_contexts are defined (something to invalidate)
|
|
184
|
+
# 3. Service is a write operation (Create/Update/Destroy)
|
|
185
|
+
#
|
|
186
|
+
# @return [Boolean] Whether cache should be invalidated
|
|
187
|
+
def should_auto_invalidate_cache?
|
|
188
|
+
return false unless self.class._auto_invalidate_cache
|
|
189
|
+
return false unless self.class.respond_to?(:_cache_contexts)
|
|
190
|
+
return false unless self.class._cache_contexts.present?
|
|
191
|
+
|
|
192
|
+
# Only auto-invalidate for write operations
|
|
193
|
+
is_a?(Services::CreateService) ||
|
|
194
|
+
is_a?(Services::UpdateService) ||
|
|
195
|
+
is_a?(Services::DestroyService)
|
|
196
|
+
end
|
|
121
197
|
|
|
122
198
|
# Error handlers for runtime errors
|
|
123
199
|
#
|
|
@@ -222,25 +298,6 @@ module BetterService
|
|
|
222
298
|
}
|
|
223
299
|
end
|
|
224
300
|
|
|
225
|
-
def failure_result(message, errors = {}, code: nil)
|
|
226
|
-
result = {
|
|
227
|
-
success: false,
|
|
228
|
-
error: message,
|
|
229
|
-
errors: errors
|
|
230
|
-
}
|
|
231
|
-
result[:code] = code if code
|
|
232
|
-
result
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def error_result(message, code: nil)
|
|
236
|
-
result = {
|
|
237
|
-
success: false,
|
|
238
|
-
errors: [message]
|
|
239
|
-
}
|
|
240
|
-
result[:code] = code if code
|
|
241
|
-
result
|
|
242
|
-
end
|
|
243
|
-
|
|
244
301
|
# Prepend Instrumentation at the end, after call method is defined
|
|
245
302
|
# This wraps the entire call method (including cache logic from Cacheable)
|
|
246
303
|
prepend Concerns::Instrumentation
|
|
@@ -34,6 +34,9 @@ module BetterService
|
|
|
34
34
|
# Enable database transactions by default for create operations
|
|
35
35
|
with_transaction true
|
|
36
36
|
|
|
37
|
+
# Enable automatic cache invalidation for create operations
|
|
38
|
+
self._auto_invalidate_cache = true
|
|
39
|
+
|
|
37
40
|
# Default empty schema - subclasses MUST override with specific validations
|
|
38
41
|
schema do
|
|
39
42
|
# Override in subclass with required fields
|
|
@@ -31,6 +31,9 @@ module BetterService
|
|
|
31
31
|
# Enable database transactions by default for destroy operations
|
|
32
32
|
with_transaction true
|
|
33
33
|
|
|
34
|
+
# Enable automatic cache invalidation for destroy operations
|
|
35
|
+
self._auto_invalidate_cache = true
|
|
36
|
+
|
|
34
37
|
# Default schema - requires id parameter
|
|
35
38
|
schema do
|
|
36
39
|
required(:id).filled
|
|
@@ -32,6 +32,9 @@ module BetterService
|
|
|
32
32
|
# Enable database transactions by default for update operations
|
|
33
33
|
with_transaction true
|
|
34
34
|
|
|
35
|
+
# Enable automatic cache invalidation for update operations
|
|
36
|
+
self._auto_invalidate_cache = true
|
|
37
|
+
|
|
35
38
|
# Default schema - requires id parameter
|
|
36
39
|
schema do
|
|
37
40
|
required(:id).filled
|
|
@@ -14,6 +14,11 @@ module BetterService
|
|
|
14
14
|
# end
|
|
15
15
|
class LogSubscriber
|
|
16
16
|
class << self
|
|
17
|
+
# Storage for ActiveSupport::Notifications subscriptions
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<ActiveSupport::Notifications::Fanout::Subscriber>]
|
|
20
|
+
attr_reader :subscriptions
|
|
21
|
+
|
|
17
22
|
# Attach the subscriber to ActiveSupport::Notifications
|
|
18
23
|
#
|
|
19
24
|
# This method is called automatically when subscriber is enabled.
|
|
@@ -21,25 +26,40 @@ module BetterService
|
|
|
21
26
|
#
|
|
22
27
|
# @return [void]
|
|
23
28
|
def attach
|
|
29
|
+
@subscriptions ||= []
|
|
24
30
|
subscribe_to_service_events
|
|
25
31
|
subscribe_to_cache_events
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
# Detach the subscriber from ActiveSupport::Notifications
|
|
35
|
+
#
|
|
36
|
+
# Removes all subscriptions. Useful for testing.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
def detach
|
|
40
|
+
if @subscriptions
|
|
41
|
+
@subscriptions.each do |subscription|
|
|
42
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
@subscriptions = []
|
|
46
|
+
end
|
|
47
|
+
|
|
28
48
|
private
|
|
29
49
|
|
|
30
50
|
# Subscribe to service lifecycle events
|
|
31
51
|
#
|
|
32
52
|
# @return [void]
|
|
33
53
|
def subscribe_to_service_events
|
|
34
|
-
ActiveSupport::Notifications.subscribe("service.started") do |name, start, finish, id, payload|
|
|
54
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("service.started") do |name, start, finish, id, payload|
|
|
35
55
|
log_service_started(payload)
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
|
|
58
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
|
|
39
59
|
log_service_completed(payload)
|
|
40
60
|
end
|
|
41
61
|
|
|
42
|
-
ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
|
|
62
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
|
|
43
63
|
log_service_failed(payload)
|
|
44
64
|
end
|
|
45
65
|
end
|
|
@@ -48,11 +68,11 @@ module BetterService
|
|
|
48
68
|
#
|
|
49
69
|
# @return [void]
|
|
50
70
|
def subscribe_to_cache_events
|
|
51
|
-
ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
|
|
71
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
|
|
52
72
|
log_cache_hit(payload)
|
|
53
73
|
end
|
|
54
74
|
|
|
55
|
-
ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
|
|
75
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
|
|
56
76
|
log_cache_miss(payload)
|
|
57
77
|
end
|
|
58
78
|
end
|
data/lib/better_service.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "better_service/railtie"
|
|
|
3
3
|
require "better_service/configuration"
|
|
4
4
|
require "better_service/errors/better_service_error"
|
|
5
5
|
require "better_service/cache_service"
|
|
6
|
+
require "better_service/presenter"
|
|
6
7
|
require "better_service/concerns/instrumentation"
|
|
7
8
|
require "better_service/subscribers/log_subscriber"
|
|
8
9
|
require "better_service/subscribers/stats_subscriber"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate BetterService initializer with configuration options"
|
|
11
|
+
|
|
12
|
+
def create_initializer_file
|
|
13
|
+
template "better_service_initializer.rb.tt", "config/initializers/better_service.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def copy_locale_file
|
|
17
|
+
# Copy from gem's config/locales to Rails app's config/locales
|
|
18
|
+
locale_source = File.expand_path("../../../config/locales/better_service.en.yml", __dir__)
|
|
19
|
+
copy_file locale_source, "config/locales/better_service.en.yml"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def display_readme
|
|
23
|
+
say
|
|
24
|
+
say "BetterService initializer created!", :green
|
|
25
|
+
say
|
|
26
|
+
say "Next steps:", :yellow
|
|
27
|
+
say " 1. Review config/initializers/better_service.rb"
|
|
28
|
+
say " 2. Enable instrumentation if needed"
|
|
29
|
+
say " 3. Configure logging and stats subscribers"
|
|
30
|
+
say
|
|
31
|
+
say "Documentation:", :cyan
|
|
32
|
+
say " Getting Started: docs/start/getting-started.md"
|
|
33
|
+
say " Configuration: docs/start/configuration.md"
|
|
34
|
+
say
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Generators
|
|
7
|
+
# LocaleGenerator - Generate I18n locale files for BetterService
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# rails generate better_service:locale products
|
|
11
|
+
# rails generate better_service:locale bookings
|
|
12
|
+
#
|
|
13
|
+
# This generates config/locales/products_services.en.yml with scaffolded
|
|
14
|
+
# translations for common service actions (create, update, destroy, etc.)
|
|
15
|
+
class LocaleGenerator < Rails::Generators::NamedBase
|
|
16
|
+
source_root File.expand_path("templates", __dir__)
|
|
17
|
+
|
|
18
|
+
desc "Generate I18n locale file for BetterService messages"
|
|
19
|
+
|
|
20
|
+
# Optional: specify which actions to include
|
|
21
|
+
class_option :actions,
|
|
22
|
+
type: :array,
|
|
23
|
+
default: %w[create update destroy index show],
|
|
24
|
+
desc: "Actions to include in locale file"
|
|
25
|
+
|
|
26
|
+
def create_locale_file
|
|
27
|
+
template "locale.en.yml.tt", "config/locales/#{file_name.pluralize}_services.en.yml"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_info
|
|
31
|
+
say
|
|
32
|
+
say "Locale file created: config/locales/#{file_name.pluralize}_services.en.yml", :green
|
|
33
|
+
say
|
|
34
|
+
say "Usage in services:", :yellow
|
|
35
|
+
say " class #{class_name.pluralize}::CreateService < CreateService"
|
|
36
|
+
say " messages_namespace :#{file_name.pluralize}"
|
|
37
|
+
say " end"
|
|
38
|
+
say
|
|
39
|
+
say "Then customize the messages in the locale file to your needs.", :cyan
|
|
40
|
+
say
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def plural_name
|
|
46
|
+
file_name.pluralize
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def actions_list
|
|
50
|
+
options[:actions]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Generators
|
|
7
|
+
# PresenterGenerator - Generate presenter classes for BetterService
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# rails generate better_service:presenter Product
|
|
11
|
+
# rails generate better_service:presenter Product name:string price:decimal
|
|
12
|
+
#
|
|
13
|
+
# This generates:
|
|
14
|
+
# - app/presenters/product_presenter.rb
|
|
15
|
+
# - test/presenters/product_presenter_test.rb
|
|
16
|
+
class PresenterGenerator < Rails::Generators::NamedBase
|
|
17
|
+
source_root File.expand_path("templates", __dir__)
|
|
18
|
+
|
|
19
|
+
desc "Generate a presenter class for BetterService"
|
|
20
|
+
|
|
21
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
22
|
+
|
|
23
|
+
def create_presenter_file
|
|
24
|
+
template "presenter.rb.tt", File.join("app/presenters", class_path, "#{file_name}_presenter.rb")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_test_file
|
|
28
|
+
template "presenter_test.rb.tt", File.join("test/presenters", class_path, "#{file_name}_presenter_test.rb")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def display_info
|
|
32
|
+
say
|
|
33
|
+
say "Presenter created: app/presenters/#{file_name}_presenter.rb", :green
|
|
34
|
+
say "Test created: test/presenters/#{file_name}_presenter_test.rb", :green
|
|
35
|
+
say
|
|
36
|
+
say "Usage in services:", :yellow
|
|
37
|
+
say " class #{class_name.pluralize}::IndexService < IndexService"
|
|
38
|
+
say " presenter #{class_name}Presenter"
|
|
39
|
+
say
|
|
40
|
+
say " presenter_options do"
|
|
41
|
+
say " { current_user: user }"
|
|
42
|
+
say " end"
|
|
43
|
+
say " end"
|
|
44
|
+
say
|
|
45
|
+
say "Customize the as_json method in the presenter to format your data.", :cyan
|
|
46
|
+
say
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def presenter_class_name
|
|
52
|
+
"#{class_name}Presenter"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def attributes_list
|
|
56
|
+
attributes.map(&:name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# BetterService Configuration
|
|
4
|
+
#
|
|
5
|
+
# This file contains all available configuration options for BetterService.
|
|
6
|
+
# Uncomment and modify the options you want to change from their defaults.
|
|
7
|
+
#
|
|
8
|
+
# For detailed documentation, see: docs/start/configuration.md
|
|
9
|
+
|
|
10
|
+
BetterService.configure do |config|
|
|
11
|
+
# === Instrumentation Settings ===
|
|
12
|
+
#
|
|
13
|
+
# Enable/disable ActiveSupport::Notifications instrumentation globally.
|
|
14
|
+
# When disabled, no events will be published for any service.
|
|
15
|
+
#
|
|
16
|
+
# Default: true
|
|
17
|
+
# config.instrumentation_enabled = true
|
|
18
|
+
|
|
19
|
+
# Include service arguments in event payloads.
|
|
20
|
+
# Disable if arguments contain sensitive data (passwords, tokens, etc.)
|
|
21
|
+
#
|
|
22
|
+
# Default: true
|
|
23
|
+
# config.instrumentation_include_args = true
|
|
24
|
+
|
|
25
|
+
# Include service result in event payloads.
|
|
26
|
+
# Disable to reduce payload size or protect sensitive return values.
|
|
27
|
+
#
|
|
28
|
+
# Default: false
|
|
29
|
+
# config.instrumentation_include_result = false
|
|
30
|
+
|
|
31
|
+
# List of service class names to exclude from instrumentation.
|
|
32
|
+
# Useful for high-frequency services that would generate too many events.
|
|
33
|
+
#
|
|
34
|
+
# Default: []
|
|
35
|
+
# config.instrumentation_excluded_services = ["HealthCheckService", "MetricsService"]
|
|
36
|
+
|
|
37
|
+
# === Built-in Subscribers ===
|
|
38
|
+
#
|
|
39
|
+
# Enable the built-in log subscriber.
|
|
40
|
+
# When enabled, all service events are logged to Rails.logger.
|
|
41
|
+
#
|
|
42
|
+
# Default: false
|
|
43
|
+
# config.log_subscriber_enabled = true
|
|
44
|
+
|
|
45
|
+
# Log level for the built-in log subscriber.
|
|
46
|
+
# Valid values: :debug, :info, :warn, :error
|
|
47
|
+
#
|
|
48
|
+
# Default: :info
|
|
49
|
+
# config.log_subscriber_level = :info
|
|
50
|
+
|
|
51
|
+
# Enable the built-in stats subscriber.
|
|
52
|
+
# When enabled, statistics are collected for all service executions.
|
|
53
|
+
# Access stats with: BetterService::Subscribers::StatsSubscriber.stats
|
|
54
|
+
#
|
|
55
|
+
# Default: false
|
|
56
|
+
# config.stats_subscriber_enabled = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# === Custom Subscribers ===
|
|
60
|
+
#
|
|
61
|
+
# You can subscribe to BetterService events for custom integrations:
|
|
62
|
+
#
|
|
63
|
+
# # DataDog integration
|
|
64
|
+
# ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
|
|
65
|
+
# duration_ms = payload[:duration]
|
|
66
|
+
# service_name = payload[:service]
|
|
67
|
+
#
|
|
68
|
+
# StatsD.increment("better_service.calls", tags: ["service:#{service_name}"])
|
|
69
|
+
# StatsD.histogram("better_service.duration", duration_ms, tags: ["service:#{service_name}"])
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# # New Relic integration
|
|
73
|
+
# ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
|
|
74
|
+
# NewRelic::Agent.notice_error(
|
|
75
|
+
# payload[:error],
|
|
76
|
+
# custom_params: {
|
|
77
|
+
# service: payload[:service],
|
|
78
|
+
# duration: payload[:duration]
|
|
79
|
+
# }
|
|
80
|
+
# )
|
|
81
|
+
# end
|
|
82
|
+
#
|
|
83
|
+
# Available events:
|
|
84
|
+
# - service.started - Service execution started
|
|
85
|
+
# - service.completed - Service completed successfully
|
|
86
|
+
# - service.failed - Service raised an exception
|
|
87
|
+
# - cache.hit - Cache hit for cacheable service
|
|
88
|
+
# - cache.miss - Cache miss for cacheable service
|
|
89
|
+
#
|
|
90
|
+
# See docs/advanced/instrumentation.md for detailed event payloads and examples.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# I18n translations for <%= class_name %> services
|
|
4
|
+
#
|
|
5
|
+
# These messages are used when services define:
|
|
6
|
+
# messages_namespace :<%= plural_name %>
|
|
7
|
+
#
|
|
8
|
+
# Fallback chain:
|
|
9
|
+
# 1. <%= plural_name %>.services.{action}.{key}
|
|
10
|
+
# 2. better_service.services.default.{action}
|
|
11
|
+
# 3. Key itself
|
|
12
|
+
|
|
13
|
+
en:
|
|
14
|
+
<%= plural_name %>:
|
|
15
|
+
services:
|
|
16
|
+
<% actions_list.each do |action| -%>
|
|
17
|
+
<%= action %>:
|
|
18
|
+
success: "<%= class_name %> <%= action %>d successfully"
|
|
19
|
+
failure: "Failed to <%= action %> <%= human_name.downcase %>"
|
|
20
|
+
<% end -%>
|
|
21
|
+
|
|
22
|
+
# Custom messages
|
|
23
|
+
# Add your own message keys here
|
|
24
|
+
# Example:
|
|
25
|
+
# published:
|
|
26
|
+
# success: "<%= class_name %> published and customers notified"
|
|
27
|
+
# failure: "Failed to publish <%= human_name.downcase %>"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
# <%= presenter_class_name %> - Formats <%= class_name %> data for API/view consumption
|
|
5
|
+
#
|
|
6
|
+
# Usage in services:
|
|
7
|
+
# class <%= class_name.pluralize %>::IndexService < IndexService
|
|
8
|
+
# presenter <%= presenter_class_name %>
|
|
9
|
+
#
|
|
10
|
+
# presenter_options do
|
|
11
|
+
# { current_user: user }
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
class <%= presenter_class_name %> < BetterService::Presenter
|
|
15
|
+
# Define JSON representation
|
|
16
|
+
#
|
|
17
|
+
# @param opts [Hash] JSON serialization options
|
|
18
|
+
# @return [Hash] Formatted hash representation
|
|
19
|
+
def as_json(opts = {})
|
|
20
|
+
{
|
|
21
|
+
id: object.id,
|
|
22
|
+
<% attributes_list.each do |attr| -%>
|
|
23
|
+
<%= attr %>: object.<%= attr %>,
|
|
24
|
+
<% end -%>
|
|
25
|
+
# Add computed fields
|
|
26
|
+
# display_name: "#{object.first_name} #{object.last_name}",
|
|
27
|
+
|
|
28
|
+
# Add timestamps if needed
|
|
29
|
+
# created_at: object.created_at,
|
|
30
|
+
# updated_at: object.updated_at,
|
|
31
|
+
|
|
32
|
+
# Add conditional fields based on user permissions
|
|
33
|
+
# **(admin_fields if current_user&.admin?)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Example: Include admin-only fields
|
|
40
|
+
# def admin_fields
|
|
41
|
+
# {
|
|
42
|
+
# internal_notes: object.internal_notes,
|
|
43
|
+
# cost: object.cost
|
|
44
|
+
# }
|
|
45
|
+
# end
|
|
46
|
+
|
|
47
|
+
# Example: Check if user has specific permission
|
|
48
|
+
# def user_can_edit?
|
|
49
|
+
# return false unless current_user
|
|
50
|
+
# current_user.id == object.user_id || current_user.admin?
|
|
51
|
+
# end
|
|
52
|
+
end
|
|
53
|
+
<% end -%>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= presenter_class_name %>Test < ActiveSupport::TestCase
|
|
7
|
+
setup do
|
|
8
|
+
@<%= file_name %> = <%= class_name %>.new(
|
|
9
|
+
id: 1<%= attributes_list.map { |attr| ",\n #{attr}: \"test_#{attr}\"" }.join %>
|
|
10
|
+
)
|
|
11
|
+
@presenter = <%= presenter_class_name %>.new(@<%= file_name %>)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test "presents <%= file_name %> as json" do
|
|
15
|
+
json = @presenter.as_json
|
|
16
|
+
|
|
17
|
+
assert_equal @<%= file_name %>.id, json[:id]
|
|
18
|
+
<% attributes_list.each do |attr| -%>
|
|
19
|
+
assert_equal @<%= file_name %>.<%= attr %>, json[:<%= attr %>]
|
|
20
|
+
<% end -%>
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "accepts options" do
|
|
24
|
+
presenter = <%= presenter_class_name %>.new(@<%= file_name %>, custom: "value")
|
|
25
|
+
|
|
26
|
+
assert_equal "value", presenter.options[:custom]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
test "provides access to current_user from options" do
|
|
30
|
+
user = OpenStruct.new(id: 123, admin?: true)
|
|
31
|
+
presenter = <%= presenter_class_name %>.new(@<%= file_name %>, current_user: user)
|
|
32
|
+
|
|
33
|
+
assert_equal user, presenter.send(:current_user)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "responds to to_h" do
|
|
37
|
+
assert_respond_to @presenter, :to_h
|
|
38
|
+
assert_kind_of Hash, @presenter.to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "responds to to_json" do
|
|
42
|
+
assert_respond_to @presenter, :to_json
|
|
43
|
+
assert_kind_of String, @presenter.to_json
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
<% end -%>
|
|
@@ -12,6 +12,7 @@ module Serviceable
|
|
|
12
12
|
class_option :skip_create, type: :boolean, default: false, desc: "Skip Create service generation"
|
|
13
13
|
class_option :skip_update, type: :boolean, default: false, desc: "Skip Update service generation"
|
|
14
14
|
class_option :skip_destroy, type: :boolean, default: false, desc: "Skip Destroy service generation"
|
|
15
|
+
class_option :presenter, type: :boolean, default: false, desc: "Generate presenter class"
|
|
15
16
|
|
|
16
17
|
desc "Generate all CRUD services (Index, Show, Create, Update, Destroy)"
|
|
17
18
|
|
|
@@ -50,6 +51,13 @@ module Serviceable
|
|
|
50
51
|
generate "serviceable:destroy", name
|
|
51
52
|
end
|
|
52
53
|
|
|
54
|
+
def generate_presenter
|
|
55
|
+
return unless options[:presenter]
|
|
56
|
+
|
|
57
|
+
say "Generating Presenter...", :green
|
|
58
|
+
generate "better_service:presenter", name
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
def show_completion_message
|
|
54
62
|
say "\n" + "=" * 80
|
|
55
63
|
say "Scaffold generation completed! 🎉", :green
|
|
@@ -60,6 +68,7 @@ module Serviceable
|
|
|
60
68
|
say " - #{class_name}::CreateService" unless options[:skip_create]
|
|
61
69
|
say " - #{class_name}::UpdateService" unless options[:skip_update]
|
|
62
70
|
say " - #{class_name}::DestroyService" unless options[:skip_destroy]
|
|
71
|
+
say " - #{class_name}Presenter (app/presenters)" if options[:presenter]
|
|
63
72
|
say "\nNext steps:"
|
|
64
73
|
say " 1. Review and customize the generated services"
|
|
65
74
|
say " 2. Update schemas with your specific validations"
|
|
@@ -26,8 +26,18 @@ class <%= class_name %>::CreateService < BetterService::Services::CreateService
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Phase 4: Respond - Format response (optional override)
|
|
29
|
+
# Uses I18n message helper with fallback to default messages
|
|
30
|
+
#
|
|
31
|
+
# To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
|
|
32
|
+
# en:
|
|
33
|
+
# <%= plural_table_name %>:
|
|
34
|
+
# services:
|
|
35
|
+
# create:
|
|
36
|
+
# success: "<%= human_name %> created successfully!"
|
|
37
|
+
#
|
|
38
|
+
# Then configure namespace: messages_namespace :<%= plural_table_name %>
|
|
29
39
|
respond_with do |data|
|
|
30
|
-
success_result("
|
|
40
|
+
success_result(message("create.success"), data)
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
43
|
<% end -%>
|