trailblazer-activity 0.1.6 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGES.md +24 -0
- data/Gemfile +1 -0
- data/NOTES +36 -0
- data/README.md +57 -111
- data/lib/trailblazer/activity/subprocess.rb +15 -10
- data/lib/trailblazer/activity/trace.rb +25 -11
- data/lib/trailblazer/activity/version.rb +1 -1
- data/lib/trailblazer/activity/wrap.rb +36 -30
- data/lib/trailblazer/activity.rb +15 -8
- data/lib/trailblazer/circuit/testing.rb +14 -0
- data/lib/trailblazer/circuit.rb +31 -23
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cda2a49909b44be7195fed23b15bb82f6d4d4ac4
|
|
4
|
+
data.tar.gz: 3d17e44f5c91ca1a61797d1f3d5f8d515362c2ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70ca9ad73d4993f6295ee2dfc40b527c46fc25fc7e3384ec6dd7f57f726db5c7599affda8cfec180430020cb8f6a4340892921008bd0a1b3e54f4fa362e3dcc8
|
|
7
|
+
data.tar.gz: 13400c97fecca6f83d0282205af04c7f60f7136b4f5f6a7175ed5035277a12e9bf9a22044102577fcca26187fa6badf5185246a3dba61c87e1eab5f06b98ac57
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# 0.2.0
|
|
2
|
+
|
|
3
|
+
* The `Activity#call` API is now
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
signal, options, _ignored_circuit_options = Activity.( options, **circuit_options )
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The third return value, which is typically the `circuit_options`, is _ignored_ and for all task calls in this `Activity`, an identical, unchangeable set of `circuit_options` is passed to. This dramatically reduces unintended behavior with the task_wrap, tracing, etc. and usually simplifies tasks.
|
|
10
|
+
|
|
11
|
+
The new API allows using bare `Activity` instances as tasks without any clumsy nesting work, making nesting very simple.
|
|
12
|
+
|
|
13
|
+
A typical task will look as follows.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
->( (options, flow_options), **circuit_args ) do
|
|
17
|
+
[ signal, [options, flow_options], *this_will_be_ignored ]
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A task can only emit a signal and "options" (whatever data structure that may be), and can *not* change the `circuit_options` which usually contain activity-wide "global" configuration.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
1
25
|
# 0.1.6
|
|
2
26
|
|
|
3
27
|
* `Nested` now is `Subprocess` because it literally does nothing else but calling a _process_ (or activity).
|
data/Gemfile
CHANGED
data/NOTES
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# NOT lowest level. if you need that, use your own proc.
|
|
2
|
+
# TODO: how do we track what goes into the callable?
|
|
3
|
+
# adjust what goes into it (e.g. without direction or with kw args)?
|
|
4
|
+
# pre contract -> step -> post contract (are these all just steps, in "mini nested pipe"?)
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
# aka "Atom".
|
|
8
|
+
def self.Task(instance: :context, method: :call, id:nil)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# * ingoing contract (could be implemented as a nested pipe with 3 steps. that would allow us
|
|
12
|
+
# to compile it to native ruby method calls later)
|
|
13
|
+
->(direction, options, **flow_options) {
|
|
14
|
+
instance = flow_options[:context] if instance==:context # TODO; implement different :context (e.g. :my_context).
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# * incoming args
|
|
19
|
+
# step_args = [args] # TODO: overridable.
|
|
20
|
+
step_args = [ options, **options ]
|
|
21
|
+
|
|
22
|
+
# ** call the actual thing
|
|
23
|
+
res = instance.send(method, *step_args) # what goes in? kws?
|
|
24
|
+
|
|
25
|
+
# * interpret result (e.g. true=>Right) (should we keep doing that in the tie? so the op has it easier with success, etc?)
|
|
26
|
+
# * outgoing contract
|
|
27
|
+
# * outgoing args
|
|
28
|
+
|
|
29
|
+
[ *res, flow_options ]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# * tracing: incoming, outgoing, direction, etc. - do we want that in tasks, too?
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
end
|
data/README.md
CHANGED
|
@@ -1,163 +1,109 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Activity
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Circuit provides a simplified [flowchart](https://en.wikipedia.org/wiki/Flowchart) implementation with terminals (for example, start or end state), connectors and tasks (processes). It allows to define the flow (the actual *circuit*) and execute it.
|
|
6
|
-
|
|
7
|
-
Circuit refrains from implementing deciders. The decisions are encoded in the output signals of tasks.
|
|
8
|
-
|
|
9
|
-
`Circuit` and `workflow` use [BPMN](http://www.bpmn.org/) lingo and concepts for describing processes and flows. This document can be found in the [Trailblazer documentation](http://trailblazer.to/gems/workflow/circuit.html), too.
|
|
10
|
-
|
|
11
|
-
> The `circuit` gem is the lowest level of abstraction and is used in `operation` and `workflow`, which both provide higher-level APIs for the Railway pattern and complex BPMN workflows.
|
|
3
|
+
An _activity_ is a collection of connected _tasks_ with one _start event_ and one (or many) _end_ events.
|
|
12
4
|
|
|
13
5
|
## Installation
|
|
14
6
|
|
|
15
|
-
To use
|
|
7
|
+
To use activities you need one gem, only.
|
|
16
8
|
|
|
17
9
|
```ruby
|
|
18
|
-
gem "trailblazer-
|
|
10
|
+
gem "trailblazer-activity"
|
|
19
11
|
```
|
|
20
12
|
|
|
21
|
-
The `trailblazer-circuit` gem is often just called the `circuit` gem. It ships with the `operation` gem and implements the internal Railway.
|
|
22
|
-
|
|
23
13
|
## Overview
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<img src="http://trailblazer.to/images/diagrams/blog-bpmn1.png">
|
|
28
|
-
|
|
29
|
-
After writing and spell-checking, the author has the chance to publish the post or, in case of typos, go back, correct, and go through the same flow, again. Note that there's only a handful of defined transistions, or connections. An author, for example, is not allowed to jump from "correct" into "publish" without going through the check.
|
|
30
|
-
|
|
31
|
-
The `circuit` gem allows you to define this *activity* and takes care of implementing the control flow, running the activity and making sure no invalid paths are taken.
|
|
15
|
+
> Since TRB 2.1, we use [BPMN](http://www.bpmn.org/) lingo and concepts for describing workflows and processes.
|
|
32
16
|
|
|
33
|
-
|
|
17
|
+
An activity is a workflow that contains one or several tasks. It is the main concept to organize control flow in Trailblazer.
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
In order to define an activity, you can use the BPMN editor of your choice and run it through the Trailblazer circuit generator, use our online tool (if [you're a PRO member](http://pro.trailblazer.to)) or simply define it using plain Ruby.
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
activity = Activity.from_hash do |start, _end|
|
|
41
|
-
{
|
|
42
|
-
start => { Circuit::Right => Blog::Write },
|
|
43
|
-
Blog::Write => { Circuit::Right => Blog::SpellCheck },
|
|
44
|
-
Blog::SpellCheck => { Circuit::Right => Blog::Publish, Circuit::Left => Blog::Correct },
|
|
45
|
-
Blog::Correct => { Circuit::Right => Blog::SpellCheck },
|
|
46
|
-
Blog::Publish => { Circuit::Right => _end }
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
```
|
|
19
|
+
The following diagram illustrates an exemplary workflow where a user writes and publishes a blog post.
|
|
50
20
|
|
|
51
|
-
|
|
21
|
+
<img src="http://trb.to/images/diagrams/blog-bpmn1.png">
|
|
52
22
|
|
|
53
|
-
|
|
23
|
+
After writing and spell-checking, the author has the chance to publish the post or, in case of typos, go back, correct, and go through the same flow, again. Note that there's only a handful of defined transistions, or connections. An author, for example, is not allowed to jump from "correct" into "publish" without going through the check.
|
|
54
24
|
|
|
55
|
-
|
|
25
|
+
The `activity` gem allows you to define this *activity* and takes care of implementing the control flow, running the activity and making sure no invalid paths are taken.
|
|
56
26
|
|
|
57
|
-
|
|
27
|
+
Your job is solely to implement the tasks and deciders put into this activity - Trailblazer makes sure it is executed it in the right order, and so on.
|
|
58
28
|
|
|
59
|
-
|
|
60
|
-
module Blog
|
|
61
|
-
Write = ->(direction, options, *flow) do
|
|
62
|
-
options[:content] = options[:content].strip
|
|
63
|
-
[ Circuit::Right, options, *flow ]
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
```
|
|
29
|
+
To eventually run this activity, three things have to be done.
|
|
67
30
|
|
|
68
|
-
|
|
31
|
+
1. The activity needs be defined. Easiest is to use the [Activity.from_hash builder](#activity-fromhash).
|
|
32
|
+
2. It's the programmer's job (that's you!) to implement the actual tasks (the "boxes"). Use [tasks for that](#task).
|
|
33
|
+
3. After defining and implementing, you can run the activity with any data [by `call`ing it](#activity-call).
|
|
69
34
|
|
|
70
|
-
|
|
35
|
+
## Operation vs. Activity
|
|
71
36
|
|
|
72
|
-
|
|
37
|
+
An `Activity` allows to define and maintain a graph, that at runtime will be used as a "circuit". Or, in other words, it defines the boxes, circles, arrows and signals between them, and makes sure when running the activity, the circuit with your rules will be executed.
|
|
73
38
|
|
|
74
|
-
|
|
39
|
+
Please note that an `Operation` simply provides a neat DSL for creating an `Activity` with a railway-oriented wiring (left and right track). An operation _always_ maintains an activity internally.
|
|
75
40
|
|
|
76
41
|
```ruby
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
42
|
+
class Create < Trailblazer::Operation
|
|
43
|
+
step :exists?, pass_fast: true
|
|
44
|
+
step :policy
|
|
45
|
+
step :validate
|
|
46
|
+
fail :log_err
|
|
47
|
+
step :persist
|
|
48
|
+
fail :log_db_err
|
|
49
|
+
step :notify
|
|
80
50
|
end
|
|
81
51
|
```
|
|
82
52
|
|
|
83
|
-
|
|
53
|
+
Check the operation above. The DSL to create the activity with its graph is very different to `Activity`, but the outcome is a simple activity instance.
|
|
84
54
|
|
|
85
|
-
|
|
55
|
+
<img src="http://trb.to/images/graph/op-vs-activity.png">
|
|
86
56
|
|
|
87
|
-
|
|
57
|
+
When `call`ing an operation, several transformations on the arguments are applied, and those are passed to the `Activity#call` invocation. After the activity finished, its output is transformed into a `Result` object.
|
|
88
58
|
|
|
89
|
-
|
|
59
|
+
## Activity
|
|
90
60
|
|
|
91
|
-
|
|
92
|
-
direction, options, flow = activity.(
|
|
93
|
-
nil,
|
|
94
|
-
{ content: "Let's start writing " } # gets trimmed in Write.
|
|
95
|
-
)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The first argument is where to start the circuit. Usually, this will be the activity's `Start` event accessable via `activity[:Start]`.
|
|
61
|
+
To understand how an activity works and what it performs in your application logic, it's easiest to see how activities are defined, and used.
|
|
99
62
|
|
|
100
|
-
|
|
63
|
+
## Activity: From_Hash
|
|
101
64
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
direction #=> #<End: default {}>
|
|
106
|
-
options #=> {:content=>"Let's start writing"}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
As opposed to higher abstractions such as `Operation`, it is completely up to the developer what interfaces they provide to tasks and their return values. What is a mutable hash here could be an explicit array of return values in another implementation style, and so on.
|
|
110
|
-
|
|
111
|
-
## Tracing
|
|
112
|
-
|
|
113
|
-
For debugging or simply understanding the flows of circuits, you can use tracing.
|
|
65
|
+
Instead of using an operation, you can manually define activities by using the `Activity.from_hash` builder.
|
|
114
66
|
|
|
115
67
|
```ruby
|
|
116
68
|
activity = Activity.from_hash do |start, _end|
|
|
117
|
-
# Blog::Write=>"Blog::Write",Blog::SpellCheck=>"Blog::SpellCheck",Blog::Correct=>"Blog::Correct", Blog::Publish=>"Blog::Publish" }) { |evt|
|
|
118
69
|
{
|
|
119
|
-
start
|
|
120
|
-
Blog::Write => { Circuit::Right => Blog::SpellCheck },
|
|
121
|
-
Blog::SpellCheck => { Circuit::Right => Blog::Publish,
|
|
122
|
-
|
|
123
|
-
Blog::
|
|
70
|
+
start => { Trailblazer::Circuit::Right => Blog::Write },
|
|
71
|
+
Blog::Write => { Trailblazer::Circuit::Right => Blog::SpellCheck },
|
|
72
|
+
Blog::SpellCheck => { Trailblazer::Circuit::Right => Blog::Publish,
|
|
73
|
+
Trailblazer::Circuit::Left => Blog::Correct },
|
|
74
|
+
Blog::Correct => { Trailblazer::Circuit::Right => Blog::SpellCheck },
|
|
75
|
+
Blog::Publish => { Trailblazer::Circuit::Right => _end }
|
|
124
76
|
}
|
|
125
77
|
end
|
|
126
78
|
```
|
|
127
79
|
|
|
128
|
-
The second argument to `Activity` takes debugging information, so you can set readable names for tasks.
|
|
129
80
|
|
|
130
|
-
|
|
81
|
+
The block yields a generic start and end event instance. You then connect every _task_ in that hash (hash keys) to another task or event via the emitted _signal_.
|
|
131
82
|
|
|
132
|
-
|
|
133
|
-
stack, _ = Circuit::Trace.( activity,
|
|
134
|
-
nil,
|
|
135
|
-
{ content: "Let's start writing" }
|
|
136
|
-
)
|
|
137
|
-
```
|
|
83
|
+
## Activity: Call
|
|
138
84
|
|
|
139
|
-
|
|
85
|
+
To run the activity, you want to `call` it.
|
|
140
86
|
|
|
87
|
+
```ruby
|
|
88
|
+
my_options = {}
|
|
89
|
+
last_signal, options, flow_options, _ = activity.( nil, my_options, {} )
|
|
141
90
|
```
|
|
142
|
-
Circuit::Trace::Present.tree(stack)
|
|
143
|
-
|--> #<Start: default {}>{:content=>"Let's start writing"}
|
|
144
|
-
|--> Blog::Write{:content=>"Let's start writing"}
|
|
145
|
-
|--> Blog::SpellCheck{:content=>"Let's start writing"}
|
|
146
|
-
|--> Blog::Publish{:content=>"Let's start writing"}
|
|
147
|
-
`--> #<End: default {}>{:content=>"Let's start writing"}
|
|
148
|
-
```
|
|
149
91
|
|
|
150
|
-
|
|
92
|
+
1. The `start` event is `call`ed and per default returns the generic _signal_`Trailblazer::Circuit::Right`.
|
|
93
|
+
2. This emitted (or returned) signal is connected to the next task `Blog::Write`, which is now `call`ed.
|
|
94
|
+
3. `Blog::Write` emits another `Right` signal that leads to `Blog::SpellCheck` being `call`ed.
|
|
95
|
+
4. `Blog::SpellCheck` defines two outgoing signals and hence can decide what next task to call by emitting either `Right` if the spell check was ok, or `Left` if the post contains typos.
|
|
96
|
+
5. ...and so on.
|
|
97
|
+
|
|
98
|
+
<img src="http://trb.to/images/graph/blogpost-activity.png">
|
|
151
99
|
|
|
152
|
-
|
|
153
|
-
> Also, more options will make debugging of complex, nested workflows easier.
|
|
100
|
+
Visualizing an activity as a graph makes it very straight-forward to understanding the mechanics of the flow.
|
|
154
101
|
|
|
155
|
-
## Event
|
|
156
102
|
|
|
157
|
-
|
|
103
|
+
Note how signals translate to edges (or connections) in the graph, and tasks become vertices (or nodes).
|
|
158
104
|
|
|
159
|
-
|
|
105
|
+
The return values are the `last_signal`, which is usually the end event (they return themselves as a signal), the last `options` that usually contains all kinds of data from running the activity, and additional args.
|
|
160
106
|
|
|
161
|
-
##
|
|
107
|
+
## More
|
|
162
108
|
|
|
163
|
-
|
|
109
|
+
The [full documentation](http://trb.to/gems/activity/0.2/api.html) for this gem and many more interesting things can be found on the Trailblazer website.
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
module Trailblazer
|
|
2
2
|
class Activity
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
# A {Subprocess} is an instance of an abstract {Activity} that can be `call`ed.
|
|
4
|
+
# It is the runtime instance that runs from a specific start event.
|
|
5
|
+
def self.Subprocess(*args)
|
|
6
|
+
Subprocess.new(*args)
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
# Subprocess allows to have tasks with a different call interface and start event.
|
|
9
|
-
# @param activity Activity interface
|
|
10
|
+
# @param activity any object with an {Activity interface}
|
|
10
11
|
class Subprocess
|
|
11
|
-
def initialize(activity,
|
|
12
|
-
@activity
|
|
12
|
+
def initialize(activity, call: :call, **options)
|
|
13
|
+
@activity = activity
|
|
14
|
+
@options = options
|
|
15
|
+
@call = call
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
def call(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@activity.public_send(@call, @start_at, *args)
|
|
18
|
+
def call(args, **circuit_options)
|
|
19
|
+
@activity.public_send(@call, args, circuit_options.merge(@options))
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# @private
|
|
@@ -23,3 +24,7 @@ module Trailblazer
|
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
|
27
|
+
|
|
28
|
+
# circuit.( args, runner: Runner, start_at: raise, **circuit_flow_options )
|
|
29
|
+
|
|
30
|
+
# subprocess.( options, flow_options, *args, start_event:<Event>, last_signal: signal )
|
|
@@ -8,18 +8,26 @@ module Trailblazer
|
|
|
8
8
|
#
|
|
9
9
|
# Hooks into the TaskWrap.
|
|
10
10
|
module Trace
|
|
11
|
-
def self.call(activity,
|
|
11
|
+
def self.call(activity, (options), *args, &block)
|
|
12
12
|
tracing_flow_options = {
|
|
13
|
-
runner: Wrap::Runner,
|
|
14
13
|
stack: Trace::Stack.new,
|
|
15
|
-
wrap_runtime: ::Hash.new(Trace.wirings),
|
|
16
14
|
# Note that we don't pass :wrap_static here, that's handled by Task.__call__.
|
|
17
15
|
introspection: {}, # usually set that in Activity::call.
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
tracing_circuit_options = {
|
|
19
|
+
runner: Wrap::Runner,
|
|
20
|
+
wrap_runtime: ::Hash.new(Trace.wirings), # FIXME: this still overrides existing wrap_runtimes.
|
|
21
|
+
wrap_static: ::Hash.new( Trailblazer::Activity::Wrap.initial_activity ) # FIXME
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
last_signal, (options, flow_options) = call_circuit( activity, [
|
|
25
|
+
options,
|
|
26
|
+
# tracing_flow_options.merge(flow_options),
|
|
27
|
+
tracing_flow_options,
|
|
28
|
+
], tracing_circuit_options, &block )
|
|
21
29
|
|
|
22
|
-
return flow_options[:stack].to_a,
|
|
30
|
+
return flow_options[:stack].to_a, last_signal, options, flow_options
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
# TODO: test alterations with any wrap_circuit.
|
|
@@ -36,20 +44,26 @@ module Trailblazer
|
|
|
36
44
|
]
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
def self.capture_args(direction, options, flow_options, wrap_config, original_flow_options)
|
|
47
|
+
# def self.capture_args(direction, options, flow_options, wrap_config, original_flow_options)
|
|
48
|
+
def self.capture_args((wrap_config, original_args), **circuit_options)
|
|
49
|
+
(original_options, original_flow_options, _) = original_args[0]
|
|
50
|
+
|
|
40
51
|
original_flow_options[:stack].indent!
|
|
41
52
|
|
|
42
|
-
original_flow_options[:stack] << [ wrap_config[:task], :args, nil,
|
|
53
|
+
original_flow_options[:stack] << [ wrap_config[:task], :args, nil, {}, original_flow_options[:introspection] ]
|
|
43
54
|
|
|
44
|
-
[
|
|
55
|
+
[ Circuit::Right, [wrap_config, original_args ], **circuit_options ]
|
|
45
56
|
end
|
|
46
57
|
|
|
47
|
-
def self.capture_return(
|
|
48
|
-
|
|
58
|
+
def self.capture_return((wrap_config, original_args), **circuit_options)
|
|
59
|
+
(original_options, original_flow_options, _) = original_args[0]
|
|
60
|
+
|
|
61
|
+
original_flow_options[:stack] << [ wrap_config[:task], :return, wrap_config[:result_direction], {} ]
|
|
49
62
|
|
|
50
63
|
original_flow_options[:stack].unindent!
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
|
|
66
|
+
[ Circuit::Right, [wrap_config, original_args ], **circuit_options ]
|
|
53
67
|
end
|
|
54
68
|
|
|
55
69
|
# Mutable/stateful per design. We want a (global) stack!
|
|
@@ -5,31 +5,40 @@ class Trailblazer::Activity
|
|
|
5
5
|
#
|
|
6
6
|
# Here, we extend this, and wrap the task `call` into its own pipeline, so we can add external behavior per task.
|
|
7
7
|
module Runner
|
|
8
|
+
# Runner signature: call( task, direction, options, static_wraps )
|
|
9
|
+
#
|
|
8
10
|
# @api private
|
|
9
|
-
# Runner
|
|
10
|
-
def self.call(task,
|
|
11
|
-
|
|
12
|
-
runtime_wraps = flow_options[:wrap_runtime] || raise("Please provide :wrap_runtime")
|
|
11
|
+
# @interface Runner
|
|
12
|
+
def self.call(task, (options, *args), wrap_runtime: raise, wrap_static: raise, **circuit_options)
|
|
13
|
+
wrap_ctx = { task: task }
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
# this activity is "wrapped around" the actual `task`.
|
|
16
|
+
task_wrap_activity = apply_wirings(task, wrap_static, wrap_runtime)
|
|
15
17
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
# |-- Call (call actual task) id: "task_wrap.call_task"
|
|
20
|
-
# |-- Trace.capture_return [optional]
|
|
21
|
-
# |-- Wrap::End
|
|
22
|
-
# Pass empty flow_options to the task_wrap, so it doesn't infinite-loop.
|
|
18
|
+
# We save all original args passed into this Runner.call, because we want to return them later after this wrap
|
|
19
|
+
# is finished.
|
|
20
|
+
original_args = [ [options, *args], circuit_options.merge( wrap_runtime: wrap_runtime, wrap_static: wrap_static ) ]
|
|
23
21
|
|
|
24
22
|
# call the wrap for the task.
|
|
25
|
-
|
|
23
|
+
wrap_end_signal, ( wrap_ctx, _ ) = task_wrap_activity.(
|
|
24
|
+
[ wrap_ctx, original_args ] # we omit circuit_options here on purpose, so the wrapping activity uses the plain Runner.
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# don't return the wrap's end signal, but the one from call_task.
|
|
28
|
+
# return all original_args for the next "real" task in the circuit (this includes circuit_options).
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
# raise if wrap_ctx[:result_args][2] != static_wraps
|
|
31
|
+
|
|
32
|
+
# TODO: make circuit ignore all returned but the first
|
|
33
|
+
[ wrap_ctx[:result_direction], wrap_ctx[:result_args] ]
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
private
|
|
31
37
|
|
|
32
38
|
# Compute the task's wrap by applying alterations both static and from runtime.
|
|
39
|
+
#
|
|
40
|
+
# NOTE: this is for performance reasons: we could have only one hash containing everything but that'd mean
|
|
41
|
+
# unnecessary computations at `call`-time since steps might not even be executed.
|
|
33
42
|
def self.apply_wirings(task, wrap_static, wrap_runtime)
|
|
34
43
|
wrap_activity = wrap_static[task] # find static wrap for this specific task, or default wrap activity.
|
|
35
44
|
|
|
@@ -41,33 +50,30 @@ class Trailblazer::Activity
|
|
|
41
50
|
|
|
42
51
|
# The call_task method implements one default step `Call` in the Wrap::Activity circuit.
|
|
43
52
|
# It calls the actual, wrapped task.
|
|
44
|
-
def self.call_task(
|
|
45
|
-
task =
|
|
53
|
+
def self.call_task((wrap_ctx, original_args), **circuit_options)
|
|
54
|
+
task = wrap_ctx[:task]
|
|
46
55
|
|
|
47
56
|
# Call the actual task we're wrapping here.
|
|
48
|
-
|
|
57
|
+
puts "~~~~wrap.call: #{task}"
|
|
58
|
+
wrap_ctx[:result_direction], wrap_ctx[:result_args] = task.( *original_args )
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
# DISCUSS: do we want original_args here to be passed on, or the "effective" result_args which are different to original_args now?
|
|
61
|
+
[ Trailblazer::Circuit::Right, [ wrap_ctx, original_args ], **circuit_options ]
|
|
51
62
|
end
|
|
52
63
|
|
|
53
64
|
Call = method(:call_task)
|
|
54
65
|
|
|
55
|
-
class End < Trailblazer::Circuit::End
|
|
56
|
-
def call(direction, options, flow_options, wrap_config, *args)
|
|
57
|
-
[ wrap_config[:result_direction], options, flow_options ] # note how we don't return the appended internal args.
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
66
|
# Wrap::Activity is the actual circuit that implements the Task wrap. This circuit is
|
|
62
67
|
# also known as `task_wrap`.
|
|
63
68
|
#
|
|
64
69
|
# Example with tracing:
|
|
65
70
|
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
# Call the task_wrap circuit:
|
|
72
|
+
# |-- Start
|
|
73
|
+
# |-- Trace.capture_args [optional]
|
|
74
|
+
# |-- Call (call actual task) id: "task_wrap.call_task"
|
|
75
|
+
# |-- Trace.capture_return [optional]
|
|
76
|
+
# |-- Wrap::End
|
|
71
77
|
|
|
72
78
|
# Activity = Trailblazer::Activity::Activity({ id: "task.wrap" }, end: { default: End.new(:default) }) do |act|
|
|
73
79
|
# {
|
|
@@ -79,7 +85,7 @@ class Trailblazer::Activity
|
|
|
79
85
|
def self.initial_activity
|
|
80
86
|
Trailblazer::Activity.from_wirings(
|
|
81
87
|
[
|
|
82
|
-
[ :attach!, target: [ End.new(:default), type: :event, id: "End.default" ], edge: [ Trailblazer::Circuit::Right, {} ] ],
|
|
88
|
+
[ :attach!, target: [ Trailblazer::Circuit::End.new(:default), type: :event, id: "End.default" ], edge: [ Trailblazer::Circuit::Right, {} ] ],
|
|
83
89
|
[ :insert_before!, "End.default", node: [ Call, id: "task_wrap.call_task" ], outgoing: [ Trailblazer::Circuit::Right, {} ], incoming: ->(*) { true } ]
|
|
84
90
|
]
|
|
85
91
|
)
|
data/lib/trailblazer/activity.rb
CHANGED
|
@@ -75,14 +75,9 @@ module Trailblazer
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def initialize(graph)
|
|
78
|
-
@graph
|
|
79
|
-
@
|
|
80
|
-
@circuit
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Calls the internal circuit. `start_at` defaults to the Activity's start event if `nil` is given.
|
|
84
|
-
def call(start_at, *args)
|
|
85
|
-
@circuit.( start_at || @start_event, *args )
|
|
78
|
+
@graph = graph
|
|
79
|
+
@default_start_event = graph[:_wrapped]
|
|
80
|
+
@circuit = to_circuit( @graph ) # graph is an immutable object.
|
|
86
81
|
end
|
|
87
82
|
|
|
88
83
|
def end_events
|
|
@@ -97,6 +92,13 @@ module Trailblazer
|
|
|
97
92
|
::Hash[ graph.find_all { |node| graph.successors(node).size == 0 }.collect { |node| [ node[:_wrapped], { role: node[:role] } ] } ]
|
|
98
93
|
end
|
|
99
94
|
|
|
95
|
+
def call(args, start_event: default_start_event, **circuit_options)
|
|
96
|
+
@circuit.(
|
|
97
|
+
args,
|
|
98
|
+
circuit_options.merge( task: start_event) , # this passes :runner to the {Circuit}.
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
100
102
|
# @private
|
|
101
103
|
attr_reader :circuit
|
|
102
104
|
# @private
|
|
@@ -104,6 +106,8 @@ module Trailblazer
|
|
|
104
106
|
|
|
105
107
|
private
|
|
106
108
|
|
|
109
|
+
attr_reader :default_start_event
|
|
110
|
+
|
|
107
111
|
def to_circuit(graph)
|
|
108
112
|
Circuit.new(graph.to_h( include_leafs: false ), end_events, {})
|
|
109
113
|
end
|
|
@@ -121,5 +125,8 @@ module Trailblazer
|
|
|
121
125
|
@graph.find_all { |node| node[:_wrapped] == task }.first
|
|
122
126
|
end
|
|
123
127
|
end
|
|
128
|
+
|
|
129
|
+
module Interface
|
|
130
|
+
end
|
|
124
131
|
end
|
|
125
132
|
end
|
|
@@ -42,3 +42,17 @@ class Trailblazer::Circuit
|
|
|
42
42
|
target
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
module Trailblazer::Activity::Inspect
|
|
47
|
+
# The "matcher":
|
|
48
|
+
# Finds out appropriate serializer and calls it.
|
|
49
|
+
def self.call(inspected)
|
|
50
|
+
Instance(inspected)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Serializes an object instance with hex IDs.
|
|
54
|
+
# <Trailblazer::Circuit::Start: @name=:default, @options={}>
|
|
55
|
+
def self.Instance(object)
|
|
56
|
+
object.inspect.gsub(/0x\w+/, "")
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/trailblazer/circuit.rb
CHANGED
|
@@ -19,28 +19,36 @@ module Trailblazer
|
|
|
19
19
|
@name = name
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# @param args [Array] all arguments to be passed to the task's `call`
|
|
23
|
+
# @param task [callable] task to call
|
|
24
|
+
Run = ->(task, args, **circuit_options) { task.(args, **circuit_options) }
|
|
23
25
|
|
|
24
|
-
# Runs the circuit
|
|
25
|
-
#
|
|
26
|
+
# Runs the circuit until we hit a stop event.
|
|
27
|
+
#
|
|
28
|
+
# This method throws exceptions when the returned value of a task doesn't match
|
|
26
29
|
# any wiring.
|
|
27
30
|
#
|
|
28
|
-
# @param
|
|
29
|
-
# @param
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# @param task An event or task of this circuit from where to start
|
|
32
|
+
# @param options anything you want to pass to the first task
|
|
33
|
+
# @param flow_options Library-specific flow control data
|
|
34
|
+
# @return [last_signal, options, flow_options, *args]
|
|
35
|
+
#
|
|
36
|
+
# DISCUSS: returned circuit_options are ignored when calling the runner.
|
|
37
|
+
def call(args, task: raise, runner: Run, **circuit_options)
|
|
34
38
|
loop do
|
|
35
|
-
|
|
39
|
+
last_signal, args, _ignored_circuit_options = runner.(
|
|
40
|
+
task,
|
|
41
|
+
args,
|
|
42
|
+
circuit_options.merge( runner: runner ) # options for runner, to be discarded.
|
|
43
|
+
)
|
|
36
44
|
|
|
37
|
-
# Stop execution of the circuit when we hit a stop event (< End). This could be an
|
|
38
|
-
return [
|
|
45
|
+
# Stop execution of the circuit when we hit a stop event (< End). This could be an task's End or Suspend.
|
|
46
|
+
return [ last_signal, args ] if @stop_events.include?(task) # DISCUSS: return circuit_options here?
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
raise IllegalInputError.new("#{@name[:id]} #{
|
|
43
|
-
raise IllegalOutputSignalError.new("from #{@name[:id]}: `#{
|
|
48
|
+
task = next_for(task, last_signal) do |next_task, in_map|
|
|
49
|
+
task_name = @name[task] || task # TODO: this must be implemented only once, somewhere.
|
|
50
|
+
raise IllegalInputError.new("#{@name[:id]} #{task_name}") unless in_map
|
|
51
|
+
raise IllegalOutputSignalError.new("from #{@name[:id]}: `#{task_name}`===>[ #{last_signal.inspect} ]") unless next_task
|
|
44
52
|
end
|
|
45
53
|
end
|
|
46
54
|
end
|
|
@@ -51,16 +59,16 @@ module Trailblazer
|
|
|
51
59
|
end
|
|
52
60
|
|
|
53
61
|
private
|
|
54
|
-
def next_for(
|
|
62
|
+
def next_for(last_task, emitted_signal)
|
|
55
63
|
# p @map
|
|
56
64
|
in_map = false
|
|
57
|
-
cfg = @map.keys.find { |t| t ==
|
|
65
|
+
cfg = @map.keys.find { |t| t == last_task } and in_map = true
|
|
58
66
|
cfg = @map[cfg] if cfg
|
|
59
67
|
cfg ||= {}
|
|
60
|
-
|
|
61
|
-
yield
|
|
68
|
+
next_task = cfg[emitted_signal]
|
|
69
|
+
yield next_task, in_map
|
|
62
70
|
|
|
63
|
-
|
|
71
|
+
next_task
|
|
64
72
|
end
|
|
65
73
|
|
|
66
74
|
class IllegalInputError < RuntimeError
|
|
@@ -77,13 +85,13 @@ module Trailblazer
|
|
|
77
85
|
@options = options
|
|
78
86
|
end
|
|
79
87
|
|
|
80
|
-
def call(
|
|
88
|
+
def call(*args)
|
|
81
89
|
[ self, *args ]
|
|
82
90
|
end
|
|
83
91
|
end
|
|
84
92
|
|
|
85
93
|
class Start < End
|
|
86
|
-
def call(
|
|
94
|
+
def call(*args)
|
|
87
95
|
[ Right, *args ]
|
|
88
96
|
end
|
|
89
97
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nick Sutterer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-
|
|
11
|
+
date: 2017-10-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: hirb
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- ".travis.yml"
|
|
93
93
|
- CHANGES.md
|
|
94
94
|
- Gemfile
|
|
95
|
+
- NOTES
|
|
95
96
|
- README.md
|
|
96
97
|
- Rakefile
|
|
97
98
|
- lib/trailblazer-activity.rb
|