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.
@@ -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