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/README.md CHANGED
@@ -1,36 +1,50 @@
1
1
  # 🚀 Light Services
2
2
 
3
- Light Services is a simple yet powerful way to organize your business logic. This Ruby gem helps you build services that are easy to test, maintain, and understand.
3
+ Light Services is a simple yet powerful way to organize business logic in Ruby applications. Build services that are easy to test, maintain, and understand.
4
4
 
5
5
  ![GitHub CI](https://github.com/light-ruby/light-services/actions/workflows/ci.yml/badge.svg)
6
6
  [![Codecov](https://codecov.io/gh/light-ruby/light-services/graph/badge.svg?token=IGJNZ2BQ26)](https://codecov.io/gh/light-ruby/light-services)
7
7
 
8
+ [Get started with Quickstart](https://light-services.kodkod.me/quickstart)
9
+
8
10
  ## Features
9
11
 
10
- - 🧩 **Simple**: Define your service as a class with `arguments`, `steps`, and `outputs`
11
- - 🎢 **Transactions**: Automatically rollback database changes if any step fails
12
- - 👵 **Inheritance**: Inherit from other services to reuse logic seamlessly
13
- - 🚨 **Error Handling**: Collect errors from steps and handle them your way
14
- - ⛓️ **Context**: Run multiple services sequentially within the same context
15
- - 🤔 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
16
- - 🏗️ **Modularity**: Isolate and test your services with ease
17
- - 🐛 **100% Test Coverage**: Bugs are not welcome here!
18
- - 🛡️ **Battle-Tested**: In production use since 2017
12
+ - **Simple**: Define your service as a class with `arguments`, `steps`, and `outputs`
13
+ - 📦 **No runtime dependencies**: Works stand-alone without requiring external gems at runtime
14
+ - 🔄 **Transactions**: Automatically rollback database changes if any step fails
15
+ - 🧬 **Inheritance**: Inherit from other services to reuse logic seamlessly
16
+ - ⚠️ **Error Handling**: Collect errors from steps and handle them your way
17
+ - 🔗 **Context**: Run multiple services sequentially within the same context
18
+ - 🧪 **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
19
+ - 🌐 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
20
+ - 🧩 **Modularity**: Isolate and test your services with ease
21
+ - ✅ **100% Test Coverage**: Thoroughly tested and reliable
22
+ - ⚔️ **Battle-Tested**: In production use since 2017
23
+
24
+ ## Installation
25
+
26
+ ```ruby
27
+ gem "light-services", "~> 3.0"
28
+ ```
29
+
30
+ ```bash
31
+ rails generate light_services:install
32
+ ```
19
33
 
20
34
  ## Simple Example
21
35
 
22
36
  ```ruby
23
37
  class GreetService < Light::Services::Base
24
38
  # Arguments
25
- arg :name
26
- arg :age
39
+ arg :name, type: String
40
+ arg :age, type: Integer
27
41
 
28
42
  # Steps
29
43
  step :build_message
30
44
  step :send_message
31
45
 
32
46
  # Outputs
33
- output :message
47
+ output :message, type: String
34
48
 
35
49
  private
36
50
 
@@ -44,14 +58,14 @@ class GreetService < Light::Services::Base
44
58
  end
45
59
  ```
46
60
 
47
- ## Advanced Example
61
+ ## Advanced Example (with dry-types and conditions)
48
62
 
49
63
  ```ruby
50
64
  class User::ResetPassword < Light::Services::Base
51
- # Arguments
52
- arg :user, type: User, optional: true
53
- arg :email, type: :string, optional: true
54
- arg :send_email, type: :boolean, default: true
65
+ # Arguments with dry-types for advanced validation and coercion
66
+ arg :user, type: Types.Instance(User), optional: true
67
+ arg :email, type: Types::Coercible::String, optional: true
68
+ arg :send_email, type: Types::Params::Bool, default: true
55
69
 
56
70
  # Steps
57
71
  step :validate
@@ -60,9 +74,9 @@ class User::ResetPassword < Light::Services::Base
60
74
  step :save_reset_token
61
75
  step :send_reset_email, if: :send_email?
62
76
 
63
- # Outputs
64
- output :user, type: User
65
- output :reset_token, type: :string
77
+ # Outputs with dry-types
78
+ output :user, type: Types.Instance(User)
79
+ output :reset_token, type: Types::Strict::String
66
80
 
67
81
  private
68
82
 
@@ -96,6 +110,55 @@ class User::ResetPassword < Light::Services::Base
96
110
  end
97
111
  ```
98
112
 
113
+ [Get started with Light Services](https://light-services.kodkod.me/quickstart)
114
+
115
+ ## Rails Generators
116
+
117
+ Light Services includes Rails generators to help you quickly set up and create services in your Rails application.
118
+
119
+ ### Install Generator
120
+
121
+ Set up Light Services in your Rails application:
122
+
123
+ ```bash
124
+ bin/rails generate light_services:install
125
+ ```
126
+
127
+ This creates:
128
+ - `app/services/application_service.rb` - Base service class for your application
129
+ - `config/initializers/light_services.rb` - Configuration file
130
+ - `spec/services/application_service_spec.rb` - RSpec test file (if RSpec is detected)
131
+
132
+ **Options:**
133
+ - `--skip-initializer` - Skip creating the initializer file
134
+ - `--skip-spec` - Skip creating the spec file
135
+
136
+ ### Service Generator
137
+
138
+ Create a new service class:
139
+
140
+ ```bash
141
+ # Basic service
142
+ bin/rails generate light_services:service user/create
143
+
144
+ # Service with predefined structure
145
+ bin/rails generate light_services:service CreateOrder \
146
+ --args=user product \
147
+ --steps=validate process \
148
+ --outputs=order
149
+ ```
150
+
151
+ This creates:
152
+ - `app/services/user/create.rb` - Service class file
153
+ - `spec/services/user/create_spec.rb` - RSpec test file (if RSpec is detected)
154
+
155
+ **Options:**
156
+ - `--args` - List of arguments for the service (e.g., `--args=user product`)
157
+ - `--steps` - List of steps for the service (e.g., `--steps=validate process`)
158
+ - `--outputs` - List of outputs for the service (e.g., `--outputs=result`)
159
+ - `--skip-spec` - Skip creating the spec file
160
+ - `--parent` - Parent class (default: ApplicationService)
161
+
99
162
  ## Documentation
100
163
 
101
164
  You can find the full documentation at [light-services.kodkod.me](https://light-services.kodkod.me).
data/docs/arguments.md ADDED
@@ -0,0 +1,290 @@
1
+ # Arguments
2
+
3
+ Arguments are the inputs to a service. They are passed to the service when it is invoked.
4
+
5
+ ## TL;DR
6
+
7
+ - Define arguments with the `arg` keyword in the service class
8
+ - Validate arguments by type
9
+ - Specify arguments as required or optional
10
+ - Set default values for arguments
11
+ - Access arguments like instance variables
12
+ - Use predicate methods for arguments
13
+
14
+ ```ruby
15
+ class User::Charge < ApplicationService
16
+ arg :user, type: User
17
+ arg :amount, type: Float
18
+ arg :send_receipt, type: [TrueClass, FalseClass], default: true
19
+ # In Rails you might prefer `Date.current`.
20
+ arg :invoice_date, type: Date, default: -> { Date.today }
21
+
22
+ step :send_email_receipt, if: :send_receipt?
23
+
24
+ # ...
25
+ end
26
+ ```
27
+
28
+ ## Define Arguments
29
+
30
+ Arguments are defined using the `arg` keyword in the service class.
31
+
32
+ ```ruby
33
+ class HappyBirthdayService < ApplicationService
34
+ arg :name
35
+ arg :age
36
+ end
37
+ ```
38
+
39
+ ## Type Validation
40
+
41
+ Arguments can be validated by type.
42
+
43
+ ```ruby
44
+ class HappyBirthdayService < ApplicationService
45
+ arg :name, type: String
46
+ arg :age, type: Integer
47
+ end
48
+ ```
49
+
50
+ You can specify multiple allowed types using an array.
51
+
52
+ ```ruby
53
+ class HappyBirthdayService < ApplicationService
54
+ arg :name, type: [String, Symbol]
55
+ end
56
+ ```
57
+
58
+ ### Type Enforcement (Enabled by Default)
59
+
60
+ By default, all arguments must have a `type` option. This helps catch type-related bugs early and makes your services self-documenting.
61
+
62
+ ```ruby
63
+ class MyService < ApplicationService
64
+ arg :name, type: String # ✓ Valid
65
+ arg :age # ✗ Raises MissingTypeError
66
+ end
67
+ ```
68
+
69
+ To disable type enforcement for a specific service:
70
+
71
+ ```ruby
72
+ class LegacyService < ApplicationService
73
+ config require_type: false
74
+
75
+ arg :name # Allowed when require_type is disabled
76
+ end
77
+ ```
78
+
79
+ See the [Configuration documentation](configuration.md) for more details.
80
+
81
+ ### dry-types Support
82
+
83
+ Light Services supports [dry-types](https://dry-rb.org/gems/dry-types) for advanced type validation and coercion. When using dry-types, values are automatically coerced to the expected type.
84
+
85
+ First, set up your types module:
86
+
87
+ ```ruby
88
+ require "dry-types"
89
+
90
+ module Types
91
+ include Dry.Types()
92
+ end
93
+ ```
94
+
95
+ Then use dry-types in your service arguments:
96
+
97
+ ```ruby
98
+ class User::Create < ApplicationService
99
+ # Strict types - must match exactly
100
+ arg :name, type: Types::Strict::String
101
+
102
+ # Coercible types - automatically convert values
103
+ arg :age, type: Types::Coercible::Integer
104
+
105
+ # Constrained types - add validation rules
106
+ arg :email, type: Types::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
107
+
108
+ # Enum types - restrict to specific values
109
+ arg :status, type: Types::String.enum("active", "inactive", "pending")
110
+
111
+ # Array types with element validation
112
+ arg :tags, type: Types::Array.of(Types::String)
113
+
114
+ # Hash schemas
115
+ arg :metadata, type: Types::Hash.schema(key: Types::String)
116
+ end
117
+ ```
118
+
119
+ **Coercion Example:**
120
+
121
+ ```ruby
122
+ # With coercible types, string "25" is automatically converted to integer 25
123
+ service = User::Create.run(name: "John", age: "25")
124
+ service.age # => 25 (Integer, not String)
125
+ ```
126
+
127
+ ## Required Arguments
128
+
129
+ By default, arguments are required. You can make them optional by setting `optional` to `true`.
130
+
131
+ ```ruby
132
+ class HappyBirthdayService < ApplicationService
133
+ arg :name, type: String
134
+ arg :age, type: Integer, optional: true
135
+ end
136
+ ```
137
+
138
+ ## Default Values
139
+
140
+ Set a default value for an argument to make it optional.
141
+
142
+ ```ruby
143
+ class HappyBirthdayService < ApplicationService
144
+ arg :name, type: String
145
+ arg :age, type: Integer, default: 18
146
+ end
147
+ ```
148
+
149
+ ### Complex Default Values
150
+
151
+ Default values are deep duplicated when the service is invoked, making it safe to use mutable objects.
152
+
153
+ ```ruby
154
+ arg :options, type: Hash, default: { a: 1, b: 2 }
155
+ ```
156
+
157
+ ### Procs as Default Values
158
+
159
+ Use procs for dynamic default values.
160
+
161
+ ```ruby
162
+ arg :current_date, type: Date, default: -> { Date.current }
163
+ ```
164
+
165
+ ## Inheritance
166
+
167
+ Arguments are inherited from parent classes.
168
+
169
+ ```ruby
170
+ # UpdateRecordService
171
+ class UpdateRecordService < ApplicationService
172
+ # Arguments
173
+ arg :record, type: ApplicationRecord
174
+ arg :attributes, type: Hash
175
+
176
+ # Steps
177
+ step :authorize
178
+ step :update_record
179
+ end
180
+ ```
181
+
182
+ ```ruby
183
+ # User::Update inherited from UpdateRecordService
184
+ class User::Update < UpdateRecordService
185
+ # Nothing to do here
186
+ # Arguments and steps are inherited from UpdateRecordService
187
+ end
188
+ ```
189
+
190
+ ### Removing Inherited Arguments
191
+
192
+ To remove an inherited argument, use `remove_arg`:
193
+
194
+ ```ruby
195
+ class BaseService < ApplicationService
196
+ arg :current_user, type: User
197
+ arg :audit_log, type: [TrueClass, FalseClass], default: true
198
+ end
199
+
200
+ class SystemTaskService < BaseService
201
+ # System tasks don't need a current_user
202
+ remove_arg :current_user
203
+ end
204
+ ```
205
+
206
+ ## Context Arguments
207
+
208
+ Context arguments are automatically passed to all child services in the same context. Define them using the `context` option. This is useful for passing objects like `current_user`.
209
+
210
+ Learn more about context in the [Context documentation](context.md).
211
+
212
+ ```ruby
213
+ class ApplicationService < Light::Services::Base
214
+ arg :current_user, type: User, optional: true, context: true
215
+ end
216
+ ```
217
+
218
+ ## Accessing Arguments
219
+
220
+ Arguments are accessible like instance variables, similar to `attr_accessor`.
221
+
222
+ ```ruby
223
+ class HappyBirthdayService < ApplicationService
224
+ # Arguments
225
+ arg :name, type: String
226
+ arg :age, type: Integer
227
+
228
+ # Steps
229
+ step :greet
230
+
231
+ private
232
+
233
+ def greet
234
+ puts "Happy birthday, #{name}! You are #{age} years old."
235
+ end
236
+ end
237
+ ```
238
+
239
+ ## Accessing Arguments Using `arguments`
240
+
241
+ For dynamic access or to avoid conflicts, use the `arguments` method.
242
+
243
+ ```ruby
244
+ class HappyBirthdayService < ApplicationService
245
+ # Arguments
246
+ arg :name, type: String
247
+ arg :age, type: Integer
248
+
249
+ # Steps
250
+ step :greet
251
+
252
+ private
253
+
254
+ def greet
255
+ name = arguments[:name] # or arguments.get(:name)
256
+ age = arguments[:age] # or arguments.get(:age)
257
+
258
+ puts "Happy birthday, #{name}! You are #{age} years old."
259
+ end
260
+ end
261
+ ```
262
+
263
+ ## Argument Predicate Methods
264
+
265
+ Predicate methods are automatically generated for each argument, allowing you to check if an argument is `true` or `false`.
266
+
267
+ ```ruby
268
+ class User::GenerateInvoice < ApplicationService
269
+ # Arguments
270
+ arg :user, type: User
271
+ arg :charge, type: [TrueClass, FalseClass], default: false
272
+
273
+ # Steps
274
+ step :generate_invoice
275
+ step :charge_user, if: :charge?
276
+
277
+ # ...
278
+ end
279
+ ```
280
+
281
+ {% hint style="info" %}
282
+ The predicate methods return `true` or `false` based on Ruby's convention: `nil` and `false` are `false`, everything else is `true`.
283
+ {% endhint %}
284
+
285
+ ## What's Next?
286
+
287
+ Next step is `steps` (I love this pun). Steps are the building blocks of a service, the methods that do the actual work.
288
+
289
+ [Next: Steps](steps.md)
290
+
@@ -0,0 +1,153 @@
1
+ # Best Practices
2
+
3
+ This guide explores best practices for building applications with Light Services, keeping things simple and effective.
4
+
5
+ ## Create Top-Level Services
6
+
7
+ Creating top-level services for your application is highly recommended. This approach helps keep your services small and focused on a single task.
8
+
9
+ ### Application Service
10
+
11
+ `ApplicationService` serves as the base class for all services in your application. Use it to place common methods, helpers, context arguments, etc. Remember, it should not contain any business logic.
12
+
13
+ ### Create, Update, and Destroy Services
14
+
15
+ Since create, update, and destroy are fundamental operations in any application, having dedicated services for them is a good idea. This keeps important tasks like authorization, data sanitization, and WebSocket broadcasts close to the core of your application.
16
+
17
+ - `CreateRecordService` - for creating records
18
+ - `UpdateRecordService` - for updating records
19
+ - `DestroyRecordService` - for destroying records
20
+
21
+ Think of these services as wrappers around the `ActiveRecord::Base#create`, `#update`, and `#destroy` methods.
22
+
23
+ ### Read Services
24
+
25
+ Similar to the above services but focused on finding records. Use these for generic authorization, filtering, sorting, pagination, etc.
26
+
27
+ - `FindRecordService` - for finding a single record
28
+ - `FindAllRecordsService` - for finding multiple records
29
+
30
+ ## Avoid Defining Context Arguments Outside Top-Level Services
31
+
32
+ Using context arguments outside of top-level services can make your services less modular and more unpredictable. Keep them within the core services for better modularity.
33
+
34
+ ## Keep Services Small
35
+
36
+ Aim to keep your services small and focused on a single task. Ideally, a service should have no more than 3-5 steps. If a service has more steps, consider splitting it into multiple services.
37
+
38
+ ## Passing Arguments from Controllers
39
+
40
+ It's a good practice to create a wrapper method to extend arguments passed to the service from the controller.
41
+
42
+ Consider this example controller:
43
+
44
+ ```ruby
45
+ class PostsController < ApplicationController
46
+ def index
47
+ service = Post::FindAll.run(current_user:, current_organization:)
48
+ render json: service.posts
49
+ end
50
+
51
+ def create
52
+ service = Post::Create.run(attributes: params[:post], current_user:, current_organization:)
53
+
54
+ if service.success?
55
+ render json: service.post
56
+ else
57
+ render json: { errors: service.errors }, status: :unprocessable_entity
58
+ end
59
+ end
60
+
61
+ def unpublish
62
+ service = Post::Unpublish.run(id: params[:id], current_user:, current_organization:)
63
+
64
+ if service.success?
65
+ render json: service.post
66
+ else
67
+ render json: { errors: service.errors }, status: :unprocessable_entity
68
+ end
69
+ end
70
+
71
+ # ...
72
+ end
73
+ ```
74
+
75
+ Manually passing `current_user` and `current_organization` each time can be cumbersome. Let's simplify it with a helper method in our `ApplicationController`:
76
+
77
+ ```ruby
78
+ class ApplicationController < ActionController::API
79
+ private
80
+
81
+ def service_args(hash = {})
82
+ hash.reverse_merge(
83
+ current_user:,
84
+ current_organization:,
85
+ )
86
+ end
87
+ end
88
+ ```
89
+
90
+ Now we can refactor our controller:
91
+
92
+ ```ruby
93
+ class PostsController < ApplicationController
94
+ def index
95
+ service = Post::FindAll.run(service_args)
96
+ render json: service.posts
97
+ end
98
+
99
+ def create
100
+ service = Post::Create.run(service_args(attributes: params[:post]))
101
+
102
+ if service.success?
103
+ render json: service.post
104
+ else
105
+ render json: { errors: service.errors }, status: :unprocessable_entity
106
+ end
107
+ end
108
+
109
+ def unpublish
110
+ service = Post::Unpublish.run(service_args(id: params[:id]))
111
+
112
+ if service.success?
113
+ render json: service.post
114
+ else
115
+ render json: { errors: service.errors }, status: :unprocessable_entity
116
+ end
117
+ end
118
+
119
+ # ...
120
+ end
121
+ ```
122
+
123
+ With this setup, adding a new top-level context argument only requires a change to the `service_args` method in `ApplicationController`.
124
+
125
+ ## Use Concerns
126
+
127
+ If you have common logic that you want to share between services, use concerns. Avoid putting too much logic into your `ApplicationService` class; it's better to split it into concerns.
128
+
129
+ For example, create an `AuthorizeUser` concern for authorization logic.
130
+
131
+ ```ruby
132
+ # app/services/concerns/authorize_user.rb
133
+ module AuthorizeUser
134
+ extend ActiveSupport::Concern
135
+
136
+ included do
137
+ # ...
138
+ end
139
+ end
140
+ ```
141
+
142
+ ```ruby
143
+ # app/services/application_service.rb
144
+ class ApplicationService < Light::Services::Base
145
+ include AuthorizeUser
146
+ end
147
+ ```
148
+
149
+ ## What's Next?
150
+
151
+ Explore practical recipes for common patterns:
152
+
153
+ [Next: Recipes](recipes.md)