Skip to content

Add if/unless support#41

Open
AndriySolonyna wants to merge 2 commits into
masterfrom
SP-8689-conditional-operators
Open

Add if/unless support#41
AndriySolonyna wants to merge 2 commits into
masterfrom
SP-8689-conditional-operators

Conversation

@AndriySolonyna

Copy link
Copy Markdown

Add :if and :unless keyword arguments to step and operation DSL methods. Both accept a symbol (method name) or a lambda/proc.

# Symbol form — calls a method on the operation instance
operation :create_internal_experience, unless: :internal_experience_authorized?
step :notify_user, if: :notifications_enabled?

# Lambda form — evaluated in the operation instance context
operation :reopen_and_reset, if: -> { profile_ids.present? }
step :recalculate, unless: -> { params[:skip_recalculation] }

More details here
https://www.notion.so/profinda/RFC-Conditional-steps-in-opera-349ab9c8a39a802bbf60d1893a472435

@petergebala

Copy link
Copy Markdown
Contributor

I would suggest rethink that and look again at implementation. It doubles number of lines in builder, adds a lot of complexity makes code harder to follow. Maybe there is a simpler way of implementing this feautre?

@AndriySolonyna AndriySolonyna force-pushed the SP-8689-conditional-operators branch from 3d6e576 to 34e59d1 Compare May 1, 2026 07:10

@petergebala petergebala left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job @AndriySolonyna Thank you 🙏

raise ArgumentError, 'Cannot use both :if and :unless on the same step' if opts[:if] && opts[:unless]

cond = opts[:if] || opts[:unless]
body = cond.is_a?(Symbol) ? proc { send(cond) } : cond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rename body what I understand you are ensuring that you get proc/lambda that you can execute later. I am not sure what the best name would be.

  • callable
  • handler
  • callback
  • predicate_proc
  • condition_proc

module Opera
module Operation
module Builder
# Parses keyword options passed to a Builder instruction (`step`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo those comments are not needed. The code is clear

{ predicate: build_predicate(opts) }.compact
end

# Translates `:if` / `:unless` (Symbol or Proc) into a single Proc that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same here

# to call it in the operation instance scope.
def condition_met?(instruction)
operation.instance_exec(&instruction[:predicate])
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also mentioned that on slack. Imo you could inline that above. You don't need 7 lines explaning what is going on here. Devs would know.

end
end

describe 'conditional execution (:if / :unless)' do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would reduce number of test cases. You don't need so many, 430 lines:
probably covering 2 cases with:

step :foo, if: :blabla

and

operation :bar, unless: -> { params[enabled] }

Would be enough. You would cover all cases:

  • step
  • operation
  • if
  • unless
  • symbol
  • proc

And we could reduce number of lines to 40

Comment thread CHANGELOG.md

### 0.7.0 - Apr 30, 2026

- Add `:if` / `:unless` options to `step`, `operation`, and `operations` for declarative conditional execution. Conditions accept a Symbol (method name) or a Proc/Lambda (evaluated via `instance_exec` in the operation instance scope). Skipped steps do not execute and are not recorded in `result.executions`. 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 step raises `ArgumentError` at class load time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to update that too. It is for all instructions, not only step/operation

Comment thread README.md

### Conditional execution (`:if` / `:unless`)

`step`, `operation`, and `operations` accept `:if` and `:unless` keyword

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants