light-services 3.0.0 → 3.1.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/CHANGELOG.md +21 -0
  4. data/CLAUDE.md +1 -1
  5. data/Gemfile.lock +1 -1
  6. data/README.md +11 -11
  7. data/docs/{readme.md → README.md} +12 -11
  8. data/docs/{summary.md → SUMMARY.md} +11 -1
  9. data/docs/arguments.md +23 -0
  10. data/docs/concepts.md +19 -19
  11. data/docs/configuration.md +36 -0
  12. data/docs/errors.md +31 -1
  13. data/docs/outputs.md +23 -0
  14. data/docs/quickstart.md +1 -1
  15. data/docs/rubocop.md +285 -0
  16. data/docs/ruby-lsp.md +133 -0
  17. data/docs/steps.md +62 -8
  18. data/docs/testing.md +1 -1
  19. data/lib/light/services/base.rb +110 -7
  20. data/lib/light/services/base_with_context.rb +23 -1
  21. data/lib/light/services/callbacks.rb +293 -41
  22. data/lib/light/services/collection.rb +50 -2
  23. data/lib/light/services/concerns/execution.rb +3 -0
  24. data/lib/light/services/config.rb +83 -3
  25. data/lib/light/services/constants.rb +3 -0
  26. data/lib/light/services/dsl/arguments_dsl.rb +1 -0
  27. data/lib/light/services/dsl/outputs_dsl.rb +1 -0
  28. data/lib/light/services/dsl/validation.rb +30 -0
  29. data/lib/light/services/exceptions.rb +19 -1
  30. data/lib/light/services/message.rb +28 -3
  31. data/lib/light/services/messages.rb +74 -2
  32. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  33. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  34. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  35. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  36. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  37. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  38. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  39. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  40. data/lib/light/services/rubocop.rb +12 -0
  41. data/lib/light/services/settings/field.rb +33 -5
  42. data/lib/light/services/settings/step.rb +23 -5
  43. data/lib/light/services/version.rb +1 -1
  44. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  45. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  46. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  47. metadata +17 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a45a0a0ba2e870c7188f55865004d697646da536e3cecb05b9b0eb30fda9651d
4
- data.tar.gz: 4ad5df86f1a1ff129ac85f3f2ed1ecda11466d52c8639fbbfda410951e8dc8c5
3
+ metadata.gz: a7b40ae2a9930c6ee5831c6d5f64f84aeab383b4f35295a41faf8ae9e0735c82
4
+ data.tar.gz: 8bb7c192556ec28c904bd931abbf502c2533b39137c2d7d989469c11c61cd70d
5
5
  SHA512:
6
- metadata.gz: 75bbc704971352293401dfcfaf0191aceb3d19ebfd150ce4db1aac0140b4044ee65942ff205484379a7169342298a7164fb76f1fbbf1907d3bb33d99485d1b03
7
- data.tar.gz: 231a847015fcabfa0373693c0f13ac31d2ea2d636aae355b5ec023838efd3a15ae0b4baba0d7604f3c8980272419078268d155cd9d5b4ca7ca9e22f976667c62
6
+ metadata.gz: 671d913a895af97d0c37e45d21e930794317d60fac03bd46df3e43ef3dc6190617b5b5522f445f57143d96434104df636f5d29cf9c6c35d60604f61f54781dfa
7
+ data.tar.gz: eacef0ee6679653963d6b09a6c660c9ee503e8edc3df04411547f36e07bea91210d2683ff02ed1ee6d96924062c6e1620f95b87959e40ac80e78a211c7048f96
data/.rubocop.yml CHANGED
@@ -58,6 +58,9 @@ RSpec/MultipleDescribes:
58
58
  RSpec/MultipleExpectations:
59
59
  Max: 10
60
60
 
61
+ RSpec/MultipleMemoizedHelpers:
62
+ Max: 10
63
+
61
64
  RSpec/NestedGroups:
62
65
  Max: 5
63
66
 
@@ -101,4 +104,7 @@ Style/TrailingCommaInHashLiteral:
101
104
  EnforcedStyleForMultiline: consistent_comma
102
105
 
103
106
  Style/WordArray:
104
- EnforcedStyle: brackets
107
+ EnforcedStyle: brackets
108
+
109
+ Style/NumericPredicate:
110
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.1 (2025-12-13)
4
+
5
+ ### Added
6
+
7
+ - Better IDE support for callbacks DSL
8
+
9
+ ## 3.1.0 (2025-12-13)
10
+
11
+ ### Breaking changes
12
+
13
+ - Enforce arguments and output types by default. Add `config.require_type = false` to your config to disable this behavior.
14
+
15
+ ### Added
16
+
17
+ - `stop!` and `stopped?` methods for early exit (renamed from `done!` and `done?`)
18
+ - `stop_immediately!` method for immediate execution halt within the current step
19
+ - `done!` and `done?` are deprecated, but remain available as aliases for backward compatibility
20
+ - Ruby LSP support with step navigation and indexing
21
+ - Rubocop cops `StepMethodExists`, `ConditionMethodExists`, `DslOrder`, `MissingPrivateKeyword`, `NoDirectInstantiation`, `ArgumentTypeRequired`, `OutputTypeRequired`, `DeprecatedMethods`
22
+ - Comprehensive YARD documentation
23
+
3
24
  ## 3.0.0 (2025-12-12)
4
25
 
5
26
  ### Breaking changes
data/CLAUDE.md CHANGED
@@ -93,7 +93,7 @@ class ExampleService < Light::Services::Base
93
93
  end
94
94
 
95
95
  def cleanup
96
- # Runs regardless of errors/warnings, unless done! was called
96
+ # Runs regardless of errors/warnings, unless stop! was called
97
97
  end
98
98
 
99
99
  def should_process?
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- light-services (3.0.0)
4
+ light-services (3.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -36,15 +36,15 @@ rails generate light_services:install
36
36
  ```ruby
37
37
  class GreetService < Light::Services::Base
38
38
  # Arguments
39
- arg :name
40
- arg :age
39
+ arg :name, type: String
40
+ arg :age, type: Integer
41
41
 
42
42
  # Steps
43
43
  step :build_message
44
44
  step :send_message
45
45
 
46
46
  # Outputs
47
- output :message
47
+ output :message, type: String
48
48
 
49
49
  private
50
50
 
@@ -58,14 +58,14 @@ class GreetService < Light::Services::Base
58
58
  end
59
59
  ```
60
60
 
61
- ## Advanced Example
61
+ ## Advanced Example (with dry-types and conditions)
62
62
 
63
63
  ```ruby
64
64
  class User::ResetPassword < Light::Services::Base
65
- # Arguments
66
- arg :user, type: User, optional: true
67
- arg :email, type: String, optional: true
68
- arg :send_email, type: [TrueClass, FalseClass], 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
69
69
 
70
70
  # Steps
71
71
  step :validate
@@ -74,9 +74,9 @@ class User::ResetPassword < Light::Services::Base
74
74
  step :save_reset_token
75
75
  step :send_reset_email, if: :send_email?
76
76
 
77
- # Outputs
78
- output :user, type: User
79
- 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
80
80
 
81
81
  private
82
82
 
@@ -13,6 +13,7 @@ Light Services is a simple yet powerful way to organize business logic in Ruby a
13
13
  - ⚠️ **Error Handling**: Collect errors from steps and handle them your way
14
14
  - 🔗 **Context**: Run multiple services sequentially within the same context
15
15
  - 🧪 **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
16
+ - 🔍 **RuboCop Integration**: Custom cops to enforce best practices at lint time
16
17
  - 🌐 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
17
18
  - 🧩 **Modularity**: Isolate and test your services with ease
18
19
  - ✅ **100% Test Coverage**: Thoroughly tested and reliable
@@ -23,15 +24,15 @@ Light Services is a simple yet powerful way to organize business logic in Ruby a
23
24
  ```ruby
24
25
  class GreetService < Light::Services::Base
25
26
  # Arguments
26
- arg :name
27
- arg :age
27
+ arg :name, type: String
28
+ arg :age, type: Integer
28
29
 
29
30
  # Steps
30
31
  step :build_message
31
32
  step :send_message
32
33
 
33
34
  # Outputs
34
- output :message
35
+ output :message, type: String
35
36
 
36
37
  private
37
38
 
@@ -45,14 +46,14 @@ class GreetService < Light::Services::Base
45
46
  end
46
47
  ```
47
48
 
48
- ## Advanced Example
49
+ ## Advanced Example (with dry-types and conditions)
49
50
 
50
51
  ```ruby
51
52
  class User::ResetPassword < Light::Services::Base
52
- # Arguments
53
- arg :user, type: User, optional: true
54
- arg :email, type: String, optional: true
55
- arg :send_email, type: [TrueClass, FalseClass], default: true
53
+ # Arguments with dry-types for advanced validation and coercion
54
+ arg :user, type: Types.Instance(User), optional: true
55
+ arg :email, type: Types::Coercible::String, optional: true
56
+ arg :send_email, type: Types::Params::Bool, default: true
56
57
 
57
58
  # Steps
58
59
  step :validate
@@ -61,9 +62,9 @@ class User::ResetPassword < Light::Services::Base
61
62
  step :save_reset_token
62
63
  step :send_reset_email, if: :send_email?
63
64
 
64
- # Outputs
65
- output :user, type: User
66
- output :reset_token, type: String
65
+ # Outputs with dry-types
66
+ output :user, type: Types.Instance(User)
67
+ output :reset_token, type: Types::Strict::String
67
68
 
68
69
  private
69
70
 
@@ -1,8 +1,13 @@
1
- # Table of contents
1
+ # Summary​
2
+
3
+ ## Introduction
2
4
 
3
5
  * [Light Services](README.md)
4
6
  * [Quickstart](quickstart.md)
5
7
  * [Concepts](concepts.md)
8
+
9
+ ## Deep Dive
10
+
6
11
  * [Arguments](arguments.md)
7
12
  * [Steps](steps.md)
8
13
  * [Outputs](outputs.md)
@@ -12,6 +17,11 @@
12
17
  * [Configuration](configuration.md)
13
18
  * [Testing](testing.md)
14
19
  * [Rails Generators](generators.md)
20
+ * [RuboCop Integration](rubocop.md)
21
+ * [Ruby LSP Integration](ruby-lsp.md)
22
+
23
+ ## Examples
24
+
15
25
  * [Best Practices](best-practices.md)
16
26
  * [Recipes](recipes.md)
17
27
  * [CRUD](crud.md)
data/docs/arguments.md CHANGED
@@ -55,6 +55,29 @@ class HappyBirthdayService < ApplicationService
55
55
  end
56
56
  ```
57
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
+
58
81
  ### dry-types Support
59
82
 
60
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.
data/docs/concepts.md CHANGED
@@ -7,39 +7,39 @@ This section covers the core concepts of Light Services: **Arguments**, **Steps*
7
7
  When you call `MyService.run(args)`, the following happens:
8
8
 
9
9
  ```
10
- ┌─────────────────────────────────────────────────────────────┐
10
+ ┌──────────────────────────────────────────────────────────────┐
11
11
  │ Service.run(args) │
12
- ├─────────────────────────────────────────────────────────────┤
12
+ ├──────────────────────────────────────────────────────────────┤
13
13
  │ 1. Load default values for arguments and outputs │
14
14
  │ 2. Validate argument types │
15
15
  │ 3. Run before_service_run callbacks │
16
- ├─────────────────────────────────────────────────────────────┤
16
+ ├──────────────────────────────────────────────────────────────┤
17
17
  │ 4. Begin around_service_run callback │
18
18
  │ 5. Begin database transaction (if use_transactions: true) │
19
- │ ┌─────────────────────────────────────────────────────┐
20
- │ │ 6. Execute steps in order │
21
- │ │ - Run before_step_run / around_step_run │
22
- │ │ - Execute step method │
23
- │ │ - Run after_step_run / on_step_success │
24
- │ │ - Skip if condition (if:/unless:) not met │
25
- │ │ - Stop if errors.break? is true │
26
- │ │ - Stop if done! was called │
27
- │ ├─────────────────────────────────────────────────────┤
28
- │ │ 7. On error → Rollback transaction │
29
- │ │ On success → Commit transaction │
30
- │ └─────────────────────────────────────────────────────┘
19
+ │ ┌─────────────────────────────────────────────────────┐
20
+ │ │ 6. Execute steps in order │
21
+ │ │ - Run before_step_run / around_step_run │
22
+ │ │ - Execute step method │
23
+ │ │ - Run after_step_run / on_step_success │
24
+ │ │ - Skip if condition (if:/unless:) not met │
25
+ │ │ - Stop if errors.break? is true │
26
+ │ │ - Stop if stop! was called │
27
+ │ ├─────────────────────────────────────────────────────┤
28
+ │ │ 7. On error → Rollback transaction │
29
+ │ │ On success → Commit transaction │
30
+ │ └─────────────────────────────────────────────────────┘
31
31
  │ 8. End around_service_run callback │
32
- ├─────────────────────────────────────────────────────────────┤
33
- │ 9. Run steps marked with always: true (unless done! called) │
32
+ ├──────────────────────────────────────────────────────────────┤
33
+ │ 9. Run steps marked with always: true (unless stop! called) │
34
34
  │ 10. Validate output types (if success) │
35
35
  │ 11. Copy errors/warnings to parent service (if in context) │
36
36
  │ 12. Run after_service_run callback │
37
37
  │ 13. Run on_service_success or on_service_failure callback │
38
- ├─────────────────────────────────────────────────────────────┤
38
+ ├──────────────────────────────────────────────────────────────┤
39
39
  │ 14. Return service instance │
40
40
  │ - service.success? / service.failed? │
41
41
  │ - service.outputs / service.errors │
42
- └─────────────────────────────────────────────────────────────┘
42
+ └──────────────────────────────────────────────────────────────┘
43
43
  ```
44
44
 
45
45
  ## Arguments
@@ -8,6 +8,9 @@ Configure Light Services globally using an initializer. For Rails applications,
8
8
 
9
9
  ```ruby
10
10
  Light::Services.configure do |config|
11
+ # Type enforcement
12
+ config.require_type = true # Require type option for all arguments and outputs
13
+
11
14
  # Transaction settings
12
15
  config.use_transactions = true # Wrap each service in a database transaction
13
16
 
@@ -29,6 +32,7 @@ end
29
32
 
30
33
  | Option | Default | Description |
31
34
  |--------|---------|-------------|
35
+ | `require_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments or outputs without a `type` option |
32
36
  | `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
33
37
  | `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
34
38
  | `break_on_error` | `true` | Stops executing remaining steps when an error is added |
@@ -138,6 +142,38 @@ class BackgroundTaskService < ApplicationService
138
142
  end
139
143
  ```
140
144
 
145
+ ### Type Enforcement (Enabled by Default)
146
+
147
+ By default, all arguments and outputs must have a `type` option. This helps catch type-related bugs early and makes your services self-documenting.
148
+
149
+ ```ruby
150
+ class User::Create < ApplicationService
151
+ arg :name, type: String # ✓ Valid
152
+ arg :email # ✗ Raises MissingTypeError
153
+ output :user, type: User # ✓ Valid
154
+ output :token # ✗ Raises MissingTypeError
155
+ end
156
+ ```
157
+
158
+ To disable type enforcement globally (not recommended):
159
+
160
+ ```ruby
161
+ Light::Services.configure do |config|
162
+ config.require_type = false
163
+ end
164
+ ```
165
+
166
+ Or disable for specific services:
167
+
168
+ ```ruby
169
+ class LegacyService < ApplicationService
170
+ config require_type: false
171
+
172
+ arg :data # Allowed when require_type is disabled
173
+ output :result # Allowed when require_type is disabled
174
+ end
175
+ ```
176
+
141
177
  ## Disabling Transactions
142
178
 
143
179
  If you're not using ActiveRecord or want to manage transactions yourself:
data/docs/errors.md CHANGED
@@ -201,10 +201,40 @@ Light Services defines several exception classes for different error scenarios:
201
201
  | Exception | Description |
202
202
  |-----------|-------------|
203
203
  | `Light::Services::Error` | Base exception class for all Light Services errors |
204
- | `Light::Services::ArgTypeError` | Raised when an argument type validation fails |
204
+ | `Light::Services::ArgTypeError` | Raised when an argument or output type validation fails |
205
205
  | `Light::Services::ReservedNameError` | Raised when using a reserved name for arguments, outputs, or steps |
206
206
  | `Light::Services::InvalidNameError` | Raised when using an invalid name format |
207
207
  | `Light::Services::NoStepsError` | Raised when a service has no steps defined and no `run` method |
208
+ | `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `require_type` is enabled |
209
+
210
+ ### MissingTypeError
211
+
212
+ This exception is raised when you define an argument or output without a `type` option. Since `require_type` is enabled by default, all arguments and outputs must have a type.
213
+
214
+ ```ruby
215
+ class MyService < ApplicationService
216
+ arg :name # => raises Light::Services::MissingTypeError
217
+ end
218
+ ```
219
+
220
+ To fix this, add a `type` option to all arguments and outputs:
221
+
222
+ ```ruby
223
+ class MyService < ApplicationService
224
+ arg :name, type: String
225
+ output :result, type: Hash
226
+ end
227
+ ```
228
+
229
+ If you need to disable type enforcement for legacy services, you can use the `config` method:
230
+
231
+ ```ruby
232
+ class LegacyService < ApplicationService
233
+ config require_type: false
234
+
235
+ arg :data # Allowed when require_type is disabled
236
+ end
237
+ ```
208
238
 
209
239
  ### NoStepsError
210
240
 
data/docs/outputs.md CHANGED
@@ -78,6 +78,29 @@ class AI::Chat < ApplicationService
78
78
  end
79
79
  ```
80
80
 
81
+ ### Type Enforcement (Enabled by Default)
82
+
83
+ By default, all outputs must have a `type` option. This helps catch type-related bugs early and makes your services self-documenting.
84
+
85
+ ```ruby
86
+ class MyService < ApplicationService
87
+ output :result, type: Hash # ✓ Valid
88
+ output :data # ✗ Raises MissingTypeError
89
+ end
90
+ ```
91
+
92
+ To disable type enforcement for a specific service:
93
+
94
+ ```ruby
95
+ class LegacyService < ApplicationService
96
+ config require_type: false
97
+
98
+ output :data # Allowed when require_type is disabled
99
+ end
100
+ ```
101
+
102
+ See the [Configuration documentation](configuration.md) for more details.
103
+
81
104
  ### dry-types Support
82
105
 
83
106
  Outputs also support [dry-types](https://dry-rb.org/gems/dry-types) for advanced type validation and coercion.
data/docs/quickstart.md CHANGED
@@ -74,7 +74,7 @@ class GreetService < ApplicationService
74
74
  step :greet
75
75
 
76
76
  # Outputs
77
- output :greeted, default: false
77
+ output :greeted, type: [TrueClass, FalseClass], default: false
78
78
 
79
79
  private
80
80