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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6185e8d95445bb10e611433a6885be07ec85524cdb5d45e1aed6449610391dd1
4
- data.tar.gz: e0af2bf5eba1bbb381c35675c3352ba99b5a3598964e9deffeec75d8e6b3be58
3
+ metadata.gz: 40f3c629e47f86039e0028f0e82f78a51f9b4d7d9e6c953a3605510b1241c050
4
+ data.tar.gz: 54a79c00d4a4505c0da36dcffb209f1fdf6a5c438156620c665443d481039f7d
5
5
  SHA512:
6
- metadata.gz: 3ae89d32dddf81784df0abb2d103c3672125c80f6637e5cae76a3a460addb75e7707352a43643d8359273cb7c2e6c5e96636e19c714adebe5da657cd74d5c73e
7
- data.tar.gz: c9f56bcb32fd1e1ba4adb7e8b39af43093d96a0eadb9ff21cbe35a2207a4c7b70728ee1eda7de00af1bf0972afbb51744143ff6f3d05595c6457c209f31b38c4
6
+ metadata.gz: c4c2d3df3fff7dbae401d840c1a4efca1397be7a7839b2d89b05300bd17f4b41df0f86c32c19ea98c2a61e8d1a045278592041605c06c7e4a46797e8f5bc97b7
7
+ data.tar.gz: daa3b6f02156a479bda2b6359583be835a6b908415a388714c6e3d17004e6dff3f8682619e83ace151259deca834744de7521d30916626df05a9e207b2705e7e
@@ -6,7 +6,7 @@ jobs:
6
6
  fail-fast: false
7
7
  matrix:
8
8
  # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
9
- ruby: [2.5, 2.6, 2.7, '3.0', head, jruby, jruby-head]
9
+ ruby: [2.5, 2.6, 2.7, '3.0', head, jruby]
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
12
  - uses: actions/checkout@v2
data/CHANGES.md CHANGED
@@ -1,3 +1,55 @@
1
+ # 1.0.0
2
+
3
+ ## Additions
4
+
5
+ * Introduce composable input/output filters with `In()`, `Out()` and `Inject()`. # FIXME: add link
6
+ * We no longer store arbitrary variables from `#step` calls in the sequence row's `data` field.
7
+ Use the `DataVariable` helper to mark variables for storage in `data`.
8
+
9
+ ```ruby
10
+ step :find_model,
11
+ model_class: Song,
12
+ Trailblazer::Activity::DSL::Linear::Helper.DataVariable() => :model_class
13
+ ```
14
+ * Add `Normalizer.extend!` to add steps to a particular normalizer. # FIXME: add link
15
+ * Add `Strategy.terminus` to add termini. # FIXME: add link
16
+ * The `Sequence` instance is now readable via `#to_h`: `Strategy.to_h[:sequence]`.
17
+ * In Normalizer, the `path.wirings` step is now named `activity.wirings`.
18
+
19
+ ## Design
20
+
21
+ * DSL logic: move as much as possible into the normalizer as it's much easier to understand and follow (and debug).
22
+ * Each DSL method now directly invokes a normalizer pipeline that processes the user options and produces an ADDS structure.
23
+ * We now need `Sequence::Row` instances in `Sequence` to adhere to the Adds specification.
24
+ * Rename `Linear::State` to `Linear::Sequence::Builder`. This is now a stateless function, only.
25
+ Sequence::Builder.()
26
+ * @state ?
27
+ * Remove `Strategy@activity` instance variable and move it to `@state[:activity]`.
28
+ * Much better file structuring.
29
+
30
+ ## Internals
31
+
32
+ * Use `Trailblazer::Declarative::State` to maintain sequence and other fields. This makes inheritance consistent.
33
+ * Make `Strategy` a class. It makes constant management much simpler to understand.
34
+ * `Linear.end_id` now accepts keyword arguments (mainly, `:semantic`).
35
+ * `Strategy.apply_step_on_state!` is now an immutable `Sequence::Builder.update_sequence_for`.
36
+ * The `Railway.Path()` helper returns a `DSL::PathBranch` non-symbol that is then picked up and processed by the normalizer (exactly how we do it with `In()`, `Track()` etc.). Branching implementation is handled in `helper/path.rb`.
37
+ * Remove `State.update_options`. Use `@state.update!`.
38
+ * Remove `Helper.normalize`.
39
+ * Remove `Linear::DSL.insert_task`. The canonical way to add steps is using the ADDS interface going through a normalizer.
40
+ That's why there's a normalizer for `end` (or "terminus") now for consistency.
41
+ * Remove `Helper::ClassMethods`, `Helper` is now the namespace to mix in your own functions (and ours, like `Output()`).
42
+ * Introduce `Helper::Constants` for namespaced macros such as `Policy::Pundit()`.
43
+
44
+ ## Renaming
45
+
46
+ * Rename `Linear::State::Normalizer` to `Linear::Normalizer::Normalizers` as it represents a container for normalizers.
47
+ * Move `Linear::Insert` to `Activity::Adds::Insert` in the `trailblazer-activity` gem.
48
+ * Move `Linear::Search` to `Linear::Sequence::Search` and `Linear::Compiler` to `Linear::Sequence::Compiler`.
49
+ * `TaskWrap::Pipeline.prepend` is now `Linear::Normalizer.prepend_to`. To use the `:replace` option you can use `Linear::Normalizer.replace`.
50
+ * Move `Sequence::IndexError` to `Activity::Adds::IndexError` in the `trailblazer-activity` gem. Remove `IndexError#step_id`.
51
+ * Move DSL structures like `OutputSemantic` to `Linear` namespace.
52
+
1
53
  # 0.5.0
2
54
 
3
55
  * Introduce `:inject` option to pass-through injected variables and to default input variables.
data/Gemfile CHANGED
@@ -8,6 +8,8 @@ gem "minitest-line"
8
8
  gem "rubocop", require: false
9
9
 
10
10
  # gem "trailblazer-developer", path: "../trailblazer-developer"
11
+ # gem "trailblazer-developer", github: "trailblazer/trailblazer-developer"
12
+ # gem "trailblazer-declarative", path: "../trailblazer-declarative"
11
13
  # gem "trailblazer-activity", path: "../trailblazer-activity"
12
- gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
14
+ # gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
13
15
  # gem "trailblazer-activity", path: "../circuit"
data/README.md CHANGED
@@ -1,25 +1,24 @@
1
1
  # Activity-DSL-Linear
2
2
 
3
- The `activity-dsl-linear` gem brings:
4
- - [Path](https://trailblazer.to/2.1/docs/activity.html#activity-strategy-path)
5
- - [Railway](https://trailblazer.to/2.1/docs/activity.html#activity-strategy-railway)
6
- - [Fasttrack](https://trailblazer.to/2.1/docs/activity.html#activity-strategy-fasttrack)
7
-
8
- DSLs strategies for buildig activities. It is build around [`activity`](https://github.com/trailblazer/trailblazer-activity) gem.
3
+ The `trailblazer-activity-dsl-linear` gem brings the popular `step` DSL around the [`activity`](https://github.com/trailblazer/trailblazer-activity) gem. It allows to create classes that you might mostly know as _operations_ - service objects that execute your business logic in a certain order, depending on how you harness the `step` DSL.
9
4
 
10
5
  Please find the [full documentation on the Trailblazer website](https://trailblazer.to/2.1/docs/activity.html#activity-strategy).
11
6
 
12
7
  ## Example
13
8
 
14
- The `activity-dsl-linear` gem provides three default patterns to model processes: `Path`, `Railway` and `FastTrack`. Here's an example of what a railway activity could look like, along with some more complex connections (you can read more about Railway strategy in the [docs](https://trailblazer.to/2.1/docs/activity.html#activity-strategy-railway)).
9
+ The `activity-dsl-linear` gem provides three default patterns to model activities: `Path`, `Railway` and `FastTrack`. Here's an example of what a railway activity could look like, along with some more complex connections (you can read more about Railway strategy in the [docs](https://trailblazer.to/2.1/docs/activity.html#activity-strategy-railway)).
15
10
 
16
11
  ```ruby
17
- require "trailblazer-activity"
18
12
  require "trailblazer-activity-dsl-linear"
19
13
 
20
14
  class Memo::Update < Trailblazer::Activity::Railway
21
- # here goes your business logic
22
- #
15
+ # Use the DSL to describe the layout of the activity.
16
+ step :find_model
17
+ step :validate, Output(:failure) => End(:validation_error)
18
+ step :save
19
+ fail :log_error
20
+
21
+ # Here comes your business logic.
23
22
  def find_model(ctx, id:, **)
24
23
  ctx[:model] = Memo.find_by(id: id)
25
24
  end
@@ -37,13 +36,6 @@ class Memo::Update < Trailblazer::Activity::Railway
37
36
  def log_error(ctx, params:, **)
38
37
  ctx[:log] = "Some idiot wrote #{params.inspect}"
39
38
  end
40
-
41
- # here comes the DSL describing the layout of the activity
42
- #
43
- step :find_model
44
- step :validate, Output(:failure) => End(:validation_error)
45
- step :save
46
- fail :log_error
47
39
  end
48
40
  ```
49
41
 
@@ -0,0 +1,36 @@
1
+ class Trailblazer::Activity
2
+ module DSL
3
+ module Linear
4
+ module Merge
5
+ # Class methods for {Strategy}.
6
+ module DSL
7
+ def merge!(activity)
8
+ old_seq = to_h[:sequence]
9
+ new_seq = activity.to_h[:sequence]
10
+
11
+ seq = Merge.call(old_seq, new_seq, end_id: "End.success")
12
+
13
+ # Update the DSL's sequence, then recompile the actual activity.
14
+ recompile!(seq)
15
+ end
16
+ end
17
+
18
+ # Compile-time logic to merge two activities.
19
+ def self.call(old_seq, new_seq, end_id: "End.success") # DISCUSS: also Insert
20
+ new_seq = strip_start_and_ends(new_seq, end_id: end_id)
21
+
22
+ _seq = Adds.apply_adds(
23
+ old_seq,
24
+ new_seq.collect { |row| {insert: [Adds::Insert.method(:Prepend), end_id], row: row } }
25
+ )
26
+ end
27
+
28
+ def self.strip_start_and_ends(seq, end_id:)
29
+ cut_off_index = end_id.nil? ? seq.size : Adds::Insert.find_index(seq, end_id) # find the "first" end.
30
+
31
+ seq[1..cut_off_index-1]
32
+ end
33
+ end # Merge
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ class Trailblazer::Activity
2
+ module DSL
3
+ module Linear
4
+ module Patch
5
+ # DISCUSS: we could make this a generic DSL option, not just for Subprocess().
6
+ # Currently, this is called from the Subprocess() helper.
7
+ def self.customize(activity, options:)
8
+ options = options.is_a?(Proc) ?
9
+ { [] => options } : # hash-wrapping with empty path, for patching given activity itself
10
+ options
11
+
12
+ options.each do |path, patch|
13
+ activity = call(activity, path, patch) # TODO: test if multiple patches works!
14
+ end
15
+
16
+ activity
17
+ end
18
+
19
+ def self.call(activity, path, customization)
20
+ task_id, *path = path
21
+
22
+ patch =
23
+ if task_id
24
+ segment_activity = Introspect::Graph(activity).find(task_id).task
25
+ patched_segment_activity = call(segment_activity, path, customization)
26
+
27
+ # Replace the patched subprocess.
28
+ -> { step Subprocess(patched_segment_activity), inherit: true, replace: task_id, id: task_id }
29
+ else
30
+ customization # apply the *actual* patch from the Subprocess() call.
31
+ end
32
+
33
+ patched_activity = Class.new(activity)
34
+ patched_activity.class_exec(&patch)
35
+ patched_activity
36
+ end
37
+ end # Patch
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,281 @@
1
+ module Trailblazer
2
+ class Activity
3
+ module DSL
4
+ module Linear
5
+ module VariableMapping
6
+ # Code invoked through the normalizer, building runtime structures.
7
+ # Naming
8
+ # Option: Tuple => user filter
9
+ # Tuple: #<In ...>
10
+ module DSL
11
+ module_function
12
+
13
+ # Compute pipeline for {:input} option.
14
+ def pipe_for_mono_input(input: [], inject: [], in_filters: [], output: [], **)
15
+ has_input = Array(input).any?
16
+ has_mono_options = has_input || Array(inject).any? || Array(output).any? # :input, :inject and :output are "mono options".
17
+ has_composable_options = in_filters.any? # DISCUSS: why are we not testing Inject()?
18
+
19
+ if has_mono_options && has_composable_options
20
+ warn "[Trailblazer] You are mixing `:input` and `In() => ...`. `In()` and Inject () options are ignored and `:input` wins: #{input} #{inject} #{output} <> #{in_filters} / "
21
+ end
22
+
23
+ pipeline = initial_input_pipeline(add_default_ctx: !has_input)
24
+ pipeline = add_steps_for_input_option(pipeline, input: input)
25
+ pipeline = add_steps_for_inject_option(pipeline, inject: inject)
26
+
27
+ return pipeline, has_mono_options, has_composable_options
28
+ end
29
+
30
+ # Compute pipeline for In() and Inject().
31
+ # We allow to inject {:initial_input_pipeline} here in order to skip creating a new input pipeline and instead
32
+ # use the inherit one.
33
+ def pipe_for_composable_input(in_filters: [], inject_filters: [], initial_input_pipeline: initial_input_pipeline_for(in_filters), **)
34
+ inject_filters = DSL::Inject.filters_for_injects(inject_filters) # {Inject() => ...} the pure user input gets translated into AddVariable aggregate steps.
35
+ in_filters = DSL::Tuple.filters_from_options(in_filters)
36
+
37
+ # With only injections defined, we do not filter out anything, we use the original ctx
38
+ # and _add_ defaulting for injected variables.
39
+ pipeline = add_filter_steps(initial_input_pipeline, in_filters)
40
+ pipeline = add_filter_steps(pipeline, inject_filters)
41
+ end
42
+
43
+ # initial pipleline depending on whether or not we got any In() filters.
44
+ def initial_input_pipeline_for(in_filters)
45
+ is_inject_only = Array(in_filters).empty?
46
+
47
+ initial_input_pipeline(add_default_ctx: is_inject_only)
48
+ end
49
+
50
+
51
+ # Adds the default_ctx step as per option {:add_default_ctx}
52
+ def initial_input_pipeline(add_default_ctx: false)
53
+ # No In() or {:input}. Use default ctx, which is the original ctxx.
54
+ # When using Inject without In/:input, we also need a {default_input} ctx.
55
+ default_ctx_row =
56
+ add_default_ctx ? Activity::TaskWrap::Pipeline.Row(*default_input_ctx_config) : nil
57
+
58
+ pipe = Activity::TaskWrap::Pipeline.new(
59
+ [
60
+ Activity::TaskWrap::Pipeline.Row("input.init_hash", VariableMapping.method(:initial_aggregate)), # very first step
61
+ default_ctx_row,
62
+ Activity::TaskWrap::Pipeline.Row("input.scope", VariableMapping.method(:scope)), # last step
63
+ ].compact
64
+ )
65
+ end
66
+
67
+ def default_input_ctx_config # almost a Row.
68
+ ["input.default_input", VariableMapping.method(:default_input_ctx)]
69
+ end
70
+
71
+ # Handle {:input} and {:inject} option, the "old" interface.
72
+ def add_steps_for_input_option(pipeline, input:)
73
+ tuple = DSL.In(name: ":input") # simulate {In() => input}
74
+ input_filter = DSL::Tuple.filters_from_options([[tuple, input]])
75
+
76
+ add_filter_steps(pipeline, input_filter)
77
+ end
78
+
79
+
80
+ def pipe_for_mono_output(output_with_outer_ctx: false, output: [], out_filters: [], **)
81
+ # No Out(), no {:output} will result in a default_output_ctx step.
82
+ has_output = Array(output).any?
83
+ has_mono_options = has_output
84
+ has_composable_options = Array(out_filters).any?
85
+
86
+ if has_mono_options && has_composable_options
87
+ warn "[Trailblazer] You are mixing `:output` and `Out() => ...`. `Out()` options are ignored and `:output` wins."
88
+ end
89
+
90
+ pipeline = initial_output_pipeline(add_default_ctx: !has_output)
91
+ pipeline = add_steps_for_output_option(pipeline, output: output, output_with_outer_ctx: output_with_outer_ctx)
92
+
93
+ return pipeline, has_mono_options, has_composable_options
94
+ end
95
+
96
+ def add_steps_for_output_option(pipeline, output:, output_with_outer_ctx:)
97
+ tuple = DSL.Out(name: ":output", with_outer_ctx: output_with_outer_ctx) # simulate {Out() => output}
98
+ output_filter = DSL::Tuple.filters_from_options([[tuple, output]])
99
+
100
+ add_filter_steps(pipeline, output_filter, prepend_to: "output.merge_with_original")
101
+ end
102
+
103
+ def pipe_for_composable_output(out_filters: [], initial_output_pipeline: initial_output_pipeline(add_default_ctx: Array(out_filters).empty?), **)
104
+ out_filters = DSL::Tuple.filters_from_options(out_filters)
105
+
106
+ add_filter_steps(initial_output_pipeline, out_filters, prepend_to: "output.merge_with_original")
107
+ end
108
+
109
+ def initial_output_pipeline(add_default_ctx: false)
110
+ default_ctx_row =
111
+ add_default_ctx ? Activity::TaskWrap::Pipeline.Row(*default_output_ctx_config) : nil
112
+
113
+ Activity::TaskWrap::Pipeline.new(
114
+ [
115
+ Activity::TaskWrap::Pipeline.Row("output.init_hash", VariableMapping.method(:initial_aggregate)), # very first step
116
+ default_ctx_row,
117
+ Activity::TaskWrap::Pipeline.Row("output.merge_with_original", VariableMapping.method(:merge_with_original)), # last step
118
+ ].compact
119
+ )
120
+ end
121
+
122
+ def default_output_ctx_config # almost a Row.
123
+ ["output.default_output", VariableMapping.method(:default_output_ctx)]
124
+ end
125
+
126
+ def add_steps_for_inject_option(pipeline, inject:)
127
+ injects = inject.collect { |name| name.is_a?(Symbol) ? [DSL.Inject(), [name]] : [DSL.Inject(), name] }
128
+
129
+ tuples = DSL::Inject.filters_for_injects(injects) # DISCUSS: should we add passthrough/defaulting here at Inject()-time?
130
+
131
+ add_filter_steps(pipeline, tuples)
132
+ end
133
+
134
+ def add_filter_steps(pipeline, rows, prepend_to: "input.scope") # FIXME: do we need all this?
135
+ rows = add_variables_steps_for_filters(rows)
136
+
137
+ adds = Activity::Adds::FriendlyInterface.adds_for(
138
+ rows.collect { |row| [row[1], id: row[0], prepend: prepend_to] }
139
+ )
140
+
141
+ Activity::Adds.apply_adds(pipeline, adds)
142
+ end
143
+
144
+ # Returns array of step rows ("sequence").
145
+ # @param filters [Array] List of {Filter} objects
146
+ def add_variables_steps_for_filters(filters) # FIXME: allow output too!
147
+ filters.collect do |filter|
148
+ ["input.add_variables.#{filter.name}", filter.aggregate_step] # FIXME: config name sucks, of course, if we want to allow inserting etc.
149
+ end
150
+ end
151
+
152
+
153
+ # Filter code
154
+ # Converting user options to callable filters.
155
+
156
+ # @param [Array, Hash, Proc] User option coming from the DSL, like {[:model]}
157
+ #
158
+ # Returns a "filter interface" callable that's invoked in {AddVariables}:
159
+ # filter.(new_ctx, ..., keyword_arguments: new_ctx.to_hash, **circuit_options)
160
+ def self.build_filter(user_filter)
161
+ Trailblazer::Option(filter_for(user_filter))
162
+ end
163
+
164
+ # Convert a user option such as {[:model]} to a filter.
165
+ #
166
+ # Returns a filter proc to be called in an Option.
167
+ # @private
168
+ def self.filter_for(filter)
169
+ if filter.is_a?(::Array) || filter.is_a?(::Hash)
170
+ filter_from_dsl(filter)
171
+ else
172
+ filter
173
+ end
174
+ end
175
+
176
+ # The returned filter compiles a new hash for Scoped/Unscoped that only contains
177
+ # the desired i/o variables.
178
+ #
179
+ # Filter expects a "filter interface" {(ctx, **)}.
180
+ def self.filter_from_dsl(map)
181
+ hsh = DSL.hash_for(map)
182
+
183
+ ->(incoming_ctx, **kwargs) { Hash[hsh.collect { |from_name, to_name| [to_name, incoming_ctx[from_name]] }] }
184
+ end
185
+
186
+ def self.hash_for(ary)
187
+ return ary if ary.instance_of?(::Hash)
188
+ Hash[ary.collect { |name| [name, name] }]
189
+ end
190
+
191
+ # Keeps user's DSL configuration for a particular io-pipe step.
192
+ # Implements the interface for the actual I/O code and is DSL code happening in the normalizer.
193
+ # The actual I/O code expects {DSL::In} and {DSL::Out} objects to generate the two io-pipes.
194
+ #
195
+ # If a user needs to inject their own private iop step they can create this data structure with desired values here.
196
+ # This is also the reason why a lot of options computation such as {:with_outer_ctx} happens here and not in the IO code.
197
+
198
+ class Tuple < Struct.new(:name, :add_variables_class, :filter_builder, :insert_args)
199
+ def self.filters_from_options(tuples_to_user_filters)
200
+ tuples_to_user_filters.collect { |tuple, user_filter| tuple.(user_filter) }
201
+ end
202
+
203
+
204
+ # @return [Filter] Filter instance that keeps {name} and {aggregate_step}.
205
+ def call(user_filter)
206
+ filter = filter_builder.(user_filter)
207
+ aggregate_step = add_variables_class.new(filter, user_filter)
208
+
209
+ VariableMapping::Filter.new(aggregate_step, filter, name, add_variables_class)
210
+ end
211
+ end # TODO: implement {:insert_args}
212
+
213
+ # In, Out and Inject are objects instantiated when using the DSL, for instance {In() => [:model]}.
214
+ class In < Tuple; end
215
+ class Out < Tuple; end
216
+
217
+ def self.In(name: rand, add_variables_class: AddVariables, filter_builder: method(:build_filter))
218
+ In.new(name, add_variables_class, filter_builder)
219
+ end
220
+
221
+ # Builder for a DSL Output() object.
222
+ def self.Out(name: rand, add_variables_class: AddVariables::Output, with_outer_ctx: false, delete: false, filter_builder: method(:build_filter), read_from_aggregate: false)
223
+ add_variables_class = AddVariables::Output::WithOuterContext if with_outer_ctx
224
+ add_variables_class = AddVariables::Output::Delete if delete
225
+ filter_builder = ->(user_filter) { user_filter } if delete
226
+ add_variables_class = AddVariables::ReadFromAggregate if read_from_aggregate
227
+
228
+ Out.new(name, add_variables_class, filter_builder)
229
+ end
230
+
231
+ def self.Inject()
232
+ Inject.new
233
+ end
234
+
235
+ # This class is supposed to hold configuration options for Inject().
236
+ class Inject
237
+ # Translate the raw input of the user to {In} tuples
238
+ # @return Array of VariableMapping::Filter
239
+ def self.filters_for_injects(injects)
240
+ injects.collect do |inject, user_filter| # iterate all {Inject() => user_filter} calls
241
+ DSL::Inject.compute_filters_for_inject(inject, user_filter)
242
+ end.flatten(1)
243
+ end
244
+
245
+ # Compute {In} tuples from the user's DSL input.
246
+ # We simply use AddVariables but use our own {inject_filter} which checks if the particular
247
+ # variable is already present in the incoming ctx.
248
+ def self.compute_filters_for_inject(inject, user_filter) # {user_filter} either [:current_user, :model] or {model: ->{}}
249
+ return filters_for_array(inject, user_filter) if user_filter.is_a?(Array)
250
+ filters_for_hash_of_callables(inject, user_filter)
251
+ end
252
+
253
+ # [:model, :current_user]
254
+ def self.filters_for_array(inject, user_filter)
255
+ user_filter.collect do |name|
256
+ inject_filter = ->(original_ctx, **) { original_ctx.key?(name) ? {name => original_ctx[name]} : {} } # FIXME: make me an {Inject::} method.
257
+
258
+ filter_for(inject, inject_filter, name, "passthrough")
259
+ end
260
+ end
261
+
262
+ # {model: ->(*) { snippet }}
263
+ def self.filters_for_hash_of_callables(inject, user_filter)
264
+ user_filter.collect do |name, defaulting_filter|
265
+ inject_filter = ->(original_ctx, **kws) { original_ctx.key?(name) ? {name => original_ctx[name]} : {name => defaulting_filter.(original_ctx, **kws)} }
266
+
267
+ filter_for(inject, inject_filter, name, "defaulting_callable")
268
+ end
269
+ end
270
+
271
+ def self.filter_for(inject, inject_filter, name, type)
272
+ DSL.In(name: "inject.#{type}.#{name.inspect}", add_variables_class: AddVariables).(inject_filter)
273
+ end
274
+ end
275
+
276
+ end # DSL
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,38 @@
1
+ module Trailblazer
2
+ class Activity
3
+ module DSL
4
+ module Linear
5
+ module VariableMapping
6
+ # Implements the {inherit: [:variable_mapping]} feature.
7
+ module Inherit
8
+ def self.extended(strategy) # FIXME: who implements {extend!}
9
+ Linear::Normalizer.extend!(strategy, :step) do |normalizer|
10
+ Linear::Normalizer.prepend_to(
11
+ normalizer,
12
+ "activity.normalize_input_output_filters",
13
+ {
14
+ "variable_mapping.inherit_option" => Linear::Normalizer.Task(VariableMapping::Inherit::Normalizer.method(:inherit_option)),
15
+ }
16
+ )
17
+ end
18
+ end
19
+
20
+ module Normalizer
21
+ # Inheriting the original I/O happens by grabbing the variable_mapping_pipelines
22
+ # from the original sequence and pass it on in the normalizer.
23
+ # It will eventually get processed by {VariableMapping#pipe_for_composable_input} etc.
24
+ def self.inherit_option(ctx, inherit: [], sequence:, id:, **)
25
+ return unless inherit.include?(:variable_mapping)
26
+
27
+ inherited_input_pipeline, inherited_output_pipeline = Linear::Normalizer::InheritOption.find_row(sequence, id).data[:variable_mapping_pipelines]
28
+
29
+ ctx[:initial_input_pipeline] = inherited_input_pipeline
30
+ ctx[:initial_output_pipeline] = inherited_output_pipeline
31
+ end
32
+ end
33
+ end
34
+ end # VariableMapping
35
+ end
36
+ end
37
+ end
38
+ end