opera 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d601d62d89303ad4efaa6ab156894bdcd247964977f3bb23fd43f2236cdf7655
4
- data.tar.gz: 42c7cb409cec7f9dc66cd63dd723d39170cc5c02c5704e79d886869570b7775e
3
+ metadata.gz: a69b196af65cb01e20f10d595444751505e0a241f6264e00c8de370c05def135
4
+ data.tar.gz: b17458445a2f96f47abb45064084498d8e4efb208a2dc04aa1e4abba1d2ab7ee
5
5
  SHA512:
6
- metadata.gz: 3725001498c05f07eae31e1f85e9801833820a1f4a4c8175e9fff13977390c6aaad4da5d3787b669fe7ddf4f84fd65752857f16bc0c84d822a3bff97b10b7467
7
- data.tar.gz: 50a96d11c1f419bfc82934403793814029f088ba2b48d8df9083ee3f5383937dc95cc312339c1ce8619fc1358bd9e2b23aac3124aca6e4b61269fd5efacb93ae
6
+ metadata.gz: f4531f3b0cea01d642df058dc6a956de5e036b1259dd2dff659b4a67dc0956a34e965032442860b3b03af1997e1af9a41fe18845fbff4a13ef04a22f11adddc1
7
+ data.tar.gz: ea82fa32434682ec8f9dbbe3754204c3b9edcd586a64805debee5332ef05a69ef913523e309bf729bff626fa21eb79383058740bd3d9ee2cc5a7770f82c710d1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Opera Changelog
2
2
 
3
+ ### 0.7.0 - Apr 30, 2026
4
+
5
+ - Add `:if` / `:unless` options to all instructions except `always` (`validate`, `transaction`, `step`, `success`, `finish_if`, `operation`, `operations`, `within`) for declarative conditional execution. Conditions accept a Symbol (method name) or a Proc/Lambda (evaluated via `instance_exec` in the operation instance scope). Skipped instructions do not execute and are not recorded in `result.executions`; for block instructions (`transaction`, `within`) the whole block, including nested instructions, is skipped. For `operation` / `operations`, the conventional `<method>_output` slot in context is set to `nil` when skipped, matching the historical `return Opera::Operation::Result.new` early-exit behavior. Passing both `:if` and `:unless` on the same instruction raises `ArgumentError` at class load time.
6
+
3
7
  ### 0.6.0 - Apr 15, 2026
4
8
 
5
9
  - Add `always` executor: runs its step unconditionally after all regular steps, regardless of failure or an early finish
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opera (0.6.0)
4
+ opera (0.7.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -124,6 +124,38 @@ end
124
124
  | `within :method do ... end` | Wraps nested steps with a custom method that must `yield`. If it doesn't yield, nested steps are skipped. |
125
125
  | `always :method` | Executes a step unconditionally after all regular steps, even after a failure or an early finish. Must appear at the end of the operation — only other `always` steps may follow. Cannot be used inside blocks. Use `result.success?` / `result.failure?` inside the method to branch on outcome. |
126
126
 
127
+ ### Conditional execution (`:if` / `:unless`)
128
+
129
+ The `:if` and `:unless` keyword arguments provide declarative conditional
130
+ execution. They are supported on `validate`, `transaction`, `step`, `success`,
131
+ `finish_if`, `operation`, `operations`, and `within` (every instruction except
132
+ `always`). For block instructions (`transaction`, `within`), a falsy condition
133
+ skips the whole block, including its nested instructions.
134
+
135
+ The condition is evaluated **before** the instruction runs -- if the condition
136
+ is not met the instruction is skipped entirely (no method invocation, no side
137
+ effects, not recorded in `result.executions`).
138
+
139
+ The condition value can be a **Symbol** (method name on the operation) or a
140
+ **Proc/Lambda** (evaluated via `instance_exec` in the operation instance
141
+ scope).
142
+
143
+ ```ruby
144
+ # Symbol form
145
+ step :notify_user, if: :notifications_enabled?
146
+
147
+ # Lambda form
148
+ step :recalculate, unless: -> { params[:skip_recalculation] }
149
+ ```
150
+
151
+ When an `operation` or `operations` instruction is skipped, its
152
+ `context[:<method>_output]` slot is set to `nil` (matching the historical
153
+ `return Opera::Operation::Result.new` early-exit behavior). For other
154
+ instructions no context output is set.
155
+
156
+ Passing both `:if` and `:unless` on the same instruction raises `ArgumentError`
157
+ at class load time.
158
+
127
159
  ### Combining instructions
128
160
 
129
161
  ```ruby
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Builder
6
+ class OptionsBuilder
7
+ ALLOWED_OPTIONS = %i[if unless].freeze
8
+
9
+ def self.build(opts)
10
+ return {} if opts.empty?
11
+
12
+ unknown = opts.keys - ALLOWED_OPTIONS
13
+ raise ArgumentError, "Unknown option(s): #{unknown.inspect}. Allowed: #{ALLOWED_OPTIONS}" if unknown.any?
14
+
15
+ { predicate: build_predicate(opts) }.compact
16
+ end
17
+
18
+ def self.build_predicate(opts)
19
+ return nil unless opts[:if] || opts[:unless]
20
+ raise ArgumentError, 'Cannot use :if and :unless together' if opts[:if] && opts[:unless]
21
+
22
+ cond = opts[:if] || opts[:unless]
23
+ condition_proc = cond.is_a?(Symbol) ? proc { send(cond) } : cond
24
+ opts.key?(:if) ? condition_proc : proc { !instance_exec(&condition_proc) }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -16,7 +16,7 @@ module Opera
16
16
  end
17
17
 
18
18
  INNER_INSTRUCTIONS.each do |instruction|
19
- define_method instruction do |method = nil, &blk|
19
+ define_method instruction do |method = nil, **opts, &blk|
20
20
  if instructions.any? { |i| i[:kind] == :always }
21
21
  raise ArgumentError,
22
22
  "`#{instruction}` cannot appear after `always`. " \
@@ -24,7 +24,7 @@ module Opera
24
24
  end
25
25
 
26
26
  check_method_availability!(method) if method
27
- instructions.concat(InnerBuilder.new.send(instruction, method, &blk))
27
+ instructions.concat(InnerBuilder.new.send(instruction, method, **opts, &blk))
28
28
  end
29
29
  end
30
30
 
@@ -43,19 +43,13 @@ module Opera
43
43
  end
44
44
 
45
45
  INNER_INSTRUCTIONS.each do |instruction|
46
- define_method instruction do |method = nil, &blk|
47
- instructions << if !blk.nil?
48
- {
49
- kind: instruction,
50
- label: method,
51
- instructions: InnerBuilder.new(&blk).instructions
52
- }
53
- else
54
- {
55
- kind: instruction,
56
- method: method
57
- }
58
- end
46
+ define_method instruction do |method = nil, **opts, &blk|
47
+ entry = if blk
48
+ { kind: instruction, label: method, instructions: InnerBuilder.new(&blk).instructions }
49
+ else
50
+ { kind: instruction, method: method }
51
+ end
52
+ instructions << entry.merge(OptionsBuilder.build(opts))
59
53
  end
60
54
  end
61
55
 
@@ -41,6 +41,11 @@ module Opera
41
41
 
42
42
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
43
43
  def evaluate_instruction(instruction)
44
+ if instruction[:predicate] && !operation.instance_exec(&instruction[:predicate])
45
+ add_instruction_output(instruction, nil) if %i[operation operations].include?(instruction[:kind])
46
+ return
47
+ end
48
+
44
49
  case instruction[:kind]
45
50
  when :step
46
51
  Instructions::Executors::Step.new(operation).call(instruction)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'opera/operation/attributes_dsl'
4
4
  require 'opera/operation/builder'
5
+ require 'opera/operation/builder/options_builder'
5
6
  require 'opera/operation/base'
6
7
  require 'opera/operation/executor'
7
8
  require 'opera/operation/instrumentation'
data/lib/opera/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Opera
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
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.6.0
4
+ version: 0.7.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-16 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-validation
@@ -92,6 +92,7 @@ files:
92
92
  - lib/opera/operation/attributes_dsl.rb
93
93
  - lib/opera/operation/base.rb
94
94
  - lib/opera/operation/builder.rb
95
+ - lib/opera/operation/builder/options_builder.rb
95
96
  - lib/opera/operation/config.rb
96
97
  - lib/opera/operation/executor.rb
97
98
  - lib/opera/operation/instructions/executors/always.rb