light-services 2.2.1 → 3.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -4
  3. data/.github/workflows/ci.yml +12 -12
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +83 -7
  6. data/CHANGELOG.md +38 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +84 -21
  11. data/docs/arguments.md +290 -0
  12. data/docs/best-practices.md +153 -0
  13. data/docs/callbacks.md +476 -0
  14. data/docs/concepts.md +80 -0
  15. data/docs/configuration.md +204 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +280 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +158 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +101 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/rubocop.md +285 -0
  26. data/docs/ruby-lsp.md +133 -0
  27. data/docs/service-rendering.md +222 -0
  28. data/docs/steps.md +391 -0
  29. data/docs/summary.md +21 -0
  30. data/docs/testing.md +549 -0
  31. data/lib/generators/light_services/install/USAGE +15 -0
  32. data/lib/generators/light_services/install/install_generator.rb +41 -0
  33. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  34. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  35. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  36. data/lib/generators/light_services/service/USAGE +21 -0
  37. data/lib/generators/light_services/service/service_generator.rb +68 -0
  38. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  39. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  40. data/lib/light/services/base.rb +134 -122
  41. data/lib/light/services/base_with_context.rb +23 -1
  42. data/lib/light/services/callbacks.rb +157 -0
  43. data/lib/light/services/collection.rb +145 -0
  44. data/lib/light/services/concerns/execution.rb +79 -0
  45. data/lib/light/services/concerns/parent_service.rb +34 -0
  46. data/lib/light/services/concerns/state_management.rb +30 -0
  47. data/lib/light/services/config.rb +82 -16
  48. data/lib/light/services/constants.rb +100 -0
  49. data/lib/light/services/dsl/arguments_dsl.rb +85 -0
  50. data/lib/light/services/dsl/outputs_dsl.rb +81 -0
  51. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  52. data/lib/light/services/dsl/validation.rb +162 -0
  53. data/lib/light/services/exceptions.rb +25 -2
  54. data/lib/light/services/message.rb +28 -3
  55. data/lib/light/services/messages.rb +92 -32
  56. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  57. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  58. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  59. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  60. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  61. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  62. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  63. data/lib/light/services/rspec.rb +15 -0
  64. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  65. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  66. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  67. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  68. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  69. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  70. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  71. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  72. data/lib/light/services/rubocop.rb +12 -0
  73. data/lib/light/services/settings/field.rb +114 -0
  74. data/lib/light/services/settings/step.rb +53 -20
  75. data/lib/light/services/utils.rb +38 -0
  76. data/lib/light/services/version.rb +1 -1
  77. data/lib/light/services.rb +2 -0
  78. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  79. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  80. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  81. data/light-services.gemspec +6 -8
  82. metadata +68 -26
  83. data/lib/light/services/class_based_collection/base.rb +0 -86
  84. data/lib/light/services/class_based_collection/mount.rb +0 -33
  85. data/lib/light/services/collection/arguments.rb +0 -34
  86. data/lib/light/services/collection/base.rb +0 -59
  87. data/lib/light/services/collection/outputs.rb +0 -16
  88. data/lib/light/services/settings/argument.rb +0 -68
  89. data/lib/light/services/settings/output.rb +0 -34
data/docs/rubocop.md ADDED
@@ -0,0 +1,285 @@
1
+ # RuboCop Integration
2
+
3
+ Light Services provides custom RuboCop cops to help enforce best practices in your service definitions.
4
+
5
+ ## Setup
6
+
7
+ Add this to your `.rubocop.yml`:
8
+
9
+ ```yaml
10
+ require:
11
+ - light/services/rubocop
12
+ ```
13
+
14
+ ## Available Cops
15
+
16
+ ### LightServices/ArgumentTypeRequired
17
+
18
+ Ensures all `arg` declarations include a `type:` option.
19
+
20
+ ```ruby
21
+ # bad
22
+ arg :user_id
23
+ arg :params, default: {}
24
+
25
+ # good
26
+ arg :user_id, type: Integer
27
+ arg :params, type: Hash, default: {}
28
+ ```
29
+
30
+ ### LightServices/OutputTypeRequired
31
+
32
+ Ensures all `output` declarations include a `type:` option.
33
+
34
+ ```ruby
35
+ # bad
36
+ output :result
37
+ output :data, optional: true
38
+
39
+ # good
40
+ output :result, type: Hash
41
+ output :data, type: Hash, optional: true
42
+ ```
43
+
44
+ ### LightServices/StepMethodExists
45
+
46
+ Ensures all `step` declarations have a corresponding method defined.
47
+
48
+ ```ruby
49
+ # bad
50
+ class MyService < ApplicationService
51
+ step :validate
52
+ step :process # missing method
53
+
54
+ private
55
+
56
+ def validate; end
57
+ end
58
+
59
+ # good
60
+ class MyService < ApplicationService
61
+ step :validate
62
+ step :process
63
+
64
+ private
65
+
66
+ def validate; end
67
+ def process; end
68
+ end
69
+ ```
70
+
71
+ **Configuration:** Use `ExcludedSteps` for inherited steps:
72
+
73
+ ```yaml
74
+ LightServices/StepMethodExists:
75
+ ExcludedSteps:
76
+ - initialize_entity
77
+ - assign_attributes
78
+ ```
79
+
80
+ ### LightServices/ConditionMethodExists
81
+
82
+ Ensures symbol conditions (`:if`, `:unless`) have corresponding methods defined.
83
+
84
+ This cop automatically recognizes predicate methods generated by `arg` and `output` declarations (e.g., `arg :user` creates `user?`).
85
+
86
+ ```ruby
87
+ # bad
88
+ class MyService < ApplicationService
89
+ step :notify, if: :should_notify? # missing method
90
+
91
+ private
92
+
93
+ def notify; end
94
+ end
95
+
96
+ # good - explicit method
97
+ class MyService < ApplicationService
98
+ step :notify, if: :should_notify?
99
+
100
+ private
101
+
102
+ def notify; end
103
+ def should_notify?; true; end
104
+ end
105
+
106
+ # good - predicate from arg/output
107
+ class MyService < ApplicationService
108
+ arg :user, type: User, optional: true
109
+
110
+ step :greet, if: :user? # user? is auto-generated
111
+
112
+ private
113
+
114
+ def greet; end
115
+ end
116
+ ```
117
+
118
+ **Configuration:** Use `ExcludedMethods` for inherited condition methods:
119
+
120
+ ```yaml
121
+ LightServices/ConditionMethodExists:
122
+ ExcludedMethods:
123
+ - admin?
124
+ - guest?
125
+ ```
126
+
127
+ ### LightServices/DslOrder
128
+
129
+ Enforces consistent ordering of DSL declarations: `config` → `arg` → `step` → `output`
130
+
131
+ ```ruby
132
+ # bad
133
+ class MyService < ApplicationService
134
+ step :process
135
+ arg :name, type: String
136
+ config raise_on_error: true
137
+ end
138
+
139
+ # good
140
+ class MyService < ApplicationService
141
+ config raise_on_error: true
142
+
143
+ arg :name, type: String
144
+
145
+ step :process
146
+
147
+ output :result, type: Hash
148
+ end
149
+ ```
150
+
151
+ ### LightServices/MissingPrivateKeyword
152
+
153
+ Ensures step methods are defined as private.
154
+
155
+ ```ruby
156
+ # bad
157
+ class MyService < ApplicationService
158
+ step :process
159
+
160
+ def process # should be private
161
+ # implementation
162
+ end
163
+ end
164
+
165
+ # good
166
+ class MyService < ApplicationService
167
+ step :process
168
+
169
+ private
170
+
171
+ def process
172
+ # implementation
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### LightServices/NoDirectInstantiation
178
+
179
+ Prevents direct instantiation of service classes with `.new`.
180
+
181
+ ```ruby
182
+ # bad
183
+ UserService.new(name: "John")
184
+
185
+ # good
186
+ UserService.run(name: "John")
187
+ UserService.run!(name: "John")
188
+ UserService.call(name: "John")
189
+ ```
190
+
191
+ **Configuration:** Customize the pattern for service class detection:
192
+
193
+ ```yaml
194
+ LightServices/NoDirectInstantiation:
195
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
196
+ ```
197
+
198
+ ### LightServices/DeprecatedMethods
199
+
200
+ Detects deprecated `done!` and `done?` method calls and suggests using `stop!` and `stopped?` instead. Includes autocorrection.
201
+
202
+ ```ruby
203
+ # bad
204
+ class MyService < ApplicationService
205
+ step :process
206
+
207
+ private
208
+
209
+ def process
210
+ done! if condition_met?
211
+ return if done?
212
+ end
213
+ end
214
+
215
+ # good
216
+ class MyService < ApplicationService
217
+ step :process
218
+
219
+ private
220
+
221
+ def process
222
+ stop! if condition_met?
223
+ return if stopped?
224
+ end
225
+ end
226
+ ```
227
+
228
+ **Configuration:** Customize the pattern for service class detection:
229
+
230
+ ```yaml
231
+ LightServices/DeprecatedMethods:
232
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
233
+ ```
234
+
235
+ ## Configuration
236
+
237
+ Full configuration example:
238
+
239
+ ```yaml
240
+ require:
241
+ - light/services/rubocop
242
+
243
+ LightServices/ArgumentTypeRequired:
244
+ Enabled: true
245
+
246
+ LightServices/OutputTypeRequired:
247
+ Enabled: true
248
+
249
+ LightServices/StepMethodExists:
250
+ Enabled: true
251
+ ExcludedSteps: []
252
+
253
+ LightServices/ConditionMethodExists:
254
+ Enabled: true
255
+ ExcludedMethods: []
256
+
257
+ LightServices/DslOrder:
258
+ Enabled: true
259
+
260
+ LightServices/MissingPrivateKeyword:
261
+ Enabled: true
262
+
263
+ LightServices/NoDirectInstantiation:
264
+ Enabled: true
265
+ ServicePattern: 'Service$'
266
+
267
+ LightServices/DeprecatedMethods:
268
+ Enabled: true
269
+ ServicePattern: 'Service$'
270
+ ```
271
+
272
+ To disable a cop for specific files:
273
+
274
+ ```yaml
275
+ LightServices/ArgumentTypeRequired:
276
+ Exclude:
277
+ - 'spec/**/*'
278
+ - 'test/**/*'
279
+ ```
280
+
281
+ ## What's Next?
282
+
283
+ Learn more about testing your services:
284
+
285
+ [Next: Testing](testing.md)
data/docs/ruby-lsp.md ADDED
@@ -0,0 +1,133 @@
1
+ # Ruby LSP Integration
2
+
3
+ Light Services provides a Ruby LSP add-on that enhances your editor experience by informing the language server about methods generated by the `arg` and `output` DSL keywords.
4
+
5
+ ## Features
6
+
7
+ When you use the `arg` or `output` keywords, Light Services dynamically generates methods at runtime:
8
+
9
+ ```ruby
10
+ class MyService < ApplicationService
11
+ arg :user, type: User
12
+ output :result, type: Hash
13
+ end
14
+ ```
15
+
16
+ This generates the following methods:
17
+ - `user` - getter method (returns `User`)
18
+ - `user?` - predicate method (returns boolean)
19
+ - `user=` - setter method (private, accepts `User`)
20
+ - `result` - getter method (returns `Hash`)
21
+ - `result?` - predicate method (returns boolean)
22
+ - `result=` - setter method (private, accepts `Hash`)
23
+
24
+ The Ruby LSP add-on teaches the language server about these generated methods, enabling:
25
+
26
+ - **Go to Definition** - Navigate to the `arg`/`output` declaration
27
+ - **Completion** - Autocomplete generated method names
28
+ - **Hover** - See information about generated methods, including return types
29
+ - **Signature Help** - Get parameter hints for setter methods
30
+ - **Workspace Symbol** - Find generated methods in symbol search
31
+
32
+ ## Setup
33
+
34
+ The add-on is automatically discovered by Ruby LSP when Light Services is in your project's dependencies. No additional configuration is required.
35
+
36
+ ### Requirements
37
+
38
+ - Ruby LSP `~> 0.26` or later
39
+ - Light Services gem installed in your project
40
+
41
+ ### Verification
42
+
43
+ To verify the add-on is loaded, check the Ruby LSP output in your editor. You should see "Ruby LSP Light Services" listed among the active add-ons.
44
+
45
+ ## How It Works
46
+
47
+ The add-on uses Ruby LSP's **indexing enhancement** system to register generated methods during code indexing. When the indexer encounters an `arg` or `output` call with a symbol argument, it automatically registers the three generated methods (getter, predicate, setter) in the index.
48
+
49
+ This is a static analysis approach - the add-on analyzes your source code without executing it. This means:
50
+
51
+ - Methods are recognized immediately as you type
52
+ - No running application is required
53
+ - Works with any editor that supports Ruby LSP
54
+
55
+ ## Type Inference
56
+
57
+ The add-on extracts type information from the `type:` option and includes it as YARD-style documentation comments. This enables hover information to display return types for generated methods.
58
+
59
+ ### Simple Ruby Types
60
+
61
+ ```ruby
62
+ arg :user, type: User # → User
63
+ arg :items, type: Array # → Array
64
+ arg :name, type: String # → String
65
+ ```
66
+
67
+ ### Namespaced Types
68
+
69
+ ```ruby
70
+ arg :payment, type: Stripe::Charge # → Stripe::Charge
71
+ arg :config, type: MyApp::Configuration # → MyApp::Configuration
72
+ ```
73
+
74
+ ### Dry-Types
75
+
76
+ Common dry-types are mapped to their underlying Ruby types:
77
+
78
+ | Dry-Type | Ruby Type |
79
+ |----------|-----------|
80
+ | `Types::String`, `Types::Strict::String`, `Types::Coercible::String` | `String` |
81
+ | `Types::Integer`, `Types::Strict::Integer`, `Types::Coercible::Integer` | `Integer` |
82
+ | `Types::Float`, `Types::Strict::Float`, `Types::Coercible::Float` | `Float` |
83
+ | `Types::Bool`, `Types::Strict::Bool` | `TrueClass \| FalseClass` |
84
+ | `Types::Array`, `Types::Strict::Array` | `Array` |
85
+ | `Types::Hash`, `Types::Strict::Hash` | `Hash` |
86
+ | `Types::Symbol`, `Types::Strict::Symbol` | `Symbol` |
87
+ | `Types::Date`, `Types::DateTime`, `Types::Time` | `Date`, `DateTime`, `Time` |
88
+
89
+ Constrained and parameterized types extract their base type:
90
+
91
+ ```ruby
92
+ arg :email, type: Types::String.constrained(format: /@/) # → String
93
+ arg :tags, type: Types::Array.of(Types::String) # → Array
94
+ arg :status, type: Types::String.enum("active", "pending") # → String
95
+ ```
96
+
97
+ ### Custom Type Mappings
98
+
99
+ You can add custom type mappings through the Light Services configuration:
100
+
101
+ ```ruby
102
+ # config/initializers/light_services.rb
103
+ Light::Services.configure do |config|
104
+ config.ruby_lsp_type_mappings = {
105
+ "Types::UUID" => "String",
106
+ "Types::Money" => "BigDecimal",
107
+ "Types::JSON" => "Hash",
108
+ "CustomTypes::Email" => "String",
109
+ "MyApp::Types::PhoneNumber" => "String",
110
+ }
111
+ end
112
+ ```
113
+
114
+ Custom mappings take precedence over the default dry-types mappings, allowing you to:
115
+
116
+ - Add mappings for your own custom types
117
+ - Override default mappings if needed
118
+ - Support domain-specific type modules
119
+
120
+ ## Limitations
121
+
122
+ - Only `arg` and `output` declarations with a symbol as the first argument are recognized
123
+ - The add-on cannot detect dynamically computed argument names (e.g., `arg some_variable`)
124
+ - Inherited arguments/outputs from parent classes are not automatically discovered
125
+ - Parameterized dry-types like `Types::Array.of(Types::String)` resolve to the container type (`Array`), not the full generic type
126
+ - Custom dry-type definitions outside the standard `Types::` namespace are not mapped
127
+
128
+ ## What's Next?
129
+
130
+ Learn more about other integrations:
131
+
132
+ - [RuboCop Integration](rubocop.md) - Static analysis cops for services
133
+ - [Testing](testing.md) - Testing your services with RSpec matchers
@@ -0,0 +1,222 @@
1
+ # Service Rendering
2
+
3
+ This recipe provides a clean way to render service results and errors in your Rails controllers, reducing boilerplate and ensuring consistent API responses.
4
+
5
+ ## The Problem
6
+
7
+ Without a helper, controller actions become repetitive:
8
+
9
+ ```ruby
10
+ class PostsController < ApplicationController
11
+ def create
12
+ service = Post::Create.run(service_args(attributes: params[:post]))
13
+
14
+ if service.success?
15
+ render json: service.post, status: :created
16
+ else
17
+ render json: { errors: service.errors.to_h }, status: :unprocessable_entity
18
+ end
19
+ end
20
+
21
+ def update
22
+ service = Post::Update.run(service_args(record: @post, attributes: params[:post]))
23
+
24
+ if service.success?
25
+ render json: service.post
26
+ else
27
+ render json: { errors: service.errors.to_h }, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+ # ... same pattern repeated for every action
32
+ end
33
+ ```
34
+
35
+ ## The Solution
36
+
37
+ Create a `render_service` helper that handles success and failure automatically.
38
+
39
+ ## Implementation
40
+
41
+ ### Basic Helper
42
+
43
+ Add this to your `ApplicationController`:
44
+
45
+ ```ruby
46
+ class ApplicationController < ActionController::API
47
+ private
48
+
49
+ def render_service(service, success_status: :ok, error_status: :unprocessable_entity)
50
+ if service.success?
51
+ yield(service) if block_given?
52
+ render json: service_response(service), status: success_status
53
+ else
54
+ render json: { errors: service.errors.to_h }, status: error_status
55
+ end
56
+ end
57
+
58
+ def service_response(service)
59
+ # Returns the first output that is set
60
+ service.class.outputs.each do |name, _|
61
+ value = service.public_send(name)
62
+ return value unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
63
+ end
64
+
65
+ {}
66
+ end
67
+ end
68
+ ```
69
+
70
+ ### Usage
71
+
72
+ ```ruby
73
+ class PostsController < ApplicationController
74
+ def create
75
+ render_service Post::Create.run(service_args(attributes: params[:post])),
76
+ success_status: :created
77
+ end
78
+
79
+ def update
80
+ render_service Post::Update.run(service_args(record: @post))
81
+ end
82
+
83
+ def destroy
84
+ render_service Post::Destroy.run(service_args(record: @post))
85
+ end
86
+ end
87
+ ```
88
+
89
+ ## Advanced Implementation
90
+
91
+ ### With Custom Response Building
92
+
93
+ ```ruby
94
+ class ApplicationController < ActionController::API
95
+ private
96
+
97
+ def render_service(service, **options)
98
+ if service.success?
99
+ render_service_success(service, options)
100
+ else
101
+ render_service_failure(service, options)
102
+ end
103
+ end
104
+
105
+ def render_service_success(service, options)
106
+ status = options[:success_status] || :ok
107
+
108
+ response = if options[:response]
109
+ options[:response]
110
+ elsif options[:output]
111
+ service.public_send(options[:output])
112
+ else
113
+ auto_detect_response(service)
114
+ end
115
+
116
+ render json: response, status: status
117
+ end
118
+
119
+ def render_service_failure(service, options)
120
+ status = options[:error_status] || :unprocessable_entity
121
+
122
+ render json: {
123
+ errors: service.errors.to_h,
124
+ warnings: service.warnings.to_h
125
+ }.compact, status: status
126
+ end
127
+
128
+ def auto_detect_response(service)
129
+ service.class.outputs.each do |name, _|
130
+ value = service.public_send(name)
131
+ return value unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
132
+ end
133
+
134
+ { success: true }
135
+ end
136
+ end
137
+ ```
138
+
139
+ ### Usage with Options
140
+
141
+ ```ruby
142
+ class PostsController < ApplicationController
143
+ def create
144
+ service = Post::Create.run(service_args(attributes: params[:post]))
145
+
146
+ render_service service,
147
+ success_status: :created,
148
+ output: :post
149
+ end
150
+
151
+ def bulk_create
152
+ service = Post::BulkCreate.run(service_args(items: params[:posts]))
153
+
154
+ render_service service,
155
+ success_status: :created,
156
+ response: { posts: service.posts, count: service.posts.count }
157
+ end
158
+ end
159
+ ```
160
+
161
+ ## With Serializers
162
+
163
+ If you're using a serializer library (like Alba, Blueprinter, or ActiveModel::Serializers):
164
+
165
+ ```ruby
166
+ class ApplicationController < ActionController::API
167
+ private
168
+
169
+ def render_service(service, serializer: nil, **options)
170
+ if service.success?
171
+ response = auto_detect_response(service)
172
+ response = serializer.new(response).to_h if serializer && response
173
+
174
+ render json: response, status: options[:success_status] || :ok
175
+ else
176
+ render json: { errors: service.errors.to_h },
177
+ status: options[:error_status] || :unprocessable_entity
178
+ end
179
+ end
180
+ end
181
+ ```
182
+
183
+ ```ruby
184
+ class PostsController < ApplicationController
185
+ def show
186
+ service = Post::Find.run(service_args(id: params[:id]))
187
+ render_service service, serializer: PostSerializer
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## Handling Different Error Types
193
+
194
+ ```ruby
195
+ def render_service(service, **options)
196
+ if service.success?
197
+ render_service_success(service, options)
198
+ else
199
+ status = determine_error_status(service, options)
200
+ render json: { errors: service.errors.to_h }, status: status
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ def determine_error_status(service, options)
207
+ return options[:error_status] if options[:error_status]
208
+
209
+ # Map specific error keys to HTTP statuses
210
+ return :not_found if service.errors[:record]&.any?
211
+ return :forbidden if service.errors[:authorization]&.any?
212
+ return :unauthorized if service.errors[:authentication]&.any?
213
+
214
+ :unprocessable_entity
215
+ end
216
+ ```
217
+
218
+ ## What's Next?
219
+
220
+ Learn how to integrate Pundit authorization with Light Services:
221
+
222
+ [Next: Pundit Authorization](pundit-authorization.md)