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

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