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.
- checksums.yaml +7 -0
- data/.cursor/rules/services/RULE.md +269 -0
- data/.cursor/rules/services-rspec/RULE.md +354 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yml +77 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/.rubocop.yml +134 -0
- data/.ruby-version +1 -0
- data/.vscode/cspell.json +18 -0
- data/.vscode/project-words.txt +12 -0
- data/AGENTS.md +139 -0
- data/CHANGELOG.md +111 -0
- data/CLAUDE.md +139 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +28 -0
- data/Gemfile.lock +149 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/default.yml +57 -0
- data/docs/README.md +105 -0
- data/docs/SUMMARY.md +31 -0
- data/docs/arguments.md +275 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +475 -0
- data/docs/concepts.md +79 -0
- data/docs/configuration.md +218 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +331 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +150 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +133 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +430 -0
- data/docs/ruby-lsp.md +121 -0
- data/docs/service-rendering.md +222 -0
- data/docs/sorbet-runtime.md +283 -0
- data/docs/steps.md +438 -0
- data/docs/tapioca.md +188 -0
- data/docs/testing.md +548 -0
- data/lib/generators/operandi/install/USAGE +15 -0
- data/lib/generators/operandi/install/install_generator.rb +45 -0
- data/lib/generators/operandi/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/operandi/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/operandi/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/operandi/service/USAGE +21 -0
- data/lib/generators/operandi/service/service_generator.rb +80 -0
- data/lib/generators/operandi/service/templates/service.rb.tt +48 -0
- data/lib/generators/operandi/service/templates/service_spec.rb.tt +40 -0
- data/lib/operandi/base.rb +230 -0
- data/lib/operandi/base_with_context.rb +57 -0
- data/lib/operandi/callbacks.rb +353 -0
- data/lib/operandi/collection.rb +166 -0
- data/lib/operandi/concerns/execution.rb +80 -0
- data/lib/operandi/concerns/parent_service.rb +32 -0
- data/lib/operandi/concerns/state_management.rb +34 -0
- data/lib/operandi/config.rb +142 -0
- data/lib/operandi/constants.rb +96 -0
- data/lib/operandi/dsl/arguments_dsl.rb +83 -0
- data/lib/operandi/dsl/outputs_dsl.rb +79 -0
- data/lib/operandi/dsl/steps_dsl.rb +206 -0
- data/lib/operandi/dsl/validation.rb +171 -0
- data/lib/operandi/exceptions.rb +66 -0
- data/lib/operandi/message.rb +52 -0
- data/lib/operandi/messages.rb +185 -0
- data/lib/operandi/rspec/matchers/define_argument.rb +172 -0
- data/lib/operandi/rspec/matchers/define_output.rb +145 -0
- data/lib/operandi/rspec/matchers/define_step.rb +223 -0
- data/lib/operandi/rspec/matchers/execute_step.rb +228 -0
- data/lib/operandi/rspec/matchers/have_error_on.rb +144 -0
- data/lib/operandi/rspec/matchers/have_warning_on.rb +146 -0
- data/lib/operandi/rspec/matchers/trigger_callback.rb +136 -0
- data/lib/operandi/rspec.rb +15 -0
- data/lib/operandi/rubocop/cop/operandi/argument_type_required.rb +52 -0
- data/lib/operandi/rubocop/cop/operandi/condition_method_exists.rb +173 -0
- data/lib/operandi/rubocop/cop/operandi/deprecated_accessors.rb +113 -0
- data/lib/operandi/rubocop/cop/operandi/deprecated_methods.rb +113 -0
- data/lib/operandi/rubocop/cop/operandi/dsl_order.rb +181 -0
- data/lib/operandi/rubocop/cop/operandi/missing_private_keyword.rb +102 -0
- data/lib/operandi/rubocop/cop/operandi/no_direct_instantiation.rb +66 -0
- data/lib/operandi/rubocop/cop/operandi/no_hash_argument.rb +101 -0
- data/lib/operandi/rubocop/cop/operandi/output_type_required.rb +52 -0
- data/lib/operandi/rubocop/cop/operandi/prefer_fail_method.rb +112 -0
- data/lib/operandi/rubocop/cop/operandi/prefer_optional_over_default_nil.rb +124 -0
- data/lib/operandi/rubocop/cop/operandi/redundant_optional.rb +103 -0
- data/lib/operandi/rubocop/cop/operandi/reserved_name.rb +56 -0
- data/lib/operandi/rubocop/cop/operandi/step_method_exists.rb +134 -0
- data/lib/operandi/rubocop.rb +22 -0
- data/lib/operandi/settings/field.rb +147 -0
- data/lib/operandi/settings/step.rb +105 -0
- data/lib/operandi/utils.rb +36 -0
- data/lib/operandi/version.rb +5 -0
- data/lib/operandi.rb +11 -0
- data/lib/ruby_lsp/operandi/addon.rb +37 -0
- data/lib/ruby_lsp/operandi/definition.rb +134 -0
- data/lib/ruby_lsp/operandi/indexing_enhancement.rb +224 -0
- data/lib/tapioca/dsl/compilers/operandi.rb +378 -0
- data/operandi.gemspec +33 -0
- data/rbi/operandi.rbi +197 -0
- data/sorbet/cache/data.mdb +0 -0
- data/sorbet/cache/lock.mdb +0 -0
- 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
|
+

|
|
6
|
+
[](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
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
data/config/default.yml
ADDED
|
@@ -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)
|