Skip to content

Commit cbabd9e

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

File tree

4 files changed

+132
-16
lines changed

4 files changed

+132
-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: 58 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,70 @@ 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+
unless @outputs_evalulated
60+
@outputs.each_pair do |key, value|
61+
@outputs[key] = value.is_a?(LazyExecutor) ? value.value : value
62+
end
63+
@outputs_evalulated = true
64+
end
65+
66+
@outputs
67+
end
68+
4269
def output(name, value)
4370
name = name.to_sym
4471
unless output_configurations.key?(name)
4572
raise ::Subroutine::Outputs::UnknownOutputError, name
4673
end
4774

48-
outputs[name] = value
75+
@outputs[name] = output_configurations[name].lazy? ? LazyExecutor.new(value) : value
4976
end
5077

5178
def get_output(name)
5279
name = name.to_sym
5380
raise ::Subroutine::Outputs::UnknownOutputError, name unless output_configurations.key?(name)
5481

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

5896
def validate_outputs!
5997
output_configurations.each_pair do |name, config|
6098
if config.required? && !output_provided?(name)
6199
raise ::Subroutine::Outputs::OutputNotSetError, name
62100
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-
)
101+
unless output_configurations[name].lazy?
102+
ensure_output_type_valid!(name)
70103
end
71104
end
72105
end
73106

107+
def ensure_output_type_valid!(name)
108+
return if valid_output_type?(name)
109+
110+
name = name.to_sym
111+
raise ::Subroutine::Outputs::InvalidOutputTypeError.new(
112+
name: name,
113+
actual_type: @outputs[name].class,
114+
expected_type: output_configurations[name][:type]
115+
)
116+
end
117+
74118
def output_provided?(name)
75119
name = name.to_sym
76120

77-
outputs.key?(name)
121+
@outputs.key?(name)
78122
end
79123

80124
def valid_output_type?(name)
@@ -84,9 +128,9 @@ def valid_output_type?(name)
84128

85129
output_configuration = output_configurations[name]
86130
return true unless output_configuration[:type]
87-
return true if !output_configuration.required? && outputs[name].nil?
131+
return true if !output_configuration.required? && @outputs[name].nil?
88132

89-
outputs[name].is_a?(output_configuration[:type])
133+
@outputs[name].is_a?(output_configuration[:type])
90134
end
91135
end
92136
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)