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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +253 -16
  3. data/Rakefile +1 -1
  4. data/config/locales/better_service.en.yml +37 -0
  5. data/lib/better_service/concerns/serviceable/messageable.rb +45 -2
  6. data/lib/better_service/concerns/serviceable/validatable.rb +0 -5
  7. data/lib/better_service/concerns/serviceable/viewable.rb +0 -16
  8. data/lib/better_service/presenter.rb +131 -0
  9. data/lib/better_service/railtie.rb +17 -0
  10. data/lib/better_service/services/base.rb +78 -21
  11. data/lib/better_service/services/create_service.rb +3 -0
  12. data/lib/better_service/services/destroy_service.rb +3 -0
  13. data/lib/better_service/services/update_service.rb +3 -0
  14. data/lib/better_service/subscribers/log_subscriber.rb +25 -5
  15. data/lib/better_service/version.rb +1 -1
  16. data/lib/better_service.rb +1 -0
  17. data/lib/generators/better_service/install_generator.rb +38 -0
  18. data/lib/generators/better_service/locale_generator.rb +54 -0
  19. data/lib/generators/better_service/presenter_generator.rb +60 -0
  20. data/lib/generators/better_service/templates/better_service_initializer.rb.tt +90 -0
  21. data/lib/generators/better_service/templates/locale.en.yml.tt +27 -0
  22. data/lib/generators/better_service/templates/presenter.rb.tt +53 -0
  23. data/lib/generators/better_service/templates/presenter_test.rb.tt +46 -0
  24. data/lib/generators/serviceable/scaffold_generator.rb +9 -0
  25. data/lib/generators/serviceable/templates/create_service.rb.tt +11 -1
  26. data/lib/generators/serviceable/templates/destroy_service.rb.tt +11 -1
  27. data/lib/generators/serviceable/templates/index_service.rb.tt +19 -1
  28. data/lib/generators/serviceable/templates/show_service.rb.tt +19 -1
  29. data/lib/generators/serviceable/templates/update_service.rb.tt +11 -1
  30. 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 - Handled by Viewable concern
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
@@ -1,3 +1,3 @@
1
1
  module BetterService
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
@@ -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("<%= human_name %> created successfully", data)
40
+ success_result(message("create.success"), data)
31
41
  end
32
42
  end
33
43
  <% end -%>