trailblazer-activity-dsl-linear 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CHANGES.md +3 -0
- data/DSL_IDEAS +91 -0
- data/Gemfile +13 -0
- data/README.md +23 -0
- data/Rakefile +13 -0
- data/lib/trailblazer-activity-dsl-linear.rb +2 -0
- data/lib/trailblazer/activity/dsl/linear.rb +163 -0
- data/lib/trailblazer/activity/dsl/linear/compiler.rb +70 -0
- data/lib/trailblazer/activity/dsl/linear/helper.rb +78 -0
- data/lib/trailblazer/activity/dsl/linear/normalizer.rb +220 -0
- data/lib/trailblazer/activity/dsl/linear/state.rb +58 -0
- data/lib/trailblazer/activity/dsl/linear/strategy.rb +92 -0
- data/lib/trailblazer/activity/dsl/linear/variable_mapping.rb +82 -0
- data/lib/trailblazer/activity/dsl/linear/version.rb +11 -0
- data/lib/trailblazer/activity/fast_track.rb +163 -0
- data/lib/trailblazer/activity/path.rb +179 -0
- data/lib/trailblazer/activity/railway.rb +172 -0
- data/trailblazer-activity-dsl-linear.gemspec +29 -0
- metadata +119 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL
|
4
|
+
module Linear
|
5
|
+
# Normalizers are linear activities that process and normalize the options from a DSL call. They're
|
6
|
+
# usually invoked from {Strategy#task_for}, which is called from {Path#step}, {Railway#pass}, etc.
|
7
|
+
module Normalizer
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# activity_normalizer.([{options:, user_options:, normalizer_options: }])
|
11
|
+
def activity_normalizer(sequence)
|
12
|
+
seq = Activity::Path::DSL.prepend_to_path( # this doesn't particularly put the steps after the Path steps.
|
13
|
+
sequence,
|
14
|
+
|
15
|
+
{
|
16
|
+
"activity.normalize_step_interface" => method(:normalize_step_interface), # first
|
17
|
+
"activity.normalize_override" => method(:normalize_override),
|
18
|
+
"activity.normalize_for_macro" => method(:merge_user_options),
|
19
|
+
"activity.normalize_normalizer_options" => method(:merge_normalizer_options),
|
20
|
+
"activity.normalize_context" => method(:normalize_context),
|
21
|
+
"activity.normalize_id" => method(:normalize_id),
|
22
|
+
"activity.wrap_task_with_step_interface" => method(:wrap_task_with_step_interface), # last
|
23
|
+
},
|
24
|
+
|
25
|
+
Linear::Insert.method(:Append), "Start.default"
|
26
|
+
)
|
27
|
+
|
28
|
+
seq = Trailblazer::Activity::Path::DSL.prepend_to_path( # this doesn't particularly put the steps after the Path steps.
|
29
|
+
seq,
|
30
|
+
|
31
|
+
{
|
32
|
+
"activity.normalize_outputs_from_dsl" => method(:normalize_outputs_from_dsl), # Output(Signal, :semantic) => Id()
|
33
|
+
"activity.normalize_connections_from_dsl" => method(:normalize_connections_from_dsl),
|
34
|
+
},
|
35
|
+
|
36
|
+
Linear::Insert.method(:Prepend), "path.wirings"
|
37
|
+
)
|
38
|
+
|
39
|
+
seq = Trailblazer::Activity::Path::DSL.prepend_to_path( # this doesn't particularly put the steps after the Path steps.
|
40
|
+
seq,
|
41
|
+
|
42
|
+
{
|
43
|
+
"activity.cleanup_options" => method(:cleanup_options),
|
44
|
+
},
|
45
|
+
|
46
|
+
Linear::Insert.method(:Prepend), "End.success"
|
47
|
+
)
|
48
|
+
# pp seq
|
49
|
+
seq
|
50
|
+
end
|
51
|
+
|
52
|
+
# Specific to the "step DSL": if the first argument is a callable, wrap it in a {step_interface_builder}
|
53
|
+
# since its interface expects the step interface, but the circuit will call it with circuit interface.
|
54
|
+
def normalize_step_interface((ctx, flow_options), *)
|
55
|
+
options = ctx[:options] # either a <#task> or {} from macro
|
56
|
+
|
57
|
+
unless options.is_a?(::Hash)
|
58
|
+
# task = wrap_with_step_interface(task: options, step_interface_builder: ctx[:user_options][:step_interface_builder]) # TODO: make this optional with appropriate wiring.
|
59
|
+
task = options
|
60
|
+
|
61
|
+
ctx = ctx.merge(options: {task: task, wrap_task: true})
|
62
|
+
end
|
63
|
+
|
64
|
+
return Trailblazer::Activity::Right, [ctx, flow_options]
|
65
|
+
end
|
66
|
+
|
67
|
+
def wrap_task_with_step_interface((ctx, flow_options), **)
|
68
|
+
return Trailblazer::Activity::Right, [ctx, flow_options] unless ctx[:wrap_task]
|
69
|
+
|
70
|
+
step_interface_builder = ctx[:step_interface_builder] # FIXME: use kw!
|
71
|
+
task = ctx[:task] # FIXME: use kw!
|
72
|
+
|
73
|
+
wrapped_task = step_interface_builder.(task)
|
74
|
+
|
75
|
+
return Trailblazer::Activity::Right, [ctx.merge(task: wrapped_task), flow_options]
|
76
|
+
end
|
77
|
+
|
78
|
+
def normalize_id((ctx, flow_options), **)
|
79
|
+
id = ctx[:id] || ctx[:task]
|
80
|
+
|
81
|
+
return Trailblazer::Activity::Right, [ctx.merge(id: id), flow_options]
|
82
|
+
end
|
83
|
+
|
84
|
+
# {:override} really only makes sense for {step Macro(), {override: true}} where the {user_options}
|
85
|
+
# dictate the overriding.
|
86
|
+
def normalize_override((ctx, flow_options), *)
|
87
|
+
user_options = ctx[:user_options]
|
88
|
+
user_options = user_options.merge(replace: ctx[:options][:id] || raise) if ctx[:user_options][:override]
|
89
|
+
|
90
|
+
return Trailblazer::Activity::Right, [ctx.merge(user_options: user_options), flow_options]
|
91
|
+
end
|
92
|
+
|
93
|
+
# make ctx[:options] the actual ctx
|
94
|
+
def merge_user_options((ctx, flow_options), *)
|
95
|
+
options = ctx[:options] # either a <#task> or {} from macro
|
96
|
+
|
97
|
+
ctx = ctx.merge(options: options.merge(ctx[:user_options])) # Note that the user options are merged over the macro options.
|
98
|
+
|
99
|
+
return Trailblazer::Activity::Right, [ctx, flow_options]
|
100
|
+
end
|
101
|
+
|
102
|
+
# {:normalizer_options} such as {:track_name} get overridden by user/macro.
|
103
|
+
def merge_normalizer_options((ctx, flow_options), *)
|
104
|
+
normalizer_options = ctx[:normalizer_options] # either a <#task> or {} from macro
|
105
|
+
|
106
|
+
ctx = ctx.merge(options: normalizer_options.merge(ctx[:options])) #
|
107
|
+
|
108
|
+
return Trailblazer::Activity::Right, [ctx, flow_options]
|
109
|
+
end
|
110
|
+
|
111
|
+
def normalize_context((ctx, flow_options), *)
|
112
|
+
ctx = ctx[:options]
|
113
|
+
|
114
|
+
return Trailblazer::Activity::Right, [ctx, flow_options]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Compile the actual {Seq::Row}'s {wiring}.
|
118
|
+
def compile_wirings((ctx, flow_options), *)
|
119
|
+
connections = ctx[:connections] || raise # FIXME
|
120
|
+
outputs = ctx[:outputs] || raise # FIXME
|
121
|
+
|
122
|
+
ctx[:wirings] =
|
123
|
+
connections.collect do |semantic, (search_strategy_builder, *search_args)|
|
124
|
+
output = outputs[semantic] || raise("No `#{semantic}` output found for #{outputs.inspect}")
|
125
|
+
|
126
|
+
search_strategy_builder.( # return proc to be called when compiling Seq, e.g. {ById(output, :id)}
|
127
|
+
output,
|
128
|
+
*search_args
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
return Trailblazer::Activity::Right, [ctx, flow_options]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Process {Output(:semantic) => target}.
|
136
|
+
def normalize_connections_from_dsl((ctx, flow_options), *)
|
137
|
+
new_ctx = ctx.reject { |output, cfg| output.kind_of?(Activity::DSL::Linear::OutputSemantic) }
|
138
|
+
connections = new_ctx[:connections]
|
139
|
+
adds = new_ctx[:adds]
|
140
|
+
|
141
|
+
# Find all {Output() => Track()/Id()/End()}
|
142
|
+
(ctx.keys - new_ctx.keys).each do |output|
|
143
|
+
cfg = ctx[output]
|
144
|
+
|
145
|
+
new_connections, add =
|
146
|
+
if cfg.is_a?(Activity::DSL::Linear::Track)
|
147
|
+
[output_to_track(ctx, output, cfg), cfg.adds]
|
148
|
+
elsif cfg.is_a?(Activity::DSL::Linear::Id)
|
149
|
+
[output_to_id(ctx, output, cfg.value), []]
|
150
|
+
elsif cfg.is_a?(Activity::End)
|
151
|
+
_adds = []
|
152
|
+
|
153
|
+
end_id = Linear.end_id(cfg)
|
154
|
+
end_exists = Insert.find_index(ctx[:sequence], end_id)
|
155
|
+
|
156
|
+
_adds = [add_end(cfg, magnetic_to: end_id, id: end_id)] unless end_exists
|
157
|
+
|
158
|
+
[output_to_id(ctx, output, end_id), _adds]
|
159
|
+
end
|
160
|
+
|
161
|
+
connections = connections.merge(new_connections)
|
162
|
+
adds += add
|
163
|
+
end
|
164
|
+
|
165
|
+
new_ctx = new_ctx.merge(connections: connections, adds: adds)
|
166
|
+
|
167
|
+
return Trailblazer::Activity::Right, [new_ctx, flow_options]
|
168
|
+
end
|
169
|
+
|
170
|
+
def output_to_track(ctx, output, target)
|
171
|
+
{output.value => [Linear::Search.method(:Forward), target.color]}
|
172
|
+
end
|
173
|
+
|
174
|
+
def output_to_id(ctx, output, target)
|
175
|
+
{output.value => [Linear::Search.method(:ById), target]}
|
176
|
+
end
|
177
|
+
|
178
|
+
# {#insert_task} options to add another end.
|
179
|
+
def add_end(end_event, magnetic_to:, id:)
|
180
|
+
|
181
|
+
options = Path::DSL.append_end_options(task: end_event, magnetic_to: magnetic_to, id: id)
|
182
|
+
row = Linear::Sequence.create_row(options)
|
183
|
+
|
184
|
+
{
|
185
|
+
row: row,
|
186
|
+
insert: row[3][:sequence_insert]
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
# Output(Signal, :semantic) => Id()
|
191
|
+
def normalize_outputs_from_dsl((ctx, flow_options), *)
|
192
|
+
new_ctx = ctx.reject { |output, cfg| output.kind_of?(Activity::Output) }
|
193
|
+
|
194
|
+
outputs = ctx[:outputs]
|
195
|
+
dsl_options = {}
|
196
|
+
|
197
|
+
(ctx.keys - new_ctx.keys).collect do |output|
|
198
|
+
cfg = ctx[output] # e.g. Track(:success)
|
199
|
+
|
200
|
+
outputs = outputs.merge(output.semantic => output)
|
201
|
+
dsl_options = dsl_options.merge(Linear.Output(output.semantic) => cfg)
|
202
|
+
end
|
203
|
+
|
204
|
+
new_ctx = new_ctx.merge(outputs: outputs).merge(dsl_options)
|
205
|
+
|
206
|
+
return Trailblazer::Activity::Right, [new_ctx, flow_options]
|
207
|
+
end
|
208
|
+
|
209
|
+
# TODO: make this extendable!
|
210
|
+
def cleanup_options((ctx, flow_options), *)
|
211
|
+
new_ctx = ctx.reject { |k, v| [:connections, :outputs, :end_id, :step_interface_builder, :failure_end, :track_name, :sequence].include?(k) }
|
212
|
+
|
213
|
+
return Trailblazer::Activity::Right, [new_ctx, flow_options]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end # Normalizer
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL
|
4
|
+
module Linear
|
5
|
+
# A {State} instance is kept per DSL client, which usually is a subclass of {Path}, {Railway}, etc.
|
6
|
+
class State
|
7
|
+
# remembers how to call normalizers (e.g. track_color), TaskBuilder
|
8
|
+
# remembers sequence
|
9
|
+
def initialize(normalizers:, initial_sequence:, **normalizer_options)
|
10
|
+
@normalizer = normalizers # compiled normalizers.
|
11
|
+
@sequence = initial_sequence
|
12
|
+
@normalizer_options = normalizer_options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Called to "inherit" a state.
|
16
|
+
def copy
|
17
|
+
self.class.new(normalizers: @normalizer, initial_sequence: @sequence, **@normalizer_options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
{sequence: @sequence, normalizers: @normalizer, normalizer_options: @normalizer_options} # FIXME.
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_sequence(&block)
|
25
|
+
@sequence = yield(to_h)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Compiles and maintains all final normalizers for a specific DSL.
|
29
|
+
class Normalizer
|
30
|
+
def compile_normalizer(normalizer_sequence)
|
31
|
+
process = Trailblazer::Activity::DSL::Linear::Compiler.(normalizer_sequence)
|
32
|
+
process.to_h[:circuit]
|
33
|
+
end
|
34
|
+
|
35
|
+
# [gets instantiated at compile time.]
|
36
|
+
#
|
37
|
+
# We simply compile the activities that represent the normalizers for #step, #pass, etc.
|
38
|
+
# This can happen at compile-time, as normalizers are stateless.
|
39
|
+
def initialize(normalizer_sequences)
|
40
|
+
@normalizers = Hash[
|
41
|
+
normalizer_sequences.collect { |name, seq| [name, compile_normalizer(seq)] }
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Execute the specific normalizer (step, fail, pass) for a particular option set provided
|
46
|
+
# by the DSL user. This is usually when you call Operation::step.
|
47
|
+
def call(name, *args)
|
48
|
+
normalizer = @normalizers.fetch(name)
|
49
|
+
signal, (options, _) = normalizer.(*args)
|
50
|
+
options
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end # State
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL
|
4
|
+
module Linear
|
5
|
+
# {Activity}
|
6
|
+
# holds the {@schema}
|
7
|
+
# provides DSL step/merge!
|
8
|
+
# provides DSL inheritance
|
9
|
+
# provides run-time {call}
|
10
|
+
# maintains the {state} with {seq} and normalizer options
|
11
|
+
module Strategy
|
12
|
+
def initialize!(state)
|
13
|
+
@state = state
|
14
|
+
|
15
|
+
recompile_activity!(@state.to_h[:sequence])
|
16
|
+
end
|
17
|
+
|
18
|
+
def inherited(inheriter)
|
19
|
+
super
|
20
|
+
|
21
|
+
# inherits the {@sequence}, and options.
|
22
|
+
inheriter.initialize!(@state.copy)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Called from {#step} and friends.
|
26
|
+
def self.task_for!(state, type, task, options={}, &block)
|
27
|
+
options = options.merge(dsl_track: type)
|
28
|
+
|
29
|
+
# {#update_sequence} is the only way to mutate the state instance.
|
30
|
+
state.update_sequence do |sequence:, normalizers:, normalizer_options:|
|
31
|
+
# Compute the sequence rows.
|
32
|
+
options = normalizers.(type, normalizer_options: normalizer_options, options: task, user_options: options.merge(sequence: sequence))
|
33
|
+
|
34
|
+
sequence = Activity::DSL::Linear::DSL.apply_adds_from_dsl(sequence, options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @public
|
39
|
+
private def step(*args, &block)
|
40
|
+
recompile_activity_for(:step, *args, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
private def recompile_activity_for(type, *args, &block)
|
44
|
+
args = forward_block(args, block)
|
45
|
+
|
46
|
+
seq = @state.send(type, *args)
|
47
|
+
|
48
|
+
recompile_activity!(seq)
|
49
|
+
end
|
50
|
+
|
51
|
+
private def recompile_activity!(seq)
|
52
|
+
schema = Compiler.(seq)
|
53
|
+
|
54
|
+
@activity = Activity.new(schema)
|
55
|
+
end
|
56
|
+
|
57
|
+
private def forward_block(args, block)
|
58
|
+
options = args[1]
|
59
|
+
if options.is_a?(Hash) # FIXME: doesn't account {task: <>} and repeats logic from Normalizer.
|
60
|
+
output, proxy = (options.find { |k,v| v.is_a?(BlockProxy) } or return args)
|
61
|
+
shared_options = {step_interface_builder: @state.instance_variable_get(:@normalizer_options)[:step_interface_builder]} # FIXME: how do we know what to pass on and what not?
|
62
|
+
return args[0], options.merge(output => Linear.Path(**shared_options, **proxy.options, &block))
|
63
|
+
end
|
64
|
+
|
65
|
+
args
|
66
|
+
end
|
67
|
+
|
68
|
+
extend Forwardable
|
69
|
+
def_delegators Linear, :Output, :End, :Track, :Id, :Subprocess
|
70
|
+
|
71
|
+
def Path(options) # we can't access {block} here, syntactically.
|
72
|
+
BlockProxy.new(options)
|
73
|
+
end
|
74
|
+
|
75
|
+
BlockProxy = Struct.new(:options)
|
76
|
+
|
77
|
+
private def merge!(activity)
|
78
|
+
old_seq = @state.instance_variable_get(:@sequence) # TODO: fixme
|
79
|
+
new_seq = activity.instance_variable_get(:@state).instance_variable_get(:@sequence) # TODO: fix the interfaces
|
80
|
+
|
81
|
+
seq = Linear.Merge(old_seq, new_seq, end_id: "End.success")
|
82
|
+
|
83
|
+
@state.instance_variable_set(:@sequence, seq) # FIXME: hate this so much.
|
84
|
+
end
|
85
|
+
|
86
|
+
extend Forwardable
|
87
|
+
def_delegators :@activity, :to_h, :call
|
88
|
+
end # Strategy
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# DSL step for Magnetic::Normalizer.
|
2
|
+
# Translates `:input` and `:output` into VariableMapping taskWrap extensions.
|
3
|
+
def self.normalizer_step_for_input_output(ctx, *)
|
4
|
+
options, io_config = Magnetic::Options.normalize( ctx[:options], [:input, :output] )
|
5
|
+
|
6
|
+
return if io_config.empty?
|
7
|
+
|
8
|
+
ctx[:options] = options # without :input and :output
|
9
|
+
ctx[:options] = options.merge(Trailblazer::Activity::TaskWrap::VariableMapping(io_config) => true)
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
# @private
|
14
|
+
def self.filter_for(filter)
|
15
|
+
if filter.is_a?(::Array) || filter.is_a?(::Hash)
|
16
|
+
TaskWrap::DSL.filter_from_dsl(filter)
|
17
|
+
else
|
18
|
+
filter
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns an Extension instance to be thrown into the `step` DSL arguments.
|
23
|
+
def self.VariableMapping(input:, output:)
|
24
|
+
input = Input.new(
|
25
|
+
Input::Scoped.new(
|
26
|
+
Trailblazer::Option::KW( filter_for(input) )
|
27
|
+
)
|
28
|
+
)
|
29
|
+
|
30
|
+
output = Output.new(
|
31
|
+
Output::Unscoped.new(
|
32
|
+
Trailblazer::Option::KW( filter_for(output) )
|
33
|
+
)
|
34
|
+
)
|
35
|
+
|
36
|
+
VariableMapping.extension_for(input, output)
|
37
|
+
end
|
38
|
+
|
39
|
+
module Input
|
40
|
+
class Scoped
|
41
|
+
def initialize(filter)
|
42
|
+
@filter = filter
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(original_ctx, circuit_options)
|
46
|
+
Trailblazer::Context( # TODO: make this interchangeable so we can work on faster contexts?
|
47
|
+
@filter.(original_ctx, **circuit_options)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module DSL
|
54
|
+
# The returned filter compiles a new hash for Scoped/Unscoped that only contains
|
55
|
+
# the desired i/o variables.
|
56
|
+
def self.filter_from_dsl(map)
|
57
|
+
hsh = DSL.hash_for(map)
|
58
|
+
|
59
|
+
->(incoming_ctx, kwargs) { Hash[hsh.collect { |from_name, to_name| [to_name, incoming_ctx[from_name]] }] }
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.hash_for(ary)
|
63
|
+
return ary if ary.instance_of?(::Hash)
|
64
|
+
Hash[ary.collect { |name| [name, name] }]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module Output
|
69
|
+
# Merge the resulting {@filter.()} hash back into the original ctx.
|
70
|
+
# DISCUSS: do we need the original_ctx as a filter argument?
|
71
|
+
class Unscoped
|
72
|
+
def initialize(filter)
|
73
|
+
@filter = filter
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(original_ctx, new_ctx, **circuit_options)
|
77
|
+
original_ctx.merge(
|
78
|
+
@filter.(new_ctx, **circuit_options)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|