better_service 1.0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1321 -0
- data/Rakefile +15 -0
- data/lib/better_service/cache_service.rb +310 -0
- data/lib/better_service/concerns/instrumentation.rb +242 -0
- data/lib/better_service/concerns/serviceable/authorizable.rb +106 -0
- data/lib/better_service/concerns/serviceable/cacheable.rb +97 -0
- data/lib/better_service/concerns/serviceable/messageable.rb +30 -0
- data/lib/better_service/concerns/serviceable/presentable.rb +66 -0
- data/lib/better_service/concerns/serviceable/transactional.rb +51 -0
- data/lib/better_service/concerns/serviceable/validatable.rb +58 -0
- data/lib/better_service/concerns/serviceable/viewable.rb +49 -0
- data/lib/better_service/concerns/serviceable.rb +12 -0
- data/lib/better_service/concerns/workflowable/callbacks.rb +116 -0
- data/lib/better_service/concerns/workflowable/context.rb +108 -0
- data/lib/better_service/concerns/workflowable/step.rb +141 -0
- data/lib/better_service/concerns/workflowable.rb +12 -0
- data/lib/better_service/configuration.rb +113 -0
- data/lib/better_service/errors/better_service_error.rb +271 -0
- data/lib/better_service/errors/configuration/configuration_error.rb +21 -0
- data/lib/better_service/errors/configuration/invalid_configuration_error.rb +28 -0
- data/lib/better_service/errors/configuration/invalid_schema_error.rb +28 -0
- data/lib/better_service/errors/configuration/nil_user_error.rb +37 -0
- data/lib/better_service/errors/configuration/schema_required_error.rb +29 -0
- data/lib/better_service/errors/runtime/authorization_error.rb +38 -0
- data/lib/better_service/errors/runtime/database_error.rb +38 -0
- data/lib/better_service/errors/runtime/execution_error.rb +27 -0
- data/lib/better_service/errors/runtime/resource_not_found_error.rb +38 -0
- data/lib/better_service/errors/runtime/runtime_error.rb +22 -0
- data/lib/better_service/errors/runtime/transaction_error.rb +34 -0
- data/lib/better_service/errors/runtime/validation_error.rb +42 -0
- data/lib/better_service/errors/workflowable/configuration/duplicate_step_error.rb +27 -0
- data/lib/better_service/errors/workflowable/configuration/invalid_step_error.rb +12 -0
- data/lib/better_service/errors/workflowable/configuration/step_not_found_error.rb +29 -0
- data/lib/better_service/errors/workflowable/configuration/workflow_configuration_error.rb +24 -0
- data/lib/better_service/errors/workflowable/runtime/rollback_error.rb +46 -0
- data/lib/better_service/errors/workflowable/runtime/step_execution_error.rb +47 -0
- data/lib/better_service/errors/workflowable/runtime/workflow_execution_error.rb +40 -0
- data/lib/better_service/errors/workflowable/runtime/workflow_runtime_error.rb +25 -0
- data/lib/better_service/railtie.rb +6 -0
- data/lib/better_service/services/action_service.rb +60 -0
- data/lib/better_service/services/base.rb +249 -0
- data/lib/better_service/services/create_service.rb +60 -0
- data/lib/better_service/services/destroy_service.rb +57 -0
- data/lib/better_service/services/index_service.rb +56 -0
- data/lib/better_service/services/show_service.rb +44 -0
- data/lib/better_service/services/update_service.rb +58 -0
- data/lib/better_service/subscribers/log_subscriber.rb +131 -0
- data/lib/better_service/subscribers/stats_subscriber.rb +208 -0
- data/lib/better_service/version.rb +3 -0
- data/lib/better_service/workflows/base.rb +106 -0
- data/lib/better_service/workflows/dsl.rb +59 -0
- data/lib/better_service/workflows/execution.rb +89 -0
- data/lib/better_service/workflows/result_builder.rb +67 -0
- data/lib/better_service/workflows/rollback_support.rb +44 -0
- data/lib/better_service/workflows/transaction_support.rb +32 -0
- data/lib/better_service.rb +28 -0
- data/lib/generators/serviceable/action_generator.rb +29 -0
- data/lib/generators/serviceable/create_generator.rb +27 -0
- data/lib/generators/serviceable/destroy_generator.rb +27 -0
- data/lib/generators/serviceable/index_generator.rb +27 -0
- data/lib/generators/serviceable/scaffold_generator.rb +70 -0
- data/lib/generators/serviceable/show_generator.rb +27 -0
- data/lib/generators/serviceable/templates/action_service.rb.tt +42 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +33 -0
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +40 -0
- data/lib/generators/serviceable/templates/index_service.rb.tt +54 -0
- data/lib/generators/serviceable/templates/service_test.rb.tt +23 -0
- data/lib/generators/serviceable/templates/show_service.rb.tt +37 -0
- data/lib/generators/serviceable/templates/update_service.rb.tt +50 -0
- data/lib/generators/serviceable/update_generator.rb +27 -0
- data/lib/generators/workflowable/WORKFLOW_README +27 -0
- data/lib/generators/workflowable/templates/workflow.rb.tt +72 -0
- data/lib/generators/workflowable/templates/workflow_test.rb.tt +62 -0
- data/lib/generators/workflowable/workflow_generator.rb +60 -0
- data/lib/tasks/better_service_tasks.rake +4 -0
- metadata +180 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-schema"
|
|
4
|
+
require_relative "../concerns/serviceable/messageable"
|
|
5
|
+
require_relative "../concerns/serviceable/validatable"
|
|
6
|
+
require_relative "../concerns/serviceable/authorizable"
|
|
7
|
+
require_relative "../concerns/serviceable/presentable"
|
|
8
|
+
require_relative "../concerns/serviceable/cacheable"
|
|
9
|
+
require_relative "../concerns/serviceable/viewable"
|
|
10
|
+
require_relative "../concerns/serviceable/transactional"
|
|
11
|
+
require_relative "../concerns/instrumentation"
|
|
12
|
+
|
|
13
|
+
module BetterService
|
|
14
|
+
module Services
|
|
15
|
+
class Base
|
|
16
|
+
class_attribute :_allow_nil_user, default: false
|
|
17
|
+
class_attribute :_action_name, default: nil
|
|
18
|
+
class_attribute :_search_block, default: nil
|
|
19
|
+
class_attribute :_process_block, default: nil
|
|
20
|
+
class_attribute :_transform_block, default: nil
|
|
21
|
+
class_attribute :_respond_block, default: nil
|
|
22
|
+
|
|
23
|
+
include Concerns::Serviceable::Messageable
|
|
24
|
+
include Concerns::Serviceable::Validatable
|
|
25
|
+
include Concerns::Serviceable::Authorizable
|
|
26
|
+
include Concerns::Serviceable::Presentable
|
|
27
|
+
include Concerns::Serviceable::Viewable
|
|
28
|
+
|
|
29
|
+
# Prepend Transactional so it can wrap the process method
|
|
30
|
+
prepend Concerns::Serviceable::Transactional
|
|
31
|
+
|
|
32
|
+
# Default empty schema - subclasses should override
|
|
33
|
+
schema do
|
|
34
|
+
# Override in subclass with specific validations
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :user, :params
|
|
38
|
+
|
|
39
|
+
def initialize(user, params: {})
|
|
40
|
+
validate_user_presence!(user) unless self.class._allow_nil_user
|
|
41
|
+
validate_schema_presence!
|
|
42
|
+
@user = user
|
|
43
|
+
@params = safe_params_to_hash(params)
|
|
44
|
+
@validation_errors = {}
|
|
45
|
+
validate_params!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# DSL methods to define phase blocks
|
|
49
|
+
def self.search_with(&block)
|
|
50
|
+
self._search_block = block
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.process_with(&block)
|
|
54
|
+
self._process_block = block
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.transform_with(&block)
|
|
58
|
+
self._transform_block = block
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.respond_with(&block)
|
|
62
|
+
self._respond_block = block
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Main entry point - executes the 5-phase flow
|
|
66
|
+
def call
|
|
67
|
+
# Validation already raises ValidationError in initialize
|
|
68
|
+
# Authorization already raises AuthorizationError
|
|
69
|
+
authorize!
|
|
70
|
+
|
|
71
|
+
data = search
|
|
72
|
+
processed = process(data)
|
|
73
|
+
transformed = transform(processed)
|
|
74
|
+
result = respond(transformed)
|
|
75
|
+
|
|
76
|
+
# Phase 5: Viewer (if enabled)
|
|
77
|
+
if respond_to?(:viewer_enabled?, true) && viewer_enabled?
|
|
78
|
+
view_config = execute_viewer(processed, transformed, result)
|
|
79
|
+
result = result.merge(view: view_config)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
result
|
|
83
|
+
rescue Errors::Runtime::ValidationError, Errors::Runtime::AuthorizationError
|
|
84
|
+
# Let validation and authorization errors propagate without wrapping
|
|
85
|
+
raise
|
|
86
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
87
|
+
handle_not_found_error(e)
|
|
88
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
89
|
+
handle_database_error(e)
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
handle_unexpected_error(e)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Include Cacheable AFTER call method is defined so it can wrap it
|
|
95
|
+
# This must be before Instrumentation prepend so cache works correctly
|
|
96
|
+
include Concerns::Serviceable::Cacheable
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Phase 1: Search - Load raw data (override in subclass)
|
|
101
|
+
def search
|
|
102
|
+
if self.class._search_block
|
|
103
|
+
instance_exec(&self.class._search_block)
|
|
104
|
+
else
|
|
105
|
+
{}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Phase 2: Process - Transform and aggregate data (override in subclass)
|
|
110
|
+
def process(data)
|
|
111
|
+
if self.class._process_block
|
|
112
|
+
instance_exec(data, &self.class._process_block)
|
|
113
|
+
else
|
|
114
|
+
data
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Phase 3: Transform - Handled by Presentable concern
|
|
119
|
+
|
|
120
|
+
# Phase 4: Respond - Handled by Viewable concern
|
|
121
|
+
|
|
122
|
+
# Error handlers for runtime errors
|
|
123
|
+
#
|
|
124
|
+
# These methods handle unexpected errors during service execution by raising
|
|
125
|
+
# appropriate runtime exceptions. They log the error and wrap it with service context.
|
|
126
|
+
|
|
127
|
+
# Handle resource not found errors (ActiveRecord::RecordNotFound)
|
|
128
|
+
#
|
|
129
|
+
# @param error [ActiveRecord::RecordNotFound] The original not found error
|
|
130
|
+
# @raise [Errors::Runtime::ResourceNotFoundError] Wrapped error with context
|
|
131
|
+
def handle_not_found_error(error)
|
|
132
|
+
Rails.logger.error "Resource not found in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
133
|
+
|
|
134
|
+
raise Errors::Runtime::ResourceNotFoundError.new(
|
|
135
|
+
"Resource not found: #{error.message}",
|
|
136
|
+
code: BetterService::ErrorCodes::RESOURCE_NOT_FOUND,
|
|
137
|
+
original_error: error,
|
|
138
|
+
context: { service: self.class.name, params: @params }
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Handle database errors (ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved)
|
|
143
|
+
#
|
|
144
|
+
# @param error [ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved] The original database error
|
|
145
|
+
# @raise [Errors::Runtime::DatabaseError] Wrapped error with context
|
|
146
|
+
def handle_database_error(error)
|
|
147
|
+
Rails.logger.error "Database error in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
148
|
+
Rails.logger.error error.backtrace.join("\n") if defined?(Rails)
|
|
149
|
+
|
|
150
|
+
raise Errors::Runtime::DatabaseError.new(
|
|
151
|
+
"Database error: #{error.message}",
|
|
152
|
+
code: BetterService::ErrorCodes::DATABASE_ERROR,
|
|
153
|
+
original_error: error,
|
|
154
|
+
context: { service: self.class.name, params: @params }
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Handle unexpected errors (all other StandardError)
|
|
159
|
+
#
|
|
160
|
+
# @param error [StandardError] The original unexpected error
|
|
161
|
+
# @raise [Errors::Runtime::ExecutionError] Wrapped error with context
|
|
162
|
+
def handle_unexpected_error(error)
|
|
163
|
+
Rails.logger.error "Unexpected error in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
164
|
+
Rails.logger.error error.backtrace.join("\n") if defined?(Rails)
|
|
165
|
+
|
|
166
|
+
raise Errors::Runtime::ExecutionError.new(
|
|
167
|
+
"Service execution failed: #{error.message}",
|
|
168
|
+
code: BetterService::ErrorCodes::EXECUTION_ERROR,
|
|
169
|
+
original_error: error,
|
|
170
|
+
context: { service: self.class.name, params: @params }
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_user_presence!(user)
|
|
175
|
+
return if user.present?
|
|
176
|
+
|
|
177
|
+
raise Errors::Configuration::NilUserError,
|
|
178
|
+
"User cannot be nil for #{self.class.name}. " \
|
|
179
|
+
"Add 'allow_nil_user true' in config block if this is intentional."
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_schema_presence!
|
|
183
|
+
return if self.class.schema_defined?
|
|
184
|
+
|
|
185
|
+
raise Errors::Configuration::SchemaRequiredError,
|
|
186
|
+
"#{self.class.name} must define a schema block. " \
|
|
187
|
+
"Add 'schema do ... end' to validate params."
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def safe_params_to_hash(params)
|
|
191
|
+
return {} if params.nil?
|
|
192
|
+
|
|
193
|
+
# Handle ActionController::Parameters by converting to unsafe hash first
|
|
194
|
+
if params.respond_to?(:to_unsafe_h)
|
|
195
|
+
params.to_unsafe_h.deep_symbolize_keys
|
|
196
|
+
elsif params.respond_to?(:to_h)
|
|
197
|
+
params.to_h.deep_symbolize_keys
|
|
198
|
+
elsif params.is_a?(Hash)
|
|
199
|
+
params.deep_symbolize_keys
|
|
200
|
+
else
|
|
201
|
+
{}
|
|
202
|
+
end
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
Rails.logger.warn "Failed to convert params to hash: #{e.message}" if defined?(Rails)
|
|
205
|
+
{}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def success_result(message, data = {})
|
|
209
|
+
# Extract metadata if provided in data, or initialize empty
|
|
210
|
+
provided_metadata = data.delete(:metadata) || {}
|
|
211
|
+
|
|
212
|
+
# Build metadata with action if action_name is set
|
|
213
|
+
metadata = {}
|
|
214
|
+
metadata[:action] = self.class._action_name if self.class._action_name.present?
|
|
215
|
+
metadata.merge!(provided_metadata)
|
|
216
|
+
|
|
217
|
+
{
|
|
218
|
+
success: true,
|
|
219
|
+
message: message,
|
|
220
|
+
metadata: metadata,
|
|
221
|
+
**data
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
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
|
+
# Prepend Instrumentation at the end, after call method is defined
|
|
245
|
+
# This wraps the entire call method (including cache logic from Cacheable)
|
|
246
|
+
prepend Concerns::Instrumentation
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Services
|
|
7
|
+
# CreateService - Specialized service for creating new resources
|
|
8
|
+
#
|
|
9
|
+
# Returns: { resource: {}, metadata: { action: :created } }
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# class Bookings::CreateService < BetterService::Services::CreateService
|
|
13
|
+
# schema do
|
|
14
|
+
# required(:title).filled(:string)
|
|
15
|
+
# required(:date).filled(:date)
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# search_with do
|
|
19
|
+
# {}
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# process_with do |data|
|
|
23
|
+
# booking = user.bookings.create!(
|
|
24
|
+
# title: params[:title],
|
|
25
|
+
# date: params[:date]
|
|
26
|
+
# )
|
|
27
|
+
# { resource: booking }
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
class CreateService < Services::Base
|
|
31
|
+
# Set action_name for metadata
|
|
32
|
+
self._action_name = :created
|
|
33
|
+
|
|
34
|
+
# Enable database transactions by default for create operations
|
|
35
|
+
with_transaction true
|
|
36
|
+
|
|
37
|
+
# Default empty schema - subclasses MUST override with specific validations
|
|
38
|
+
schema do
|
|
39
|
+
# Override in subclass with required fields
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Override respond to ensure resource key is present
|
|
45
|
+
def respond(data)
|
|
46
|
+
# Get base result (from custom respond_with block or default)
|
|
47
|
+
if self.class._respond_block
|
|
48
|
+
result = instance_exec(data, &self.class._respond_block)
|
|
49
|
+
else
|
|
50
|
+
result = success_result("Resource created successfully", data)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Ensure resource key exists (default to nil if not provided)
|
|
54
|
+
result[:resource] ||= nil
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Services
|
|
7
|
+
# DestroyService - Specialized service for deleting resources
|
|
8
|
+
#
|
|
9
|
+
# Returns: { resource: {}, metadata: { action: :deleted } }
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# class Bookings::DestroyService < BetterService::Services::DestroyService
|
|
13
|
+
# schema do
|
|
14
|
+
# required(:id).filled(:integer)
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# search_with do
|
|
18
|
+
# { resource: user.bookings.find(params[:id]) }
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# process_with do |data|
|
|
22
|
+
# booking = data[:resource]
|
|
23
|
+
# booking.destroy!
|
|
24
|
+
# { resource: booking }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class DestroyService < Services::Base
|
|
28
|
+
# Set action_name for metadata
|
|
29
|
+
self._action_name = :deleted
|
|
30
|
+
|
|
31
|
+
# Enable database transactions by default for destroy operations
|
|
32
|
+
with_transaction true
|
|
33
|
+
|
|
34
|
+
# Default schema - requires id parameter
|
|
35
|
+
schema do
|
|
36
|
+
required(:id).filled
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Override respond to ensure resource key is present
|
|
42
|
+
def respond(data)
|
|
43
|
+
# Get base result (from custom respond_with block or default)
|
|
44
|
+
if self.class._respond_block
|
|
45
|
+
result = instance_exec(data, &self.class._respond_block)
|
|
46
|
+
else
|
|
47
|
+
result = success_result("Resource deleted successfully", data)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Ensure resource key exists (default to nil if not provided)
|
|
51
|
+
result[:resource] ||= nil
|
|
52
|
+
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Services
|
|
7
|
+
# IndexService - Specialized service for list/collection endpoints
|
|
8
|
+
#
|
|
9
|
+
# Returns: { items: [], metadata: { action: :index, stats: {}, pagination: {} } }
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# class Bookings::IndexService < BetterService::Services::IndexService
|
|
13
|
+
# search_with do
|
|
14
|
+
# { items: user.bookings.to_a }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# process_with do |data|
|
|
18
|
+
# {
|
|
19
|
+
# items: data[:items],
|
|
20
|
+
# metadata: {
|
|
21
|
+
# stats: { total: data[:items].count },
|
|
22
|
+
# pagination: { page: params[:page] || 1 }
|
|
23
|
+
# }
|
|
24
|
+
# }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class IndexService < Services::Base
|
|
28
|
+
# Set action_name for metadata
|
|
29
|
+
self._action_name = :index
|
|
30
|
+
|
|
31
|
+
# Default schema - can be overridden in subclasses
|
|
32
|
+
schema do
|
|
33
|
+
optional(:page).filled(:integer, gteq?: 1)
|
|
34
|
+
optional(:per_page).filled(:integer, gteq?: 1, lteq?: 100)
|
|
35
|
+
optional(:search).maybe(:string)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Override respond to ensure items key is present
|
|
41
|
+
def respond(data)
|
|
42
|
+
# Get base result (from custom respond_with block or default)
|
|
43
|
+
if self.class._respond_block
|
|
44
|
+
result = instance_exec(data, &self.class._respond_block)
|
|
45
|
+
else
|
|
46
|
+
result = success_result("Operation completed successfully", data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Ensure items key exists (default to empty array if not provided)
|
|
50
|
+
result[:items] ||= []
|
|
51
|
+
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Services
|
|
7
|
+
# ShowService - Specialized service for single resource detail endpoints
|
|
8
|
+
#
|
|
9
|
+
# Returns: { resource: {}, metadata: { action: :show } }
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# class Bookings::ShowService < BetterService::Services::ShowService
|
|
13
|
+
# search_with do
|
|
14
|
+
# { resource: user.bookings.find(params[:id]) }
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
class ShowService < Services::Base
|
|
18
|
+
# Set action_name for metadata
|
|
19
|
+
self._action_name = :show
|
|
20
|
+
|
|
21
|
+
# Default schema - requires id parameter
|
|
22
|
+
schema do
|
|
23
|
+
required(:id).filled
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Override respond to ensure resource key is present
|
|
29
|
+
def respond(data)
|
|
30
|
+
# Get base result (from custom respond_with block or default)
|
|
31
|
+
if self.class._respond_block
|
|
32
|
+
result = instance_exec(data, &self.class._respond_block)
|
|
33
|
+
else
|
|
34
|
+
result = success_result("Operation completed successfully", data)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Ensure resource key exists (default to nil if not provided)
|
|
38
|
+
result[:resource] ||= nil
|
|
39
|
+
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module BetterService
|
|
6
|
+
module Services
|
|
7
|
+
# UpdateService - Specialized service for updating existing resources
|
|
8
|
+
#
|
|
9
|
+
# Returns: { resource: {}, metadata: { action: :updated } }
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# class Bookings::UpdateService < BetterService::Services::UpdateService
|
|
13
|
+
# schema do
|
|
14
|
+
# required(:id).filled(:integer)
|
|
15
|
+
# optional(:title).filled(:string)
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# search_with do
|
|
19
|
+
# { resource: user.bookings.find(params[:id]) }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# process_with do |data|
|
|
23
|
+
# booking = data[:resource]
|
|
24
|
+
# booking.update!(params.except(:id))
|
|
25
|
+
# { resource: booking }
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
class UpdateService < Services::Base
|
|
29
|
+
# Set action_name for metadata
|
|
30
|
+
self._action_name = :updated
|
|
31
|
+
|
|
32
|
+
# Enable database transactions by default for update operations
|
|
33
|
+
with_transaction true
|
|
34
|
+
|
|
35
|
+
# Default schema - requires id parameter
|
|
36
|
+
schema do
|
|
37
|
+
required(:id).filled
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Override respond to ensure resource key is present
|
|
43
|
+
def respond(data)
|
|
44
|
+
# Get base result (from custom respond_with block or default)
|
|
45
|
+
if self.class._respond_block
|
|
46
|
+
result = instance_exec(data, &self.class._respond_block)
|
|
47
|
+
else
|
|
48
|
+
result = success_result("Resource updated successfully", data)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Ensure resource key exists (default to nil if not provided)
|
|
52
|
+
result[:resource] ||= nil
|
|
53
|
+
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Subscribers
|
|
5
|
+
# LogSubscriber - Built-in subscriber that logs service events
|
|
6
|
+
#
|
|
7
|
+
# This subscriber logs all service events to Rails.logger.
|
|
8
|
+
# Enable it in configuration to get automatic logging for all services.
|
|
9
|
+
#
|
|
10
|
+
# @example Enable in initializer
|
|
11
|
+
# BetterService.configure do |config|
|
|
12
|
+
# config.log_subscriber_enabled = true
|
|
13
|
+
# config.log_subscriber_level = :info
|
|
14
|
+
# end
|
|
15
|
+
class LogSubscriber
|
|
16
|
+
class << self
|
|
17
|
+
# Attach the subscriber to ActiveSupport::Notifications
|
|
18
|
+
#
|
|
19
|
+
# This method is called automatically when subscriber is enabled.
|
|
20
|
+
# It subscribes to all service.* events.
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
def attach
|
|
24
|
+
subscribe_to_service_events
|
|
25
|
+
subscribe_to_cache_events
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Subscribe to service lifecycle events
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def subscribe_to_service_events
|
|
34
|
+
ActiveSupport::Notifications.subscribe("service.started") do |name, start, finish, id, payload|
|
|
35
|
+
log_service_started(payload)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
|
|
39
|
+
log_service_completed(payload)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
|
|
43
|
+
log_service_failed(payload)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Subscribe to cache events
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def subscribe_to_cache_events
|
|
51
|
+
ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
|
|
52
|
+
log_cache_hit(payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
|
|
56
|
+
log_cache_miss(payload)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Log service started event
|
|
61
|
+
#
|
|
62
|
+
# @param payload [Hash] Event payload
|
|
63
|
+
# @return [void]
|
|
64
|
+
def log_service_started(payload)
|
|
65
|
+
message = "[BetterService] #{payload[:service_name]} started"
|
|
66
|
+
message += " (user: #{payload[:user_id]})" if payload[:user_id]
|
|
67
|
+
|
|
68
|
+
log(message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Log service completed event
|
|
72
|
+
#
|
|
73
|
+
# @param payload [Hash] Event payload
|
|
74
|
+
# @return [void]
|
|
75
|
+
def log_service_completed(payload)
|
|
76
|
+
message = "[BetterService] #{payload[:service_name]} completed in #{payload[:duration]}ms"
|
|
77
|
+
message += " (user: #{payload[:user_id]})" if payload[:user_id]
|
|
78
|
+
message += " [CACHED]" if payload[:cache_hit]
|
|
79
|
+
|
|
80
|
+
log(message)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Log service failed event
|
|
84
|
+
#
|
|
85
|
+
# @param payload [Hash] Event payload
|
|
86
|
+
# @return [void]
|
|
87
|
+
def log_service_failed(payload)
|
|
88
|
+
message = "[BetterService] #{payload[:service_name]} failed after #{payload[:duration]}ms"
|
|
89
|
+
message += " (user: #{payload[:user_id]})" if payload[:user_id]
|
|
90
|
+
message += " - #{payload[:error_class]}: #{payload[:error_message]}"
|
|
91
|
+
|
|
92
|
+
log(message, :error)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Log cache hit event
|
|
96
|
+
#
|
|
97
|
+
# @param payload [Hash] Event payload
|
|
98
|
+
# @return [void]
|
|
99
|
+
def log_cache_hit(payload)
|
|
100
|
+
message = "[BetterService::Cache] HIT - #{payload[:service_name]}"
|
|
101
|
+
message += " (context: #{payload[:context]})" if payload[:context]
|
|
102
|
+
|
|
103
|
+
log(message, :debug)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Log cache miss event
|
|
107
|
+
#
|
|
108
|
+
# @param payload [Hash] Event payload
|
|
109
|
+
# @return [void]
|
|
110
|
+
def log_cache_miss(payload)
|
|
111
|
+
message = "[BetterService::Cache] MISS - #{payload[:service_name]}"
|
|
112
|
+
message += " (context: #{payload[:context]})" if payload[:context]
|
|
113
|
+
|
|
114
|
+
log(message, :debug)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Write to Rails logger
|
|
118
|
+
#
|
|
119
|
+
# @param message [String] Log message
|
|
120
|
+
# @param level [Symbol] Log level (:debug, :info, :warn, :error)
|
|
121
|
+
# @return [void]
|
|
122
|
+
def log(message, level = nil)
|
|
123
|
+
return unless defined?(Rails) && Rails.logger
|
|
124
|
+
|
|
125
|
+
level ||= BetterService.configuration.log_subscriber_level
|
|
126
|
+
Rails.logger.send(level, message)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|