operandi 5.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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/rules/services/RULE.md +269 -0
  3. data/.cursor/rules/services-rspec/RULE.md +354 -0
  4. data/.github/dependabot.yml +11 -0
  5. data/.github/workflows/ci.yml +77 -0
  6. data/.gitignore +25 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +134 -0
  9. data/.ruby-version +1 -0
  10. data/.vscode/cspell.json +18 -0
  11. data/.vscode/project-words.txt +12 -0
  12. data/AGENTS.md +139 -0
  13. data/CHANGELOG.md +111 -0
  14. data/CLAUDE.md +139 -0
  15. data/CODE_OF_CONDUCT.md +74 -0
  16. data/Gemfile +28 -0
  17. data/Gemfile.lock +149 -0
  18. data/LICENSE.txt +21 -0
  19. data/README.md +172 -0
  20. data/Rakefile +8 -0
  21. data/bin/console +15 -0
  22. data/bin/setup +8 -0
  23. data/config/default.yml +57 -0
  24. data/docs/README.md +105 -0
  25. data/docs/SUMMARY.md +31 -0
  26. data/docs/arguments.md +275 -0
  27. data/docs/best-practices.md +153 -0
  28. data/docs/callbacks.md +475 -0
  29. data/docs/concepts.md +79 -0
  30. data/docs/configuration.md +218 -0
  31. data/docs/context.md +128 -0
  32. data/docs/crud.md +525 -0
  33. data/docs/errors.md +331 -0
  34. data/docs/generators.md +250 -0
  35. data/docs/outputs.md +150 -0
  36. data/docs/pundit-authorization.md +320 -0
  37. data/docs/quickstart.md +133 -0
  38. data/docs/recipes.md +14 -0
  39. data/docs/rubocop.md +430 -0
  40. data/docs/ruby-lsp.md +121 -0
  41. data/docs/service-rendering.md +222 -0
  42. data/docs/sorbet-runtime.md +283 -0
  43. data/docs/steps.md +438 -0
  44. data/docs/tapioca.md +188 -0
  45. data/docs/testing.md +548 -0
  46. data/lib/generators/operandi/install/USAGE +15 -0
  47. data/lib/generators/operandi/install/install_generator.rb +45 -0
  48. data/lib/generators/operandi/install/templates/application_service.rb.tt +8 -0
  49. data/lib/generators/operandi/install/templates/application_service_spec.rb.tt +7 -0
  50. data/lib/generators/operandi/install/templates/initializer.rb.tt +30 -0
  51. data/lib/generators/operandi/service/USAGE +21 -0
  52. data/lib/generators/operandi/service/service_generator.rb +80 -0
  53. data/lib/generators/operandi/service/templates/service.rb.tt +48 -0
  54. data/lib/generators/operandi/service/templates/service_spec.rb.tt +40 -0
  55. data/lib/operandi/base.rb +230 -0
  56. data/lib/operandi/base_with_context.rb +57 -0
  57. data/lib/operandi/callbacks.rb +353 -0
  58. data/lib/operandi/collection.rb +166 -0
  59. data/lib/operandi/concerns/execution.rb +80 -0
  60. data/lib/operandi/concerns/parent_service.rb +32 -0
  61. data/lib/operandi/concerns/state_management.rb +34 -0
  62. data/lib/operandi/config.rb +142 -0
  63. data/lib/operandi/constants.rb +96 -0
  64. data/lib/operandi/dsl/arguments_dsl.rb +83 -0
  65. data/lib/operandi/dsl/outputs_dsl.rb +79 -0
  66. data/lib/operandi/dsl/steps_dsl.rb +206 -0
  67. data/lib/operandi/dsl/validation.rb +171 -0
  68. data/lib/operandi/exceptions.rb +66 -0
  69. data/lib/operandi/message.rb +52 -0
  70. data/lib/operandi/messages.rb +185 -0
  71. data/lib/operandi/rspec/matchers/define_argument.rb +172 -0
  72. data/lib/operandi/rspec/matchers/define_output.rb +145 -0
  73. data/lib/operandi/rspec/matchers/define_step.rb +223 -0
  74. data/lib/operandi/rspec/matchers/execute_step.rb +228 -0
  75. data/lib/operandi/rspec/matchers/have_error_on.rb +144 -0
  76. data/lib/operandi/rspec/matchers/have_warning_on.rb +146 -0
  77. data/lib/operandi/rspec/matchers/trigger_callback.rb +136 -0
  78. data/lib/operandi/rspec.rb +15 -0
  79. data/lib/operandi/rubocop/cop/operandi/argument_type_required.rb +52 -0
  80. data/lib/operandi/rubocop/cop/operandi/condition_method_exists.rb +173 -0
  81. data/lib/operandi/rubocop/cop/operandi/deprecated_accessors.rb +113 -0
  82. data/lib/operandi/rubocop/cop/operandi/deprecated_methods.rb +113 -0
  83. data/lib/operandi/rubocop/cop/operandi/dsl_order.rb +181 -0
  84. data/lib/operandi/rubocop/cop/operandi/missing_private_keyword.rb +102 -0
  85. data/lib/operandi/rubocop/cop/operandi/no_direct_instantiation.rb +66 -0
  86. data/lib/operandi/rubocop/cop/operandi/no_hash_argument.rb +101 -0
  87. data/lib/operandi/rubocop/cop/operandi/output_type_required.rb +52 -0
  88. data/lib/operandi/rubocop/cop/operandi/prefer_fail_method.rb +112 -0
  89. data/lib/operandi/rubocop/cop/operandi/prefer_optional_over_default_nil.rb +124 -0
  90. data/lib/operandi/rubocop/cop/operandi/redundant_optional.rb +103 -0
  91. data/lib/operandi/rubocop/cop/operandi/reserved_name.rb +56 -0
  92. data/lib/operandi/rubocop/cop/operandi/step_method_exists.rb +134 -0
  93. data/lib/operandi/rubocop.rb +22 -0
  94. data/lib/operandi/settings/field.rb +147 -0
  95. data/lib/operandi/settings/step.rb +105 -0
  96. data/lib/operandi/utils.rb +36 -0
  97. data/lib/operandi/version.rb +5 -0
  98. data/lib/operandi.rb +11 -0
  99. data/lib/ruby_lsp/operandi/addon.rb +37 -0
  100. data/lib/ruby_lsp/operandi/definition.rb +134 -0
  101. data/lib/ruby_lsp/operandi/indexing_enhancement.rb +224 -0
  102. data/lib/tapioca/dsl/compilers/operandi.rb +378 -0
  103. data/operandi.gemspec +33 -0
  104. data/rbi/operandi.rbi +197 -0
  105. data/sorbet/cache/data.mdb +0 -0
  106. data/sorbet/cache/lock.mdb +0 -0
  107. metadata +151 -0
data/docs/steps.md ADDED
@@ -0,0 +1,438 @@
1
+ # Steps
2
+
3
+ Steps are the core components of a service, each representing a unit of work executed in sequence when the service is called.
4
+
5
+ ## TL;DR
6
+
7
+ - Define steps using the `step` keyword within the service class
8
+ - Use `if` and `unless` options for conditional steps
9
+ - Inherit steps from parent classes
10
+ - Inject steps into the execution flow with `before` and `after` options
11
+ - Ensure cleanup steps run with the `always: true` option (unless `stop!` was called)
12
+ - Use a `run` method as a simple alternative for single-step services
13
+
14
+ ```ruby
15
+ class GeneralParserService < ApplicationService
16
+ step :create_browser, unless: :browser
17
+ step :parse_content
18
+ step :quit_browser, always: true
19
+ end
20
+
21
+ class ParsePage < GeneralParserService
22
+ step :parse_additional_content, after: :parse_content
23
+ end
24
+ ```
25
+
26
+ ## Define Steps
27
+
28
+ Steps are declared using the `step` keyword in your service class.
29
+
30
+ ```ruby
31
+ class User::Charge < ApplicationService
32
+ step :authorize
33
+ step :charge
34
+ step :send_email_receipt
35
+
36
+ private
37
+
38
+ def authorize
39
+ # ...
40
+ end
41
+
42
+ def charge
43
+ # ...
44
+ end
45
+
46
+ def send_email_receipt
47
+ # ...
48
+ end
49
+ end
50
+ ```
51
+
52
+ ## Conditional Steps
53
+
54
+ Steps can be conditional, executed based on specified conditions using the `if` or `unless` keywords.
55
+
56
+ ```ruby
57
+ class User::Charge < ApplicationService
58
+ step :authorize
59
+ step :charge
60
+ step :send_email_receipt, if: :send_receipt?
61
+
62
+ # ...
63
+
64
+ def send_receipt?
65
+ rand(2).zero?
66
+ end
67
+ end
68
+ ```
69
+
70
+ This feature works well with argument predicates.
71
+
72
+ ```ruby
73
+ class User::Charge < ApplicationService
74
+ arg :send_receipt, type: [TrueClass, FalseClass], default: true
75
+
76
+ step :send_email_receipt, if: :send_receipt?
77
+
78
+ # ...
79
+ end
80
+ ```
81
+
82
+ ### Using Procs for Conditions
83
+
84
+ You can also use Procs (lambdas) for inline conditions:
85
+
86
+ ```ruby
87
+ class User::Charge < ApplicationService
88
+ arg :amount, type: Float
89
+
90
+ step :apply_discount, if: -> { amount > 100 }
91
+ step :charge
92
+ step :send_large_purchase_alert, if: -> { amount > 1000 }
93
+
94
+ # ...
95
+ end
96
+ ```
97
+
98
+ {% hint style="info" %}
99
+ Using Procs can make simple conditions more readable, but for complex logic, prefer extracting to a method.
100
+ {% endhint %}
101
+
102
+ ## Inheritance
103
+
104
+ Steps are inherited from parent classes, making it easy to build upon existing services.
105
+
106
+ ```ruby
107
+ # UpdateRecordService
108
+ class UpdateRecordService < ApplicationService
109
+ arg :record, type: ApplicationRecord
110
+ arg :attributes, type: Hash
111
+
112
+ step :authorize
113
+ step :update_record
114
+ end
115
+ ```
116
+
117
+ ```ruby
118
+ # User::Update inherited from UpdateRecordService
119
+ class User::Update < UpdateRecordService
120
+ # Arguments and steps are inherited from UpdateRecordService
121
+ end
122
+ ```
123
+
124
+ ## Injecting Steps into Execution Flow
125
+
126
+ Steps can be injected at specific points in the execution flow using `before` and `after` options.
127
+
128
+ Let's enhance the previous example by adding a step to send a notification after updating the record.
129
+
130
+ ```ruby
131
+ # User::Update inherited from UpdateRecordService
132
+ class User::Update < UpdateRecordService
133
+ step :log_action, before: :authorize
134
+ step :send_notification, after: :update_record
135
+
136
+ private
137
+
138
+ def log_action
139
+ # ...
140
+ end
141
+
142
+ def send_notification
143
+ # ...
144
+ end
145
+ end
146
+ ```
147
+
148
+ Combine this with `if` and `unless` options for more control.
149
+
150
+ ```ruby
151
+ step :send_notification, after: :update_record, if: :send_notification?
152
+ ```
153
+
154
+ {% hint style="info" %}
155
+ By default, if neither `before` nor `after` is specified, the step is added at the end of the execution flow.
156
+ {% endhint %}
157
+
158
+ ## Always Running Steps
159
+
160
+ To ensure certain steps run regardless of previous step outcomes (errors, warnings, failed validations), use the `always: true` option. This is particularly useful for cleanup tasks, error logging, etc.
161
+
162
+ Note: if `stop!` was called, the service exits early and `always: true` steps will **not** run.
163
+
164
+ ```ruby
165
+ class ParsePage < ApplicationService
166
+ arg :url, type: String
167
+
168
+ step :create_browser
169
+ step :parse_content
170
+ step :quit_browser, always: true
171
+
172
+ private
173
+
174
+ attr_accessor :browser
175
+
176
+ def create_browser
177
+ self.browser = Watir::Browser.new
178
+ end
179
+
180
+ def parse_content
181
+ # ...
182
+ end
183
+
184
+ def quit_browser
185
+ browser&.quit
186
+ end
187
+ end
188
+ ```
189
+
190
+ ## Early Exit with `stop!`
191
+
192
+ Use `stop!` to stop executing remaining steps without adding an error. This is useful when you've completed the service's goal early and don't need to run subsequent steps.
193
+
194
+ ```ruby
195
+ class User::FindOrCreate < ApplicationService
196
+ arg :email, type: String
197
+
198
+ step :find_existing_user
199
+ step :create_user
200
+ step :send_welcome_email
201
+
202
+ output :user
203
+
204
+ private
205
+
206
+ def find_existing_user
207
+ self.user = User.find_by(email:)
208
+ stop! if user # Skip remaining steps if user already exists
209
+ end
210
+
211
+ def create_user
212
+ self.user = User.create!(email:)
213
+ end
214
+
215
+ def send_welcome_email
216
+ # Only runs for newly created users
217
+ Mailer.welcome(user).deliver_later
218
+ end
219
+ end
220
+ ```
221
+
222
+ You can check if `stop!` was called using `stopped?`:
223
+
224
+ ```ruby
225
+ def some_step
226
+ stop!
227
+
228
+ # This code still runs within the same step
229
+ puts "Stopped? #{stopped?}" # => "Stopped? true"
230
+ end
231
+
232
+ def next_step
233
+ # This step will NOT run because stop! was called
234
+ end
235
+ ```
236
+
237
+ {% hint style="info" %}
238
+ `stop!` stops subsequent steps from running, including steps marked with `always: true`. Code after `stop!` within the same step method will still execute.
239
+ {% endhint %}
240
+
241
+ {% hint style="success" %}
242
+ **Database Transactions:** Calling `stop!` does NOT rollback database transactions. All database changes made before `stop!` was called will be committed.
243
+ {% endhint %}
244
+
245
+ ## Immediate Exit with `stop_immediately!`
246
+
247
+ Use `stop_immediately!` when you need to halt execution immediately, even within the current step. Unlike `stop!`, code after `stop_immediately!` in the same step method will NOT execute.
248
+
249
+ ```ruby
250
+ class Payment::Process < ApplicationService
251
+ arg :amount, type: Integer
252
+ arg :card_token, type: String
253
+
254
+ step :validate_card
255
+ step :charge_card
256
+ step :send_receipt
257
+
258
+ output :transaction_id, type: String
259
+
260
+ private
261
+
262
+ def validate_card
263
+ unless valid_card?(card_token)
264
+ errors.add(:card, "is invalid")
265
+ stop_immediately! # Exit immediately - don't run any more code
266
+ end
267
+
268
+ # This code won't run if card is invalid
269
+ log_validation_success
270
+ end
271
+
272
+ def charge_card
273
+ # This step won't run if stop_immediately! was called
274
+ self.transaction_id = PaymentGateway.charge(amount, card_token)
275
+ end
276
+
277
+ def send_receipt
278
+ Mailer.receipt(transaction_id).deliver_later
279
+ end
280
+ end
281
+ ```
282
+
283
+ {% hint style="warning" %}
284
+ `stop_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will NOT run when `stop_immediately!` is called.
285
+ {% endhint %}
286
+
287
+ {% hint style="success" %}
288
+ **Database Transactions:** Calling `stop_immediately!` does NOT rollback database transactions. All database changes made before `stop_immediately!` was called will be committed.
289
+ {% endhint %}
290
+
291
+ ## Immediate Failure with `fail_immediately!`
292
+
293
+ Use `fail_immediately!` when you need to halt execution immediately AND rollback any database transactions. Unlike `stop_immediately!`, this method adds an error and causes transaction rollback.
294
+
295
+ ```ruby
296
+ class Payment::Process < ApplicationService
297
+ arg :amount, type: Integer
298
+ arg :card_token, type: String
299
+
300
+ step :validate_card
301
+ step :charge_card
302
+ step :send_receipt
303
+
304
+ output :transaction_id, type: String
305
+
306
+ private
307
+
308
+ def validate_card
309
+ unless valid_card?(card_token)
310
+ fail_immediately!("Card validation failed")
311
+ end
312
+
313
+ # This code won't run if card is invalid
314
+ log_validation_success
315
+ end
316
+
317
+ def charge_card
318
+ # This step won't run if fail_immediately! was called
319
+ self.transaction_id = PaymentGateway.charge(amount, card_token)
320
+ end
321
+ end
322
+ ```
323
+
324
+ {% hint style="info" %}
325
+ `fail_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will still run when `fail_immediately!` is called, allowing for cleanup operations.
326
+ {% endhint %}
327
+
328
+ {% hint style="danger" %}
329
+ **Database Transactions:** Calling `fail_immediately!` DOES rollback database transactions. All database changes made before `fail_immediately!` was called will be rolled back.
330
+ {% endhint %}
331
+
332
+ ### Comparison Table
333
+
334
+ | Method | Adds Error | Stops Execution | Transaction Rollback |
335
+ |--------|------------|-----------------|---------------------|
336
+ | `stop!` | No | After current step | No |
337
+ | `stop_immediately!` | No | Immediately | No |
338
+ | `fail!(msg)` | Yes (:base) | After current step* | No |
339
+ | `fail_immediately!(msg)` | Yes (:base) | Immediately | Yes |
340
+
341
+ *By default, adding an error stops subsequent steps from running due to `break_on_add` configuration.
342
+
343
+ ## Removing Inherited Steps
344
+
345
+ When inheriting from a parent service, you can remove steps using `remove_step`:
346
+
347
+ ```ruby
348
+ class UpdateRecordService < ApplicationService
349
+ step :authorize
350
+ step :validate
351
+ step :update_record
352
+ step :send_notification
353
+ end
354
+
355
+ class InternalUpdate < UpdateRecordService
356
+ # Remove authorization for internal system updates
357
+ remove_step :authorize
358
+ remove_step :send_notification
359
+ end
360
+ ```
361
+
362
+ ## Using `run` Method as a Simple Alternative
363
+
364
+ For simple services that don't need multiple steps, you can define a `run` method instead of using the `step` DSL. If no steps are defined, Operandi will automatically use the `run` method as a single step.
365
+
366
+ ```ruby
367
+ class User::SendWelcomeEmail < ApplicationService
368
+ arg :user, type: User
369
+
370
+ private
371
+
372
+ def run
373
+ Mailer.welcome(user).deliver_later
374
+ end
375
+ end
376
+ ```
377
+
378
+ This is equivalent to:
379
+
380
+ ```ruby
381
+ class User::SendWelcomeEmail < ApplicationService
382
+ arg :user, type: User
383
+
384
+ step :run
385
+
386
+ private
387
+
388
+ def run
389
+ Mailer.welcome(user).deliver_later
390
+ end
391
+ end
392
+ ```
393
+
394
+ ### Inheritance with `run` Method
395
+
396
+ The `run` method works with inheritance. If a parent service defines a `run` method, child services will inherit it:
397
+
398
+ ```ruby
399
+ class BaseNotificationService < ApplicationService
400
+ arg :message, type: String
401
+
402
+ private
403
+
404
+ def run
405
+ send_notification(message)
406
+ end
407
+
408
+ def send_notification(msg)
409
+ raise NotImplementedError
410
+ end
411
+ end
412
+
413
+ class SlackNotification < BaseNotificationService
414
+ private
415
+
416
+ def send_notification(msg)
417
+ SlackClient.post(msg)
418
+ end
419
+ end
420
+
421
+ class EmailNotification < BaseNotificationService
422
+ private
423
+
424
+ def send_notification(msg)
425
+ Mailer.notify(msg).deliver_later
426
+ end
427
+ end
428
+ ```
429
+
430
+ {% hint style="info" %}
431
+ If a service has no steps defined and no `run` method (including from parent classes), a `Operandi::NoStepsError` will be raised when the service is executed.
432
+ {% endhint %}
433
+
434
+ # What's Next?
435
+
436
+ Next step is to learn about outputs. Outputs are the results of a service, returned upon completion of service execution.
437
+
438
+ [Next: Outputs](outputs.md)
data/docs/tapioca.md ADDED
@@ -0,0 +1,188 @@
1
+ # Tapioca / Sorbet Integration
2
+
3
+ Operandi provides a [Tapioca](https://github.com/Shopify/tapioca) DSL compiler that generates RBI signatures for methods automatically created by the `arg` and `output` DSL macros. This enables full Sorbet type checking for your services.
4
+
5
+ ## Features
6
+
7
+ When you use the `arg` or `output` keywords, Operandi dynamically generates methods at runtime:
8
+
9
+ ```ruby
10
+ class CreateUser < ApplicationService
11
+ arg :name, type: String
12
+ arg :email, type: String, optional: true
13
+ arg :role, type: [Symbol, String]
14
+
15
+ output :user, type: User
16
+ end
17
+ ```
18
+
19
+ The Tapioca compiler generates RBI signatures for these methods:
20
+
21
+ ```rbi
22
+ # sorbet/rbi/dsl/create_user.rbi
23
+ # typed: true
24
+
25
+ class CreateUser
26
+ sig { returns(String) }
27
+ def name; end
28
+
29
+ sig { returns(T::Boolean) }
30
+ def name?; end
31
+
32
+ sig { returns(T.nilable(String)) }
33
+ def email; end
34
+
35
+ sig { returns(T::Boolean) }
36
+ def email?; end
37
+
38
+ sig { returns(T.any(Symbol, String)) }
39
+ def role; end
40
+
41
+ sig { returns(T::Boolean) }
42
+ def role?; end
43
+
44
+ sig { returns(User) }
45
+ def user; end
46
+
47
+ sig { returns(T::Boolean) }
48
+ def user?; end
49
+
50
+ private
51
+
52
+ sig { params(value: String).returns(String) }
53
+ def name=(value); end
54
+
55
+ sig { params(value: T.nilable(String)).returns(T.nilable(String)) }
56
+ def email=(value); end
57
+
58
+ sig { params(value: T.any(Symbol, String)).returns(T.any(Symbol, String)) }
59
+ def role=(value); end
60
+
61
+ sig { params(value: User).returns(User) }
62
+ def user=(value); end
63
+ end
64
+ ```
65
+
66
+ ## Setup
67
+
68
+ ### 1. Install Tapioca
69
+
70
+ Add Tapioca to your Gemfile:
71
+
72
+ ```ruby
73
+ group :development do
74
+ gem "tapioca", require: false
75
+ end
76
+ ```
77
+
78
+ Then run:
79
+
80
+ ```bash
81
+ bundle install
82
+ bundle exec tapioca init
83
+ ```
84
+
85
+ ### 2. Generate RBI Files
86
+
87
+ The Operandi compiler is automatically discovered by Tapioca. Generate RBI files with:
88
+
89
+ ```bash
90
+ bundle exec tapioca dsl
91
+ ```
92
+
93
+ This will create RBI files in `sorbet/rbi/dsl/` for all your services.
94
+
95
+ ### 3. Re-generate After Changes
96
+
97
+ After adding or modifying `arg`/`output` declarations, regenerate the RBI files:
98
+
99
+ ```bash
100
+ bundle exec tapioca dsl Operandi
101
+ ```
102
+
103
+ ## Type Mappings
104
+
105
+ ### Ruby Types
106
+
107
+ Standard Ruby types are mapped directly:
108
+
109
+ | Ruby Type | Sorbet Type |
110
+ |-----------|-------------|
111
+ | `String` | `::String` |
112
+ | `Integer` | `::Integer` |
113
+ | `Float` | `::Float` |
114
+ | `Hash` | `::Hash` |
115
+ | `Array` | `::Array` |
116
+ | `Symbol` | `::Symbol` |
117
+ | `User` (custom) | `::User` |
118
+
119
+ ### Boolean Types
120
+
121
+ Boolean types are mapped to `T::Boolean`:
122
+
123
+ ```ruby
124
+ arg :active, type: [TrueClass, FalseClass]
125
+ # Generates: sig { returns(T::Boolean) }
126
+ ```
127
+
128
+ ### Union Types
129
+
130
+ Multiple types create union types:
131
+
132
+ ```ruby
133
+ arg :id, type: [String, Integer]
134
+ # Generates: sig { returns(T.any(::String, ::Integer)) }
135
+ ```
136
+
137
+ ### Optional Types
138
+
139
+ Optional arguments/outputs are wrapped in `T.nilable`:
140
+
141
+ ```ruby
142
+ arg :nickname, type: String, optional: true
143
+ # Generates: sig { returns(T.nilable(::String)) }
144
+ ```
145
+
146
+ ### Sorbet Runtime Types
147
+
148
+ Sorbet runtime types are automatically resolved:
149
+
150
+ | Sorbet Type | Generated RBI |
151
+ |-------------|---------------|
152
+ | `T::Boolean` | `T::Boolean` |
153
+ | `T.nilable(String)` | `T.nilable(::String)` |
154
+ | `T::Array[String]` | `T::Array[::String]` |
155
+ | `T::Hash[Symbol, String]` | `T::Hash[::Symbol, ::String]` |
156
+ | `T.any(String, Integer)` | `T.any(::String, ::Integer)` |
157
+
158
+ ## Generated Methods
159
+
160
+ For each `arg` or `output`, three methods are generated:
161
+
162
+ | Method | Return Type | Visibility |
163
+ |--------|-------------|------------|
164
+ | `name` | The declared type | public |
165
+ | `name?` | `T::Boolean` | public |
166
+ | `name=` | The declared type | **private** |
167
+
168
+ ## Inheritance
169
+
170
+ The compiler handles inherited arguments and outputs. If a child service inherits from a parent, the RBI will include methods for both parent and child fields.
171
+
172
+ ## Troubleshooting
173
+
174
+ ### RBI files not generated
175
+
176
+ Ensure Operandi is properly loaded in your application. The compiler only runs if `Operandi::Base` is defined.
177
+
178
+ ### Types showing as `T.untyped`
179
+
180
+ This happens when:
181
+ - No `type:` option is specified for the argument/output
182
+ - The type cannot be resolved (e.g., undefined constant)
183
+
184
+ ## See Also
185
+
186
+ - [Ruby LSP Integration](ruby-lsp.md) - Editor integration without Sorbet
187
+ - [Arguments](arguments.md) - Full `arg` DSL documentation
188
+ - [Outputs](outputs.md) - Full `output` DSL documentation