trailblazer-activity 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|