trailblazer-activity 0.12.2 → 0.14.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64e6b7c70557dea00024b9e16b1d85ee3046589a5b759028a2178cdd6da99231
4
- data.tar.gz: 2283d2744d77ca89de067f1a368c98ddc60b4e4ca39337b59066d35f642a7c65
3
+ metadata.gz: 5ea76d00d1fbeedcd5160134fff2223300697bd777fb58475fc8de2311c1a5ae
4
+ data.tar.gz: 833e2c3b9fae46af07f07a4422405e1b66606460425d39a8777b84cfed8a0518
5
5
  SHA512:
6
- metadata.gz: c9a1c011210c61b7e1ae55df77d003e4029160968093b3c71c73ccd65e460bdb911a97a016b7c524f9d3806074816f195a85f8e4e66b67245f670d7c69650417
7
- data.tar.gz: cc45abe8914b199fa98e31e0e6e9e9b9582e4e097c296f3407f3e365851813a632eb45133f37bf95c71212c08c15e0deafc723d952d1f00bf6e8087035bd07cd
6
+ metadata.gz: ff4c7f7bcd260b495348ba7286e18a60cc104d83ff176d5e82c3c80c22e23bc69cd73cb7976f3637e64b9067932651478b0538b9eaf6731ca793a31ac70e5535
7
+ data.tar.gz: 34c21e990a083a2a08f203f21304b0ad33ab3616450b285f7ddc4c90861c160c5c548e1ea3af92e4a4828df2f789324d21a0deb00aa1e67c07c8084654430c1f
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -6,10 +6,10 @@ 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', 3.1, head, jruby, jruby-head]
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v2
12
+ - uses: actions/checkout@v3
13
13
  - uses: ruby/setup-ruby@v1
14
14
  with:
15
15
  ruby-version: ${{ matrix.ruby }}
data/CHANGES.md CHANGED
@@ -1,3 +1,24 @@
1
+ # 0.14.0
2
+
3
+ * Remove `Pipeline.insert_before` and friends. Pipeline is now altered using ADDS mechanics, just
4
+ as we do it with the `Sequence` in the `trailblazer-activity-dsl-linear` gem.
5
+ * `Pipeline::Merge` is now `TaskWrap::Extension`. The "pre-friendly interface" you used to leverage for creating
6
+ taskWrap (tw) extensions is now deprecated and you will see warnings. See https://trailblazer.to/2.1/docs/activity.html#activity-taskwrap-extension
7
+ * Replace `TaskWrap::Extension()` with `TaskWrap::Extension.WrapStatic()` as a consistent interface for creating tW extensions at compile-time.
8
+ * Remove `Insert.find`.
9
+ * Rename `Activity::State::Config` to `Activity::Config`.
10
+ * Move `VariableMapping` to the `trailblazer-activity-dsl-linear` gem.
11
+ * Move `Pipeline.prepend` to the `trailblazer-activity-linear-dsl` gem.
12
+ * Add `Testing#assert_call` as a consistent test implementation. (0.14.0.beta2)
13
+
14
+ # 0.13.0
15
+
16
+ * Removed `TaskWrap::Inject::Defaults`. This is now implemented through `dsl`'s `:inject` option.
17
+ * Removed `TaskWrap::VariableMapping.Extension`.
18
+ * Renamed private `TaskWrap::VariableMapping.merge_for` to `.merge_instructions_for` as there's no {Merge} instance, yet.
19
+ * Extract invocation logic in `TaskBuilder::Task` into `Task#call_option`.
20
+ * Add `TaskWrap::Pipeline::prepend`.
21
+
1
22
  # 0.12.2
2
23
 
3
24
  * Use extracted `trailblazer-option`.
@@ -25,7 +46,7 @@
25
46
 
26
47
  # 0.11.2
27
48
 
28
- * Updrading `trailblazer-context` version :drum:
49
+ * Upgrading `trailblazer-context` version :drum:
29
50
 
30
51
  # 0.11.1
31
52
 
data/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Implements Intermediate, Implementation and compiler
4
4
 
5
- The `activity` gem brings a light-weight DSL to define business processes, and the runtime logic to run those activities.
5
+ The `activity` gem implements the runtime logic to invoke a new abstraction called "activities". Ideally, activities are defined using the [`dsl-linear` DSL gem](https://github.com/trailblazer/trailblazer-activity-dsl-linear).
6
6
 
7
7
  A process is a set of arbitrary pieces of logic you define, chained together and put into a meaningful context by an activity. Activity lets you focus on the implementation of steps while Trailblazer takes care of the control flow.
8
8
 
9
- Please find the [full documentation on the Trailblazer website](https://2019.trailblazer.to/2.1/docs/activity.html). [Note that the docs are WIP.]
9
+ Please find the [full documentation on the Trailblazer website](https://trailblazer.to/2.1/docs/activity.html).
10
10
 
11
11
  ## Example
12
12
 
13
- In conjunction with [`dsl-linear`](https://github.com/trailblazer/trailblazer-activity-dsl-linear), the `activity` 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://2019.trailblazer.to/2.1/docs/activity.html#activity-strategy-railway)).
13
+ In conjunction with [`dsl-linear`](https://github.com/trailblazer/trailblazer-activity-dsl-linear), the `activity` 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)).
14
14
 
15
15
  ```ruby
16
16
  require "trailblazer-activity"
@@ -78,15 +78,15 @@ With Activity, modeling business processes turns out to be ridiculously simple:
78
78
 
79
79
  ## Operation
80
80
 
81
- Trailblazer's [`Operation`](https://2019.trailblazer.to/2.1/docs/operation.html#operation-overview) internally uses an activity to model the processes.
81
+ Trailblazer's [`Operation`](https://trailblazer.to/2.1/docs/operation.html#operation-overview) internally uses an activity to model the processes.
82
82
 
83
83
  ## Workflow
84
- Activities can be formed into bigger compounds and using workflow, you can build long-running processes such as a moderated blog post or a parcel delivery. Also, you don't have to use the DSL but can use the [`editor`](https://2019.trailblazer.to/2.1/docs/pro.html#pro-editor)instead(cool for more complex, long-running flows). Here comes a sample screenshot.
84
+ Activities can be formed into bigger compounds and using workflow, you can build long-running processes such as a moderated blog post or a parcel delivery. Also, you don't have to use the DSL but can use the [`editor`](https://trailblazer.to/2.1/docs/pro.html#pro-editor)instead(cool for more complex, long-running flows). Here comes a sample screenshot.
85
85
 
86
- <img src="http://2019.trailblazer.to/2.1/dist/img/flow.png">
86
+ <img src="http://trailblazer.to/2.1/dist/img/flow.png">
87
87
 
88
88
  ## License
89
89
 
90
90
  © Copyright 2018, Trailblazer GmbH
91
91
 
92
- Licensed under the LGPLv3 license. We also offer a commercial-friendly [license](https://2019.trailblazer.to/2.1/docs/pro.html#pro-license).
92
+ Licensed under the LGPLv3 license. We also offer a commercial-friendly [license](https://trailblazer.to/2.1/docs/pro.html#pro-license).
@@ -0,0 +1,153 @@
1
+ module Trailblazer
2
+ class Activity
3
+ # The Adds interface are mechanics to alter sequences/pipelines.
4
+ # "one" ADDS structure: {row: ..., insert: [Insert, "id"]}
5
+ #
6
+ # To work with the instructions provided here, the pipeline structure
7
+ # needs to expose {#to_a}.
8
+ module Adds
9
+ module_function
10
+ # @returns Sequence/Pipeline New sequence instance
11
+ # @private
12
+ def insert_row(pipeline, row:, insert:)
13
+ insert_function, *args = insert
14
+
15
+ insert_function.(pipeline, row, *args)
16
+ end
17
+
18
+ # Inserts one or more {Adds} into {pipeline}.
19
+ def apply_adds(pipeline, adds)
20
+ adds.each do |add|
21
+ pipeline = insert_row(pipeline, **add)
22
+ end
23
+
24
+ pipeline
25
+ end
26
+
27
+ # @param inserts Array of friendly interface insertions
28
+ # def call(pipeline, *inserts)
29
+ # adds = build_adds_for_friendly_interface(inserts)
30
+
31
+ # Adds.apply_adds(pipeline, adds)
32
+ # end
33
+
34
+ module FriendlyInterface
35
+ # @public
36
+ # @return Array of ADDS
37
+ #
38
+ # Translate a collection of friendly interface to ADDS.
39
+ # This is a mini-DSL, if you want.
40
+ def self.adds_for(inserts)
41
+ inserts.collect do |task, options|
42
+ build_adds(task, **options)
43
+ end
44
+ end
45
+
46
+ # @private
47
+ def self.build_adds(task, id:, prepend: "task_wrap.call_task", append: nil)
48
+ insert, insert_id = append ? [:Append, append] : [:Prepend, prepend]
49
+
50
+ {
51
+ insert: [Activity::Adds::Insert.method(insert), insert_id],
52
+ row: TaskWrap::Pipeline::Row(id, task)
53
+ }
54
+ end
55
+ end
56
+
57
+ # Functions to alter the Sequence/Pipeline by inserting, replacing, or deleting a row.
58
+ #
59
+ # they don't mutate the data structure but rebuild it, has to respond to {to_a}
60
+ #
61
+ # These methods are invoked via {Adds.apply_adds} and should never be called directly.
62
+ module Insert
63
+ module_function
64
+
65
+ # Append {new_row} after {insert_id}.
66
+ def Append(pipeline, new_row, insert_id=nil)
67
+ build_from_ary(pipeline, insert_id) do |ary, index|
68
+ index = ary.size if index.nil? # append to end of pipeline.
69
+
70
+ range_before_index(ary, index+1) + [new_row] + Array(ary[index+1..-1])
71
+ end
72
+ end
73
+
74
+ # Insert {new_row} before {insert_id}.
75
+ def Prepend(pipeline, new_row, insert_id=nil)
76
+ build_from_ary(pipeline, insert_id) do |ary, index|
77
+ index = 0 if index.nil? # Prepend to beginning of pipeline.
78
+
79
+ range_before_index(ary, index) + [new_row] + ary[index..-1]
80
+ end
81
+ end
82
+
83
+ def Replace(pipeline, new_row, insert_id)
84
+ build_from_ary(pipeline, insert_id) do |ary, index|
85
+ range_before_index(ary, index) + [new_row] + ary[index+1..-1]
86
+ end
87
+ end
88
+
89
+ def Delete(pipeline, _, insert_id)
90
+ build_from_ary(pipeline, insert_id) do |ary, index|
91
+ range_before_index(ary, index) + ary[index+1..-1]
92
+ end
93
+ end
94
+
95
+ # @private
96
+ def build(sequence, rows)
97
+ sequence.class.new(rows)
98
+ end
99
+
100
+ # @private
101
+ def find_index(ary, insert_id)
102
+ ary.find_index { |row| row.id == insert_id }
103
+ end
104
+
105
+ # Converts the pipeline structure to an array,
106
+ # automatically finds the index for {insert_id},
107
+ # and calls the user block with the computed values.
108
+ #
109
+ # Single-entry point, could be named {#call}.
110
+ # @private
111
+ def apply_on_ary(pipeline, insert_id, raise_index_error: true, &block)
112
+ ary = pipeline.to_a
113
+
114
+ if insert_id.nil?
115
+ index = nil
116
+ else
117
+ index = find_index(ary, insert_id) # DISCUSS: this only makes sense if there are more than {Append} using this.
118
+ raise IndexError.new(pipeline, insert_id) if index.nil? && raise_index_error
119
+ end
120
+
121
+ _new_ary = yield(ary, index) # call the block.
122
+ end
123
+
124
+ def build_from_ary(pipeline, insert_id, &block)
125
+ new_ary = apply_on_ary(pipeline, insert_id, &block)
126
+
127
+ # Wrap the sequence/pipeline array into a concrete Sequence/Pipeline.
128
+ build(pipeline, new_ary)
129
+ end
130
+
131
+ # Always returns a valid, concat-able array for all indices
132
+ # before the {index}.
133
+ # @private
134
+ def range_before_index(ary, index)
135
+ return [] if index == 0
136
+ ary[0..index-1]
137
+ end
138
+ end # Insert
139
+
140
+ class IndexError < ::IndexError
141
+ def initialize(sequence, step_id)
142
+ valid_ids = sequence.to_a.collect{ |row| row.id.inspect }
143
+
144
+ message = "\n" \
145
+ "\e[31m#{step_id.inspect} is not a valid step ID. Did you mean any of these ?\e[0m\n" \
146
+ "\e[32m#{valid_ids.join("\n")}\e[0m"
147
+
148
+ super(message)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -76,13 +76,10 @@ module Trailblazer
76
76
  outputs[signal]
77
77
  end
78
78
 
79
- # Common reasons to raise IllegalSignalError are
80
- # * Returning invalid signal from custom Macros
81
- # * Returning invalid signal from steps which are not taskWrapped, for example: `step task: method(:validate)`
82
- #
83
- # Rest assured, it won't be raised in case of below scenarios where they can return any value,
84
- # * Steps with instance method signature, for example, `step :load_user`
85
- # * Steps with proc signature, for example `step ->(ctx, **){}`
79
+ # Common reasons to raise IllegalSignalError are when returning signals from
80
+ # * macros which are not registered
81
+ # * subprocesses where parent process have not registered that signal
82
+ # * ciruit interface steps, for example: `step task: method(:validate)`
86
83
  class IllegalSignalError < RuntimeError
87
84
  attr_reader :task, :signal
88
85
 
@@ -90,9 +87,9 @@ module Trailblazer
90
87
  @task = task
91
88
  @signal = signal
92
89
 
93
- message = "#{exec_context.class}: \n\t" \
94
- "\sUnrecognized Signal `#{signal.inspect}` returned from #{task.inspect}. Registered signals are, \n" \
95
- "- #{outputs.keys.join("\n- ")}"
90
+ message = "#{exec_context.class}: \n" \
91
+ "\e[31mUnrecognized Signal `#{signal.inspect}` returned from #{task.inspect}. Registered signals are, \e[0m\n" \
92
+ "\e[32m#{outputs.keys.map(&:inspect).join("\n")}\e[0m"
96
93
 
97
94
  super(message)
98
95
  end
@@ -1,33 +1,32 @@
1
1
  module Trailblazer
2
- module Activity::State
3
- # Compile-time
4
- #
5
- # DISCUSS: we could replace parts with Hamster::Hash.
2
+ class Activity
3
+ # Config API allows you to read and write immutably to the activity's
4
+ # {:config} field. Most of the times, this only contains {:wrap_static}.
6
5
  module Config
7
6
  module_function
8
7
 
9
- def set(state, *args)
8
+ def set(config, *args)
10
9
  if args.size == 2
11
10
  key, value = *args
12
11
 
13
- state = state.merge(key => value)
12
+ config = config.merge(key => value)
14
13
  else
15
14
  directive, key, value = *args
16
15
 
17
- state = state.merge( directive => {}.freeze ) unless state.key?(directive)
16
+ config = config.merge( directive => {}.freeze ) unless config.key?(directive)
18
17
 
19
- directive_hash = state[directive].merge(key => value)
20
- state = state.merge( directive => directive_hash.freeze )
18
+ directive_hash = config[directive].merge(key => value)
19
+ config = config.merge( directive => directive_hash.freeze )
21
20
  end
22
21
 
23
- state
22
+ config
24
23
  end
25
24
 
26
- def get(state, *args)
25
+ def get(config, *args)
27
26
  directive, key = *args
28
27
 
29
- return state[directive] if args.size == 1
30
- return state[directive][key] if state.key?(directive)
28
+ return config[directive] if args.size == 1
29
+ return config[directive][key] if config.key?(directive)
31
30
 
32
31
  nil
33
32
  end
@@ -76,7 +76,8 @@ class Trailblazer::Activity
76
76
  end.flatten(1)
77
77
  end
78
78
 
79
- # Invoke each task's extensions (usually coming from the DSL or some macro).
79
+ # Invoke each task's extensions (usually coming from the DSL user or some macro).
80
+ # We're expecting each {ext} to be a {TaskWrap::Extension::WrapStatic} instance.
80
81
  def self.config(implementation, config:)
81
82
  implementation.each do |id, task|
82
83
  task.extensions.each { |ext| config = ext.(config: config, id: id, task: task) } # DISCUSS: ext must return new {Config}.
@@ -11,5 +11,3 @@ module Trailblazer
11
11
  end # Schema
12
12
  end
13
13
  end
14
- require "trailblazer/activity/schema/implementation"
15
- require "trailblazer/activity/schema/intermediate"
@@ -18,17 +18,20 @@ module Trailblazer
18
18
  end
19
19
  end
20
20
 
21
+ # Wraps a {task} (that usually expects the task interface) into a circuit interface
22
+ # that can be used directly in a {Circuit}.
23
+ # We expect {task} to be exposing an {Option()} interface when calling it.
21
24
  class Task
22
25
  def initialize(task, user_proc)
23
26
  @task = task
24
27
  @user_proc = user_proc
25
-
26
28
  freeze
27
29
  end
28
30
 
29
- def call( (ctx, flow_options), **circuit_options )
31
+ def call((ctx, flow_options), **circuit_options)
30
32
  # Execute the user step with TRB's kw args.
31
- result = @task.(ctx, keyword_arguments: ctx.to_hash, **circuit_options) # circuit_options contains :exec_context.
33
+ # {@task} is/implements {Trailblazer::Option} interface.
34
+ result = call_option(@task, [ctx, flow_options], **circuit_options)
32
35
 
33
36
  # Return an appropriate signal which direction to go next.
34
37
  signal = Activity::TaskBuilder.binary_signal_for(result, Activity::Right, Activity::Left)
@@ -36,6 +39,11 @@ module Trailblazer
36
39
  return signal, [ctx, flow_options]
37
40
  end
38
41
 
42
+ # Invoke the original {user_proc} that is wrapped in an {Option()}.
43
+ private def call_option(task_with_option_interface, (ctx, _flow_options), **circuit_options)
44
+ task_with_option_interface.(ctx, keyword_arguments: ctx.to_hash, **circuit_options) # circuit_options contains :exec_context.
45
+ end
46
+
39
47
  def inspect # TODO: make me private!
40
48
  %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=#{Trailblazer::Activity::Introspect.render_task(@user_proc)}>}
41
49
  end
@@ -0,0 +1,90 @@
1
+ module Trailblazer
2
+ class Activity
3
+ #
4
+ # Example with tracing:
5
+ #
6
+ # Call the task_wrap circuit:
7
+ # |-- Start
8
+ # |-- Trace.capture_args [optional]
9
+ # |-- Call (call actual task) id: "task_wrap.call_task"
10
+ # |-- Trace.capture_return [optional]
11
+ # |-- Wrap::End
12
+ module TaskWrap
13
+ # inserts must be
14
+ # An {Extension} can be used for {:wrap_runtime}. It expects a collection of
15
+ # "friendly interface" arrays.
16
+ #
17
+ # TaskWrap.Extension([ [task, id: "my_logger", append: "task_wrap.call_task"] ])
18
+ #
19
+ # If you want a {wrap_static} extension, wrap it using `Extension.WrapStatic.new`.
20
+ def self.Extension(*inserts, merge: nil)
21
+ if merge
22
+ return Extension::WrapStatic.new(extension: Extension.new(*merge))
23
+ # TODO: remove me once we drop the pre-friendly interface.
24
+ end
25
+
26
+ Extension.build(*inserts)
27
+ end
28
+
29
+ # An {Extension} is a collection of ADDS objects to be inserted into a taskWrap.
30
+ # It gets called either at
31
+ # * compile-time and adds its steps to the wrap_static (see Extension::WrapStatic)
32
+ # * run-time in {TaskWrap::Runner} and adds its steps dynamically at runtime to the
33
+ # step's taskWrap
34
+ class Extension
35
+ # Build a taskWrap extension from the "friendly interface" {[task, id:, ...]}
36
+ def self.build(*inserts)
37
+ # For performance reasons we're computing the ADDS here and not in {#call}.
38
+ extension_rows = Activity::Adds::FriendlyInterface.adds_for(inserts)
39
+
40
+ new(*extension_rows)
41
+ end
42
+
43
+ def initialize(*extension_rows)
44
+ extension_rows = deprecated_extension_for(extension_rows) # TODO: remove me soon!
45
+
46
+ @extension_rows = extension_rows # those rows are simple ADDS instructions.
47
+ end
48
+
49
+ # Merges {extension_rows} into the {Pipeline} instance.
50
+ # This is usually used in step extensions or at runtime for {wrap_runtime}.
51
+ def call(task_wrap_pipeline)
52
+ Adds.apply_adds(task_wrap_pipeline, @extension_rows)
53
+ end
54
+
55
+ # TODO: remove me once we drop the pre-friendly interface.
56
+ def deprecated_extension_for(extension_rows)
57
+ return extension_rows unless extension_rows.find { |ext| ext.is_a?(Array) }
58
+
59
+ warn "[Trailblazer] You are using the old API for taskWrap extensions.
60
+ Please update to the new TaskWrap.Extension() API: # FIXME !!!!!"
61
+
62
+ extension_rows.collect do |ary|
63
+ {
64
+ insert: ary[0..1],
65
+ row: Pipeline.Row(*ary[2])
66
+ }
67
+ end
68
+ end
69
+
70
+ # Extension are used at compile-time with {wrap_static}, mostly with the {dsl} gem.
71
+ # {WrapStatic} extensions are called for setup through {Intermediate.config} at compile-time.
72
+ # Each extension alters the activity's wrap_static taskWrap.
73
+ class WrapStatic
74
+ def initialize(extension:)
75
+ @extension = extension
76
+ end
77
+
78
+ def call(config:, task:, **)
79
+ # Add the extension's task(s) to the activity's {wrap_static} taskWrap
80
+ # by using the immutable {Config} interface.
81
+ before_pipe = Config.get(config, :wrap_static, task.circuit_task)
82
+
83
+ Config.set(config, :wrap_static, task.circuit_task, @extension.(before_pipe))
84
+ end
85
+ end # WrapStatic
86
+
87
+ end # Extension
88
+ end # TaskWrap
89
+ end
90
+ end
@@ -3,53 +3,62 @@ class Trailblazer::Activity
3
3
  # This "circuit" is optimized for
4
4
  # a) merging speed at run-time, since features like tracing will be applied here.
5
5
  # b) execution speed. Every task in the real circuit is wrapped with one of us.
6
+ #
7
+ # It doesn't come with built-in insertion mechanics (except for {Pipeline.prepend}).
8
+ # Please add/remove steps using the {Activity::Adds} methods.
6
9
  class Pipeline
7
10
  def initialize(sequence)
8
11
  @sequence = sequence # [[id, task], ..]
9
12
  end
10
13
 
14
+ # Execute the pipeline and call all its steps, passing around the {wrap_ctx}.
11
15
  def call(wrap_ctx, original_args)
12
16
  @sequence.each { |(_id, task)| wrap_ctx, original_args = task.(wrap_ctx, original_args) }
13
17
 
14
18
  return wrap_ctx, original_args
15
19
  end
16
20
 
17
- attr_reader :sequence
18
-
19
- def self.insert_before(pipe, before_id, insertion)
20
- index = pipe.sequence.find_index { |(id, _)| id == before_id }
21
-
22
- seq = pipe.sequence.dup
23
-
24
- Pipeline.new(seq.insert(index, insertion))
21
+ # Comply with the Adds interface.
22
+ def to_a
23
+ @sequence
25
24
  end
26
25
 
27
- def self.insert_after(pipe, after_id, insertion)
28
- index = pipe.sequence.find_index { |(id, _)| id == after_id }
26
+ # TODO: remove me when old tW extension API is deprecated.
27
+ def self.method(name)
28
+ new_name = {
29
+ insert_before: :Prepend,
30
+ insert_after: :Append,
31
+ append: :Append,
32
+ prepend: :Prepend,
33
+ }.fetch(name)
29
34
 
30
- seq = pipe.sequence.dup
35
+ warn "[Trailblazer] Using `Trailblazer::Activity::TaskWrap::Pipeline.method(:#{name})` is deprecated.
36
+ Please use the new API: #FIXME!!!"
31
37
 
32
- Pipeline.new(seq.insert(index+1, insertion))
38
+ Trailblazer::Activity::Adds::Insert.method(new_name)
33
39
  end
34
40
 
35
- def self.append(pipe, _, insertion) # TODO: test me.
36
- Pipeline.new(pipe.sequence + [insertion])
41
+ def self.Row(id, task)
42
+ Row[id, task]
37
43
  end
38
44
 
39
- # Merges {extension_rows} into the {task_wrap_pipeline}.
40
- # This is usually used in step extensions or at runtime for {wrap_runtime}.
41
- #
42
- # {Extension} API
43
- class Merge # TODO: RENAME TO TaskWrap::Extension(::Merge)
44
- def initialize(*extension_rows)
45
- @extension_rows = extension_rows
45
+ class Row < Array
46
+ def id
47
+ self[0]
46
48
  end
49
+ end
50
+
51
+ # TODO: remove {Merge} when old tW extension API is deprecated.
52
+ class Merge
53
+ def self.new(*inserts)
54
+ warn "[Trailblazer] Using `Trailblazer::Activity::TaskWrap::Pipeline::Merge.new` is deprecated.
55
+ Please use the new TaskWrap.Extension() API: #FIXME!!!"
47
56
 
48
- def call(task_wrap_pipeline)
49
- @extension_rows.collect { |(insert_function, target_id, row)| task_wrap_pipeline = insert_function.(task_wrap_pipeline, target_id, row) }
50
- task_wrap_pipeline
57
+ # We can safely assume that users calling {Merge.new} are using the old tW extension API, not
58
+ # the "friendly API". That's why we don't go through {Extension.build}.
59
+ TaskWrap::Extension.new(*inserts)
51
60
  end
52
- end
61
+ end # Merge
53
62
  end
54
63
  end
55
64
  end
@@ -14,7 +14,8 @@ module Trailblazer
14
14
 
15
15
  # Compute runtime arguments necessary to execute a taskWrap per task of the activity.
16
16
  # This method is the top-level entry, called only once for the entire activity graph.
17
- def invoke(activity, args, wrap_runtime: {}, wrap_static: initial_wrap_static, **circuit_options)
17
+ # [:wrap_static] The taskWrap used for the topmost activity/operation.
18
+ def invoke(activity, args, wrap_runtime: {}, wrap_static: initial_wrap_static, **circuit_options) # FIXME: why do we need this method?
18
19
  circuit_options = circuit_options.merge(
19
20
  runner: TaskWrap::Runner,
20
21
  wrap_runtime: wrap_runtime,
@@ -24,7 +25,7 @@ module Trailblazer
24
25
  )
25
26
 
26
27
  # signal, (ctx, flow), circuit_options =
27
- Runner.(activity, args, **circuit_options)
28
+ TaskWrap::Runner.(activity, args, **circuit_options)
28
29
  end
29
30
 
30
31
  # {:extension} API
@@ -32,28 +33,7 @@ module Trailblazer
32
33
  # Gets executed in {Intermediate.call} which also provides {config}.
33
34
 
34
35
  def initial_wrap_static(*)
35
- # return initial_sequence
36
- TaskWrap::Pipeline.new([["task_wrap.call_task", TaskWrap.method(:call_task)]])
37
- end
38
-
39
- # Use this in your macros if you want to extend the {taskWrap}.
40
- def Extension(merge:)
41
- Extension.new(merge: Pipeline::Merge.new(*merge))
42
- end
43
-
44
- class Extension
45
- def initialize(merge:)
46
- @merge = merge
47
- end
48
-
49
- # Compile-time:
50
- # Gets called via the {Normalizer} and represents an {:extensions} item.
51
- # Adds/alters the activity's {wrap_static}.
52
- def call(config:, task:, **)
53
- before_pipe = State::Config.get(config, :wrap_static, task.circuit_task)
54
-
55
- State::Config.set(config, :wrap_static, task.circuit_task, @merge.(before_pipe))
56
- end
36
+ Pipeline.new([Pipeline.Row("task_wrap.call_task", TaskWrap.method(:call_task))])
57
37
  end
58
38
  end # TaskWrap
59
39
  end
@@ -61,5 +41,4 @@ end
61
41
  require "trailblazer/activity/task_wrap/pipeline"
62
42
  require "trailblazer/activity/task_wrap/call_task"
63
43
  require "trailblazer/activity/task_wrap/runner"
64
- require "trailblazer/activity/task_wrap/variable_mapping"
65
- require "trailblazer/activity/task_wrap/inject"
44
+ require "trailblazer/activity/task_wrap/extension"
@@ -54,6 +54,38 @@ module Trailblazer
54
54
  Schema = Trailblazer::Activity::Schema
55
55
  TaskWrap = Trailblazer::Activity::TaskWrap
56
56
 
57
+ # `:seq` is always passed into ctx.
58
+ # @param :seq String What the {:seq} variable in the result ctx looks like. (expected seq)
59
+ # @param :expected_ctx_variables Variables that are added during the call by the asserted activity.
60
+ def assert_call(activity, terminus: :success, seq: "[]", expected_ctx_variables: {}, **ctx_variables)
61
+ # Call without taskWrap!
62
+ signal, (ctx, _) = activity.([{seq: [], **ctx_variables}, _flow_options = {}]) # simply call the activity with the input you want to assert.
63
+
64
+ assert_call_for(signal, ctx, terminus: terminus, seq: seq, **expected_ctx_variables, **ctx_variables)
65
+ end
66
+
67
+ # Use {TaskWrap.invoke} to call the activity.
68
+ def assert_invoke(activity, terminus: :success, seq: "[]", circuit_options: {}, expected_ctx_variables: {}, **ctx_variables) # DISCUSS: only for {activity} gem?
69
+ signal, (ctx, _flow_options) = TaskWrap.invoke(
70
+ activity,
71
+ [
72
+ {seq: [], **ctx_variables},
73
+ {} # flow_options
74
+ ],
75
+ **circuit_options
76
+ )
77
+
78
+ assert_call_for(signal, ctx, terminus: terminus, seq: seq, **ctx_variables, **expected_ctx_variables) # DISCUSS: ordering of variables?
79
+ end
80
+
81
+ def assert_call_for(signal, ctx, terminus: :success, seq: "[]", **ctx_variables)
82
+ assert_equal signal.to_h[:semantic], terminus, "assert_call expected #{terminus} terminus, not #{signal}. Use assert_call(activity, terminus: #{signal.to_h[:semantic].inspect})"
83
+
84
+ assert_equal ctx.inspect, {seq: "%%%"}.merge(ctx_variables).inspect.sub('"%%%"', seq)
85
+
86
+ return ctx
87
+ end
88
+
57
89
  module Implementing
58
90
  extend Activity::Testing.def_tasks(:a, :b, :c, :d, :f, :g)
59
91
 
@@ -67,25 +99,27 @@ module Trailblazer
67
99
  Implementing
68
100
  end
69
101
 
70
- def flat_activity
102
+ def flat_activity(implementing: Implementing)
71
103
  return @_flat_activity if defined?(@_flat_activity)
72
104
 
73
105
  intermediate = Inter.new(
74
106
  {
75
107
  Inter::TaskRef("Start.default") => [Inter::Out(:success, :B)],
76
- Inter::TaskRef(:B, additional: true) => [Inter::Out(:success, :C)],
108
+ Inter::TaskRef(:B, additional: true) => [Inter::Out(:success, :C), Inter::Out(:failure, "End.failure")],
77
109
  Inter::TaskRef(:C) => [Inter::Out(:success, "End.success")],
78
- Inter::TaskRef("End.success", stop_event: true) => [Inter::Out(:success, nil)]
110
+ Inter::TaskRef("End.success", stop_event: true) => [Inter::Out(:success, nil)],
111
+ Inter::TaskRef("End.failure", stop_event: true) => [Inter::Out(:failure, nil)],
79
112
  },
80
- ["End.success"],
113
+ ["End.success", "End.failure"],
81
114
  ["Start.default"], # start
82
115
  )
83
116
 
84
117
  implementation = {
85
118
  "Start.default" => Schema::Implementation::Task(st = Implementing::Start, [Activity::Output(Activity::Right, :success)], []),
86
- :B => Schema::Implementation::Task(b = Implementing.method(:b), [Activity::Output(Activity::Right, :success)], []),
87
- :C => Schema::Implementation::Task(c = Implementing.method(:c), [Activity::Output(Activity::Right, :success)], []),
119
+ :B => Schema::Implementation::Task(b = implementing.method(:b), [Activity::Output(Activity::Right, :success), Activity::Output(Activity::Left, :failure)], []),
120
+ :C => Schema::Implementation::Task(c = implementing.method(:c), [Activity::Output(Activity::Right, :success)], []),
88
121
  "End.success" => Schema::Implementation::Task(_es = Implementing::Success, [Activity::Output(Implementing::Success, :success)], []), # DISCUSS: End has one Output, signal is itself?
122
+ "End.failure" => Schema::Implementation::Task(Implementing::Failure, [Activity::Output(Implementing::Failure, :failure)], []), # DISCUSS: End has one Output, signal is itself?
89
123
  }
90
124
 
91
125
  schema = Inter.(intermediate, implementation)
@@ -1,7 +1,7 @@
1
1
  module Trailblazer
2
2
  module Version
3
3
  module Activity
4
- VERSION = '0.12.2'.freeze
4
+ VERSION = '0.14.0.beta2'.freeze
5
5
  end
6
6
  end
7
7
  end
@@ -14,12 +14,8 @@ module Trailblazer
14
14
  )
15
15
  end
16
16
 
17
- # Reader and writer method for an Activity.
18
- # The writer {dsl[:key] = "value"} exposes immutable behavior and will replace the old
19
- # @state with a new, modified copy.
20
- #
21
- # Always use the accessors to avoid leaking state to other components
22
- # due to mutable write operations.
17
+ # DISCUSS: we could remove this reader in the future
18
+ # and use {Activity.to_h[:config]}.
23
19
  def [](*key)
24
20
  @schema[:config][*key]
25
21
  end
@@ -36,10 +32,13 @@ end
36
32
 
37
33
  require "trailblazer/activity/structures"
38
34
  require "trailblazer/activity/schema"
35
+ require "trailblazer/activity/schema/implementation"
36
+ require "trailblazer/activity/schema/intermediate"
39
37
  require "trailblazer/activity/circuit"
40
38
  require "trailblazer/activity/config"
41
39
  require "trailblazer/activity/introspect"
42
40
  require "trailblazer/activity/task_wrap"
41
+ require "trailblazer/activity/adds"
43
42
  require "trailblazer/activity/task_builder"
44
43
 
45
44
  require "trailblazer/option"
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "minitest", "~> 5.0"
25
25
  spec.add_development_dependency "minitest-line"
26
26
  spec.add_development_dependency "rake"
27
- spec.add_development_dependency "trailblazer-developer", ">= 0.0.7"
27
+ spec.add_development_dependency "trailblazer-developer", ">= 0.0.23"
28
28
 
29
29
  spec.required_ruby_version = '>= 2.1.0'
30
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailblazer-activity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.2
4
+ version: 0.14.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-12 00:00:00.000000000 Z
11
+ date: 2022-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: trailblazer-context
@@ -100,21 +100,22 @@ dependencies:
100
100
  requirements:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: 0.0.7
103
+ version: 0.0.23
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: 0.0.7
111
- description:
110
+ version: 0.0.23
111
+ description:
112
112
  email:
113
113
  - apotonick@gmail.com
114
114
  executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - ".github/dependabot.yml"
118
119
  - ".github/workflows/ci.yml"
119
120
  - ".gitignore"
120
121
  - CHANGES.md
@@ -125,6 +126,7 @@ files:
125
126
  - Rakefile
126
127
  - lib/trailblazer-activity.rb
127
128
  - lib/trailblazer/activity.rb
129
+ - lib/trailblazer/activity/adds.rb
128
130
  - lib/trailblazer/activity/circuit.rb
129
131
  - lib/trailblazer/activity/config.rb
130
132
  - lib/trailblazer/activity/introspect.rb
@@ -135,10 +137,9 @@ files:
135
137
  - lib/trailblazer/activity/task_builder.rb
136
138
  - lib/trailblazer/activity/task_wrap.rb
137
139
  - lib/trailblazer/activity/task_wrap/call_task.rb
138
- - lib/trailblazer/activity/task_wrap/inject.rb
140
+ - lib/trailblazer/activity/task_wrap/extension.rb
139
141
  - lib/trailblazer/activity/task_wrap/pipeline.rb
140
142
  - lib/trailblazer/activity/task_wrap/runner.rb
141
- - lib/trailblazer/activity/task_wrap/variable_mapping.rb
142
143
  - lib/trailblazer/activity/testing.rb
143
144
  - lib/trailblazer/activity/version.rb
144
145
  - trailblazer-activity.gemspec
@@ -146,7 +147,7 @@ homepage: http://trailblazer.to
146
147
  licenses:
147
148
  - LGPL-3.0
148
149
  metadata: {}
149
- post_install_message:
150
+ post_install_message:
150
151
  rdoc_options: []
151
152
  require_paths:
152
153
  - lib
@@ -157,12 +158,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
158
  version: 2.1.0
158
159
  required_rubygems_version: !ruby/object:Gem::Requirement
159
160
  requirements:
160
- - - ">="
161
+ - - ">"
161
162
  - !ruby/object:Gem::Version
162
- version: '0'
163
+ version: 1.3.1
163
164
  requirements: []
164
- rubygems_version: 3.0.8
165
- signing_key:
165
+ rubygems_version: 3.2.3
166
+ signing_key:
166
167
  specification_version: 4
167
168
  summary: Runtime code for Trailblazer activities.
168
169
  test_files: []
@@ -1,39 +0,0 @@
1
- class Trailblazer::Activity
2
- module TaskWrap
3
- # Allows to inject attributes for a task and defaults them if not.
4
- # Per default, the defaulting is scoped, meaning only the task will see it.
5
- module Inject
6
- module Defaults
7
- module_function
8
-
9
- def Extension(defaults)
10
- # Returns new ctx.
11
- input = ->((original_ctx, flow_options), circuit_options) do
12
- defaulted_options = defaults_for(defaults, original_ctx)
13
-
14
- ctx = original_ctx.merge(defaulted_options)
15
-
16
- Trailblazer::Context(ctx, {}, flow_options[:context_options])
17
- end
18
-
19
- output = ->(new_ctx, (original_ctx, _flow_options), _circuit_options) { # FIXME: use Unscope
20
- _, mutable_data = new_ctx.decompose
21
-
22
- # we are only interested in the {mutable_data} part since the disposed part
23
- # represents the injected/defaulted data.
24
- original_ctx.merge(mutable_data)
25
- }
26
-
27
- VariableMapping::Extension(input, output, id: input)
28
- end
29
-
30
- # go through all defaultable options and default them if appropriate.
31
- def defaults_for(defaults, original_ctx)
32
- Hash[
33
- defaults.collect { |k, v| [k, original_ctx[k] || v] } # FIXME: doesn't allow {false/nil} currently.
34
- ]
35
- end
36
- end
37
- end # Inject
38
- end
39
- end
@@ -1,90 +0,0 @@
1
- class Trailblazer::Activity
2
- module TaskWrap
3
- # Creates taskWrap steps to map variables before and after the actual step.
4
- # We hook into the Normalizer, process `:input` and `:output` directives and
5
- # translate them into a {DSL::Extension}.
6
- #
7
- # Note that the two options are not the only way to create filters, you can use the
8
- # more low-level {Scoped()} etc., too, and write your own filter logic.
9
- module VariableMapping
10
- # The taskWrap extension that's included into the static taskWrap for a task.
11
- def self.Extension(input, output, id: input.object_id)
12
- Trailblazer::Activity::TaskWrap::Extension(
13
- merge: merge_for(input, output, id: id),
14
- )
15
- end
16
-
17
- # DISCUSS: do we want the automatic wrapping of {input} and {output}?
18
- def self.merge_for(input, output, id:) # TODO: rename
19
- [
20
- [TaskWrap::Pipeline.method(:insert_before), "task_wrap.call_task", ["task_wrap.input", TaskWrap::Input.new(input, id: id)]],
21
- [TaskWrap::Pipeline.method(:insert_after), "task_wrap.call_task", ["task_wrap.output", TaskWrap::Output.new(output, id: id)]],
22
- ]
23
- end
24
- end
25
-
26
- # TaskWrap step to compute the incoming {Context} for the wrapped task.
27
- # This allows renaming, filtering, hiding, of the options passed into the wrapped task.
28
- #
29
- # Both Input and Output are typically to be added before and after task_wrap.call_task.
30
- #
31
- # @note Assumption: we always have :input _and_ :output, where :input produces a Context and :output decomposes it.
32
-
33
- # Calls your {@filter} and replaces the original ctx with your returned one.
34
- class Input
35
- def initialize(filter, id:)
36
- @filter = filter
37
- @id = id
38
- end
39
-
40
- # {input.call()} is invoked in the circuit.
41
- # `original_args` are the actual args passed to the wrapped task: [ [ctx, ..], circuit_options ]
42
- #
43
- def call(wrap_ctx, original_args)
44
- # let user compute new ctx for the wrapped task.
45
- input_ctx = apply_filter(*original_args)
46
-
47
- # decompose the original_args since we want to modify them.
48
- (original_ctx, original_flow_options), original_circuit_options = original_args
49
-
50
- wrap_ctx = wrap_ctx.merge(@id => original_ctx) # remember the original ctx by the key {@id}.
51
-
52
- # instead of the original Context, pass on the filtered `input_ctx` in the wrap.
53
- return wrap_ctx, [[input_ctx, original_flow_options], original_circuit_options]
54
- end
55
-
56
- private
57
-
58
- # Invoke the @filter callable with the original circuit interface.
59
- def apply_filter((ctx, original_flow_options), original_circuit_options)
60
- @filter.([ctx, original_flow_options], **original_circuit_options) # returns {new_ctx}.
61
- end
62
- end
63
-
64
- # TaskWrap step to compute the outgoing {Context} from the wrapped task.
65
- # This allows renaming, filtering, hiding, of the options returned from the wrapped task.
66
- class Output
67
- def initialize(filter, id:)
68
- @filter = filter
69
- @id = id
70
- end
71
-
72
- # Runs your filter and replaces the ctx in `wrap_ctx[:return_args]` with the filtered one.
73
- def call(wrap_ctx, original_args)
74
- (original_ctx, original_flow_options), original_circuit_options = original_args
75
-
76
- return_args = wrap_ctx[:return_args]
77
-
78
- returned_ctx, returned_flow_options = wrap_ctx[:return_args] # this is the Context returned from {call}ing the wrapped user task.
79
- original_ctx = wrap_ctx[@id] # grab the original ctx from before which was set in the {:input} filter.
80
- # let user compute the output.
81
- output_ctx = @filter.(returned_ctx, [original_ctx, returned_flow_options], **original_circuit_options)
82
-
83
- wrap_ctx = wrap_ctx.merge( return_args: [output_ctx, returned_flow_options] )
84
-
85
- # and then pass on the "new" context.
86
- return wrap_ctx, original_args
87
- end
88
- end
89
- end # Wrap
90
- end