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/rubocop.md ADDED
@@ -0,0 +1,430 @@
1
+ # RuboCop Integration
2
+
3
+ Operandi provides custom RuboCop cops to help enforce best practices in your service definitions.
4
+
5
+ ## Setup
6
+
7
+ Add this to your `.rubocop.yml`:
8
+
9
+ ```yaml
10
+ require:
11
+ - operandi/rubocop
12
+ ```
13
+
14
+ ## Available Cops
15
+
16
+ ### Operandi/ArgumentTypeRequired
17
+
18
+ Ensures all `arg` declarations include a `type:` option.
19
+
20
+ ```ruby
21
+ # bad
22
+ arg :user_id
23
+ arg :params, default: {}
24
+
25
+ # good
26
+ arg :user_id, type: Integer
27
+ arg :params, type: Hash, default: {}
28
+ ```
29
+
30
+ ### Operandi/OutputTypeRequired
31
+
32
+ Ensures all `output` declarations include a `type:` option.
33
+
34
+ ```ruby
35
+ # bad
36
+ output :result
37
+ output :data, optional: true
38
+
39
+ # good
40
+ output :result, type: Hash
41
+ output :data, type: Hash, optional: true
42
+ ```
43
+
44
+ ### Operandi/StepMethodExists
45
+
46
+ Ensures all `step` declarations have a corresponding method defined.
47
+
48
+ ```ruby
49
+ # bad
50
+ class MyService < ApplicationService
51
+ step :validate
52
+ step :process # missing method
53
+
54
+ private
55
+
56
+ def validate; end
57
+ end
58
+
59
+ # good
60
+ class MyService < ApplicationService
61
+ step :validate
62
+ step :process
63
+
64
+ private
65
+
66
+ def validate; end
67
+ def process; end
68
+ end
69
+ ```
70
+
71
+ **Configuration:** Use `ExcludedSteps` for inherited steps:
72
+
73
+ ```yaml
74
+ Operandi/StepMethodExists:
75
+ ExcludedSteps:
76
+ - initialize_entity
77
+ - assign_attributes
78
+ ```
79
+
80
+ ### Operandi/ConditionMethodExists
81
+
82
+ Ensures symbol conditions (`:if`, `:unless`) have corresponding methods defined.
83
+
84
+ This cop automatically recognizes predicate methods generated by `arg` and `output` declarations (e.g., `arg :user` creates `user?`).
85
+
86
+ ```ruby
87
+ # bad
88
+ class MyService < ApplicationService
89
+ step :notify, if: :should_notify? # missing method
90
+
91
+ private
92
+
93
+ def notify; end
94
+ end
95
+
96
+ # good - explicit method
97
+ class MyService < ApplicationService
98
+ step :notify, if: :should_notify?
99
+
100
+ private
101
+
102
+ def notify; end
103
+ def should_notify?; true; end
104
+ end
105
+
106
+ # good - predicate from arg/output
107
+ class MyService < ApplicationService
108
+ arg :user, type: User, optional: true
109
+
110
+ step :greet, if: :user? # user? is auto-generated
111
+
112
+ private
113
+
114
+ def greet; end
115
+ end
116
+ ```
117
+
118
+ **Configuration:** Use `ExcludedMethods` for inherited condition methods:
119
+
120
+ ```yaml
121
+ Operandi/ConditionMethodExists:
122
+ ExcludedMethods:
123
+ - admin?
124
+ - guest?
125
+ ```
126
+
127
+ ### Operandi/DslOrder
128
+
129
+ Enforces consistent ordering of DSL declarations: `config` → `arg` → `step` → `output`
130
+
131
+ ```ruby
132
+ # bad
133
+ class MyService < ApplicationService
134
+ step :process
135
+ arg :name, type: String
136
+ config raise_on_error: true
137
+ end
138
+
139
+ # good
140
+ class MyService < ApplicationService
141
+ config raise_on_error: true
142
+
143
+ arg :name, type: String
144
+
145
+ step :process
146
+
147
+ output :result, type: Hash
148
+ end
149
+ ```
150
+
151
+ ### Operandi/MissingPrivateKeyword
152
+
153
+ Ensures step methods are defined as private.
154
+
155
+ ```ruby
156
+ # bad
157
+ class MyService < ApplicationService
158
+ step :process
159
+
160
+ def process # should be private
161
+ # implementation
162
+ end
163
+ end
164
+
165
+ # good
166
+ class MyService < ApplicationService
167
+ step :process
168
+
169
+ private
170
+
171
+ def process
172
+ # implementation
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### Operandi/NoDirectInstantiation
178
+
179
+ Prevents direct instantiation of service classes with `.new`.
180
+
181
+ ```ruby
182
+ # bad
183
+ UserService.new(name: "John")
184
+
185
+ # good
186
+ UserService.run(name: "John")
187
+ UserService.run!(name: "John")
188
+ UserService.call(name: "John")
189
+ ```
190
+
191
+ **Configuration:** Customize the pattern for service class detection:
192
+
193
+ ```yaml
194
+ Operandi/NoDirectInstantiation:
195
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
196
+ ```
197
+
198
+ ### Operandi/DeprecatedMethods
199
+
200
+ Detects deprecated `done!` and `done?` method calls and suggests using `stop!` and `stopped?` instead. Includes autocorrection.
201
+
202
+ ```ruby
203
+ # bad
204
+ class MyService < ApplicationService
205
+ step :process
206
+
207
+ private
208
+
209
+ def process
210
+ done! if condition_met?
211
+ return if done?
212
+ end
213
+ end
214
+
215
+ # good
216
+ class MyService < ApplicationService
217
+ step :process
218
+
219
+ private
220
+
221
+ def process
222
+ stop! if condition_met?
223
+ return if stopped?
224
+ end
225
+ end
226
+ ```
227
+
228
+ **Configuration:** Customize the pattern for service class detection:
229
+
230
+ ```yaml
231
+ Operandi/DeprecatedMethods:
232
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
233
+ ```
234
+
235
+ ### Operandi/PreferFailMethod
236
+
237
+ Detects `errors.add(:base, "message")` calls and suggests using the `fail!("message")` helper instead. Includes autocorrection.
238
+
239
+ ```ruby
240
+ # bad
241
+ class MyService < ApplicationService
242
+ step :process
243
+
244
+ private
245
+
246
+ def process
247
+ errors.add(:base, "user is required")
248
+ errors.add(:base, "invalid input", rollback: false)
249
+ end
250
+ end
251
+
252
+ # good
253
+ class MyService < ApplicationService
254
+ step :process
255
+
256
+ private
257
+
258
+ def process
259
+ fail!("user is required")
260
+ fail!("invalid input", rollback: false)
261
+ end
262
+ end
263
+ ```
264
+
265
+ The cop only detects `errors.add(:base, ...)` calls. It does not flag `errors.add(:field_name, ...)` calls for specific fields, as those should not use `fail!`.
266
+
267
+ **Configuration:** Customize the base service classes to check:
268
+
269
+ ```yaml
270
+ Operandi/PreferFailMethod:
271
+ BaseServiceClasses:
272
+ - ApplicationService
273
+ - BaseCreator
274
+ ```
275
+
276
+ ### Operandi/PreferOptionalOverDefaultNil
277
+
278
+ Detects `default: nil` usage and suggests using `optional: true` instead. Includes autocorrection.
279
+
280
+ ```ruby
281
+ # bad
282
+ class MyService < ApplicationService
283
+ arg :user, type: User, default: nil
284
+ output :result, type: Hash, default: nil
285
+ end
286
+
287
+ # good
288
+ class MyService < ApplicationService
289
+ arg :user, type: User, optional: true
290
+ output :result, type: Hash, optional: true
291
+ end
292
+
293
+ # bad - redundant default: nil
294
+ class MyService < ApplicationService
295
+ arg :user, type: User, optional: true, default: nil
296
+ end
297
+
298
+ # good
299
+ class MyService < ApplicationService
300
+ arg :user, type: User, optional: true
301
+ end
302
+ ```
303
+
304
+ ### Operandi/RedundantOptional
305
+
306
+ Detects redundant `optional: true` when a `default:` value is provided. Includes autocorrection.
307
+
308
+ ```ruby
309
+ # bad
310
+ class MyService < ApplicationService
311
+ arg :name, type: String, optional: true, default: "guest"
312
+ end
313
+
314
+ # good
315
+ class MyService < ApplicationService
316
+ arg :name, type: String, default: "guest"
317
+ end
318
+ ```
319
+
320
+ ### Operandi/ReservedName
321
+
322
+ Detects reserved argument and output names that conflict with Operandi internals.
323
+
324
+ ```ruby
325
+ # bad
326
+ class MyService < ApplicationService
327
+ arg :errors, type: Array
328
+ arg :warnings, type: Array
329
+ output :context, type: Hash
330
+ end
331
+
332
+ # good
333
+ class MyService < ApplicationService
334
+ arg :validation_errors, type: Array
335
+ arg :user_warnings, type: Array
336
+ output :result_context, type: Hash
337
+ end
338
+ ```
339
+
340
+ ### Operandi/NoHashArgument
341
+
342
+ Detects hash arguments passed to `.run` or `.run!` instead of keyword arguments. Disabled by default.
343
+
344
+ ```ruby
345
+ # bad
346
+ UserService.run({ name: "John", age: 30 })
347
+ UserService.run!(options)
348
+
349
+ # good
350
+ UserService.run(name: "John", age: 30)
351
+ UserService.run!(**options)
352
+ ```
353
+
354
+ **Configuration:** Customize the pattern for service class detection:
355
+
356
+ ```yaml
357
+ Operandi/NoHashArgument:
358
+ Enabled: true
359
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
360
+ ```
361
+
362
+ ## Configuration
363
+
364
+ Full configuration example:
365
+
366
+ ```yaml
367
+ require:
368
+ - operandi/rubocop
369
+
370
+ Operandi/ArgumentTypeRequired:
371
+ Enabled: true
372
+
373
+ Operandi/OutputTypeRequired:
374
+ Enabled: true
375
+
376
+ Operandi/StepMethodExists:
377
+ Enabled: true
378
+ ExcludedSteps: []
379
+
380
+ Operandi/ConditionMethodExists:
381
+ Enabled: true
382
+ ExcludedMethods: []
383
+
384
+ Operandi/DslOrder:
385
+ Enabled: true
386
+
387
+ Operandi/MissingPrivateKeyword:
388
+ Enabled: true
389
+
390
+ Operandi/NoDirectInstantiation:
391
+ Enabled: true
392
+ ServicePattern: 'Service$'
393
+
394
+ Operandi/DeprecatedMethods:
395
+ Enabled: true
396
+ ServicePattern: 'Service$'
397
+
398
+ Operandi/PreferFailMethod:
399
+ Enabled: true
400
+ BaseServiceClasses:
401
+ - ApplicationService
402
+
403
+ Operandi/PreferOptionalOverDefaultNil:
404
+ Enabled: true
405
+
406
+ Operandi/RedundantOptional:
407
+ Enabled: true
408
+
409
+ Operandi/ReservedName:
410
+ Enabled: true
411
+
412
+ Operandi/NoHashArgument:
413
+ Enabled: false
414
+ ServicePattern: 'Service$'
415
+ ```
416
+
417
+ To disable a cop for specific files:
418
+
419
+ ```yaml
420
+ Operandi/ArgumentTypeRequired:
421
+ Exclude:
422
+ - 'spec/**/*'
423
+ - 'test/**/*'
424
+ ```
425
+
426
+ ## What's Next?
427
+
428
+ Learn more about testing your services:
429
+
430
+ [Next: Testing](testing.md)
data/docs/ruby-lsp.md ADDED
@@ -0,0 +1,121 @@
1
+ # Ruby LSP Integration
2
+
3
+ Operandi provides a Ruby LSP add-on that enhances your editor experience by informing the language server about methods generated by the `arg` and `output` DSL keywords.
4
+
5
+ ## Features
6
+
7
+ When you use the `arg` or `output` keywords, Operandi dynamically generates methods at runtime:
8
+
9
+ ```ruby
10
+ class MyService < ApplicationService
11
+ arg :user, type: User
12
+ output :result, type: Hash
13
+ end
14
+ ```
15
+
16
+ This generates the following methods:
17
+ - `user` - getter method (returns `User`)
18
+ - `user?` - predicate method (returns boolean)
19
+ - `user=` - setter method (private, accepts `User`)
20
+ - `result` - getter method (returns `Hash`)
21
+ - `result?` - predicate method (returns boolean)
22
+ - `result=` - setter method (private, accepts `Hash`)
23
+
24
+ The Ruby LSP add-on teaches the language server about these generated methods, enabling:
25
+
26
+ - **Go to Definition** - Navigate to the `arg`/`output` declaration
27
+ - **Completion** - Autocomplete generated method names
28
+ - **Hover** - See information about generated methods, including return types
29
+ - **Signature Help** - Get parameter hints for setter methods
30
+ - **Workspace Symbol** - Find generated methods in symbol search
31
+
32
+ ## Setup
33
+
34
+ The add-on is automatically discovered by Ruby LSP when Operandi is in your project's dependencies. No additional configuration is required.
35
+
36
+ ### Requirements
37
+
38
+ - Ruby LSP `~> 0.26` or later
39
+ - Operandi gem installed in your project
40
+
41
+ ### Verification
42
+
43
+ To verify the add-on is loaded, check the Ruby LSP output in your editor. You should see "Ruby LSP Operandi" listed among the active add-ons.
44
+
45
+ ## How It Works
46
+
47
+ The add-on uses Ruby LSP's **indexing enhancement** system to register generated methods during code indexing. When the indexer encounters an `arg` or `output` call with a symbol argument, it automatically registers the three generated methods (getter, predicate, setter) in the index.
48
+
49
+ This is a static analysis approach - the add-on analyzes your source code without executing it. This means:
50
+
51
+ - Methods are recognized immediately as you type
52
+ - No running application is required
53
+ - Works with any editor that supports Ruby LSP
54
+
55
+ ## Type Inference
56
+
57
+ The add-on extracts type information from the `type:` option and includes it as YARD-style documentation comments. This enables hover information to display return types for generated methods.
58
+
59
+ ### Simple Ruby Types
60
+
61
+ ```ruby
62
+ arg :user, type: User # → User
63
+ arg :items, type: Array # → Array
64
+ arg :name, type: String # → String
65
+ ```
66
+
67
+ ### Namespaced Types
68
+
69
+ ```ruby
70
+ arg :payment, type: Stripe::Charge # → Stripe::Charge
71
+ arg :config, type: MyApp::Configuration # → MyApp::Configuration
72
+ ```
73
+
74
+ ### Sorbet Runtime Types
75
+
76
+ Sorbet runtime types are resolved to their underlying Ruby types:
77
+
78
+ | Sorbet Type | Ruby Type |
79
+ |-------------|-----------|
80
+ | `T::Boolean` | `TrueClass \| FalseClass` |
81
+ | `T.nilable(String)` | `String \| NilClass` |
82
+ | `T::Array[String]` | `Array` |
83
+ | `T::Hash[Symbol, String]` | `Hash` |
84
+ | `T.any(String, Integer)` | `String \| Integer` |
85
+
86
+ ### Custom Type Mappings
87
+
88
+ You can add custom type mappings through the Operandi configuration:
89
+
90
+ ```ruby
91
+ # config/initializers/operandi.rb
92
+ Operandi.configure do |config|
93
+ config.ruby_lsp_type_mappings = {
94
+ "Types::UUID" => "String",
95
+ "Types::Money" => "BigDecimal",
96
+ "Types::JSON" => "Hash",
97
+ "CustomTypes::Email" => "String",
98
+ "MyApp::Types::PhoneNumber" => "String",
99
+ }
100
+ end
101
+ ```
102
+
103
+ Custom mappings allow you to:
104
+
105
+ - Add mappings for your own custom types
106
+ - Override default mappings if needed
107
+ - Support domain-specific type modules
108
+
109
+ ## Limitations
110
+
111
+ - Only `arg` and `output` declarations with a symbol as the first argument are recognized
112
+ - The add-on cannot detect dynamically computed argument names (e.g., `arg some_variable`)
113
+ - Inherited arguments/outputs from parent classes are not automatically discovered
114
+ - Parameterized types like `T::Array[String]` resolve to the container type (`Array`), not the full generic type
115
+
116
+ ## What's Next?
117
+
118
+ Learn more about other integrations:
119
+
120
+ - [RuboCop Integration](rubocop.md) - Static analysis cops for services
121
+ - [Testing](testing.md) - Testing your services with RSpec matchers