opera 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +109 -1060
- data/benchmarks/operation_benchmark.rb +385 -0
- data/docs/examples/always.md +267 -0
- data/docs/examples/basic-operation.md +79 -0
- data/docs/examples/context-params-dependencies.md +122 -0
- data/docs/examples/finish-if.md +67 -0
- data/docs/examples/inner-operations.md +94 -0
- data/docs/examples/success-blocks.md +68 -0
- data/docs/examples/transactions.md +227 -0
- data/docs/examples/validations.md +139 -0
- data/docs/examples/within.md +166 -0
- data/lib/opera/operation/builder.rb +21 -3
- data/lib/opera/operation/config.rb +2 -6
- data/lib/opera/operation/executor.rb +16 -4
- data/lib/opera/operation/instructions/executors/always.rb +15 -0
- data/lib/opera/operation/instructions/executors/finish_if.rb +1 -2
- data/lib/opera/operation/instructions/executors/operation.rb +1 -2
- data/lib/opera/operation/instructions/executors/operations.rb +1 -2
- data/lib/opera/operation/instructions/executors/step.rb +1 -6
- data/lib/opera/operation/instructions/executors/success.rb +5 -2
- data/lib/opera/operation/instructions/executors/validate.rb +1 -2
- data/lib/opera/operation/instructions/executors/within.rb +1 -1
- data/lib/opera/operation.rb +1 -0
- data/lib/opera/version.rb +1 -1
- metadata +13 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Within
|
|
2
|
+
|
|
3
|
+
`within` wraps one or more steps with a method you define on the operation. The method must `yield` to execute the nested steps. If it does not yield, the nested steps are skipped. Normal break conditions (errors, `finish!`) still apply inside the block.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Profile::Create < Opera::Operation::Base
|
|
7
|
+
context do
|
|
8
|
+
attr_accessor :profile
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
dependencies do
|
|
12
|
+
attr_reader :current_account
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
step :build
|
|
16
|
+
|
|
17
|
+
within :read_from_replica do
|
|
18
|
+
step :check_duplicate
|
|
19
|
+
step :validate_quota
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
step :create
|
|
23
|
+
step :output
|
|
24
|
+
|
|
25
|
+
def build
|
|
26
|
+
self.profile = current_account.profiles.build(params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_duplicate
|
|
30
|
+
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_quota
|
|
34
|
+
result.add_error(:base, 'quota exceeded') if current_account.profiles.count >= 100
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create
|
|
38
|
+
profile.save!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def output
|
|
42
|
+
result.output = { model: profile }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def read_from_replica(&block)
|
|
48
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Inline usage
|
|
54
|
+
|
|
55
|
+
The wrapper method can also be used inline inside any step method when you need the wrapper for only part of that method's logic:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
def some_step
|
|
59
|
+
value = read_from_replica { Profile.count }
|
|
60
|
+
result.output = { count: value }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def read_from_replica(&block)
|
|
66
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Mixing step and operation inside within
|
|
71
|
+
|
|
72
|
+
`within` can wrap any combination of `step` and `operation` instructions. All of them execute inside the wrapper, and their outputs are available in context afterwards as usual.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Profile::Create < Opera::Operation::Base
|
|
76
|
+
context do
|
|
77
|
+
attr_accessor :profile
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
dependencies do
|
|
81
|
+
attr_reader :current_account, :quota_checker
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
within :read_from_replica do
|
|
85
|
+
step :check_duplicate
|
|
86
|
+
operation :fetch_quota
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
step :create
|
|
90
|
+
step :output
|
|
91
|
+
|
|
92
|
+
def check_duplicate
|
|
93
|
+
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def fetch_quota
|
|
97
|
+
quota_checker.call(params: params)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def create
|
|
101
|
+
self.profile = current_account.profiles.create(params)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def output
|
|
105
|
+
result.output = { model: profile, quota: context[:fetch_quota_output] }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def read_from_replica(&block)
|
|
111
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Nesting within inside a transaction
|
|
117
|
+
|
|
118
|
+
`within` can be placed inside a `transaction` block alongside other instructions. If any step or operation inside `within` fails, the error propagates up and the transaction is rolled back as normal.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class Profile::Create < Opera::Operation::Base
|
|
122
|
+
configure do |config|
|
|
123
|
+
config.transaction_class = ActiveRecord::Base
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
context do
|
|
127
|
+
attr_accessor :profile
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
dependencies do
|
|
131
|
+
attr_reader :current_account, :quota_checker, :audit_logger
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
transaction do
|
|
135
|
+
within :read_from_replica do
|
|
136
|
+
step :check_duplicate
|
|
137
|
+
operation :fetch_quota
|
|
138
|
+
end
|
|
139
|
+
operation :write_audit_log
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
step :output
|
|
143
|
+
|
|
144
|
+
def check_duplicate
|
|
145
|
+
result.add_error(:base, 'already exists') if Profile.exists?(email: params[:email])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def fetch_quota
|
|
149
|
+
quota_checker.call(params: params)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def write_audit_log
|
|
153
|
+
audit_logger.call(params: params)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def output
|
|
157
|
+
result.output = { quota: context[:fetch_quota_output] }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def read_from_replica(&block)
|
|
163
|
+
ActiveRecord::Base.connected_to(role: :reading, &block)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Opera
|
|
4
4
|
module Operation
|
|
5
5
|
module Builder
|
|
6
|
-
INSTRUCTIONS = %I[validate transaction step success finish_if operation operations within].freeze
|
|
6
|
+
INSTRUCTIONS = %I[validate transaction step success finish_if operation operations within always].freeze
|
|
7
|
+
INNER_INSTRUCTIONS = (INSTRUCTIONS - %I[always]).freeze
|
|
7
8
|
|
|
8
9
|
def self.included(base)
|
|
9
10
|
base.extend(ClassMethods)
|
|
@@ -14,12 +15,23 @@ module Opera
|
|
|
14
15
|
@instructions ||= []
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
INNER_INSTRUCTIONS.each do |instruction|
|
|
18
19
|
define_method instruction do |method = nil, &blk|
|
|
20
|
+
if instructions.any? { |i| i[:kind] == :always }
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"`#{instruction}` cannot appear after `always`. " \
|
|
23
|
+
'All `always` steps must be at the end of the operation.'
|
|
24
|
+
end
|
|
25
|
+
|
|
19
26
|
check_method_availability!(method) if method
|
|
20
27
|
instructions.concat(InnerBuilder.new.send(instruction, method, &blk))
|
|
21
28
|
end
|
|
22
29
|
end
|
|
30
|
+
|
|
31
|
+
def always(method)
|
|
32
|
+
check_method_availability!(method)
|
|
33
|
+
instructions << { kind: :always, method: method }
|
|
34
|
+
end
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
class InnerBuilder
|
|
@@ -30,7 +42,7 @@ module Opera
|
|
|
30
42
|
instance_eval(&block) if block_given?
|
|
31
43
|
end
|
|
32
44
|
|
|
33
|
-
|
|
45
|
+
INNER_INSTRUCTIONS.each do |instruction|
|
|
34
46
|
define_method instruction do |method = nil, &blk|
|
|
35
47
|
instructions << if !blk.nil?
|
|
36
48
|
{
|
|
@@ -46,6 +58,12 @@ module Opera
|
|
|
46
58
|
end
|
|
47
59
|
end
|
|
48
60
|
end
|
|
61
|
+
|
|
62
|
+
def always(_method)
|
|
63
|
+
raise ArgumentError,
|
|
64
|
+
'`always` cannot be used inside a block (transaction, within, success, validate). ' \
|
|
65
|
+
'Place `always` steps at the top level of the operation, after all other instructions.'
|
|
66
|
+
end
|
|
49
67
|
end
|
|
50
68
|
end
|
|
51
69
|
end
|
|
@@ -17,7 +17,7 @@ module Opera
|
|
|
17
17
|
@instrumentation_class = self.class.instrumentation_class
|
|
18
18
|
|
|
19
19
|
@mode = self.class.mode || DEVELOPMENT_MODE
|
|
20
|
-
@reporter =
|
|
20
|
+
@reporter = self.class.reporter
|
|
21
21
|
|
|
22
22
|
validate!
|
|
23
23
|
end
|
|
@@ -26,10 +26,6 @@ module Opera
|
|
|
26
26
|
yield self
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def custom_reporter
|
|
30
|
-
Rails.application.config.x.reporter.presence if defined?(Rails)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
29
|
private
|
|
34
30
|
|
|
35
31
|
def validate!
|
|
@@ -47,7 +43,7 @@ module Opera
|
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
def development_mode?
|
|
50
|
-
mode ==
|
|
46
|
+
mode == DEVELOPMENT_MODE
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def production_mode?
|
|
@@ -20,12 +20,22 @@ module Opera
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def evaluate_instructions(instructions = [])
|
|
23
|
-
|
|
23
|
+
instructions.each do |instruction|
|
|
24
|
+
next if instruction[:kind] != :always && break_condition
|
|
24
25
|
|
|
25
|
-
while instruction_copy.any?
|
|
26
|
-
instruction = instruction_copy.shift
|
|
27
26
|
evaluate_instruction(instruction)
|
|
28
|
-
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Executes the operation method named in the instruction, instruments it,
|
|
31
|
+
# and records the execution. This is the shared primitive that all executors
|
|
32
|
+
# use to invoke a step method without mutating the instruction hash.
|
|
33
|
+
def execute_step(instruction)
|
|
34
|
+
method = instruction[:method]
|
|
35
|
+
|
|
36
|
+
Instrumentation.new(operation).instrument(name: "##{method}", level: :step) do
|
|
37
|
+
result.add_execution(method) unless production_mode?
|
|
38
|
+
operation.send(method)
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
|
|
@@ -48,6 +58,8 @@ module Opera
|
|
|
48
58
|
Instructions::Executors::FinishIf.new(operation).call(instruction)
|
|
49
59
|
when :within
|
|
50
60
|
Instructions::Executors::Within.new(operation).call(instruction)
|
|
61
|
+
when :always
|
|
62
|
+
Instructions::Executors::Always.new(operation).call(instruction)
|
|
51
63
|
else
|
|
52
64
|
raise(UnknownInstructionError, "Unknown instruction #{instruction[:kind]}")
|
|
53
65
|
end
|
|
@@ -6,12 +6,7 @@ module Opera
|
|
|
6
6
|
module Executors
|
|
7
7
|
class Step < Executor
|
|
8
8
|
def call(instruction)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Instrumentation.new(operation).instrument(name: "##{method}", level: :step) do
|
|
12
|
-
operation.result.add_execution(method) unless production_mode?
|
|
13
|
-
operation.send(method)
|
|
14
|
-
end
|
|
9
|
+
execute_step(instruction)
|
|
15
10
|
end
|
|
16
11
|
end
|
|
17
12
|
end
|
|
@@ -6,8 +6,11 @@ module Opera
|
|
|
6
6
|
module Executors
|
|
7
7
|
class Success < Executor
|
|
8
8
|
def call(instruction)
|
|
9
|
-
instruction[:
|
|
10
|
-
|
|
9
|
+
if instruction[:instructions]
|
|
10
|
+
evaluate_instructions(instruction[:instructions])
|
|
11
|
+
else
|
|
12
|
+
execute_step(instruction)
|
|
13
|
+
end
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def break_condition
|
data/lib/opera/operation.rb
CHANGED
|
@@ -15,6 +15,7 @@ require 'opera/operation/instructions/executors/operation'
|
|
|
15
15
|
require 'opera/operation/instructions/executors/operations'
|
|
16
16
|
require 'opera/operation/instructions/executors/step'
|
|
17
17
|
require 'opera/operation/instructions/executors/within'
|
|
18
|
+
require 'opera/operation/instructions/executors/always'
|
|
18
19
|
|
|
19
20
|
module Opera
|
|
20
21
|
module Operation
|
data/lib/opera/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opera
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ProFinda Development Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-validation
|
|
@@ -73,9 +73,19 @@ files:
|
|
|
73
73
|
- LICENSE.txt
|
|
74
74
|
- README.md
|
|
75
75
|
- Rakefile
|
|
76
|
+
- benchmarks/operation_benchmark.rb
|
|
76
77
|
- bin/console
|
|
77
78
|
- bin/setup
|
|
78
79
|
- docker-compose.yml
|
|
80
|
+
- docs/examples/always.md
|
|
81
|
+
- docs/examples/basic-operation.md
|
|
82
|
+
- docs/examples/context-params-dependencies.md
|
|
83
|
+
- docs/examples/finish-if.md
|
|
84
|
+
- docs/examples/inner-operations.md
|
|
85
|
+
- docs/examples/success-blocks.md
|
|
86
|
+
- docs/examples/transactions.md
|
|
87
|
+
- docs/examples/validations.md
|
|
88
|
+
- docs/examples/within.md
|
|
79
89
|
- lib/opera.rb
|
|
80
90
|
- lib/opera/errors.rb
|
|
81
91
|
- lib/opera/operation.rb
|
|
@@ -84,6 +94,7 @@ files:
|
|
|
84
94
|
- lib/opera/operation/builder.rb
|
|
85
95
|
- lib/opera/operation/config.rb
|
|
86
96
|
- lib/opera/operation/executor.rb
|
|
97
|
+
- lib/opera/operation/instructions/executors/always.rb
|
|
87
98
|
- lib/opera/operation/instructions/executors/finish_if.rb
|
|
88
99
|
- lib/opera/operation/instructions/executors/operation.rb
|
|
89
100
|
- lib/opera/operation/instructions/executors/operations.rb
|