better_service 2.0.0 → 2.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 +2 -0
- data/README.md +98 -45
- data/Rakefile +7 -209
- data/config/locales/better_service.en.yml +15 -0
- data/lib/better_service/cache_service.rb +4 -4
- data/lib/better_service/concerns/instrumentation.rb +59 -14
- data/lib/better_service/concerns/serviceable/authorizable.rb +1 -1
- data/lib/better_service/concerns/serviceable/messageable.rb +70 -1
- data/lib/better_service/concerns/serviceable/repository_aware.rb +8 -3
- data/lib/better_service/concerns/workflowable/callbacks.rb +27 -27
- data/lib/better_service/concerns/workflowable/step.rb +39 -5
- data/lib/better_service/errors/better_service_error.rb +4 -0
- data/lib/better_service/errors/runtime/authorization_error.rb +4 -1
- data/lib/better_service/errors/runtime/database_error.rb +4 -1
- data/lib/better_service/errors/runtime/execution_error.rb +4 -1
- data/lib/better_service/errors/runtime/invalid_result_error.rb +28 -0
- data/lib/better_service/errors/runtime/resource_not_found_error.rb +4 -1
- data/lib/better_service/errors/runtime/validation_error.rb +4 -1
- data/lib/better_service/repository/base_repository.rb +1 -1
- data/lib/better_service/result.rb +110 -0
- data/lib/better_service/services/base.rb +216 -57
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/branch_group.rb +1 -1
- data/lib/better_service.rb +1 -6
- data/lib/generators/serviceable/action_generator.rb +11 -0
- data/lib/generators/serviceable/base_generator.rb +109 -0
- data/lib/generators/serviceable/create_generator.rb +11 -0
- data/lib/generators/serviceable/destroy_generator.rb +11 -0
- data/lib/generators/serviceable/index_generator.rb +11 -0
- data/lib/generators/serviceable/scaffold_generator.rb +29 -7
- data/lib/generators/serviceable/show_generator.rb +11 -0
- data/lib/generators/serviceable/templates/action_service.rb.tt +8 -3
- data/lib/generators/serviceable/templates/base_locale.en.yml.tt +53 -0
- data/lib/generators/serviceable/templates/base_service.rb.tt +78 -0
- data/lib/generators/serviceable/templates/base_service_test.rb.tt +64 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +29 -18
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +16 -29
- data/lib/generators/serviceable/templates/index_service.rb.tt +16 -34
- data/lib/generators/serviceable/templates/repository.rb.tt +76 -0
- data/lib/generators/serviceable/templates/repository_test.rb.tt +124 -0
- data/lib/generators/serviceable/templates/show_service.rb.tt +10 -38
- data/lib/generators/serviceable/templates/update_service.rb.tt +24 -38
- data/lib/generators/serviceable/update_generator.rb +11 -0
- metadata +13 -12
- data/lib/better_service/concerns/serviceable/viewable.rb +0 -33
- data/lib/better_service/services/action_service.rb +0 -60
- data/lib/better_service/services/create_service.rb +0 -63
- data/lib/better_service/services/destroy_service.rb +0 -60
- data/lib/better_service/services/index_service.rb +0 -56
- data/lib/better_service/services/show_service.rb +0 -44
- data/lib/better_service/services/update_service.rb +0 -61
|
@@ -6,7 +6,6 @@ require_relative "../concerns/serviceable/validatable"
|
|
|
6
6
|
require_relative "../concerns/serviceable/authorizable"
|
|
7
7
|
require_relative "../concerns/serviceable/presentable"
|
|
8
8
|
require_relative "../concerns/serviceable/cacheable"
|
|
9
|
-
require_relative "../concerns/serviceable/viewable"
|
|
10
9
|
require_relative "../concerns/serviceable/transactional"
|
|
11
10
|
require_relative "../concerns/instrumentation"
|
|
12
11
|
|
|
@@ -25,7 +24,6 @@ module BetterService
|
|
|
25
24
|
include Concerns::Serviceable::Validatable
|
|
26
25
|
include Concerns::Serviceable::Authorizable
|
|
27
26
|
include Concerns::Serviceable::Presentable
|
|
28
|
-
include Concerns::Serviceable::Viewable
|
|
29
27
|
|
|
30
28
|
# Prepend Transactional so it can wrap the process method
|
|
31
29
|
prepend Concerns::Serviceable::Transactional
|
|
@@ -65,6 +63,24 @@ module BetterService
|
|
|
65
63
|
self._allow_nil_user = value
|
|
66
64
|
end
|
|
67
65
|
|
|
66
|
+
# Configure the action name for metadata tracking
|
|
67
|
+
#
|
|
68
|
+
# @param name [Symbol, String] The action name (e.g., :publish, :approve)
|
|
69
|
+
# @return [void]
|
|
70
|
+
#
|
|
71
|
+
# @example Custom action service
|
|
72
|
+
# class Order::ApproveService < Order::BaseService
|
|
73
|
+
# performed_action :approve
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @example CRUD service with standard action
|
|
77
|
+
# class Product::CreateService < Product::BaseService
|
|
78
|
+
# performed_action :created
|
|
79
|
+
# end
|
|
80
|
+
def self.performed_action(name)
|
|
81
|
+
self._action_name = name.to_sym
|
|
82
|
+
end
|
|
83
|
+
|
|
68
84
|
def self.search_with(&block)
|
|
69
85
|
self._search_block = block
|
|
70
86
|
end
|
|
@@ -107,6 +123,26 @@ module BetterService
|
|
|
107
123
|
end
|
|
108
124
|
|
|
109
125
|
# Main entry point - executes the 5-phase flow
|
|
126
|
+
#
|
|
127
|
+
# @return [BetterService::Result] Result wrapper containing resource and metadata
|
|
128
|
+
# - result.resource: The resource (single AR model, array of models, or nil on error)
|
|
129
|
+
# - result.meta: Hash with success status, action, message, and validation errors if any
|
|
130
|
+
# - Supports destructuring: `resource, meta = service.call`
|
|
131
|
+
#
|
|
132
|
+
# @example Success
|
|
133
|
+
# result = ProductService.new(user, params: params).call
|
|
134
|
+
# result.success? # => true
|
|
135
|
+
# result.resource.persisted? # => true
|
|
136
|
+
#
|
|
137
|
+
# @example Using destructuring
|
|
138
|
+
# product, meta = ProductService.new(user, params: params).call
|
|
139
|
+
# meta[:success] # => true
|
|
140
|
+
#
|
|
141
|
+
# @example Validation failure (returns object with errors)
|
|
142
|
+
# result = ProductService.new(user, params: invalid_params).call
|
|
143
|
+
# result.failure? # => true
|
|
144
|
+
# result.validation_errors # => { name: ["can't be blank"] }
|
|
145
|
+
# result.resource.errors.any? # => true (object still available for form re-render)
|
|
110
146
|
def call
|
|
111
147
|
# Validation already raises ValidationError in initialize
|
|
112
148
|
# Authorization already raises AuthorizationError
|
|
@@ -123,22 +159,26 @@ module BetterService
|
|
|
123
159
|
transformed = transform(processed)
|
|
124
160
|
result = respond(transformed)
|
|
125
161
|
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
162
|
+
# Build Result response
|
|
163
|
+
build_result_response(result)
|
|
164
|
+
rescue Errors::Runtime::ValidationError => e
|
|
165
|
+
# Schema validation errors (from initialize) - no object available
|
|
166
|
+
wrap_response(nil, validation_error_metadata(e))
|
|
167
|
+
rescue Errors::Runtime::AuthorizationError => e
|
|
168
|
+
# Authorization errors - no object available
|
|
169
|
+
wrap_response(nil, authorization_error_metadata(e))
|
|
170
|
+
rescue Errors::Runtime::ResourceNotFoundError => e
|
|
171
|
+
# Resource not found (BetterService error) - no object available
|
|
172
|
+
wrap_response(nil, resource_not_found_error_metadata(e))
|
|
136
173
|
rescue ActiveRecord::RecordNotFound => e
|
|
137
|
-
|
|
174
|
+
# Resource not found (ActiveRecord error) - no object available
|
|
175
|
+
wrap_response(nil, not_found_error_metadata(e))
|
|
138
176
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
139
|
-
|
|
177
|
+
# AR validation errors - return the object with errors for form re-render
|
|
178
|
+
wrap_response(e.record, ar_validation_error_metadata(e))
|
|
140
179
|
rescue StandardError => e
|
|
141
|
-
|
|
180
|
+
# Unexpected errors
|
|
181
|
+
wrap_response(nil, unexpected_error_metadata(e))
|
|
142
182
|
end
|
|
143
183
|
|
|
144
184
|
# Include Cacheable AFTER call method is defined so it can wrap it
|
|
@@ -181,7 +221,7 @@ module BetterService
|
|
|
181
221
|
# Auto-invalidation happens when:
|
|
182
222
|
# 1. auto_invalidate_cache is enabled (true)
|
|
183
223
|
# 2. cache_contexts are defined (something to invalidate)
|
|
184
|
-
# 3. Service is a write operation (
|
|
224
|
+
# 3. Service is a write operation (detected by action name or class name)
|
|
185
225
|
#
|
|
186
226
|
# @return [Boolean] Whether cache should be invalidated
|
|
187
227
|
def should_auto_invalidate_cache?
|
|
@@ -189,62 +229,181 @@ module BetterService
|
|
|
189
229
|
return false unless self.class.respond_to?(:_cache_contexts)
|
|
190
230
|
return false unless self.class._cache_contexts.present?
|
|
191
231
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
232
|
+
# Detect write operations by action name or class name pattern
|
|
233
|
+
write_actions = %i[created updated destroyed]
|
|
234
|
+
action_name = self.class._action_name
|
|
235
|
+
return true if write_actions.include?(action_name)
|
|
236
|
+
|
|
237
|
+
# Also check class name pattern as fallback
|
|
238
|
+
class_name = self.class.name.to_s
|
|
239
|
+
class_name.end_with?("CreateService", "UpdateService", "DestroyService")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# ============================================
|
|
243
|
+
# RESULT RESPONSE BUILDERS
|
|
244
|
+
# ============================================
|
|
245
|
+
|
|
246
|
+
# Build response from service result hash
|
|
247
|
+
#
|
|
248
|
+
# @param result [Hash] The result from respond phase
|
|
249
|
+
# @return [BetterService::Result, Array] Result wrapper or [object, metadata] tuple
|
|
250
|
+
def build_result_response(result)
|
|
251
|
+
object = extract_object(result)
|
|
252
|
+
metadata = build_success_metadata(result)
|
|
253
|
+
|
|
254
|
+
wrap_response(object, metadata)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Wrap object and metadata in configured format
|
|
258
|
+
#
|
|
259
|
+
# @param object [Object] The resource object
|
|
260
|
+
# @param metadata [Hash] The metadata hash
|
|
261
|
+
# @return [BetterService::Result] Result wrapper
|
|
262
|
+
def wrap_response(object, metadata)
|
|
263
|
+
Result.new(object, meta: metadata)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Extract the object from result hash
|
|
267
|
+
# Supports :object, :resource, and :items keys
|
|
268
|
+
#
|
|
269
|
+
# @param result [Hash] Result hash from respond phase
|
|
270
|
+
# @return [Object, Array, nil] The extracted object
|
|
271
|
+
def extract_object(result)
|
|
272
|
+
result[:object] || result[:resource] || result[:items]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Build metadata hash for successful responses
|
|
276
|
+
#
|
|
277
|
+
# @param result [Hash] Result hash from respond phase
|
|
278
|
+
# @return [Hash] Metadata hash
|
|
279
|
+
def build_success_metadata(result)
|
|
280
|
+
# Check if respond phase already signaled failure
|
|
281
|
+
success = result.fetch(:success, true)
|
|
282
|
+
|
|
283
|
+
metadata = {
|
|
284
|
+
success: success,
|
|
285
|
+
action: self.class._action_name,
|
|
286
|
+
message: result[:message]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Merge any additional metadata provided
|
|
290
|
+
if result[:metadata].is_a?(Hash)
|
|
291
|
+
metadata.merge!(result[:metadata])
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Add validation errors if this is a failure response from process_with
|
|
295
|
+
if !success && result[:object].respond_to?(:errors) && result[:object].errors.any?
|
|
296
|
+
metadata[:validation_errors] = result[:object].errors.messages
|
|
297
|
+
metadata[:full_messages] = result[:object].errors.full_messages
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
metadata
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# ============================================
|
|
304
|
+
# ERROR METADATA BUILDERS
|
|
305
|
+
# ============================================
|
|
306
|
+
|
|
307
|
+
# Build metadata for schema validation errors (from Dry::Schema)
|
|
308
|
+
#
|
|
309
|
+
# @param error [Errors::Runtime::ValidationError] The validation error
|
|
310
|
+
# @return [Hash] Error metadata
|
|
311
|
+
def validation_error_metadata(error)
|
|
312
|
+
Rails.logger.error "Validation error in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
313
|
+
|
|
314
|
+
{
|
|
315
|
+
success: false,
|
|
316
|
+
action: self.class._action_name,
|
|
317
|
+
message: error.message,
|
|
318
|
+
error_code: error.code,
|
|
319
|
+
validation_errors: error.context[:validation_errors] || {}
|
|
320
|
+
}
|
|
196
321
|
end
|
|
197
322
|
|
|
198
|
-
#
|
|
323
|
+
# Build metadata for authorization errors
|
|
199
324
|
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
325
|
+
# @param error [Errors::Runtime::AuthorizationError] The authorization error
|
|
326
|
+
# @return [Hash] Error metadata
|
|
327
|
+
def authorization_error_metadata(error)
|
|
328
|
+
Rails.logger.error "Authorization error in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
329
|
+
|
|
330
|
+
{
|
|
331
|
+
success: false,
|
|
332
|
+
action: self.class._action_name,
|
|
333
|
+
message: error.message,
|
|
334
|
+
error_code: error.code
|
|
335
|
+
}
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Build metadata for BetterService resource not found errors
|
|
339
|
+
#
|
|
340
|
+
# @param error [Errors::Runtime::ResourceNotFoundError] The BetterService not found error
|
|
341
|
+
# @return [Hash] Error metadata
|
|
342
|
+
def resource_not_found_error_metadata(error)
|
|
343
|
+
Rails.logger.error "Resource not found in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
202
344
|
|
|
203
|
-
|
|
345
|
+
{
|
|
346
|
+
success: false,
|
|
347
|
+
action: self.class._action_name,
|
|
348
|
+
message: error.message,
|
|
349
|
+
error_code: error.code
|
|
350
|
+
}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Build metadata for ActiveRecord not found errors
|
|
204
354
|
#
|
|
205
|
-
# @param error [ActiveRecord::RecordNotFound] The
|
|
206
|
-
# @
|
|
207
|
-
def
|
|
355
|
+
# @param error [ActiveRecord::RecordNotFound] The not found error
|
|
356
|
+
# @return [Hash] Error metadata
|
|
357
|
+
def not_found_error_metadata(error)
|
|
208
358
|
Rails.logger.error "Resource not found in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
209
359
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
360
|
+
{
|
|
361
|
+
success: false,
|
|
362
|
+
action: self.class._action_name,
|
|
363
|
+
message: "Resource not found: #{error.message}",
|
|
364
|
+
error_code: BetterService::ErrorCodes::RESOURCE_NOT_FOUND
|
|
365
|
+
}
|
|
216
366
|
end
|
|
217
367
|
|
|
218
|
-
#
|
|
368
|
+
# Build metadata for ActiveRecord validation errors
|
|
369
|
+
# Returns the record so it can be used to re-render forms
|
|
219
370
|
#
|
|
220
|
-
# @param error [ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved] The
|
|
221
|
-
# @
|
|
222
|
-
def
|
|
223
|
-
Rails.logger.error "
|
|
224
|
-
Rails.logger.error error.backtrace.join("\n") if defined?(Rails)
|
|
371
|
+
# @param error [ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved] The AR error
|
|
372
|
+
# @return [Hash] Error metadata with validation details
|
|
373
|
+
def ar_validation_error_metadata(error)
|
|
374
|
+
Rails.logger.error "AR validation error in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
225
375
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
376
|
+
metadata = {
|
|
377
|
+
success: false,
|
|
378
|
+
action: self.class._action_name,
|
|
379
|
+
message: "Validation failed: #{error.message}",
|
|
380
|
+
error_code: BetterService::ErrorCodes::DATABASE_ERROR
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Extract validation errors from the record if available
|
|
384
|
+
if error.respond_to?(:record) && error.record
|
|
385
|
+
metadata[:validation_errors] = error.record.errors.messages
|
|
386
|
+
metadata[:full_messages] = error.record.errors.full_messages
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
metadata
|
|
232
390
|
end
|
|
233
391
|
|
|
234
|
-
#
|
|
392
|
+
# Build metadata for unexpected errors
|
|
235
393
|
#
|
|
236
|
-
# @param error [StandardError] The
|
|
237
|
-
# @
|
|
238
|
-
def
|
|
394
|
+
# @param error [StandardError] The unexpected error
|
|
395
|
+
# @return [Hash] Error metadata
|
|
396
|
+
def unexpected_error_metadata(error)
|
|
239
397
|
Rails.logger.error "Unexpected error in #{self.class.name}: #{error.message}" if defined?(Rails)
|
|
240
|
-
Rails.logger.error error.backtrace.join("\n") if defined?(Rails)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
398
|
+
Rails.logger.error error.backtrace.join("\n") if defined?(Rails) && error.backtrace
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
success: false,
|
|
402
|
+
action: self.class._action_name,
|
|
403
|
+
message: "Service execution failed: #{error.message}",
|
|
404
|
+
error_code: BetterService::ErrorCodes::EXECUTION_ERROR,
|
|
405
|
+
error_class: error.class.name
|
|
406
|
+
}
|
|
248
407
|
end
|
|
249
408
|
|
|
250
409
|
def validate_user_presence!(user)
|
|
@@ -96,7 +96,7 @@ module BetterService
|
|
|
96
96
|
|
|
97
97
|
# Track this branch decision
|
|
98
98
|
branch_decision = "#{@name}:#{selected_branch.name}"
|
|
99
|
-
branch_decisions = [branch_decision]
|
|
99
|
+
branch_decisions = [ branch_decision ]
|
|
100
100
|
|
|
101
101
|
# Execute the selected branch
|
|
102
102
|
executed_steps = selected_branch.execute(context, user, params, branch_decisions)
|
data/lib/better_service.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "better_service/version"
|
|
|
2
2
|
require "better_service/railtie"
|
|
3
3
|
require "better_service/configuration"
|
|
4
4
|
require "better_service/errors/better_service_error"
|
|
5
|
+
require "better_service/result"
|
|
5
6
|
require "better_service/cache_service"
|
|
6
7
|
require "better_service/presenter"
|
|
7
8
|
require "better_service/repository/base_repository"
|
|
@@ -10,12 +11,6 @@ require "better_service/concerns/instrumentation"
|
|
|
10
11
|
require "better_service/subscribers/log_subscriber"
|
|
11
12
|
require "better_service/subscribers/stats_subscriber"
|
|
12
13
|
require "better_service/services/base"
|
|
13
|
-
require "better_service/services/index_service"
|
|
14
|
-
require "better_service/services/show_service"
|
|
15
|
-
require "better_service/services/create_service"
|
|
16
|
-
require "better_service/services/update_service"
|
|
17
|
-
require "better_service/services/destroy_service"
|
|
18
|
-
require "better_service/services/action_service"
|
|
19
14
|
require "better_service/concerns/workflowable/context"
|
|
20
15
|
require "better_service/concerns/workflowable/step"
|
|
21
16
|
require "better_service/concerns/workflowable/callbacks"
|
|
@@ -11,6 +11,9 @@ module Serviceable
|
|
|
11
11
|
|
|
12
12
|
desc "Generate an Action service for custom state transitions"
|
|
13
13
|
|
|
14
|
+
class_option :base_class, type: :string, default: nil,
|
|
15
|
+
desc: "Custom base class to inherit from (e.g., Articles::BaseService)"
|
|
16
|
+
|
|
14
17
|
def create_service_file
|
|
15
18
|
template "action_service.rb.tt", File.join("app/services", class_path, "#{file_name}/#{action_name}_service.rb")
|
|
16
19
|
end
|
|
@@ -24,6 +27,14 @@ module Serviceable
|
|
|
24
27
|
def service_class_name
|
|
25
28
|
"#{class_name}::#{action_name.camelize}Service"
|
|
26
29
|
end
|
|
30
|
+
|
|
31
|
+
def parent_class
|
|
32
|
+
options[:base_class] || "BetterService::Services::Base"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def using_base_service?
|
|
36
|
+
options[:base_class].present?
|
|
37
|
+
end
|
|
27
38
|
end
|
|
28
39
|
end
|
|
29
40
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
# BaseGenerator - Generate a base service class for a resource namespace
|
|
8
|
+
#
|
|
9
|
+
# Creates a ResourceBaseService that centralizes:
|
|
10
|
+
# - Repository initialization via RepositoryAware concern
|
|
11
|
+
# - Cache configuration
|
|
12
|
+
# - Messages namespace (I18n)
|
|
13
|
+
# - Presenter configuration
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# rails generate serviceable:base Articles
|
|
17
|
+
# rails generate serviceable:base Admin::Articles
|
|
18
|
+
# rails generate serviceable:base Articles --skip_repository
|
|
19
|
+
# rails generate serviceable:base Articles --skip_locale
|
|
20
|
+
#
|
|
21
|
+
# This generates:
|
|
22
|
+
# app/services/articles/base_service.rb
|
|
23
|
+
# app/repositories/articles_repository.rb
|
|
24
|
+
# config/locales/articles_services.en.yml
|
|
25
|
+
# test/services/articles/base_service_test.rb
|
|
26
|
+
# test/repositories/articles_repository_test.rb
|
|
27
|
+
class BaseGenerator < Rails::Generators::NamedBase
|
|
28
|
+
source_root File.expand_path("templates", __dir__)
|
|
29
|
+
|
|
30
|
+
desc "Generate a base service with repository, cache, and I18n configuration"
|
|
31
|
+
|
|
32
|
+
class_option :skip_repository, type: :boolean, default: false,
|
|
33
|
+
desc: "Skip repository generation"
|
|
34
|
+
class_option :skip_locale, type: :boolean, default: false,
|
|
35
|
+
desc: "Skip locale file generation"
|
|
36
|
+
class_option :skip_presenter, type: :boolean, default: false,
|
|
37
|
+
desc: "Skip presenter configuration in base service"
|
|
38
|
+
class_option :skip_test, type: :boolean, default: false,
|
|
39
|
+
desc: "Skip test file generation"
|
|
40
|
+
|
|
41
|
+
def create_base_service
|
|
42
|
+
template "base_service.rb.tt",
|
|
43
|
+
File.join("app/services", class_path, file_name, "base_service.rb")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_repository
|
|
47
|
+
return if options[:skip_repository]
|
|
48
|
+
|
|
49
|
+
template "repository.rb.tt",
|
|
50
|
+
File.join("app/repositories", class_path, "#{file_name}_repository.rb")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_locale
|
|
54
|
+
return if options[:skip_locale]
|
|
55
|
+
|
|
56
|
+
template "base_locale.en.yml.tt",
|
|
57
|
+
File.join("config/locales", "#{file_name}_services.en.yml")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_base_service_test
|
|
61
|
+
return if options[:skip_test]
|
|
62
|
+
|
|
63
|
+
template "base_service_test.rb.tt",
|
|
64
|
+
File.join("test/services", class_path, file_name, "base_service_test.rb")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_repository_test
|
|
68
|
+
return if options[:skip_test] || options[:skip_repository]
|
|
69
|
+
|
|
70
|
+
template "repository_test.rb.tt",
|
|
71
|
+
File.join("test/repositories", class_path, "#{file_name}_repository_test.rb")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def show_completion_message
|
|
75
|
+
say "\n" + "=" * 80
|
|
76
|
+
say "Base service generation completed! 🎉", :green
|
|
77
|
+
say "=" * 80
|
|
78
|
+
say "\nGenerated files:"
|
|
79
|
+
say " - #{class_name}::BaseService (app/services/#{file_path}/base_service.rb)"
|
|
80
|
+
say " - #{class_name}Repository (app/repositories/#{file_path}_repository.rb)" unless options[:skip_repository]
|
|
81
|
+
say " - I18n locale (config/locales/#{file_name}_services.en.yml)" unless options[:skip_locale]
|
|
82
|
+
say "\nNext steps:"
|
|
83
|
+
say " 1. Customize the base service with resource-specific methods"
|
|
84
|
+
say " 2. Add custom repository methods for your queries"
|
|
85
|
+
say " 3. Generate CRUD services that inherit from BaseService:"
|
|
86
|
+
say " rails generate serviceable:scaffold #{name} --base"
|
|
87
|
+
say "\nServices will inherit like: #{class_name}::IndexService < #{class_name}::BaseService\n\n"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def file_path
|
|
93
|
+
File.join(class_path, file_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def repository_class_name
|
|
97
|
+
"#{class_name}Repository"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def presenter_class_name
|
|
101
|
+
"#{class_name}Presenter"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def base_service_class_name
|
|
105
|
+
"#{class_name}::BaseService"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -9,6 +9,9 @@ module Serviceable
|
|
|
9
9
|
|
|
10
10
|
desc "Generate a Create service for creating new resources"
|
|
11
11
|
|
|
12
|
+
class_option :base_class, type: :string, default: nil,
|
|
13
|
+
desc: "Custom base class to inherit from (e.g., Articles::BaseService)"
|
|
14
|
+
|
|
12
15
|
def create_service_file
|
|
13
16
|
template "create_service.rb.tt", File.join("app/services", class_path, "#{file_name}/create_service.rb")
|
|
14
17
|
end
|
|
@@ -22,6 +25,14 @@ module Serviceable
|
|
|
22
25
|
def service_class_name
|
|
23
26
|
"#{class_name}::CreateService"
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
def parent_class
|
|
30
|
+
options[:base_class] || "BetterService::Services::Base"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def using_base_service?
|
|
34
|
+
options[:base_class].present?
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
end
|
|
@@ -9,6 +9,9 @@ module Serviceable
|
|
|
9
9
|
|
|
10
10
|
desc "Generate a Destroy service for deleting resources"
|
|
11
11
|
|
|
12
|
+
class_option :base_class, type: :string, default: nil,
|
|
13
|
+
desc: "Custom base class to inherit from (e.g., Articles::BaseService)"
|
|
14
|
+
|
|
12
15
|
def create_service_file
|
|
13
16
|
template "destroy_service.rb.tt", File.join("app/services", class_path, "#{file_name}/destroy_service.rb")
|
|
14
17
|
end
|
|
@@ -22,6 +25,14 @@ module Serviceable
|
|
|
22
25
|
def service_class_name
|
|
23
26
|
"#{class_name}::DestroyService"
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
def parent_class
|
|
30
|
+
options[:base_class] || "BetterService::Services::Base"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def using_base_service?
|
|
34
|
+
options[:base_class].present?
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
end
|
|
@@ -9,6 +9,9 @@ module Serviceable
|
|
|
9
9
|
|
|
10
10
|
desc "Generate an Index service for listing resources"
|
|
11
11
|
|
|
12
|
+
class_option :base_class, type: :string, default: nil,
|
|
13
|
+
desc: "Custom base class to inherit from (e.g., Articles::BaseService)"
|
|
14
|
+
|
|
12
15
|
def create_service_file
|
|
13
16
|
template "index_service.rb.tt", File.join("app/services", class_path, "#{file_name}/index_service.rb")
|
|
14
17
|
end
|
|
@@ -22,6 +25,14 @@ module Serviceable
|
|
|
22
25
|
def service_class_name
|
|
23
26
|
"#{class_name}::IndexService"
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
def parent_class
|
|
30
|
+
options[:base_class] || "BetterService::Services::Base"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def using_base_service?
|
|
34
|
+
options[:base_class].present?
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
end
|
|
@@ -13,42 +13,52 @@ module Serviceable
|
|
|
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
15
|
class_option :presenter, type: :boolean, default: false, desc: "Generate presenter class"
|
|
16
|
+
class_option :skip_repository, type: :boolean, default: false, desc: "Skip repository generation"
|
|
17
|
+
class_option :skip_locale, type: :boolean, default: false, desc: "Skip locale file generation"
|
|
16
18
|
|
|
17
|
-
desc "Generate all CRUD services (Index, Show, Create, Update, Destroy)"
|
|
19
|
+
desc "Generate all CRUD services (Index, Show, Create, Update, Destroy) with BaseService"
|
|
20
|
+
|
|
21
|
+
def generate_base_service
|
|
22
|
+
say "Generating BaseService, Repository and Locale...", :green
|
|
23
|
+
args = [ name ]
|
|
24
|
+
args << "--skip_repository" if options[:skip_repository]
|
|
25
|
+
args << "--skip_locale" if options[:skip_locale]
|
|
26
|
+
generate "serviceable:base", *args
|
|
27
|
+
end
|
|
18
28
|
|
|
19
29
|
def generate_index_service
|
|
20
30
|
return if options[:skip_index]
|
|
21
31
|
|
|
22
32
|
say "Generating Index service...", :green
|
|
23
|
-
generate "serviceable:index",
|
|
33
|
+
generate "serviceable:index", *service_generator_args
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
def generate_show_service
|
|
27
37
|
return if options[:skip_show]
|
|
28
38
|
|
|
29
39
|
say "Generating Show service...", :green
|
|
30
|
-
generate "serviceable:show",
|
|
40
|
+
generate "serviceable:show", *service_generator_args
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
def generate_create_service
|
|
34
44
|
return if options[:skip_create]
|
|
35
45
|
|
|
36
46
|
say "Generating Create service...", :green
|
|
37
|
-
generate "serviceable:create",
|
|
47
|
+
generate "serviceable:create", *service_generator_args
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
def generate_update_service
|
|
41
51
|
return if options[:skip_update]
|
|
42
52
|
|
|
43
53
|
say "Generating Update service...", :green
|
|
44
|
-
generate "serviceable:update",
|
|
54
|
+
generate "serviceable:update", *service_generator_args
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
def generate_destroy_service
|
|
48
58
|
return if options[:skip_destroy]
|
|
49
59
|
|
|
50
60
|
say "Generating Destroy service...", :green
|
|
51
|
-
generate "serviceable:destroy",
|
|
61
|
+
generate "serviceable:destroy", *service_generator_args
|
|
52
62
|
end
|
|
53
63
|
|
|
54
64
|
def generate_presenter
|
|
@@ -62,7 +72,11 @@ module Serviceable
|
|
|
62
72
|
say "\n" + "=" * 80
|
|
63
73
|
say "Scaffold generation completed! 🎉", :green
|
|
64
74
|
say "=" * 80
|
|
65
|
-
say "\nGenerated
|
|
75
|
+
say "\nGenerated base infrastructure:"
|
|
76
|
+
say " - #{class_name}::BaseService (app/services/#{file_name}/base_service.rb)"
|
|
77
|
+
say " - #{class_name}Repository (app/repositories/#{file_name}_repository.rb)" unless options[:skip_repository]
|
|
78
|
+
say " - I18n locale (config/locales/#{file_name}_services.en.yml)" unless options[:skip_locale]
|
|
79
|
+
say "\nGenerated services (inheriting from #{class_name}::BaseService):"
|
|
66
80
|
say " - #{class_name}::IndexService" unless options[:skip_index]
|
|
67
81
|
say " - #{class_name}::ShowService" unless options[:skip_show]
|
|
68
82
|
say " - #{class_name}::CreateService" unless options[:skip_create]
|
|
@@ -74,6 +88,14 @@ module Serviceable
|
|
|
74
88
|
say " 2. Update schemas with your specific validations"
|
|
75
89
|
say " 3. Run the tests: rails test test/services/#{file_name}\n\n"
|
|
76
90
|
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Build arguments array for CRUD service generators
|
|
95
|
+
# Always includes --base_class since BaseService is always generated
|
|
96
|
+
def service_generator_args
|
|
97
|
+
[ name, "--base_class=#{class_name}::BaseService" ]
|
|
98
|
+
end
|
|
77
99
|
end
|
|
78
100
|
end
|
|
79
101
|
end
|