Skip to content

Commit 4e26476

Browse files
committed
add lazy option to outputs to delay execution of output if not needed
1 parent 82a3ca8 commit 4e26476

File tree

4 files changed

+129
-16
lines changed

4 files changed

+129
-16
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class SignupOp < ::Subroutine::Op
2222

2323
outputs :user
2424
outputs :business, type: Business # validate that output type is an instance of Business
25+
outputs :heavy_operation, lazy: true # delay the execution of the output until accessed
2526

2627
protected
2728

@@ -33,6 +34,7 @@ class SignupOp < ::Subroutine::Op
3334

3435
output :user, u
3536
output :business, b
37+
output :heavy_operation, -> { some_heavy_operation }
3638
end
3739

3840
def create_user!
@@ -41,7 +43,11 @@ class SignupOp < ::Subroutine::Op
4143

4244
def create_business!(owner)
4345
Business.create!(company_name: company_name, owner: owner)
44-
end
46+
end
47+
48+
def some_heavy_operation
49+
# ...
50+
end
4551

4652
def deliver_welcome_email(u)
4753
UserMailer.welcome(u.id).deliver_later

lib/subroutine/outputs.rb

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,27 @@ module Outputs
1010

1111
extend ActiveSupport::Concern
1212

13+
class LazyExecutor
14+
def initialize(value)
15+
@value_block = value
16+
@executed = false
17+
end
18+
19+
def value
20+
return @value if @executed
21+
@value = @value_block.respond_to?(:call) ? @value_block.call : @value
22+
@executed = true
23+
@value
24+
end
25+
26+
def executed?
27+
@executed
28+
end
29+
end
30+
1331
included do
1432
class_attribute :output_configurations
1533
self.output_configurations = {}
16-
17-
attr_reader :outputs
1834
end
1935

2036
module ClassMethods
@@ -39,42 +55,67 @@ def setup_outputs
3955
@outputs = {} # don't do with_indifferent_access because it will turn provided objects into with_indifferent_access objects, which may not be the desired behavior
4056
end
4157

58+
def outputs
59+
@outputs.each_pair do |key, value|
60+
@outputs[key] = value.is_a?(LazyExecutor) ? value.value : value
61+
end
62+
63+
@outputs
64+
end
65+
4266
def output(name, value)
4367
name = name.to_sym
4468
unless output_configurations.key?(name)
4569
raise ::Subroutine::Outputs::UnknownOutputError, name
4670
end
4771

48-
outputs[name] = value
72+
@outputs[name] = output_configurations[name].lazy? ? LazyExecutor.new(value) : value
4973
end
5074

5175
def get_output(name)
5276
name = name.to_sym
5377
raise ::Subroutine::Outputs::UnknownOutputError, name unless output_configurations.key?(name)
5478

55-
outputs[name]
79+
output = @outputs[name]
80+
unless output.is_a?(LazyExecutor)
81+
output
82+
else
83+
# if its not executed, validate the type
84+
unless output.executed?
85+
@outputs[name] = output.value
86+
ensure_output_type_valid!(name)
87+
end
88+
89+
@outputs[name]
90+
end
5691
end
5792

5893
def validate_outputs!
5994
output_configurations.each_pair do |name, config|
6095
if config.required? && !output_provided?(name)
6196
raise ::Subroutine::Outputs::OutputNotSetError, name
6297
end
63-
unless valid_output_type?(name)
64-
name = name.to_sym
65-
raise ::Subroutine::Outputs::InvalidOutputTypeError.new(
66-
name: name,
67-
actual_type: outputs[name].class,
68-
expected_type: output_configurations[name][:type]
69-
)
98+
unless output_configurations[name].lazy?
99+
ensure_output_type_valid!(name)
70100
end
71101
end
72102
end
73103

104+
def ensure_output_type_valid!(name)
105+
return if valid_output_type?(name)
106+
107+
name = name.to_sym
108+
raise ::Subroutine::Outputs::InvalidOutputTypeError.new(
109+
name: name,
110+
actual_type: @outputs[name].class,
111+
expected_type: output_configurations[name][:type]
112+
)
113+
end
114+
74115
def output_provided?(name)
75116
name = name.to_sym
76117

77-
outputs.key?(name)
118+
@outputs.key?(name)
78119
end
79120

80121
def valid_output_type?(name)
@@ -84,9 +125,9 @@ def valid_output_type?(name)
84125

85126
output_configuration = output_configurations[name]
86127
return true unless output_configuration[:type]
87-
return true if !output_configuration.required? && outputs[name].nil?
128+
return true if !output_configuration.required? && @outputs[name].nil?
88129

89-
outputs[name].is_a?(output_configuration[:type])
130+
@outputs[name].is_a?(output_configuration[:type])
90131
end
91132
end
92133
end

lib/subroutine/outputs/configuration.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ def self.from(field_name, options)
1313
end
1414
end
1515

16-
DEFAULT_OPTIONS = { required: true }.freeze
16+
DEFAULT_OPTIONS = {
17+
required: true,
18+
lazy: false
19+
}.freeze
1720

1821
attr_reader :output_name
1922

@@ -28,6 +31,10 @@ def required?
2831
!!config[:required]
2932
end
3033

34+
def lazy?
35+
!!config[:lazy]
36+
end
37+
3138
def inspect
3239
"#<#{self.class}:#{object_id} name=#{output_name} config=#{config.inspect}>"
3340
end

test/subroutine/outputs_test.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ def perform
1010
end
1111
end
1212

13+
class LazyOutputOp < ::Subroutine::Op
14+
outputs :foo, lazy: true
15+
outputs :baz, lazy: true, type: String
16+
17+
def perform
18+
output :foo, -> { call_me }
19+
output :baz, -> { call_baz }
20+
end
21+
22+
def call_me; end
23+
24+
def call_baz; end
25+
end
26+
1327
class MissingOutputSetOp < ::Subroutine::Op
1428
outputs :foo
1529
def perform
@@ -99,5 +113,50 @@ def test_it_raises_an_error_if_output_is_set_to_nil_when_there_is_type_validatio
99113
op.submit
100114
end
101115
end
116+
117+
################
118+
# lazy outputs #
119+
################
120+
121+
def test_it_does_not_call_lazy_output_values_if_not_accessed
122+
op = LazyOutputOp.new
123+
op.expects(:call_me).never
124+
op.submit!
125+
end
126+
127+
def test_it_calls_lazy_output_values_if_accessed
128+
op = LazyOutputOp.new
129+
op.expects(:call_me).once
130+
op.submit!
131+
op.foo
132+
end
133+
134+
def test_it_validates_type_when_lazy_output_is_accessed
135+
op = LazyOutputOp.new
136+
op.expects(:call_baz).once.returns("a string")
137+
op.submit!
138+
assert_silent do
139+
op.baz
140+
end
141+
end
142+
143+
def test_it_raises_error_on_invalid_type_when_lazy_output_is_accessed
144+
op = LazyOutputOp.new
145+
op.expects(:call_baz).once.returns(10)
146+
op.submit!
147+
error = assert_raises(Subroutine::Outputs::InvalidOutputTypeError) do
148+
op.baz
149+
end
150+
assert_match(/Invalid output type for 'baz' expected String but got Integer/, error.message)
151+
end
152+
153+
def test_it_returns_outputs
154+
op = LazyOutputOp.new
155+
op.expects(:call_me).once.returns(1)
156+
op.expects(:call_baz).once.returns("a string")
157+
op.submit!
158+
assert_equal({ foo: 1, baz: "a string" }, op.outputs)
159+
end
160+
102161
end
103162
end

0 commit comments

Comments
 (0)