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
@@ -0,0 +1,204 @@
1
+ # Configuration
2
+
3
+ Light Services provides a flexible configuration system that allows you to customize behavior at three levels: global, per-service, and per-call.
4
+
5
+ ## Global Configuration
6
+
7
+ Configure Light Services globally using an initializer. For Rails applications, create `config/initializers/light_services.rb`:
8
+
9
+ ```ruby
10
+ Light::Services.configure do |config|
11
+ # Type enforcement
12
+ config.require_type = true # Require type option for all arguments and outputs
13
+
14
+ # Transaction settings
15
+ config.use_transactions = true # Wrap each service in a database transaction
16
+
17
+ # Error behavior
18
+ config.load_errors = true # Copy errors to parent service in context chain
19
+ config.break_on_error = true # Stop step execution when an error is added
20
+ config.raise_on_error = false # Raise an exception when an error is added
21
+ config.rollback_on_error = true # Rollback transaction when an error is added
22
+
23
+ # Warning behavior
24
+ config.load_warnings = true # Copy warnings to parent service in context chain
25
+ config.break_on_warning = false # Stop step execution when a warning is added
26
+ config.raise_on_warning = false # Raise an exception when a warning is added
27
+ config.rollback_on_warning = false # Rollback transaction when a warning is added
28
+ end
29
+ ```
30
+
31
+ ## Default Values
32
+
33
+ | Option | Default | Description |
34
+ |--------|---------|-------------|
35
+ | `require_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments or outputs without a `type` option |
36
+ | `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
37
+ | `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
38
+ | `break_on_error` | `true` | Stops executing remaining steps when an error is added |
39
+ | `raise_on_error` | `false` | Raises `Light::Services::Error` when an error is added |
40
+ | `rollback_on_error` | `true` | Rolls back the transaction when an error is added |
41
+ | `load_warnings` | `true` | Propagates warnings to parent service when using `.with(self)` |
42
+ | `break_on_warning` | `false` | Stops executing remaining steps when a warning is added |
43
+ | `raise_on_warning` | `false` | Raises `Light::Services::Error` when a warning is added |
44
+ | `rollback_on_warning` | `false` | Rolls back the transaction when a warning is added |
45
+
46
+ ## Per-Service Configuration
47
+
48
+ Override global configuration for a specific service class using the `config` class method:
49
+
50
+ ```ruby
51
+ class CriticalPaymentService < ApplicationService
52
+ # This service will raise exceptions instead of collecting errors
53
+ config raise_on_error: true
54
+
55
+ step :process_payment
56
+ step :send_receipt
57
+
58
+ # ...
59
+ end
60
+ ```
61
+
62
+ ```ruby
63
+ class NonCriticalNotificationService < ApplicationService
64
+ # This service doesn't need transactions and shouldn't stop on errors
65
+ config use_transactions: false, break_on_error: false
66
+
67
+ step :send_push_notification
68
+ step :send_email_notification
69
+
70
+ # ...
71
+ end
72
+ ```
73
+
74
+ ## Per-Call Configuration
75
+
76
+ Override configuration for a single service call:
77
+
78
+ ```ruby
79
+ # Pass config as second argument to run
80
+ MyService.run({ name: "John" }, { raise_on_error: true })
81
+
82
+ # Or use with() for context-based calls
83
+ MyService.with({ raise_on_error: true }).run(name: "John")
84
+
85
+ # Combine with parent service context
86
+ ChildService
87
+ .with(self, { use_transactions: false })
88
+ .run(data: some_data)
89
+ ```
90
+
91
+ ## Configuration Precedence
92
+
93
+ Configuration is merged in this order (later overrides earlier):
94
+
95
+ 1. Global configuration (from initializer)
96
+ 2. Per-service configuration (from `config` class method)
97
+ 3. Per-call configuration (from `run` or `with` arguments)
98
+
99
+ ```ruby
100
+ # Global: raise_on_error = false
101
+ Light::Services.configure do |config|
102
+ config.raise_on_error = false
103
+ end
104
+
105
+ # Per-service: raise_on_error = true (overrides global)
106
+ class MyService < ApplicationService
107
+ config raise_on_error: true
108
+ end
109
+
110
+ # Per-call: raise_on_error = false (overrides per-service)
111
+ MyService.run(args, { raise_on_error: false })
112
+ ```
113
+
114
+ ## Common Configuration Patterns
115
+
116
+ ### Strict Mode for Critical Services
117
+
118
+ ```ruby
119
+ class Payment::Process < ApplicationService
120
+ config raise_on_error: true, rollback_on_error: true
121
+
122
+ # Any error will raise an exception and rollback the transaction
123
+ end
124
+ ```
125
+
126
+ ### Fire-and-Forget Services
127
+
128
+ ```ruby
129
+ class Analytics::Track < ApplicationService
130
+ config use_transactions: false, break_on_error: false, load_errors: false
131
+
132
+ # Errors won't stop execution or propagate to parent services
133
+ end
134
+ ```
135
+
136
+ ### Background Job Services
137
+
138
+ ```ruby
139
+ class BackgroundTaskService < ApplicationService
140
+ # Background jobs typically handle their own transactions
141
+ config use_transactions: false
142
+ end
143
+ ```
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
+
177
+ ## Disabling Transactions
178
+
179
+ If you're not using ActiveRecord or want to manage transactions yourself:
180
+
181
+ ```ruby
182
+ Light::Services.configure do |config|
183
+ config.use_transactions = false
184
+ end
185
+ ```
186
+
187
+ Or disable for specific services:
188
+
189
+ ```ruby
190
+ class MyService < ApplicationService
191
+ config use_transactions: false
192
+ end
193
+ ```
194
+
195
+ {% hint style="info" %}
196
+ When `use_transactions` is `true`, Light Services uses `ActiveRecord::Base.transaction(requires_new: true)` to create savepoints, allowing nested services to rollback independently.
197
+ {% endhint %}
198
+
199
+ ## What's Next?
200
+
201
+ Now that you understand configuration, learn about the core concepts:
202
+
203
+ [Next: Concepts](concepts.md)
204
+
data/docs/context.md ADDED
@@ -0,0 +1,128 @@
1
+ # Context
2
+
3
+ Context allows services to be run within the same execution scope, enabling shared state and coordinated transactions.
4
+
5
+ ## Key Features
6
+
7
+ - Services share arguments marked as `context: true`
8
+ - If any service fails, the entire context fails and rolls back database changes
9
+
10
+ ## How to Run Services in the Same Context
11
+
12
+ To run a service in the same context, call `with(self)` before the `#run` method.
13
+
14
+ ## Context Rollback
15
+
16
+ ### Example:
17
+
18
+ Let's say we have two services: `User::Create` and `Profile::Create`. We want to ensure that if either service fails, all database changes are rolled back.
19
+
20
+ ```ruby
21
+ class User::Create < ApplicationService
22
+ # Arguments
23
+ arg :attributes, type: Hash
24
+
25
+ # Steps
26
+ step :create_user
27
+ step :create_profile
28
+ step :send_welcome_email
29
+
30
+ # Outputs
31
+ output :user, type: User
32
+ output :profile, type: Profile
33
+
34
+ def create_user
35
+ self.user = User.create!(attributes)
36
+ end
37
+
38
+ def create_profile
39
+ service = Profile::Create
40
+ .with(self) # This runs the service in the same context
41
+ .run(user:)
42
+
43
+ self.profile = service.profile
44
+ end
45
+
46
+ # If the Profile::Create service fails, this step and any following steps won't execute
47
+ # And all database changes will be rolled back
48
+ def send_welcome_email
49
+ # We don't run this service in the same context
50
+ # Because we don't care too much if it fails
51
+ service = Mailer::SendWelcomeEmail.run(user:)
52
+
53
+ # Handle the failure manually if needed
54
+ if service.failed?
55
+ # Handle the failure
56
+ end
57
+ end
58
+ end
59
+ ```
60
+
61
+ ## Context Arguments
62
+
63
+ Context arguments are shared between services running in the same context. This can make them a bit less predictable and harder to test.
64
+
65
+ It's recommended to use context arguments only when necessary and keep them as close to the root service as possible. For example, you can use them to share `current_user` or `current_organization` between services.
66
+
67
+ ```ruby
68
+ class ApplicationService < Light::Services::Base
69
+ arg :current_user, type: User, context: true
70
+ end
71
+ ```
72
+
73
+ ```ruby
74
+ class Comment::Create < ApplicationService
75
+ # Arguments
76
+ # We don't need to specify current_user here
77
+ # as it's automatically inherited from the ApplicationService
78
+ arg :post_id, type: Integer
79
+ arg :text, type: String
80
+ arg :subscribe, type: [TrueClass, FalseClass]
81
+
82
+ # Steps
83
+ step :create_comment
84
+ step :subscribe_to_post, if: :subscribe?
85
+
86
+ private
87
+
88
+ def create_comment
89
+ # ...
90
+ end
91
+
92
+ def subscribe_to_post
93
+ Post::Subscribe
94
+ .with(self) # Run service in the same context
95
+ .run(post_id:) # We omit current_user here as context will handle it for us
96
+
97
+ # If we run Post::Subscribe without `with(self)`
98
+ # It'll fail because it won't have information about the `current_user`
99
+ end
100
+ end
101
+ ```
102
+
103
+ ```ruby
104
+ class Post::Subscribe < ApplicationService
105
+ # Arguments
106
+ arg :post_id, type: Integer
107
+
108
+ # Steps
109
+ step :subscribe
110
+
111
+ private
112
+
113
+ def subscribe
114
+ # We have access to current_user here because we run it in the same context
115
+ #
116
+ # Even if we would run this service without context this won't be a problem
117
+ # because we specified this argument in top-level service (ApplicationService)
118
+ current_user.subscriptions.create!(post_id:)
119
+ end
120
+ end
121
+ ```
122
+
123
+ # What's Next?
124
+
125
+ The next step is to learn about error handling in Light Service.
126
+
127
+ [Next: Errors](errors.md)
128
+