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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -0
  3. data/README.md +98 -45
  4. data/Rakefile +7 -209
  5. data/config/locales/better_service.en.yml +15 -0
  6. data/lib/better_service/cache_service.rb +4 -4
  7. data/lib/better_service/concerns/instrumentation.rb +59 -14
  8. data/lib/better_service/concerns/serviceable/authorizable.rb +1 -1
  9. data/lib/better_service/concerns/serviceable/messageable.rb +70 -1
  10. data/lib/better_service/concerns/serviceable/repository_aware.rb +8 -3
  11. data/lib/better_service/concerns/workflowable/callbacks.rb +27 -27
  12. data/lib/better_service/concerns/workflowable/step.rb +39 -5
  13. data/lib/better_service/errors/better_service_error.rb +4 -0
  14. data/lib/better_service/errors/runtime/authorization_error.rb +4 -1
  15. data/lib/better_service/errors/runtime/database_error.rb +4 -1
  16. data/lib/better_service/errors/runtime/execution_error.rb +4 -1
  17. data/lib/better_service/errors/runtime/invalid_result_error.rb +28 -0
  18. data/lib/better_service/errors/runtime/resource_not_found_error.rb +4 -1
  19. data/lib/better_service/errors/runtime/validation_error.rb +4 -1
  20. data/lib/better_service/repository/base_repository.rb +1 -1
  21. data/lib/better_service/result.rb +110 -0
  22. data/lib/better_service/services/base.rb +216 -57
  23. data/lib/better_service/version.rb +1 -1
  24. data/lib/better_service/workflows/branch_group.rb +1 -1
  25. data/lib/better_service.rb +1 -6
  26. data/lib/generators/serviceable/action_generator.rb +11 -0
  27. data/lib/generators/serviceable/base_generator.rb +109 -0
  28. data/lib/generators/serviceable/create_generator.rb +11 -0
  29. data/lib/generators/serviceable/destroy_generator.rb +11 -0
  30. data/lib/generators/serviceable/index_generator.rb +11 -0
  31. data/lib/generators/serviceable/scaffold_generator.rb +29 -7
  32. data/lib/generators/serviceable/show_generator.rb +11 -0
  33. data/lib/generators/serviceable/templates/action_service.rb.tt +8 -3
  34. data/lib/generators/serviceable/templates/base_locale.en.yml.tt +53 -0
  35. data/lib/generators/serviceable/templates/base_service.rb.tt +78 -0
  36. data/lib/generators/serviceable/templates/base_service_test.rb.tt +64 -0
  37. data/lib/generators/serviceable/templates/create_service.rb.tt +29 -18
  38. data/lib/generators/serviceable/templates/destroy_service.rb.tt +16 -29
  39. data/lib/generators/serviceable/templates/index_service.rb.tt +16 -34
  40. data/lib/generators/serviceable/templates/repository.rb.tt +76 -0
  41. data/lib/generators/serviceable/templates/repository_test.rb.tt +124 -0
  42. data/lib/generators/serviceable/templates/show_service.rb.tt +10 -38
  43. data/lib/generators/serviceable/templates/update_service.rb.tt +24 -38
  44. data/lib/generators/serviceable/update_generator.rb +11 -0
  45. metadata +13 -12
  46. data/lib/better_service/concerns/serviceable/viewable.rb +0 -33
  47. data/lib/better_service/services/action_service.rb +0 -60
  48. data/lib/better_service/services/create_service.rb +0 -63
  49. data/lib/better_service/services/destroy_service.rb +0 -60
  50. data/lib/better_service/services/index_service.rb +0 -56
  51. data/lib/better_service/services/show_service.rb +0 -44
  52. 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
- # Phase 5: Viewer (if enabled)
127
- if respond_to?(:viewer_enabled?, true) && viewer_enabled?
128
- view_config = execute_viewer(processed, transformed, result)
129
- result = result.merge(view: view_config)
130
- end
131
-
132
- result
133
- rescue Errors::Runtime::ValidationError, Errors::Runtime::AuthorizationError
134
- # Let validation and authorization errors propagate without wrapping
135
- raise
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
- handle_not_found_error(e)
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
- handle_database_error(e)
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
- handle_unexpected_error(e)
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 (Create/Update/Destroy)
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
- # Only auto-invalidate for write operations
193
- is_a?(Services::CreateService) ||
194
- is_a?(Services::UpdateService) ||
195
- is_a?(Services::DestroyService)
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
- # Error handlers for runtime errors
323
+ # Build metadata for authorization errors
199
324
  #
200
- # These methods handle unexpected errors during service execution by raising
201
- # appropriate runtime exceptions. They log the error and wrap it with service context.
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
- # Handle resource not found errors (ActiveRecord::RecordNotFound)
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 original not found error
206
- # @raise [Errors::Runtime::ResourceNotFoundError] Wrapped error with context
207
- def handle_not_found_error(error)
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
- raise Errors::Runtime::ResourceNotFoundError.new(
211
- "Resource not found: #{error.message}",
212
- code: BetterService::ErrorCodes::RESOURCE_NOT_FOUND,
213
- original_error: error,
214
- context: { service: self.class.name, params: @params }
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
- # Handle database errors (ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved)
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 original database error
221
- # @raise [Errors::Runtime::DatabaseError] Wrapped error with context
222
- def handle_database_error(error)
223
- Rails.logger.error "Database error in #{self.class.name}: #{error.message}" if defined?(Rails)
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
- raise Errors::Runtime::DatabaseError.new(
227
- "Database error: #{error.message}",
228
- code: BetterService::ErrorCodes::DATABASE_ERROR,
229
- original_error: error,
230
- context: { service: self.class.name, params: @params }
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
- # Handle unexpected errors (all other StandardError)
392
+ # Build metadata for unexpected errors
235
393
  #
236
- # @param error [StandardError] The original unexpected error
237
- # @raise [Errors::Runtime::ExecutionError] Wrapped error with context
238
- def handle_unexpected_error(error)
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
- raise Errors::Runtime::ExecutionError.new(
243
- "Service execution failed: #{error.message}",
244
- code: BetterService::ErrorCodes::EXECUTION_ERROR,
245
- original_error: error,
246
- context: { service: self.class.name, params: @params }
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)
@@ -1,3 +1,3 @@
1
1
  module BetterService
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -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)
@@ -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", name
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", name
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", name
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", name
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", name
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 services:"
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