light-services 2.2.1 → 3.0.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 (73) 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 +77 -7
  6. data/CHANGELOG.md +23 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +76 -13
  11. data/docs/arguments.md +267 -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 +168 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +250 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +135 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +100 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/service-rendering.md +222 -0
  26. data/docs/steps.md +337 -0
  27. data/docs/summary.md +19 -0
  28. data/docs/testing.md +549 -0
  29. data/lib/generators/light_services/install/USAGE +15 -0
  30. data/lib/generators/light_services/install/install_generator.rb +41 -0
  31. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  32. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  33. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  34. data/lib/generators/light_services/service/USAGE +21 -0
  35. data/lib/generators/light_services/service/service_generator.rb +68 -0
  36. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  37. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  38. data/lib/light/services/base.rb +23 -113
  39. data/lib/light/services/callbacks.rb +103 -0
  40. data/lib/light/services/collection.rb +97 -0
  41. data/lib/light/services/concerns/execution.rb +76 -0
  42. data/lib/light/services/concerns/parent_service.rb +34 -0
  43. data/lib/light/services/concerns/state_management.rb +30 -0
  44. data/lib/light/services/config.rb +4 -18
  45. data/lib/light/services/constants.rb +97 -0
  46. data/lib/light/services/dsl/arguments_dsl.rb +84 -0
  47. data/lib/light/services/dsl/outputs_dsl.rb +80 -0
  48. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  49. data/lib/light/services/dsl/validation.rb +132 -0
  50. data/lib/light/services/exceptions.rb +7 -2
  51. data/lib/light/services/messages.rb +19 -31
  52. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  53. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  54. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  55. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  56. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  57. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  58. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  59. data/lib/light/services/rspec.rb +15 -0
  60. data/lib/light/services/settings/field.rb +86 -0
  61. data/lib/light/services/settings/step.rb +31 -16
  62. data/lib/light/services/utils.rb +38 -0
  63. data/lib/light/services/version.rb +1 -1
  64. data/lib/light/services.rb +2 -0
  65. data/light-services.gemspec +6 -8
  66. metadata +54 -26
  67. data/lib/light/services/class_based_collection/base.rb +0 -86
  68. data/lib/light/services/class_based_collection/mount.rb +0 -33
  69. data/lib/light/services/collection/arguments.rb +0 -34
  70. data/lib/light/services/collection/base.rb +0 -59
  71. data/lib/light/services/collection/outputs.rb +0 -16
  72. data/lib/light/services/settings/argument.rb +0 -68
  73. data/lib/light/services/settings/output.rb +0 -34
data/docs/arguments.md ADDED
@@ -0,0 +1,267 @@
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
+ ### dry-types Support
59
+
60
+ 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.
61
+
62
+ First, set up your types module:
63
+
64
+ ```ruby
65
+ require "dry-types"
66
+
67
+ module Types
68
+ include Dry.Types()
69
+ end
70
+ ```
71
+
72
+ Then use dry-types in your service arguments:
73
+
74
+ ```ruby
75
+ class User::Create < ApplicationService
76
+ # Strict types - must match exactly
77
+ arg :name, type: Types::Strict::String
78
+
79
+ # Coercible types - automatically convert values
80
+ arg :age, type: Types::Coercible::Integer
81
+
82
+ # Constrained types - add validation rules
83
+ arg :email, type: Types::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
84
+
85
+ # Enum types - restrict to specific values
86
+ arg :status, type: Types::String.enum("active", "inactive", "pending")
87
+
88
+ # Array types with element validation
89
+ arg :tags, type: Types::Array.of(Types::String)
90
+
91
+ # Hash schemas
92
+ arg :metadata, type: Types::Hash.schema(key: Types::String)
93
+ end
94
+ ```
95
+
96
+ **Coercion Example:**
97
+
98
+ ```ruby
99
+ # With coercible types, string "25" is automatically converted to integer 25
100
+ service = User::Create.run(name: "John", age: "25")
101
+ service.age # => 25 (Integer, not String)
102
+ ```
103
+
104
+ ## Required Arguments
105
+
106
+ By default, arguments are required. You can make them optional by setting `optional` to `true`.
107
+
108
+ ```ruby
109
+ class HappyBirthdayService < ApplicationService
110
+ arg :name, type: String
111
+ arg :age, type: Integer, optional: true
112
+ end
113
+ ```
114
+
115
+ ## Default Values
116
+
117
+ Set a default value for an argument to make it optional.
118
+
119
+ ```ruby
120
+ class HappyBirthdayService < ApplicationService
121
+ arg :name, type: String
122
+ arg :age, type: Integer, default: 18
123
+ end
124
+ ```
125
+
126
+ ### Complex Default Values
127
+
128
+ Default values are deep duplicated when the service is invoked, making it safe to use mutable objects.
129
+
130
+ ```ruby
131
+ arg :options, type: Hash, default: { a: 1, b: 2 }
132
+ ```
133
+
134
+ ### Procs as Default Values
135
+
136
+ Use procs for dynamic default values.
137
+
138
+ ```ruby
139
+ arg :current_date, type: Date, default: -> { Date.current }
140
+ ```
141
+
142
+ ## Inheritance
143
+
144
+ Arguments are inherited from parent classes.
145
+
146
+ ```ruby
147
+ # UpdateRecordService
148
+ class UpdateRecordService < ApplicationService
149
+ # Arguments
150
+ arg :record, type: ApplicationRecord
151
+ arg :attributes, type: Hash
152
+
153
+ # Steps
154
+ step :authorize
155
+ step :update_record
156
+ end
157
+ ```
158
+
159
+ ```ruby
160
+ # User::Update inherited from UpdateRecordService
161
+ class User::Update < UpdateRecordService
162
+ # Nothing to do here
163
+ # Arguments and steps are inherited from UpdateRecordService
164
+ end
165
+ ```
166
+
167
+ ### Removing Inherited Arguments
168
+
169
+ To remove an inherited argument, use `remove_arg`:
170
+
171
+ ```ruby
172
+ class BaseService < ApplicationService
173
+ arg :current_user, type: User
174
+ arg :audit_log, type: [TrueClass, FalseClass], default: true
175
+ end
176
+
177
+ class SystemTaskService < BaseService
178
+ # System tasks don't need a current_user
179
+ remove_arg :current_user
180
+ end
181
+ ```
182
+
183
+ ## Context Arguments
184
+
185
+ 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`.
186
+
187
+ Learn more about context in the [Context documentation](context.md).
188
+
189
+ ```ruby
190
+ class ApplicationService < Light::Services::Base
191
+ arg :current_user, type: User, optional: true, context: true
192
+ end
193
+ ```
194
+
195
+ ## Accessing Arguments
196
+
197
+ Arguments are accessible like instance variables, similar to `attr_accessor`.
198
+
199
+ ```ruby
200
+ class HappyBirthdayService < ApplicationService
201
+ # Arguments
202
+ arg :name, type: String
203
+ arg :age, type: Integer
204
+
205
+ # Steps
206
+ step :greet
207
+
208
+ private
209
+
210
+ def greet
211
+ puts "Happy birthday, #{name}! You are #{age} years old."
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## Accessing Arguments Using `arguments`
217
+
218
+ For dynamic access or to avoid conflicts, use the `arguments` method.
219
+
220
+ ```ruby
221
+ class HappyBirthdayService < ApplicationService
222
+ # Arguments
223
+ arg :name, type: String
224
+ arg :age, type: Integer
225
+
226
+ # Steps
227
+ step :greet
228
+
229
+ private
230
+
231
+ def greet
232
+ name = arguments[:name] # or arguments.get(:name)
233
+ age = arguments[:age] # or arguments.get(:age)
234
+
235
+ puts "Happy birthday, #{name}! You are #{age} years old."
236
+ end
237
+ end
238
+ ```
239
+
240
+ ## Argument Predicate Methods
241
+
242
+ Predicate methods are automatically generated for each argument, allowing you to check if an argument is `true` or `false`.
243
+
244
+ ```ruby
245
+ class User::GenerateInvoice < ApplicationService
246
+ # Arguments
247
+ arg :user, type: User
248
+ arg :charge, type: [TrueClass, FalseClass], default: false
249
+
250
+ # Steps
251
+ step :generate_invoice
252
+ step :charge_user, if: :charge?
253
+
254
+ # ...
255
+ end
256
+ ```
257
+
258
+ {% hint style="info" %}
259
+ The predicate methods return `true` or `false` based on Ruby's convention: `nil` and `false` are `false`, everything else is `true`.
260
+ {% endhint %}
261
+
262
+ ## What's Next?
263
+
264
+ Next step is `steps` (I love this pun). Steps are the building blocks of a service, the methods that do the actual work.
265
+
266
+ [Next: Steps](steps.md)
267
+
@@ -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)