Skip to content

Commit 8def03d

Browse files
santibclaude
andcommitted
Gate executes branches directly + class-level DSL
Phase 4 of the Mars v2 refactor. - Gate: add class-level `condition`/`branch` DSL for reusable gates. Gate#run now executes the matched branch directly instead of returning a Runnable for Sequential to detect - Aggregator: context-aware — accepts ExecutionContext and passes its outputs to the operation - Sequential: remove is_a?(Runnable) check, now just chains step results Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d5df32 commit 8def03d

4 files changed

Lines changed: 116 additions & 62 deletions

File tree

lib/mars/gate.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,44 @@
22

33
module MARS
44
class Gate < Runnable
5-
def initialize(name = "Gate", condition:, branches:, **kwargs)
5+
class << self
6+
def condition(&block)
7+
@condition_block = block
8+
end
9+
10+
attr_reader :condition_block
11+
12+
def branch(key, runnable)
13+
branches_map[key] = runnable
14+
end
15+
16+
def branches_map
17+
@branches_map ||= {}
18+
end
19+
end
20+
21+
def initialize(name = "Gate", condition: nil, branches: nil, **kwargs)
622
super(name: name, **kwargs)
723

8-
@condition = condition
9-
@branches = branches
24+
@condition = condition || self.class.condition_block
25+
@branches = branches || self.class.branches_map
1026
end
1127

1228
def run(input)
1329
result = condition.call(input)
30+
branch = branches[result]
31+
32+
return input unless branch
1433

15-
branches[result] || input
34+
resolve_branch(branch).run(input)
1635
end
1736

1837
private
1938

2039
attr_reader :condition, :branches
40+
41+
def resolve_branch(branch)
42+
branch.is_a?(Class) ? branch.new : branch
43+
end
2144
end
2245
end

lib/mars/workflows/sequential.rb

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,7 @@ def initialize(name, steps:, **kwargs)
1111

1212
def run(input)
1313
@steps.each do |step|
14-
result = step.run(input)
15-
16-
if result.is_a?(Runnable)
17-
input = result.run(input)
18-
break
19-
else
20-
input = result
21-
end
14+
input = step.run(input)
2215
end
2316

2417
input

spec/mars/aggregator_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
RSpec.describe MARS::Aggregator do
44
describe "#run" do
5-
context "when called without a block" do
5+
context "when called without an operation" do
66
let(:aggregator) { described_class.new }
77

88
it "returns the input as is" do
@@ -11,10 +11,10 @@
1111
end
1212
end
1313

14-
context "when initialized with a block operation" do
14+
context "when initialized with an operation" do
1515
let(:aggregator) { described_class.new("Aggregator", operation: lambda(&:join)) }
1616

17-
it "executes the block and returns its value" do
17+
it "executes the operation and returns its value" do
1818
result = aggregator.run(%w[a b c])
1919
expect(result).to eq("abc")
2020
end

spec/mars/gate_spec.rb

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,82 +2,120 @@
22

33
RSpec.describe MARS::Gate do
44
describe "#run" do
5-
let(:gate) { described_class.new("TestGate", condition: condition, branches: branches) }
5+
context "with constructor-based configuration" do
6+
let(:short_step) do
7+
Class.new(Mars::Runnable) do
8+
def run(input)
9+
"short: #{input}"
10+
end
11+
end.new
12+
end
613

7-
context "with simple boolean condition" do
8-
let(:condition) { ->(input) { input > 5 } }
9-
let(:false_branch) { instance_spy(MARS::Runnable) }
10-
let(:branches) { { false => false_branch } }
14+
let(:long_step) do
15+
Class.new(Mars::Runnable) do
16+
def run(input)
17+
"long: #{input}"
18+
end
19+
end.new
20+
end
1121

12-
it "returns the input when no branch matches" do
13-
result = gate.run(10)
14-
expect(result).to eq(10)
22+
let(:gate) do
23+
described_class.new(
24+
"LengthGate",
25+
condition: ->(input) { input.length > 5 ? :long : :short },
26+
branches: { short: short_step, long: long_step }
27+
)
1528
end
1629

17-
it "returns the false branch when condition is false" do
18-
result = gate.run(3)
30+
it "executes the matched branch directly" do
31+
expect(gate.run("hi")).to eq("short: hi")
32+
end
1933

20-
expect(result).to eq(false_branch)
34+
it "executes the other branch for different input" do
35+
expect(gate.run("longstring")).to eq("long: longstring")
2136
end
2237

23-
it "does not run the false branch when condition is false" do
24-
gate.run(3)
38+
it "returns input when no branch matches" do
39+
gate = described_class.new(
40+
"NoMatch",
41+
condition: ->(_input) { :unknown },
42+
branches: { short: short_step }
43+
)
2544

26-
expect(false_branch).not_to have_received(:run)
45+
expect(gate.run("hello")).to eq("hello")
2746
end
2847
end
2948

30-
context "with string-based condition" do
31-
let(:condition) { ->(input) { input.length > 5 ? "long" : "short" } }
32-
let(:long_branch) { instance_spy(MARS::Runnable) }
33-
let(:short_branch) { instance_spy(MARS::Runnable) }
34-
let(:branches) { { "long" => long_branch, "short" => short_branch } }
35-
36-
it "routes to long branch for long strings" do
37-
result = gate.run("longstring")
49+
context "with class-level DSL" do
50+
let(:short_step_class) do
51+
Class.new(Mars::Runnable) do
52+
def run(input)
53+
"quick: #{input}"
54+
end
55+
end
56+
end
3857

39-
expect(result).to eq(long_branch)
58+
let(:long_step_class) do
59+
Class.new(Mars::Runnable) do
60+
def run(input)
61+
"deep: #{input}"
62+
end
63+
end
4064
end
4165

42-
it "routes to short branch for short strings" do
43-
result = gate.run("hi")
66+
it "uses condition and branch DSL" do
67+
short_cls = short_step_class
68+
long_cls = long_step_class
4469

45-
expect(result).to eq(short_branch)
70+
gate_class = Class.new(described_class) do
71+
condition { |input| input.length < 5 ? :short : :long }
72+
branch :short, short_cls
73+
branch :long, long_cls
74+
end
75+
76+
gate = gate_class.new("DSLGate")
77+
expect(gate.run("hi")).to eq("quick: hi")
78+
expect(gate.run("longstring")).to eq("deep: longstring")
4679
end
4780
end
4881

4982
context "with complex condition logic" do
50-
let(:condition) do
51-
lambda do |input|
52-
case input
53-
when 0..10 then "low"
54-
when 11..50 then "medium"
55-
else "high"
56-
end
57-
end
83+
let(:low_step) do
84+
Class.new(Mars::Runnable) { def run(input) = "low:#{input}" }.new
5885
end
5986

60-
let(:low_branch) { instance_spy(MARS::Runnable) }
61-
let(:medium_branch) { instance_spy(MARS::Runnable) }
62-
let(:high_branch) { instance_spy(MARS::Runnable) }
63-
let(:branches) { { "low" => low_branch, "medium" => medium_branch, "high" => high_branch } }
87+
let(:medium_step) do
88+
Class.new(Mars::Runnable) { def run(input) = "med:#{input}" }.new
89+
end
6490

65-
it "routes to low branch" do
66-
result = gate.run(5)
91+
let(:high_step) do
92+
Class.new(Mars::Runnable) { def run(input) = "high:#{input}" }.new
93+
end
6794

68-
expect(result).to eq(low_branch)
95+
let(:gate) do
96+
described_class.new(
97+
"SeverityGate",
98+
condition: lambda { |input|
99+
case input
100+
when 0..10 then :low
101+
when 11..50 then :medium
102+
else :high
103+
end
104+
},
105+
branches: { low: low_step, medium: medium_step, high: high_step }
106+
)
69107
end
70108

71-
it "routes to medium branch" do
72-
result = gate.run(25)
109+
it "routes to low branch" do
110+
expect(gate.run(5)).to eq("low:5")
111+
end
73112

74-
expect(result).to eq(medium_branch)
113+
it "routes to medium branch" do
114+
expect(gate.run(25)).to eq("med:25")
75115
end
76116

77117
it "routes to high branch" do
78-
result = gate.run(100)
79-
80-
expect(result).to eq(high_branch)
118+
expect(gate.run(100)).to eq("high:100")
81119
end
82120
end
83121
end

0 commit comments

Comments
 (0)