trailblazer-activity-dsl-linear 0.5.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/CHANGES.md +52 -0
  4. data/Gemfile +3 -1
  5. data/README.md +9 -17
  6. data/lib/trailblazer/activity/dsl/linear/feature/merge.rb +36 -0
  7. data/lib/trailblazer/activity/dsl/linear/feature/patch.rb +40 -0
  8. data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping/dsl.rb +281 -0
  9. data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping/inherit.rb +38 -0
  10. data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping.rb +298 -0
  11. data/lib/trailblazer/activity/dsl/linear/helper/path.rb +106 -0
  12. data/lib/trailblazer/activity/dsl/linear/helper.rb +54 -128
  13. data/lib/trailblazer/activity/dsl/linear/normalizer/terminus.rb +92 -0
  14. data/lib/trailblazer/activity/dsl/linear/normalizer.rb +194 -77
  15. data/lib/trailblazer/activity/dsl/linear/sequence/builder.rb +47 -0
  16. data/lib/trailblazer/activity/dsl/linear/sequence/compiler.rb +72 -0
  17. data/lib/trailblazer/activity/dsl/linear/sequence/search.rb +58 -0
  18. data/lib/trailblazer/activity/dsl/linear/sequence.rb +34 -0
  19. data/lib/trailblazer/activity/dsl/linear/strategy.rb +116 -71
  20. data/lib/trailblazer/activity/dsl/linear/version.rb +1 -1
  21. data/lib/trailblazer/activity/dsl/linear.rb +21 -177
  22. data/lib/trailblazer/activity/fast_track.rb +42 -53
  23. data/lib/trailblazer/activity/path.rb +52 -127
  24. data/lib/trailblazer/activity/railway.rb +48 -68
  25. data/trailblazer-activity-dsl-linear.gemspec +3 -2
  26. metadata +41 -13
  27. data/lib/trailblazer/activity/dsl/linear/compiler.rb +0 -70
  28. data/lib/trailblazer/activity/dsl/linear/state.rb +0 -63
  29. data/lib/trailblazer/activity/dsl/linear/variable_mapping.rb +0 -240
@@ -1,63 +0,0 @@
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
- # State doesn't have any immutable features - all write operations to it must guarantee they only replace
7
- # instance variables.
8
- #
9
- # @private
10
- #
11
- # DISCUSS: why do we have this structure? It doesn't cover "immutable copying", that has to be done by its clients.
12
- # also, copy with to_h
13
- class State
14
- # remembers how to call normalizers (e.g. track_color), TaskBuilder
15
- # remembers sequence
16
- def initialize(normalizers:, initial_sequence:, fields: {}.freeze, **normalizer_options)
17
- @normalizer = normalizers # compiled normalizers.
18
- @sequence = initial_sequence
19
- @normalizer_options = normalizer_options
20
- @fields = fields
21
- end
22
-
23
- # Called to "inherit" a state.
24
- def copy
25
- self.class.new(normalizers: @normalizer, initial_sequence: @sequence, fields: @fields, **@normalizer_options)
26
- end
27
-
28
- def to_h
29
- {sequence: @sequence, normalizers: @normalizer, normalizer_options: @normalizer_options, fields: @fields} # FIXME.
30
- end
31
-
32
- def update_sequence(&block)
33
- @sequence = yield(**to_h)
34
- end
35
-
36
- def update_options(fields)
37
- @fields = fields
38
- end
39
-
40
- # Compiles and maintains all final normalizers for a specific DSL.
41
- class Normalizer
42
- # [gets instantiated at compile time.]
43
- #
44
- # We simply compile the activities that represent the normalizers for #step, #pass, etc.
45
- # This can happen at compile-time, as normalizers are stateless.
46
- def initialize(normalizer_sequences)
47
- @normalizers = normalizer_sequences
48
- end
49
-
50
- # Execute the specific normalizer (step, fail, pass) for a particular option set provided
51
- # by the DSL user. This is usually when you call Operation::step.
52
- def call(name, ctx)
53
- normalizer = @normalizers.fetch(name)
54
- wrap_ctx, _ = normalizer.(ctx, nil)
55
- wrap_ctx
56
- end
57
- end
58
- end # State
59
-
60
- end
61
- end
62
- end
63
- end
@@ -1,240 +0,0 @@
1
- module Trailblazer
2
- class Activity
3
- module DSL
4
- module Linear
5
- # Normalizer-steps to implement {:input} and {:output}
6
- # Returns an Extension instance to be thrown into the `step` DSL arguments.
7
- def self.VariableMapping(input: nil, output: VariableMapping.default_output, output_with_outer_ctx: false, inject: [])
8
- merge_instructions = VariableMapping.merge_instructions_from_dsl(input: input, output: output, output_with_outer_ctx: output_with_outer_ctx, inject: inject)
9
-
10
- TaskWrap::Extension(merge: merge_instructions)
11
- end
12
-
13
- module VariableMapping
14
- module_function
15
-
16
- # For the input filter we
17
- # 1. create a separate {Pipeline} instance {pipe}. Depending on the user's options, this might have up to four steps.
18
- # 2. The {pipe} is run in a lamdba {input}, the lambda returns the pipe's ctx[:input_ctx].
19
- # 3. The {input} filter in turn is wrapped into an {Activity::TaskWrap::Input} object via {#merge_instructions_for}.
20
- # 4. The {TaskWrap::Input} instance is then finally placed into the taskWrap as {"task_wrap.input"}.
21
- #
22
- # @private
23
- def merge_instructions_from_dsl(input:, output:, output_with_outer_ctx:, inject:)
24
- # FIXME: this could (should?) be in Normalizer?
25
- inject_passthrough = inject.find_all { |name| name.is_a?(Symbol) }
26
- inject_with_default = inject.find { |name| name.is_a?(Hash) } # FIXME: we only support one default hash in the DSL so far.
27
-
28
- input_steps = [
29
- ["input.init_hash", VariableMapping.method(:initial_input_hash)],
30
- ]
31
-
32
- # With only injections defined, we do not filter out anything, we use the original ctx
33
- # and _add_ defaulting for injected variables.
34
- if !input # only injections defined
35
- input_steps << ["input.default_input", VariableMapping.method(:default_input_ctx)]
36
- end
37
-
38
- if input # :input or :input/:inject
39
- input_steps << ["input.add_variables", VariableMapping.method(:add_variables)]
40
-
41
- input_filter = Trailblazer::Option(VariableMapping::filter_for(input))
42
- end
43
-
44
- if inject_passthrough || inject_with_default
45
- input_steps << ["input.add_injections", VariableMapping.method(:add_injections)] # we now allow one filter per injected variable.
46
- end
47
-
48
- if inject_passthrough || inject_with_default
49
- injections = inject.collect do |name|
50
- if name.is_a?(Symbol)
51
- [[name, Trailblazer::Option(->(*) { [false, name] })]] # we don't want defaulting, this return value signalizes "please pass-through, only".
52
- else # we automatically assume this is a hash of callables
53
- name.collect do |_name, filter|
54
- [_name, Trailblazer::Option(->(ctx, **kws) { [true, _name, filter.(ctx, **kws)] })] # filter will compute the default value
55
- end
56
- end
57
- end.flatten(1).to_h
58
- end
59
-
60
- input_steps << ["input.scope", VariableMapping.method(:scope)]
61
-
62
-
63
- pipe = Activity::TaskWrap::Pipeline.new(input_steps)
64
-
65
- # gets wrapped by {VariableMapping::Input} and called there.
66
- # API: @filter.([ctx, original_flow_options], **original_circuit_options)
67
- # input = Trailblazer::Option(->(original_ctx, **) { })
68
- input = ->((ctx, flow_options), **circuit_options) do # This filter is called by {TaskWrap::Input#call} in the {activity} gem.
69
- wrap_ctx, _ = pipe.({injections: injections, input_filter: input_filter}, [[ctx, flow_options], circuit_options])
70
-
71
- wrap_ctx[:input_ctx]
72
- end
73
-
74
- # 1. {} empty input hash
75
- # 1. input # dynamic => hash
76
- # 2. input_map => hash
77
- # 3. inject => hash
78
- # 4. Input::Scoped()
79
-
80
- unscope_class = output_with_outer_ctx ? VariableMapping::Output::Unscoped::WithOuterContext : VariableMapping::Output::Unscoped
81
-
82
- output =
83
- unscope_class.new(
84
- Trailblazer::Option(VariableMapping::filter_for(output))
85
- )
86
-
87
- TaskWrap::VariableMapping.merge_instructions_for(input, output, id: input.object_id) # wraps filters: {Input(input), Output(output)}
88
- end
89
-
90
- # DISCUSS: improvable sections such as merge vs hash[]=
91
- def initial_input_hash(wrap_ctx, original_args)
92
- wrap_ctx = wrap_ctx.merge(input_hash: {})
93
-
94
- return wrap_ctx, original_args
95
- end
96
-
97
- # Merge all original ctx variables into the new input_ctx.
98
- # This happens when no {:input} is provided.
99
- def default_input_ctx(wrap_ctx, original_args)
100
- ((original_ctx, _), _) = original_args
101
-
102
- MergeVariables(original_ctx, wrap_ctx, original_args)
103
- end
104
-
105
- # TODO: test {nil} default
106
- # FIXME: what if you don't want inject but always the value from the config?
107
- # Add injected variables if they're present on
108
- # the original, incoming ctx.
109
- def add_injections(wrap_ctx, original_args)
110
- name2filter = wrap_ctx[:injections]
111
- ((original_ctx, _), circuit_options) = original_args
112
-
113
- injections =
114
- name2filter.collect do |name, filter|
115
- # DISCUSS: should we remove {is_defaulted} and infer type from {filter} or the return value?
116
- is_defaulted, new_name, default_value = filter.(original_ctx, keyword_arguments: original_ctx.to_hash, **circuit_options) # FIXME: interface? # {filter} exposes {Option} interface
117
-
118
- original_ctx.key?(name) ?
119
- [new_name, original_ctx[name]] : (
120
- is_defaulted ? [new_name, default_value] : nil
121
- )
122
- end.compact.to_h # FIXME: are we <2.6 safe here?
123
-
124
- MergeVariables(injections, wrap_ctx, original_args)
125
- end
126
-
127
- # Implements {:input}.
128
- def add_variables(wrap_ctx, original_args)
129
- filter = wrap_ctx[:input_filter]
130
- ((original_ctx, _), circuit_options) = original_args
131
-
132
- # this is the actual logic.
133
- variables = filter.(original_ctx, keyword_arguments: original_ctx.to_hash, **circuit_options)
134
-
135
- MergeVariables(variables, wrap_ctx, original_args)
136
- end
137
-
138
- # Finally, create a new input ctx from all the
139
- # collected input variables.
140
- # This goes into the step/nested OP.
141
- def scope(wrap_ctx, original_args)
142
- ((_, flow_options), _) = original_args
143
-
144
- # this is the actual context passed into the step.
145
- wrap_ctx[:input_ctx] = Trailblazer::Context(
146
- wrap_ctx[:input_hash],
147
- {}, # mutable variables
148
- flow_options[:context_options]
149
- )
150
-
151
- return wrap_ctx, original_args
152
- end
153
-
154
- # Last call in every step. Currently replaces {:input_ctx} by adding variables using {#merge}.
155
- # DISCUSS: improve here?
156
- def MergeVariables(variables, wrap_ctx, original_args)
157
- wrap_ctx[:input_hash] = wrap_ctx[:input_hash].merge(variables)
158
-
159
- return wrap_ctx, original_args
160
- end
161
-
162
- # @private
163
- # The default {:output} filter only returns the "mutable" part of the inner ctx.
164
- # This means only variables added using {inner_ctx[..]=} are merged on the outside.
165
- def default_output
166
- ->(scoped, **) do
167
- _wrapped, mutable = scoped.decompose # `_wrapped` is what the `:input` filter returned, `mutable` is what the task wrote to `scoped`.
168
- mutable
169
- end
170
- end
171
-
172
- # Returns a filter proc to be called in an Option.
173
- # @private
174
- def filter_for(filter)
175
- if filter.is_a?(::Array) || filter.is_a?(::Hash)
176
- DSL.filter_from_dsl(filter)
177
- else
178
- filter
179
- end
180
- end
181
-
182
- # @private
183
- def output_option_for(option, pass_outer_ctx) # DISCUSS: not sure I like this.
184
-
185
- return option if pass_outer_ctx
186
- # OutputReceivingInnerCtxOnly =
187
-
188
- # don't pass {outer_ctx}, only {inner_ctx}. this is the default.
189
- return ->(inner_ctx, outer_ctx, **kws) { option.(inner_ctx, **kws) }
190
- end
191
-
192
-
193
- module DSL
194
- # The returned filter compiles a new hash for Scoped/Unscoped that only contains
195
- # the desired i/o variables.
196
- def self.filter_from_dsl(map)
197
- hsh = DSL.hash_for(map)
198
-
199
- ->(incoming_ctx, **kwargs) { Hash[hsh.collect { |from_name, to_name| [to_name, incoming_ctx[from_name]] }] }
200
- end
201
-
202
- def self.hash_for(ary)
203
- return ary if ary.instance_of?(::Hash)
204
- Hash[ary.collect { |name| [name, name] }]
205
- end
206
- end
207
-
208
- module Output
209
- # Merge the resulting {@filter.()} hash back into the original ctx.
210
- # DISCUSS: do we need the original_ctx as a filter argument?
211
- class Unscoped
212
- def initialize(filter)
213
- @filter = filter
214
- end
215
-
216
- # The returned hash from {@filter} is merged with the original ctx.
217
- def call(new_ctx, (original_ctx, flow_options), **circuit_options)
218
- original_ctx.merge(
219
- call_filter(new_ctx, [original_ctx, flow_options], **circuit_options)
220
- )
221
- end
222
-
223
- def call_filter(new_ctx, (_original_ctx, _flow_options), **circuit_options)
224
- # Pass {inner_ctx, **inner_ctx}
225
- @filter.(new_ctx, keyword_arguments: new_ctx.to_hash, **circuit_options)
226
- end
227
-
228
- class WithOuterContext < Unscoped
229
- def call_filter(new_ctx, (original_ctx, _flow_options), **circuit_options)
230
- # Pass {inner_ctx, outer_ctx, **inner_ctx}
231
- @filter.(new_ctx, original_ctx, keyword_arguments: new_ctx.to_hash, **circuit_options)
232
- end
233
- end
234
- end
235
- end
236
- end # VariableMapping
237
- end
238
- end
239
- end
240
- end