Skip to content

Commit dcdc38d

Browse files
santibclaude
andcommitted
Refactor Runnable with name, formatter, and hooks
Phase 2 of the Mars v2 refactor. - Runnable: include Hooks, add `name` (auto-derived from class via `step_name`), add `formatter` class-level DSL with instance fallback - Gate, Aggregator, Sequential, Parallel: delegate `name` to Runnable via super instead of managing their own attr_reader - Runnable spec: cover name derivation, formatter DSL, hooks integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aa9d4f8 commit dcdc38d

File tree

6 files changed

+93
-16
lines changed

6 files changed

+93
-16
lines changed

lib/mars/aggregator.rb

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

33
module Mars
44
class Aggregator < Runnable
5-
attr_reader :name, :operation
5+
attr_reader :operation
66

77
def initialize(name = "Aggregator", operation: nil, **kwargs)
8-
super(**kwargs)
8+
super(name: name, **kwargs)
99

10-
@name = name
1110
@operation = operation || ->(inputs) { inputs }
1211
end
1312

lib/mars/gate.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
module Mars
44
class Gate < Runnable
5-
attr_reader :name
6-
75
def initialize(name = "Gate", condition:, branches:, **kwargs)
8-
super(**kwargs)
6+
super(name: name, **kwargs)
97

10-
@name = name
118
@condition = condition
129
@branches = branches
1310
end

lib/mars/runnable.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@
22

33
module Mars
44
class Runnable
5+
include Hooks
6+
7+
attr_reader :name, :formatter
58
attr_accessor :state
69

7-
def initialize(state: {})
10+
class << self
11+
def step_name
12+
return @step_name if defined?(@step_name)
13+
return unless name
14+
15+
name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
16+
end
17+
18+
attr_writer :step_name
19+
20+
def formatter(klass = nil)
21+
klass ? @formatter_class = klass : @formatter_class
22+
end
23+
end
24+
25+
def initialize(name: self.class.step_name, state: {}, formatter: nil)
26+
@name = name
827
@state = state
28+
@formatter = formatter || self.class.formatter&.new || Formatter.new
929
end
1030

1131
def run(input)

lib/mars/workflows/parallel.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
module Mars
44
module Workflows
55
class Parallel < Runnable
6-
attr_reader :name
7-
86
def initialize(name, steps:, aggregator: nil, **kwargs)
9-
super(**kwargs)
7+
super(name: name, **kwargs)
108

11-
@name = name
129
@steps = steps
1310
@aggregator = aggregator || Aggregator.new("#{name} Aggregator")
1411
end

lib/mars/workflows/sequential.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
module Mars
44
module Workflows
55
class Sequential < Runnable
6-
attr_reader :name
7-
86
def initialize(name, steps:, **kwargs)
9-
super(**kwargs)
7+
super(name: name, **kwargs)
108

11-
@name = name
129
@steps = steps
1310
end
1411

spec/mars/runnable_spec.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,73 @@ def run(input)
4242
end
4343
end
4444

45+
describe "#name" do
46+
it "defaults to nil for anonymous classes" do
47+
klass = Class.new(described_class)
48+
expect(klass.new.name).to be_nil
49+
end
50+
51+
it "can be set via the name keyword" do
52+
runnable = described_class.new(name: "my_step")
53+
expect(runnable.name).to eq("my_step")
54+
end
55+
56+
it "derives step_name from the class name" do
57+
stub_const("Mars::MyCustomStep", Class.new(described_class))
58+
expect(Mars::MyCustomStep.new.name).to eq("my_custom_step")
59+
end
60+
end
61+
62+
describe "#formatter" do
63+
it "defaults to a Formatter instance" do
64+
runnable = described_class.new
65+
expect(runnable.formatter).to be_a(Mars::Formatter)
66+
end
67+
68+
it "can be set via the formatter keyword" do
69+
custom_formatter = Mars::Formatter.new
70+
runnable = described_class.new(formatter: custom_formatter)
71+
expect(runnable.formatter).to eq(custom_formatter)
72+
end
73+
74+
it "uses the class-level formatter when declared" do
75+
custom_formatter_class = Class.new(Mars::Formatter)
76+
klass = Class.new(described_class) do
77+
formatter custom_formatter_class
78+
end
79+
80+
expect(klass.new.formatter).to be_a(custom_formatter_class)
81+
end
82+
end
83+
84+
describe "hooks" do
85+
it "includes Hooks module" do
86+
expect(described_class.ancestors).to include(Mars::Hooks)
87+
end
88+
89+
it "supports before_run hooks" do
90+
klass = Class.new(described_class)
91+
calls = []
92+
klass.before_run { |_ctx, step| calls << step.name }
93+
94+
step = klass.new(name: "test")
95+
step.run_before_hooks(Mars::ExecutionContext.new(input: "x"))
96+
97+
expect(calls).to eq(["test"])
98+
end
99+
100+
it "supports after_run hooks" do
101+
klass = Class.new(described_class)
102+
calls = []
103+
klass.after_run { |_ctx, result, _step| calls << result }
104+
105+
step = klass.new(name: "test")
106+
step.run_after_hooks(Mars::ExecutionContext.new(input: "x"), "result")
107+
108+
expect(calls).to eq(["result"])
109+
end
110+
end
111+
45112
describe "inheritance" do
46113
it "can be inherited" do
47114
subclass = Class.new(described_class)

0 commit comments

Comments
 (0)