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/Gemfile.lock ADDED
@@ -0,0 +1,149 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ operandi (5.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activemodel (8.1.2)
10
+ activesupport (= 8.1.2)
11
+ activerecord (8.1.2)
12
+ activemodel (= 8.1.2)
13
+ activesupport (= 8.1.2)
14
+ timeout (>= 0.4.0)
15
+ activesupport (8.1.2)
16
+ base64
17
+ bigdecimal
18
+ concurrent-ruby (~> 1.0, >= 1.3.1)
19
+ connection_pool (>= 2.2.5)
20
+ drb
21
+ i18n (>= 1.6, < 2)
22
+ json
23
+ logger (>= 1.4.2)
24
+ minitest (>= 5.1)
25
+ securerandom (>= 0.3)
26
+ tzinfo (~> 2.0, >= 2.0.5)
27
+ uri (>= 0.13.1)
28
+ ast (2.4.3)
29
+ base64 (0.3.0)
30
+ benchmark-malloc (0.2.0)
31
+ benchmark-perf (0.6.0)
32
+ benchmark-trend (0.4.0)
33
+ bigdecimal (4.0.1)
34
+ concurrent-ruby (1.3.6)
35
+ connection_pool (2.5.5)
36
+ database_cleaner-active_record (2.2.2)
37
+ activerecord (>= 5.a)
38
+ database_cleaner-core (~> 2.0)
39
+ database_cleaner-core (2.0.1)
40
+ diff-lcs (1.6.2)
41
+ docile (1.4.1)
42
+ drb (2.2.3)
43
+ i18n (1.14.8)
44
+ concurrent-ruby (~> 1.0)
45
+ json (2.18.0)
46
+ language_server-protocol (3.17.0.5)
47
+ lint_roller (1.1.0)
48
+ logger (1.7.0)
49
+ mini_portile2 (2.8.9)
50
+ minitest (6.0.1)
51
+ prism (~> 1.5)
52
+ openssl (3.3.2)
53
+ parallel (1.27.0)
54
+ parser (3.3.10.1)
55
+ ast (~> 2.4.1)
56
+ racc
57
+ prism (1.8.0)
58
+ racc (1.8.1)
59
+ rainbow (3.1.1)
60
+ rake (13.3.1)
61
+ regexp_parser (2.11.3)
62
+ rexml (3.4.4)
63
+ rspec (3.13.2)
64
+ rspec-core (~> 3.13.0)
65
+ rspec-expectations (~> 3.13.0)
66
+ rspec-mocks (~> 3.13.0)
67
+ rspec-benchmark (0.6.0)
68
+ benchmark-malloc (~> 0.2)
69
+ benchmark-perf (~> 0.6)
70
+ benchmark-trend (~> 0.4)
71
+ rspec (>= 3.0)
72
+ rspec-core (3.13.6)
73
+ rspec-support (~> 3.13.0)
74
+ rspec-expectations (3.13.5)
75
+ diff-lcs (>= 1.2.0, < 2.0)
76
+ rspec-support (~> 3.13.0)
77
+ rspec-mocks (3.13.7)
78
+ diff-lcs (>= 1.2.0, < 2.0)
79
+ rspec-support (~> 3.13.0)
80
+ rspec-support (3.13.6)
81
+ rubocop (1.82.1)
82
+ json (~> 2.3)
83
+ language_server-protocol (~> 3.17.0.2)
84
+ lint_roller (~> 1.1.0)
85
+ parallel (~> 1.10)
86
+ parser (>= 3.3.0.2)
87
+ rainbow (>= 2.2.2, < 4.0)
88
+ regexp_parser (>= 2.9.3, < 3.0)
89
+ rubocop-ast (>= 1.48.0, < 2.0)
90
+ ruby-progressbar (~> 1.7)
91
+ unicode-display_width (>= 2.4.0, < 4.0)
92
+ rubocop-ast (1.49.0)
93
+ parser (>= 3.3.7.2)
94
+ prism (~> 1.7)
95
+ rubocop-performance (1.26.1)
96
+ lint_roller (~> 1.1)
97
+ rubocop (>= 1.75.0, < 2.0)
98
+ rubocop-ast (>= 1.47.1, < 2.0)
99
+ rubocop-rake (0.7.1)
100
+ lint_roller (~> 1.1)
101
+ rubocop (>= 1.72.1)
102
+ rubocop-rspec (3.9.0)
103
+ lint_roller (~> 1.1)
104
+ rubocop (~> 1.81)
105
+ ruby-progressbar (1.13.0)
106
+ securerandom (0.4.1)
107
+ simplecov (0.22.0)
108
+ docile (~> 1.1)
109
+ simplecov-html (~> 0.11)
110
+ simplecov_json_formatter (~> 0.1)
111
+ simplecov-cobertura (3.1.0)
112
+ rexml
113
+ simplecov (~> 0.19)
114
+ simplecov-html (0.13.2)
115
+ simplecov_json_formatter (0.1.4)
116
+ sorbet-runtime (0.6.12894)
117
+ sqlite3 (2.9.0)
118
+ mini_portile2 (~> 2.8.0)
119
+ timeout (0.6.0)
120
+ tzinfo (2.0.6)
121
+ concurrent-ruby (~> 1.0)
122
+ unicode-display_width (3.2.0)
123
+ unicode-emoji (~> 4.1)
124
+ unicode-emoji (4.2.0)
125
+ uri (1.1.1)
126
+
127
+ PLATFORMS
128
+ ruby
129
+
130
+ DEPENDENCIES
131
+ activerecord (>= 8)
132
+ connection_pool (< 3)
133
+ database_cleaner-active_record
134
+ openssl (= 3.3.2)
135
+ operandi!
136
+ rake
137
+ rspec
138
+ rspec-benchmark
139
+ rubocop
140
+ rubocop-performance
141
+ rubocop-rake
142
+ rubocop-rspec
143
+ simplecov
144
+ simplecov-cobertura
145
+ sorbet-runtime
146
+ sqlite3
147
+
148
+ BUNDLED WITH
149
+ 2.5.10
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Andrew Emelianenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # πŸš€ Operandi
2
+
3
+ Operandi is a simple yet powerful way to organize business logic in Ruby applications. Build services that are easy to test, maintain, and understand.
4
+
5
+ ![GitHub CI](https://github.com/akodkod/operandi/actions/workflows/ci.yml/badge.svg)
6
+ [![Codecov](https://codecov.io/gh/akodkod/operandi/graph/badge.svg?token=IGJNZ2BQ26)](https://codecov.io/gh/akodkod/operandi)
7
+
8
+ [Get started with Quickstart](https://operandi.kodkod.me/quickstart)
9
+
10
+ ## Features
11
+
12
+ - ✨ **Simple**: Define your service as a class with `arguments`, `steps`, and `outputs`
13
+ - πŸ“¦ **No runtime dependencies**: Works stand-alone without requiring external gems at runtime
14
+ - πŸ”„ **Transactions**: Automatically rollback database changes if any step fails
15
+ - 🧬 **Inheritance**: Inherit from other services to reuse logic seamlessly
16
+ - ⚠️ **Error Handling**: Collect errors from steps and handle them your way
17
+ - πŸ”— **Context**: Run multiple services sequentially within the same context
18
+ - πŸ§ͺ **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
19
+ - 🌐 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
20
+ - 🧩 **Modularity**: Isolate and test your services with ease
21
+ - πŸ”· **Sorbet & Tapioca**: Full support for Sorbet type checking and Tapioca DSL generation
22
+ - βœ… **100% Test Coverage**: Thoroughly tested and reliable
23
+ - βš”οΈ **Battle-Tested**: In production use since 2017
24
+
25
+ ## Installation
26
+
27
+ ```ruby
28
+ gem "operandi", "~> 5.0"
29
+ ```
30
+
31
+ ```bash
32
+ rails generate operandi:install
33
+ ```
34
+
35
+ ## Simple Example
36
+
37
+ ```ruby
38
+ class GreetService < Operandi::Base
39
+ # Arguments
40
+ arg :name, type: String
41
+ arg :age, type: Integer
42
+
43
+ # Steps
44
+ step :build_message
45
+ step :send_message
46
+
47
+ # Outputs
48
+ output :message, type: String
49
+
50
+ private
51
+
52
+ def build_message
53
+ self.message = "Hello, #{name}! You are #{age} years old."
54
+ end
55
+
56
+ def send_message
57
+ # Send logic goes here
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Advanced Example (with Sorbet types and conditions)
63
+
64
+ ```ruby
65
+ class User::ResetPassword < Operandi::Base
66
+ # Arguments with Sorbet types
67
+ arg :user, type: User, optional: true
68
+ arg :email, type: String, optional: true
69
+ arg :send_email, type: T::Boolean, default: true
70
+ arg :metadata, type: T::Hash[Symbol, String], default: {}
71
+ arg :notify_channels, type: T::Array[Symbol], default: [:email]
72
+
73
+ # Steps
74
+ step :validate
75
+ step :find_user, unless: :user?
76
+ step :generate_reset_token
77
+ step :save_reset_token
78
+ step :send_reset_email, if: :send_email?
79
+
80
+ # Outputs
81
+ output :user, type: User
82
+ output :reset_token, type: String
83
+ output :notifications_sent, type: T::Array[Symbol]
84
+
85
+ private
86
+
87
+ def validate
88
+ errors.add(:base, "user or email is required") if !user? && !email?
89
+ end
90
+
91
+ def find_user
92
+ self.user = User.find_by("LOWER(email) = ?", email.downcase)
93
+ errors.add(:email, "not found") unless user
94
+ end
95
+
96
+ def generate_reset_token
97
+ self.reset_token = SecureRandom.hex(32)
98
+ end
99
+
100
+ def save_reset_token
101
+ user.update!(
102
+ reset_password_token: reset_token,
103
+ reset_password_sent_at: Time.current,
104
+ )
105
+ rescue ActiveRecord::RecordInvalid => e
106
+ errors.from_record(e.record)
107
+ end
108
+
109
+ def send_reset_email
110
+ Mailer::SendEmail
111
+ .with(self) # Call sub-service with the same context
112
+ .run(template: :reset_password, user:, reset_token:)
113
+ end
114
+ end
115
+ ```
116
+
117
+ [Get started with Operandi](https://operandi.kodkod.me/quickstart)
118
+
119
+ ## Rails Generators
120
+
121
+ Operandi includes Rails generators to help you quickly set up and create services in your Rails application.
122
+
123
+ ### Install Generator
124
+
125
+ Set up Operandi in your Rails application:
126
+
127
+ ```bash
128
+ bin/rails generate operandi:install
129
+ ```
130
+
131
+ This creates:
132
+ - `app/services/application_service.rb` - Base service class for your application
133
+ - `config/initializers/operandi.rb` - Configuration file
134
+ - `spec/services/application_service_spec.rb` - RSpec test file (if RSpec is detected)
135
+
136
+ **Options:**
137
+ - `--skip-initializer` - Skip creating the initializer file
138
+ - `--skip-spec` - Skip creating the spec file
139
+
140
+ ### Service Generator
141
+
142
+ Create a new service class:
143
+
144
+ ```bash
145
+ # Basic service
146
+ bin/rails generate operandi:service user/create
147
+
148
+ # Service with predefined structure
149
+ bin/rails generate operandi:service CreateOrder \
150
+ --args=user product \
151
+ --steps=validate process \
152
+ --outputs=order
153
+ ```
154
+
155
+ This creates:
156
+ - `app/services/user/create.rb` - Service class file
157
+ - `spec/services/user/create_spec.rb` - RSpec test file (if RSpec is detected)
158
+
159
+ **Options:**
160
+ - `--args` - List of arguments for the service (e.g., `--args=user product`)
161
+ - `--steps` - List of steps for the service (e.g., `--steps=validate process`)
162
+ - `--outputs` - List of outputs for the service (e.g., `--outputs=result`)
163
+ - `--skip-spec` - Skip creating the spec file
164
+ - `--parent` - Parent class (default: ApplicationService)
165
+
166
+ ## Documentation
167
+
168
+ You can find the full documentation at [operandi.kodkod.me](https://operandi.kodkod.me).
169
+
170
+ ## License
171
+
172
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "operandi"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,57 @@
1
+ Operandi/ArgumentTypeRequired:
2
+ Enabled: true
3
+ Description: "Requires a `type:` option for all arguments."
4
+
5
+ Operandi/DeprecatedAccessors:
6
+ Enabled: true
7
+ Description: "Detects deprecated `arguments` and `outputs` accessors."
8
+
9
+ Operandi/ConditionMethodExists:
10
+ Enabled: true
11
+ Description: "Ensures condition methods referenced in `if:` or `unless:` exist."
12
+
13
+ Operandi/DeprecatedMethods:
14
+ Enabled: true
15
+ Description: "Detects usage of deprecated Operandi methods."
16
+
17
+ Operandi/DslOrder:
18
+ Enabled: true
19
+ Description: "Enforces the order of DSL methods in Operandi services."
20
+
21
+ Operandi/MissingPrivateKeyword:
22
+ Enabled: true
23
+ Description: "Ensures `private` keyword is present in Operandi services."
24
+
25
+ Operandi/NoDirectInstantiation:
26
+ Enabled: true
27
+ Description: "Detects direct instantiation of Operandi services."
28
+
29
+ Operandi/NoHashArgument:
30
+ Enabled: false
31
+ Description: "Detects hash arguments passed to `.run` or `.run!` instead of keyword arguments."
32
+ ServicePattern: ~
33
+
34
+ Operandi/OutputTypeRequired:
35
+ Enabled: true
36
+ Description: "Requires a `type:` option for all outputs."
37
+
38
+ Operandi/PreferOptionalOverDefaultNil:
39
+ Enabled: true
40
+ Description: "Prefers `optional: true` over `default: nil`."
41
+
42
+ Operandi/PreferFailMethod:
43
+ Enabled: true
44
+ Description: "Prefers `fail!` over manual error handling."
45
+
46
+ Operandi/RedundantOptional:
47
+ Enabled: true
48
+ Description: "Detects redundant `optional: true` when a default value is provided."
49
+
50
+ Operandi/ReservedName:
51
+ Enabled: true
52
+ Description: "Detects reserved argument and output names."
53
+
54
+ Operandi/StepMethodExists:
55
+ Enabled: true
56
+ Description: "Ensures step methods exist in Operandi services."
57
+ ExcludedSteps: []
data/docs/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Operandi
2
+
3
+ Operandi is a simple yet powerful way to organize business logic in Ruby applications. Build services that are easy to test, maintain, and understand.
4
+
5
+ [Get started with Quickstart](quickstart.md)
6
+
7
+ ## Features
8
+
9
+ - ✨ **Simple**: Define your service as a class with `arguments`, `steps`, and `outputs`
10
+ - πŸ“¦ **No runtime dependencies**: Works stand-alone without requiring external gems at runtime
11
+ - πŸ”„ **Transactions**: Automatically rollback database changes if any step fails
12
+ - 🧬 **Inheritance**: Inherit from other services to reuse logic seamlessly
13
+ - ⚠️ **Error Handling**: Collect errors from steps and handle them your way
14
+ - πŸ”— **Context**: Run multiple services sequentially within the same context
15
+ - πŸ§ͺ **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
16
+ - πŸ” **RuboCop Integration**: Custom cops to enforce best practices at lint time
17
+ - 🌐 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
18
+ - 🧩 **Modularity**: Isolate and test your services with ease
19
+ - πŸ”· **Sorbet & Tapioca**: Full support for Sorbet type checking and Tapioca DSL generation
20
+ - βœ… **100% Test Coverage**: Thoroughly tested and reliable
21
+ - βš”οΈ **Battle-Tested**: In production use since 2017
22
+
23
+ ## Simple Example
24
+
25
+ ```ruby
26
+ class GreetService < Operandi::Base
27
+ # Arguments
28
+ arg :name, type: String
29
+ arg :age, type: Integer
30
+
31
+ # Steps
32
+ step :build_message
33
+ step :send_message
34
+
35
+ # Outputs
36
+ output :message, type: String
37
+
38
+ private
39
+
40
+ def build_message
41
+ self.message = "Hello, #{name}! You are #{age} years old."
42
+ end
43
+
44
+ def send_message
45
+ # Send logic goes here
46
+ end
47
+ end
48
+ ```
49
+
50
+ ## Advanced Example (with Sorbet types and conditions)
51
+
52
+ ```ruby
53
+ class User::ResetPassword < Operandi::Base
54
+ # Arguments with Sorbet types
55
+ arg :user, type: User, optional: true
56
+ arg :email, type: String, optional: true
57
+ arg :send_email, type: T::Boolean, default: true
58
+ arg :metadata, type: T::Hash[Symbol, String], default: {}
59
+ arg :notify_channels, type: T::Array[Symbol], default: [:email]
60
+
61
+ # Steps
62
+ step :validate
63
+ step :find_user, unless: :user?
64
+ step :generate_reset_token
65
+ step :save_reset_token
66
+ step :send_reset_email, if: :send_email?
67
+
68
+ # Outputs
69
+ output :user, type: User
70
+ output :reset_token, type: String
71
+ output :notifications_sent, type: T::Array[Symbol]
72
+
73
+ private
74
+
75
+ def validate
76
+ errors.add(:base, "user or email is required") if !user? && !email?
77
+ end
78
+
79
+ def find_user
80
+ self.user = User.find_by("LOWER(email) = ?", email.downcase)
81
+ errors.add(:email, "not found") unless user
82
+ end
83
+
84
+ def generate_reset_token
85
+ self.reset_token = SecureRandom.hex(32)
86
+ end
87
+
88
+ def save_reset_token
89
+ user.update!(
90
+ reset_password_token: reset_token,
91
+ reset_password_sent_at: Time.current,
92
+ )
93
+ rescue ActiveRecord::RecordInvalid => e
94
+ errors.from_record(e.record)
95
+ end
96
+
97
+ def send_reset_email
98
+ Mailer::SendEmail
99
+ .with(self) # Call sub-service with the same context
100
+ .run(template: :reset_password, user:, reset_token:)
101
+ end
102
+ end
103
+ ```
104
+
105
+ [Get started with Operandi](quickstart.md)
data/docs/SUMMARY.md ADDED
@@ -0,0 +1,31 @@
1
+ # Summary​
2
+
3
+ ## Introduction
4
+
5
+ * [Operandi](README.md)
6
+ * [Quickstart](quickstart.md)
7
+ * [Concepts](concepts.md)
8
+
9
+ ## Deep Dive
10
+
11
+ * [Arguments](arguments.md)
12
+ * [Steps](steps.md)
13
+ * [Outputs](outputs.md)
14
+ * [Context](context.md)
15
+ * [Errors](errors.md)
16
+ * [Callbacks](callbacks.md)
17
+ * [Configuration](configuration.md)
18
+ * [Testing](testing.md)
19
+ * [Rails Generators](generators.md)
20
+ * [RuboCop Integration](rubocop.md)
21
+ * [Ruby LSP Integration](ruby-lsp.md)
22
+ * [Sorbet Runtime Types](sorbet-runtime.md)
23
+ * [Tapioca / Sorbet Integration](tapioca.md)
24
+
25
+ ## Examples
26
+
27
+ * [Best Practices](best-practices.md)
28
+ * [Recipes](recipes.md)
29
+ * [CRUD](crud.md)
30
+ * [Service Rendering](service-rendering.md)
31
+ * [Pundit Authorization](pundit-authorization.md)