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