trailblazer-activity 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +13 -0
- data/CHANGES.md +55 -0
- data/Gemfile +7 -0
- data/README.md +117 -0
- data/Rakefile +10 -0
- data/lib/trailblazer-activity.rb +1 -0
- data/lib/trailblazer/activity.rb +119 -0
- data/lib/trailblazer/activity/graph.rb +135 -0
- data/lib/trailblazer/activity/nested.rb +25 -0
- data/lib/trailblazer/activity/version.rb +5 -0
- data/lib/trailblazer/circuit.rb +100 -0
- data/lib/trailblazer/circuit/present.rb +81 -0
- data/lib/trailblazer/circuit/testing.rb +42 -0
- data/lib/trailblazer/circuit/trace.rb +86 -0
- data/lib/trailblazer/circuit/wrap.rb +88 -0
- data/lib/trailblazer/container_chain.rb +45 -0
- data/lib/trailblazer/context.rb +100 -0
- data/lib/trailblazer/option.rb +78 -0
- data/trailblazer-activity.gemspec +29 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2f76309405b46e6388f13ecd090c1ad50a527fbc
|
4
|
+
data.tar.gz: 9c0baac268666b6c6136d78b967f7a8e436ee245
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4cc934487e044fc7a59273a03e20c77cbae3f7eda9d51d6016f0ca5dc30f83d36168aca43cbce19e0909f8b73c7a28f155f631cd9a325056e3cf678d3164c6ba
|
7
|
+
data.tar.gz: ea8fb256973d8a6084d4c7a7a70e3b990cef945e1f53ee31cd87f90061e20628631b0fcc405e6ec65b5f925ec8fc92c8a9cd28f4325ee4042b2e5e9bafda5b8a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGES.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# 0.0.12
|
2
|
+
|
3
|
+
* In `Activity::Before`, allow specifying what predecessing tasks to connect to the new_task via the
|
4
|
+
`:predecessors` option, and without knowing the direction. This will be the new preferred style in `Trailblazer:::Sequence`
|
5
|
+
where we can always assume directions are limited to `Right` and `Left` (e.g., with nested activities, this changes to a
|
6
|
+
colorful selection of directions).
|
7
|
+
|
8
|
+
# 0.0.11
|
9
|
+
|
10
|
+
* Temporarily allow injecting a `to_hash` transformer into a `ContainerChain`. This allows to ignore
|
11
|
+
certain container types such as `Dry::Container` in the KW transformation. Note that this is a temp
|
12
|
+
fix and will be replaced with proper pattern matching.
|
13
|
+
|
14
|
+
# 0.0.10
|
15
|
+
|
16
|
+
* Introduce `Context::ContainerChain` to eventually replace the heavy-weight `Skill` object.
|
17
|
+
* Fix a bug in `Option` where wrong args were passed when used without `flow_options`.
|
18
|
+
|
19
|
+
# 0.0.9
|
20
|
+
|
21
|
+
* Fix `Context#[]`, it returned `nil` when it should return `false`.
|
22
|
+
|
23
|
+
# 0.0.8
|
24
|
+
|
25
|
+
* Make `Trailblazer::Option` and `Trailblazer::Option::KW` a mix of lambda and object so it's easily extendable.
|
26
|
+
|
27
|
+
# 0.0.7
|
28
|
+
|
29
|
+
* It is now `Trailblazer::Args`.
|
30
|
+
|
31
|
+
# 0.0.6
|
32
|
+
|
33
|
+
* `Wrapped` is now `Wrap`. Also, a consistent `Alterations` interface allows tweaking here.
|
34
|
+
|
35
|
+
# 0.0.5
|
36
|
+
|
37
|
+
* The `Wrapped::Runner` now applies `Alterations` to each task's `Circuit`. This means you can inject `:task_alterations` into `Circuit#call`, which will then be merged into the task's original circuit, and then run. While this might sound like crazy talk, this allows any kind of external injection (tracing, input/output contracts, step dependency injections, ...) for specific or all tasks of any circuit.
|
38
|
+
|
39
|
+
# 0.0.4
|
40
|
+
|
41
|
+
* Simpler tracing with `Stack`.
|
42
|
+
* Added `Context`.
|
43
|
+
* Simplified `Circuit#call`.
|
44
|
+
|
45
|
+
# 0.0.3
|
46
|
+
|
47
|
+
* Make the first argument to `#Activity` (`@name`) always a Hash where `:id` is a reserved key for the name of the circuit.
|
48
|
+
|
49
|
+
# 0.0.2
|
50
|
+
|
51
|
+
* Make `flow_options` an immutable data structure just as `options`. It now needs to be returned from a `#call`.
|
52
|
+
|
53
|
+
# 0.0.1
|
54
|
+
|
55
|
+
* First release into an unsuspecting world. 🚀
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# Circuit
|
2
|
+
|
3
|
+
_The Circuit of Life._
|
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
|
+
{% callout %}
|
12
|
+
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.
|
13
|
+
{% endcallout %}
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
To use circuits, activities and nested tasks, you need one gem, only.
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem "trailblazer-circuit"
|
21
|
+
```
|
22
|
+
|
23
|
+
The `trailblazer-circuit` gem is often just called the `circuit` gem. It ships with the `operation` gem and implements the internal Railway.
|
24
|
+
|
25
|
+
## Overview
|
26
|
+
|
27
|
+
The following diagram illustrates a common use-case for `circuit`, the task of publishing a blog post.
|
28
|
+
|
29
|
+
<img src="/images/diagrams/blog-bpmn1.png">
|
30
|
+
|
31
|
+
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.
|
32
|
+
|
33
|
+
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.
|
34
|
+
|
35
|
+
Your job is solely to implement the tasks and deciders put into this activity - you don't have to take care of executing it in the right order, and so on.
|
36
|
+
|
37
|
+
## Definition
|
38
|
+
|
39
|
+
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.
|
40
|
+
|
41
|
+
{{ "test/docs/activity_test.rb:basic:../trailblazer-circuit" | tsnippet }}
|
42
|
+
|
43
|
+
The `Activity` function is a convenient tool to create an activity. Note that the yielded object allows to access *events* from the activity, such as the `Start` and `End` event that are created per default.
|
44
|
+
|
45
|
+
This defines the control flow - the next step is to actually implement the tasks in this activity.
|
46
|
+
|
47
|
+
## Task
|
48
|
+
|
49
|
+
A *task* usually maps to a particular box in your diagram. Its API is very simple: a task needs to expose a `call` method, allowing it to be a lambda or any other callable object.
|
50
|
+
|
51
|
+
{{ "test/docs/activity_test.rb:write:../trailblazer-circuit" | tsnippet }}
|
52
|
+
|
53
|
+
It receives all arguments returned from the task run before. This means a task should return everything it receives.
|
54
|
+
|
55
|
+
To transport data across the flow, you can change the return value. In this example, we use one global hash `options` that is passed from task to task and used for writing.
|
56
|
+
|
57
|
+
The first return value is crucial: it dictates what will be the next step when executing the flow.
|
58
|
+
|
59
|
+
For example, the `SpellCheck` task needs to decide which route to take.
|
60
|
+
|
61
|
+
{{ "test/docs/activity_test.rb:spell:../trailblazer-circuit" | tsnippet }}
|
62
|
+
|
63
|
+
It's as simple as returning the appropriate signal.
|
64
|
+
|
65
|
+
{% callout %}
|
66
|
+
You can use any object as a direction signal and return it, as long as it's defined in the circuit.
|
67
|
+
{% endcallout %}
|
68
|
+
|
69
|
+
## Call
|
70
|
+
|
71
|
+
After defining circuit and implementing the tasks, the circuit can be executed using its very own `call` method.
|
72
|
+
|
73
|
+
{{ "test/docs/activity_test.rb:call:../trailblazer-circuit" | tsnippet }}
|
74
|
+
|
75
|
+
The first argument is where to start the circuit. Usually, this will be the activity's `Start` event accessable via `activity[:Start]`.
|
76
|
+
|
77
|
+
All options are passed straight to the first task, which in turn has to make sure it returns an appropriate result set.
|
78
|
+
|
79
|
+
The activity's return set is the last run task and all arguments from the last task.
|
80
|
+
|
81
|
+
{{ "test/docs/activity_test.rb:call-ret:../trailblazer-circuit" | tsnippet }}
|
82
|
+
|
83
|
+
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.
|
84
|
+
|
85
|
+
## Tracing
|
86
|
+
|
87
|
+
For debugging or simply understanding the flows of circuits, you can use tracing.
|
88
|
+
|
89
|
+
{{ "test/docs/activity_test.rb:trace-act:../trailblazer-circuit" | tsnippet }}
|
90
|
+
|
91
|
+
The second argument to `Activity` takes debugging information, so you can set readable names for tasks.
|
92
|
+
|
93
|
+
When invoking the activity, the `:runner` option will activate tracing and write debugging information about any executed task onto the `:stack` array.
|
94
|
+
|
95
|
+
{{ "test/docs/activity_test.rb:trace-call:../trailblazer-circuit" | tsnippet }}
|
96
|
+
|
97
|
+
The `stack` can then be passed to a presenter.
|
98
|
+
|
99
|
+
{{ "test/docs/activity_test.rb:trace-res:../trailblazer-circuit" | tsnippet }}
|
100
|
+
|
101
|
+
Tracing is extremely efficient to find out what is going wrong and supersedes cryptic debuggers by many times. Note that tracing also works for deeply nested circuits.
|
102
|
+
|
103
|
+
{% callout %}
|
104
|
+
🌅 In future versions of Trailblazer, our own debugger will take advantage of the explicit, traceable nature of circuits and also integrate with Ruby's exception handling.
|
105
|
+
|
106
|
+
Also, more options will make debugging of complex, nested workflows easier.
|
107
|
+
{% endcallout %}
|
108
|
+
|
109
|
+
## Event
|
110
|
+
|
111
|
+
* how to add more ends, etc.
|
112
|
+
|
113
|
+
## Nested
|
114
|
+
|
115
|
+
## Operation
|
116
|
+
|
117
|
+
If you need a higher abstraction of `circuit`, check out Trailblazer's [operation](localhost:4000/gems/operation/2.0/api.html) implemenation which provides a simple Railway-oriented interface to create linear circuits.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "trailblazer/activity"
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require "trailblazer/activity/graph"
|
2
|
+
require "trailblazer/activity/nested"
|
3
|
+
require "trailblazer/activity/version"
|
4
|
+
|
5
|
+
require "trailblazer/circuit"
|
6
|
+
require "trailblazer/circuit/trace"
|
7
|
+
require "trailblazer/circuit/present"
|
8
|
+
require "trailblazer/circuit/wrap"
|
9
|
+
|
10
|
+
require "trailblazer/option"
|
11
|
+
require "trailblazer/context"
|
12
|
+
require "trailblazer/container_chain"
|
13
|
+
|
14
|
+
module Trailblazer
|
15
|
+
class Activity
|
16
|
+
|
17
|
+
# Only way to build an Activity.
|
18
|
+
def self.from_wirings(wirings, &block)
|
19
|
+
start_evt = Circuit::Start.new(:default)
|
20
|
+
start_args = [ start_evt, { type: :event, id: [:Start, :default] } ]
|
21
|
+
|
22
|
+
start = block ? Graph::Start( *start_args, &block ) : Graph::Start(*start_args)
|
23
|
+
|
24
|
+
wirings.each do |wiring|
|
25
|
+
start.send(*wiring)
|
26
|
+
end
|
27
|
+
|
28
|
+
new(start)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Build an activity from a hash.
|
32
|
+
#
|
33
|
+
# activity = Trailblazer::Activity.from_hash do |start, _end|
|
34
|
+
# {
|
35
|
+
# start => { Circuit::Right => Blog::Write },
|
36
|
+
# Blog::Write => { Circuit::Right => Blog::SpellCheck },
|
37
|
+
# Blog::SpellCheck => { Circuit::Right => Blog::Publish, Circuit::Left => Blog::Correct },
|
38
|
+
# Blog::Correct => { Circuit::Right => Blog::SpellCheck },
|
39
|
+
# Blog::Publish => { Circuit::Right => _end }
|
40
|
+
# }
|
41
|
+
# end
|
42
|
+
def self.from_hash(end_evt=Circuit::End.new(:default), start_evt=Circuit::Start.new(:default), &block)
|
43
|
+
hash = yield(start_evt, end_evt)
|
44
|
+
graph = Graph::Start( start_evt, id: [:Start, :default] )
|
45
|
+
|
46
|
+
hash.each do |source_task, connections|
|
47
|
+
source = graph.find_all { |node| node[:_wrapped] == source_task }.first or raise "#{source_task} unknown"
|
48
|
+
|
49
|
+
connections.each do |signal, task| # FIXME: id sucks
|
50
|
+
if existing = graph.find_all { |node| node[:_wrapped] == task }.first
|
51
|
+
graph.connect!( source: source[:id], target: existing, edge: [signal, {}] )
|
52
|
+
else
|
53
|
+
graph.attach!( source: source[:id], target: [task, id: task], edge: [signal, {}] )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
new(graph)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.merge(activity, wirings)
|
62
|
+
graph = activity.graph
|
63
|
+
|
64
|
+
# TODO: move this to Graph
|
65
|
+
# replace the old start node with the new one that's created in ::from_wirings.
|
66
|
+
cloned_graph_ary = graph[:graph].collect { |node, connections| [ node, connections.clone ] }
|
67
|
+
old_start_connections = cloned_graph_ary.delete_at(0)[1] # FIXME: what if some connection goes back to start?
|
68
|
+
|
69
|
+
from_wirings(wirings) do |start_node, data|
|
70
|
+
cloned_graph_ary.unshift [ start_node, old_start_connections ] # push new start node onto the graph.
|
71
|
+
|
72
|
+
data[:graph] = ::Hash[cloned_graph_ary]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(graph)
|
77
|
+
@graph = graph
|
78
|
+
@start_event = @graph[:_wrapped]
|
79
|
+
@circuit = to_circuit(@graph) # graph is an immutable object.
|
80
|
+
end
|
81
|
+
|
82
|
+
# Calls the internal circuit. `start_at` defaults to the Activity's start event if `nil` is given.
|
83
|
+
def call(start_at, *args)
|
84
|
+
@circuit.( start_at || @start_event, *args )
|
85
|
+
end
|
86
|
+
|
87
|
+
def end_events
|
88
|
+
@circuit.to_fields[1]
|
89
|
+
end
|
90
|
+
|
91
|
+
# @private
|
92
|
+
attr_reader :circuit
|
93
|
+
# @private
|
94
|
+
attr_reader :graph
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def to_circuit(graph)
|
99
|
+
end_events = graph.find_all { |node| graph.successors(node).size == 0 } # Find leafs of graph.
|
100
|
+
.collect { |n| n[:_wrapped] } # unwrap the actual End event instance from the Node.
|
101
|
+
|
102
|
+
Circuit.new(graph.to_h( include_leafs: false ), end_events, {})
|
103
|
+
end
|
104
|
+
|
105
|
+
class Introspection
|
106
|
+
# @param activity Activity
|
107
|
+
def initialize(activity)
|
108
|
+
@activity = activity
|
109
|
+
@graph = activity.graph
|
110
|
+
@circuit = activity.circuit
|
111
|
+
end
|
112
|
+
|
113
|
+
# Find the node that wraps `task` or return nil.
|
114
|
+
def [](task)
|
115
|
+
@graph.find_all { |node| node[:_wrapped] == task }.first
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
# Note that Graph is a superset of a real directed graph. For instance, it might contain detached nodes.
|
3
|
+
# == Design
|
4
|
+
# * This class is designed to maintain a graph while building up a circuit step-wise.
|
5
|
+
# * It can be imperformant as this all happens at compile-time.
|
6
|
+
module Activity::Graph
|
7
|
+
# Task => { name: "Nested{Task}", type: :subprocess, boundary_events: { Circuit::Left => {} } }
|
8
|
+
|
9
|
+
# TODO: make Edge, Node, Start Hash::Immutable ?
|
10
|
+
class Edge
|
11
|
+
def initialize(data)
|
12
|
+
@data = data
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@data[key]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Node < Edge
|
21
|
+
end
|
22
|
+
|
23
|
+
class Start < Node
|
24
|
+
def initialize(data)
|
25
|
+
yield self, data if block_given?
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
# Single entry point for adding nodes and edges to the graph.
|
30
|
+
def connect_for!(source, edge, target)
|
31
|
+
# raise if find_all( source[:id] ).any?
|
32
|
+
self[:graph][source] ||= {}
|
33
|
+
self[:graph][target] ||= {} # keep references to all nodes, even when detached.
|
34
|
+
self[:graph][source][edge] = target
|
35
|
+
end
|
36
|
+
private :connect_for!
|
37
|
+
|
38
|
+
# Builds a node from the provided `:node` argument array.
|
39
|
+
def attach!(target:raise, edge:raise, source:self)
|
40
|
+
target = target.kind_of?(Node) ? target : Node(*target)
|
41
|
+
|
42
|
+
connect!(target: target, edge: edge, source: source)
|
43
|
+
end
|
44
|
+
|
45
|
+
def connect!(target:raise, edge:raise, source:self)
|
46
|
+
target = target.kind_of?(Node) ? target : (find_all { |_target| _target[:id] == target }[0] || raise( "#{target} not found"))
|
47
|
+
source = source.kind_of?(Node) ? source : (find_all { |_source| _source[:id] == source }[0] || raise( "#{source} not found"))
|
48
|
+
|
49
|
+
edge = Edge(*edge)
|
50
|
+
|
51
|
+
connect_for!(source, edge, target)
|
52
|
+
|
53
|
+
target
|
54
|
+
end
|
55
|
+
|
56
|
+
def insert_before!(old_node, node:raise, outgoing:nil, incoming:raise)
|
57
|
+
old_node = find_all(old_node)[0] || raise( "#{old_node} not found") unless old_node.kind_of?(Node)
|
58
|
+
new_node = Node(*node)
|
59
|
+
|
60
|
+
raise IllegalNodeError.new("The ID `#{new_node[:id]}` has been added before.") if find_all( new_node[:id] ).any?
|
61
|
+
|
62
|
+
incoming_tuples = predecessors(old_node)
|
63
|
+
rewired_connections = incoming_tuples.find_all { |(node, edge)| incoming.(edge) }
|
64
|
+
|
65
|
+
# rewire old_task's predecessors to new_task.
|
66
|
+
if rewired_connections.size == 0 # this happens when we're inserting "before" an orphaned node.
|
67
|
+
self[:graph][new_node] = {} # FIXME: redundant in #connect_for!
|
68
|
+
else
|
69
|
+
rewired_connections.each { |(node, edge)| connect_for!(node, edge, new_node) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# connect new_task --> old_task.
|
73
|
+
if outgoing
|
74
|
+
edge = Edge(*outgoing)
|
75
|
+
|
76
|
+
connect_for!(new_node, edge, old_node)
|
77
|
+
end
|
78
|
+
|
79
|
+
return new_node
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_all(id=nil, &block)
|
83
|
+
nodes = self[:graph].keys + self[:graph].values.collect(&:values).flatten
|
84
|
+
nodes = nodes.uniq
|
85
|
+
|
86
|
+
block ||= ->(node) { node[:id] == id }
|
87
|
+
|
88
|
+
nodes.find_all(&block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def Edge(wrapped, options)
|
92
|
+
edge = Edge.new(options.merge( _wrapped: wrapped ))
|
93
|
+
end
|
94
|
+
|
95
|
+
def Node(wrapped, options)
|
96
|
+
Node.new( options.merge( _wrapped: wrapped ) )
|
97
|
+
end
|
98
|
+
|
99
|
+
# private
|
100
|
+
def predecessors(target_node)
|
101
|
+
self[:graph].each_with_object([]) do |(node, connections), ary|
|
102
|
+
connections.each { |edge, target| target == target_node && ary << [node, edge] }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def successors(node)
|
107
|
+
( self[:graph][node] || {} ).values
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_h(include_leafs:true)
|
111
|
+
hash = ::Hash[
|
112
|
+
self[:graph].collect do |node, connections|
|
113
|
+
connections = connections.collect { |edge, node| [ edge[:_wrapped], node[:_wrapped] ] }
|
114
|
+
|
115
|
+
[ node[:_wrapped], ::Hash[connections] ]
|
116
|
+
end
|
117
|
+
]
|
118
|
+
|
119
|
+
if include_leafs == false
|
120
|
+
hash = hash.select { |node, connections| connections.any? }
|
121
|
+
end
|
122
|
+
|
123
|
+
hash
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.Start(wrapped, graph:{}, **data, &block)
|
128
|
+
block ||= ->(node, data) { data[:graph][node] = {} }
|
129
|
+
Start.new( { _wrapped: wrapped, graph: graph }.merge(data), &block )
|
130
|
+
end
|
131
|
+
|
132
|
+
class IllegalNodeError < RuntimeError
|
133
|
+
end
|
134
|
+
end # Graph
|
135
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
# Builder for running a nested process from a specific `start_at` position.
|
4
|
+
def self.Nested(*args, &block)
|
5
|
+
Nested.new(*args, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Nested allows to have tasks with a different call interface and start event.
|
9
|
+
# @param activity Activity interface
|
10
|
+
class Nested
|
11
|
+
def initialize(activity, start_with=nil, &block)
|
12
|
+
@activity, @start_with, @block = activity, start_with, block
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(start_at, *args)
|
16
|
+
return @block.(activity: activity, start_at: @start_with, args: args) if @block
|
17
|
+
|
18
|
+
@activity.(@start_with, *args)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @private
|
22
|
+
attr_reader :activity # we actually only need this for introspection.
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
# Running a Circuit instance will run all tasks sequentially depending on the former's result.
|
3
|
+
# Each task is called and retrieves the former task's return values.
|
4
|
+
#
|
5
|
+
# Note: Please use #Activity as a public circuit builder.
|
6
|
+
#
|
7
|
+
# @param map [Hash] Defines the wiring.
|
8
|
+
# @param stop_events [Array] Tasks that stop execution of the circuit.
|
9
|
+
# @param name [Hash] Names for tracing, debugging and exceptions. `:id` is a reserved key for circuit name.
|
10
|
+
#
|
11
|
+
# result = circuit.(start_at, *args)
|
12
|
+
#
|
13
|
+
# @see Activity
|
14
|
+
# @api semi-private
|
15
|
+
class Circuit
|
16
|
+
def initialize(map, stop_events, name)
|
17
|
+
@map = map
|
18
|
+
@stop_events = stop_events
|
19
|
+
@name = name
|
20
|
+
end
|
21
|
+
|
22
|
+
Run = ->(activity, direction, *args) { activity.(direction, *args) }
|
23
|
+
|
24
|
+
# Runs the circuit. Stops when hitting a End event or subclass thereof.
|
25
|
+
# This method throws exceptions when the return value of a task doesn't match
|
26
|
+
# any wiring.
|
27
|
+
#
|
28
|
+
# @param activity A task from the circuit where to start
|
29
|
+
# @param args An array of options passed to the first task.
|
30
|
+
def call(activity, options, flow_options={}, *args)
|
31
|
+
direction = nil
|
32
|
+
runner = flow_options[:runner] || Run
|
33
|
+
|
34
|
+
loop do
|
35
|
+
direction, options, flow_options, *args = runner.( activity, direction, options, flow_options, *args )
|
36
|
+
|
37
|
+
# Stop execution of the circuit when we hit a stop event (< End). This could be an activity's End or Suspend.
|
38
|
+
return [ direction, options, flow_options, *args ] if @stop_events.include?(activity)
|
39
|
+
|
40
|
+
activity = next_for(activity, direction) do |next_activity, in_map|
|
41
|
+
activity_name = @name[activity] || activity # TODO: this must be implemented only once, somewhere.
|
42
|
+
raise IllegalInputError.new("#{@name[:id]} #{activity_name}") unless in_map
|
43
|
+
raise IllegalOutputSignalError.new("from #{@name[:id]}: `#{activity_name}`===>[ #{direction.inspect} ]") unless next_activity
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the circuit's components.
|
49
|
+
def to_fields
|
50
|
+
[ @map, @stop_events, @name]
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def next_for(last_activity, emitted_direction)
|
55
|
+
# p @map
|
56
|
+
in_map = false
|
57
|
+
cfg = @map.keys.find { |t| t == last_activity } and in_map = true
|
58
|
+
cfg = @map[cfg] if cfg
|
59
|
+
cfg ||= {}
|
60
|
+
next_activity = cfg[emitted_direction]
|
61
|
+
yield next_activity, in_map
|
62
|
+
|
63
|
+
next_activity
|
64
|
+
end
|
65
|
+
|
66
|
+
class IllegalInputError < RuntimeError
|
67
|
+
end
|
68
|
+
|
69
|
+
class IllegalOutputSignalError < RuntimeError
|
70
|
+
end
|
71
|
+
|
72
|
+
# End event is just another callable task.
|
73
|
+
# Any instance of subclass of End will halt the circuit's execution when hit.
|
74
|
+
class End
|
75
|
+
def initialize(name, options={})
|
76
|
+
@name = name
|
77
|
+
@options = options
|
78
|
+
end
|
79
|
+
|
80
|
+
def call(direction, *args)
|
81
|
+
[ self, *args ]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Start < End
|
86
|
+
def call(direction, *args)
|
87
|
+
[ Right, *args ]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Builder for Circuit::End when defining the Activity's circuit.
|
92
|
+
def self.End(name, options={})
|
93
|
+
End.new(name, options)
|
94
|
+
end
|
95
|
+
|
96
|
+
class Signal; end
|
97
|
+
class Right < Signal; end
|
98
|
+
class Left < Signal; end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "hirb"
|
2
|
+
|
3
|
+
module Trailblazer
|
4
|
+
class Circuit
|
5
|
+
module Trace
|
6
|
+
# TODO:
|
7
|
+
# * Struct for debug_item
|
8
|
+
module Present
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def tree(stack, level=1, tree=[])
|
12
|
+
tree_for(stack, level, tree)
|
13
|
+
|
14
|
+
Hirb::Console.format_output(tree, class: :tree, type: :directory)
|
15
|
+
end
|
16
|
+
|
17
|
+
# API HERE is: we only know the current element (e.g. task), input, output, and have an "introspection" object that tells us more about the element.
|
18
|
+
# TODO: the debug_item's "api" sucks, this should be a struct.
|
19
|
+
def tree_for(stack, level, tree)
|
20
|
+
stack.each do |debug_item|
|
21
|
+
task = debug_item[0][0]
|
22
|
+
|
23
|
+
if debug_item.size == 2 # flat
|
24
|
+
introspect = debug_item[0].last
|
25
|
+
|
26
|
+
name = (node = introspect[task]) ? node[:id] : task
|
27
|
+
|
28
|
+
tree << [ level, name ]
|
29
|
+
else # nesting
|
30
|
+
tree << [ level, task ]
|
31
|
+
|
32
|
+
tree_for(debug_item[1..-2], level + 1, tree)
|
33
|
+
|
34
|
+
tree << [ level+1, debug_item[-1][0] ]
|
35
|
+
end
|
36
|
+
|
37
|
+
tree
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_name(debug_item)
|
42
|
+
track = debug_item[2]
|
43
|
+
klass = track.class == Class ? track : track.class
|
44
|
+
color = color_map[klass]
|
45
|
+
|
46
|
+
return debug_item[0].to_s unless color
|
47
|
+
colorify(debug_item[0], color)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_options(debug_item)
|
51
|
+
debug_item[4]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
def colorify(string, color)
|
57
|
+
"\e[#{color_table[color]}m#{string}\e[0m"
|
58
|
+
end
|
59
|
+
|
60
|
+
def color_map
|
61
|
+
{
|
62
|
+
Trailblazer::Circuit::Start => :blue,
|
63
|
+
Trailblazer::Circuit::End => :pink,
|
64
|
+
Trailblazer::Circuit::Right => :green,
|
65
|
+
Trailblazer::Circuit::Left => :red
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def color_table
|
70
|
+
{
|
71
|
+
red: 31,
|
72
|
+
green: 32,
|
73
|
+
yellow: 33,
|
74
|
+
blue: 34,
|
75
|
+
pink: 35
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module MiniTest::Assertions
|
2
|
+
def assert_activity_inspect(text, subject)
|
3
|
+
Trailblazer::Circuit::ActivityInspect(subject).must_equal text
|
4
|
+
end
|
5
|
+
|
6
|
+
def assert_event_inspect(text, subject)
|
7
|
+
Trailblazer::Circuit::EndInspect(subject).must_equal(text)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
Trailblazer::Activity.infect_an_assertion :assert_activity_inspect, :must_inspect
|
13
|
+
Trailblazer::Circuit::End.infect_an_assertion :assert_event_inspect, :must_inspect_end_fixme
|
14
|
+
|
15
|
+
class Trailblazer::Circuit
|
16
|
+
def self.EndInspect(event)
|
17
|
+
event.instance_eval { "#<#{self.class.to_s.split("::").last}: #{@name} #{@options}>" }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.ActivityInspect(activity, strip: ["AlterTest::"])
|
21
|
+
strip += ["Trailblazer::Circuit::"]
|
22
|
+
stripped = ->(target) { strip_for(target, strip) }
|
23
|
+
|
24
|
+
map, _ = activity.circuit.to_fields
|
25
|
+
|
26
|
+
content = map.collect do |task, connections|
|
27
|
+
bla =
|
28
|
+
connections.collect do |direction, target|
|
29
|
+
target_str = target.kind_of?(End) ? EndInspect(target) : stripped.(target)
|
30
|
+
"#{stripped.(direction)}=>#{target_str}"
|
31
|
+
end.join(", ")
|
32
|
+
task_str = task.kind_of?(End) ? EndInspect(task) : stripped.(task)
|
33
|
+
"#{task_str}=>{#{bla}}"
|
34
|
+
end.join(", ")
|
35
|
+
"{#{content}}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.strip_for(target, strings)
|
39
|
+
strings.each { |stripped| target = target.to_s.gsub(stripped, "") }
|
40
|
+
target
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Circuit
|
3
|
+
# Trace#call will call the activities and trace what steps are called, options passed,
|
4
|
+
# and the order and nesting.
|
5
|
+
#
|
6
|
+
# stack, _ = Trailblazer::Circuit::Trace.(activity, activity[:Start], { id: 1 })
|
7
|
+
# puts Trailblazer::Circuit::Present.tree(stack) # renders the trail.
|
8
|
+
#
|
9
|
+
# Hooks into the TaskWrap.
|
10
|
+
module Trace
|
11
|
+
def self.call(activity, direction, options, flow_options={}, &block)
|
12
|
+
tracing_flow_options = {
|
13
|
+
runner: Wrap::Runner,
|
14
|
+
stack: Trace::Stack.new,
|
15
|
+
wrap_runtime: ::Hash.new(Trace.wirings),
|
16
|
+
# Note that we don't pass :wrap_static here, that's handled by Task.__call__.
|
17
|
+
introspection: {}, # usually set that in Activity::call.
|
18
|
+
}
|
19
|
+
|
20
|
+
direction, options, flow_options = call_circuit( activity, direction, options, tracing_flow_options.merge(flow_options), &block )
|
21
|
+
|
22
|
+
return flow_options[:stack].to_a, direction, options, flow_options
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO: test alterations with any wrap_circuit.
|
26
|
+
def self.call_circuit(activity, *args, &block)
|
27
|
+
return activity.(*args) unless block
|
28
|
+
block.(activity, *args)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Default tracing tasks to be plugged into the wrap circuit.
|
32
|
+
def self.wirings
|
33
|
+
[
|
34
|
+
[ :insert_before!, "task_wrap.call_task", node: [ Trace.method(:capture_args), id: "task_wrap.capture_args" ], outgoing: [ Right, {} ], incoming: ->(*) { true } ],
|
35
|
+
[ :insert_before!, [:End, :default], node: [ Trace.method(:capture_return), id: "task_wrap.capture_return" ], outgoing: [ Right, {} ], incoming: ->(*) { true } ],
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.capture_args(direction, options, flow_options, wrap_config, original_flow_options)
|
40
|
+
original_flow_options[:stack].indent!
|
41
|
+
|
42
|
+
original_flow_options[:stack] << [ wrap_config[:task], :args, nil, options.dup, original_flow_options[:introspection] ]
|
43
|
+
|
44
|
+
[ direction, options, flow_options, wrap_config, original_flow_options ]
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.capture_return(direction, options, flow_options, wrap_config, original_flow_options)
|
48
|
+
original_flow_options[:stack] << [ wrap_config[:task], :return, flow_options[:result_direction], options.dup ]
|
49
|
+
|
50
|
+
original_flow_options[:stack].unindent!
|
51
|
+
|
52
|
+
[ direction, options, flow_options, wrap_config, original_flow_options ]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Mutable/stateful per design. We want a (global) stack!
|
56
|
+
class Stack
|
57
|
+
def initialize
|
58
|
+
@nested = []
|
59
|
+
@stack = [ @nested ]
|
60
|
+
end
|
61
|
+
|
62
|
+
def indent!
|
63
|
+
current << indented = []
|
64
|
+
@stack << indented
|
65
|
+
end
|
66
|
+
|
67
|
+
def unindent!
|
68
|
+
@stack.pop
|
69
|
+
end
|
70
|
+
|
71
|
+
def <<(args)
|
72
|
+
current << args
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_a
|
76
|
+
@nested
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def current
|
81
|
+
@stack.last
|
82
|
+
end
|
83
|
+
end # Stack
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class Trailblazer::Circuit
|
2
|
+
module Wrap
|
3
|
+
# The runner is passed into Circuit#call( runner: Runner ) and is called for every task in the circuit.
|
4
|
+
# Its primary job is to actually `call` the task.
|
5
|
+
#
|
6
|
+
# Here, we extend this, and wrap the task `call` into its own pipeline, so we can add external behavior per task.
|
7
|
+
module Runner
|
8
|
+
# @api private
|
9
|
+
# Runner signature: call( task, direction, options, flow_options, static_wraps )
|
10
|
+
def self.call(task, direction, options, flow_options, static_wraps = Hash.new(Wrap.initial_activity))
|
11
|
+
wrap_config = { task: task }
|
12
|
+
runtime_wraps = flow_options[:wrap_runtime] || raise("Please provide :wrap_runtime")
|
13
|
+
|
14
|
+
task_wrap_activity = apply_wirings(task, static_wraps, runtime_wraps)
|
15
|
+
|
16
|
+
# Call the task_wrap circuit:
|
17
|
+
# |-- Start
|
18
|
+
# |-- Trace.capture_args [optional]
|
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.
|
23
|
+
|
24
|
+
# call the wrap for the task.
|
25
|
+
ret = task_wrap_activity.( nil, options, {}, wrap_config, flow_options )
|
26
|
+
|
27
|
+
[ *ret, static_wraps ] # return everything plus the static_wraps for the next task in the circuit.
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Compute the task's wrap by applying alterations both static and from runtime.
|
33
|
+
def self.apply_wirings(task, wrap_static, wrap_runtime)
|
34
|
+
wrap_activity = wrap_static[task] # find static wrap for this specific task, or default wrap activity.
|
35
|
+
|
36
|
+
# Apply runtime alterations.
|
37
|
+
# Grab the additional wirings for the particular `task` from `wrap_runtime` (returns default otherwise).
|
38
|
+
wrap_activity = Trailblazer::Activity.merge(wrap_activity, wrap_runtime[task])
|
39
|
+
end
|
40
|
+
end # Runner
|
41
|
+
|
42
|
+
# The call_task method implements one default step `Call` in the Wrap::Activity circuit.
|
43
|
+
# It calls the actual, wrapped task.
|
44
|
+
def self.call_task(direction, options, flow_options, wrap_config, original_flow_options)
|
45
|
+
task = wrap_config[:task]
|
46
|
+
|
47
|
+
# Call the actual task we're wrapping here.
|
48
|
+
wrap_config[:result_direction], options, flow_options = task.( direction, options, original_flow_options )
|
49
|
+
|
50
|
+
[ direction, options, flow_options, wrap_config, original_flow_options ]
|
51
|
+
end
|
52
|
+
|
53
|
+
Call = method(:call_task)
|
54
|
+
|
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
|
+
# Wrap::Activity is the actual circuit that implements the Task wrap. This circuit is
|
62
|
+
# also known as `task_wrap`.
|
63
|
+
#
|
64
|
+
# Example with tracing:
|
65
|
+
#
|
66
|
+
# |-- Start
|
67
|
+
# |-- Trace.capture_args [optional]
|
68
|
+
# |-- Call (call actual task)
|
69
|
+
# |-- Trace.capture_return [optional]
|
70
|
+
# |-- End
|
71
|
+
|
72
|
+
# Activity = Trailblazer::Circuit::Activity({ id: "task.wrap" }, end: { default: End.new(:default) }) do |act|
|
73
|
+
# {
|
74
|
+
# act[:Start] => { Right => Call }, # see Wrap::call_task
|
75
|
+
# Call => { Right => act[:End] },
|
76
|
+
# }
|
77
|
+
# end # Activity
|
78
|
+
|
79
|
+
def self.initial_activity
|
80
|
+
Trailblazer::Activity.from_wirings(
|
81
|
+
[
|
82
|
+
[ :attach!, target: [ End.new(:default), type: :event, id: [:End, :default] ], edge: [ Right, {} ] ],
|
83
|
+
[ :insert_before!, [:End, :default], node: [ Call, id: "task_wrap.call_task" ], outgoing: [ Right, {} ], incoming: ->(*) { true } ]
|
84
|
+
]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# @private
|
2
|
+
class Trailblazer::Context::ContainerChain # used to be called Resolver.
|
3
|
+
# Keeps a list of containers. When looking up a key/value, containers are traversed in
|
4
|
+
# the order they were added until key is found.
|
5
|
+
#
|
6
|
+
# Required Container interface: `#key?`, `#[]`.
|
7
|
+
#
|
8
|
+
# @note ContainerChain is an immutable data structure, it does not support writing.
|
9
|
+
# @param containers Array of <Container> objects (splatted)
|
10
|
+
def initialize(containers, to_hash: nil)
|
11
|
+
@containers = containers
|
12
|
+
@to_hash = to_hash
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param name Symbol or String to lookup a value stored in one of the containers.
|
16
|
+
def [](name)
|
17
|
+
self.class.find(@containers, name)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @private
|
21
|
+
def key?(name)
|
22
|
+
@containers.find { |container| container.key?(name) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.find(containers, name)
|
26
|
+
containers.find { |container| container.key?(name) && (return container[name]) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def keys
|
30
|
+
@containers.collect(&:keys).flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
# @private
|
34
|
+
def to_hash
|
35
|
+
return @to_hash.(@containers) if @to_hash # FIXME: introduce pattern matching so we can have different "transformers" for each container type.
|
36
|
+
@containers.each_with_object({}) { |container, hash| hash.merge!(container.to_hash) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# alternative implementation:
|
41
|
+
# containers.reverse.each do |container| @mutable_options.merge!(container) end
|
42
|
+
#
|
43
|
+
# benchmark, merging in #initialize vs. this resolver.
|
44
|
+
# merge 39.678k (± 9.1%) i/s - 198.700k in 5.056653s
|
45
|
+
# resolver 68.928k (± 6.4%) i/s - 342.836k in 5.001610s
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# TODO: mark/make all but mutable_options as frozen.
|
2
|
+
# The idea of Skill is to have a generic, ordered read/write interface that
|
3
|
+
# collects mutable runtime-computed data while providing access to compile-time
|
4
|
+
# information.
|
5
|
+
# The runtime-data takes precedence over the class data.
|
6
|
+
module Trailblazer
|
7
|
+
# Holds local options (aka `mutable_options`) and "original" options from the "outer"
|
8
|
+
# activity (aka wrapped_options).
|
9
|
+
|
10
|
+
# only public creator: Build
|
11
|
+
class Context # :data object:
|
12
|
+
def initialize(wrapped_options, mutable_options)
|
13
|
+
@wrapped_options, @mutable_options = wrapped_options, mutable_options
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](name)
|
17
|
+
ContainerChain.find( [@mutable_options, @wrapped_options], name )
|
18
|
+
end
|
19
|
+
|
20
|
+
def key?(name)
|
21
|
+
@mutable_options.key?(name) || @wrapped_options.key?(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def []=(name, value)
|
25
|
+
@mutable_options[name] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge(hash)
|
29
|
+
original, mutable_options = decompose
|
30
|
+
|
31
|
+
ctx = Trailblazer::Context( original, mutable_options.merge(hash) )
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the Context's two components. Used when computing the new output for
|
35
|
+
# the next activity.
|
36
|
+
def decompose
|
37
|
+
[ @wrapped_options, @mutable_options ]
|
38
|
+
end
|
39
|
+
|
40
|
+
def key?(name)
|
41
|
+
ContainerChain.find( [@mutable_options, @wrapped_options], name )
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def keys
|
46
|
+
@mutable_options.keys + @wrapped_options.keys # FIXME.
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
# TODO: maybe we shouldn't allow to_hash from context?
|
52
|
+
# TODO: massive performance bottleneck. also, we could already "know" here what keys the
|
53
|
+
# transformation wants.
|
54
|
+
# FIXME: ToKeywordArguments()
|
55
|
+
def to_hash
|
56
|
+
{}.tap do |hash|
|
57
|
+
# the "key" here is to call to_hash on all containers.
|
58
|
+
[ @wrapped_options.to_hash, @mutable_options.to_hash ].each do |options|
|
59
|
+
options.each { |k, v| hash[k.to_sym] = v }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# FIXME
|
65
|
+
# TODO: rename Context::Hash::Immutable
|
66
|
+
class Immutable
|
67
|
+
def initialize(hash)
|
68
|
+
@hash = hash
|
69
|
+
end
|
70
|
+
|
71
|
+
def [](key)
|
72
|
+
@hash[key]
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_hash # DISCUSS: where do we call this?
|
76
|
+
@hash.to_hash # FIXME: should we do this?
|
77
|
+
end
|
78
|
+
|
79
|
+
def key?(key)
|
80
|
+
@hash.key?(key)
|
81
|
+
end
|
82
|
+
|
83
|
+
def merge(hash)
|
84
|
+
@hash.merge(hash)
|
85
|
+
end
|
86
|
+
|
87
|
+
def keys
|
88
|
+
@hash.keys
|
89
|
+
end
|
90
|
+
|
91
|
+
# DISCUSS: raise in #[]=
|
92
|
+
# each
|
93
|
+
# TODO: Skill could inherit
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.Context(wrapped_options, mutable_options={})
|
98
|
+
Context.new(wrapped_options, mutable_options)
|
99
|
+
end
|
100
|
+
end # Trailblazer
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
# @note This might go to trailblazer-args along with `Context` at some point.
|
3
|
+
def self.Option(proc)
|
4
|
+
Option.build(Option, proc)
|
5
|
+
end
|
6
|
+
|
7
|
+
class Option
|
8
|
+
# Generic builder for a callable "option".
|
9
|
+
# @param call_implementation [Class, Module] implements the process of calling the proc
|
10
|
+
# while passing arguments/options to it in a specific style (e.g. kw args, step interface).
|
11
|
+
# @return [Proc] when called, this proc will evaluate its option (at run-time).
|
12
|
+
def self.build(call_implementation, proc)
|
13
|
+
if proc.is_a? Symbol
|
14
|
+
->(*args) { call_implementation.evaluate_method(proc, *args) }
|
15
|
+
else
|
16
|
+
->(*args) { call_implementation.evaluate_callable(proc, *args) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# A call implementation invoking `proc.(*args)` and plainly forwarding all arguments.
|
21
|
+
# Override this for your own step strategy (see KW#call!).
|
22
|
+
# @private
|
23
|
+
def self.call!(proc, *args)
|
24
|
+
proc.(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Note that both #evaluate_callable and #evaluate_method drop most of the args.
|
28
|
+
# If you need those, override this class.
|
29
|
+
# @private
|
30
|
+
def self.evaluate_callable(proc, *args, **flow_options)
|
31
|
+
call!(proc, *args)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Make the context's instance method a "lambda" and reuse #call!.
|
35
|
+
# @private
|
36
|
+
def self.evaluate_method(proc, *args, exec_context:raise, **flow_options)
|
37
|
+
call!(exec_context.method(proc), *args)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a {Proc} that, when called, invokes the `proc` argument with keyword arguments.
|
41
|
+
# This is known as "step (call) interface".
|
42
|
+
#
|
43
|
+
# This is commonly used by `Operation::step` to wrap the argument and make it
|
44
|
+
# callable in the circuit.
|
45
|
+
#
|
46
|
+
# my_proc = ->(options, **kws) { options["i got called"] = true }
|
47
|
+
# task = Trailblazer::Option::KW(my_proc)
|
48
|
+
# task.(options = {})
|
49
|
+
# options["i got called"] #=> true
|
50
|
+
#
|
51
|
+
# Alternatively, you can pass a symbol and an `:exec_context`.
|
52
|
+
#
|
53
|
+
# my_proc = :some_method
|
54
|
+
# task = Trailblazer::Option::KW(my_proc)
|
55
|
+
#
|
56
|
+
# class A
|
57
|
+
# def some_method(options, **kws)
|
58
|
+
# options["i got called"] = true
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# task.(options = {}, exec_context: A.new)
|
63
|
+
# options["i got called"] #=> true
|
64
|
+
def self.KW(proc)
|
65
|
+
Option.build(KW, proc)
|
66
|
+
end
|
67
|
+
|
68
|
+
# TODO: It would be cool if call! was typed and had `options SymbolizedHash` or something.
|
69
|
+
class KW < Option
|
70
|
+
# A different call implementation that calls `proc` with a "step interface".
|
71
|
+
# your_code.(options, **options)
|
72
|
+
# @private
|
73
|
+
def self.call!(proc, options, *)
|
74
|
+
proc.(options, **options.to_hash) # Step interface: (options, **)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'trailblazer/activity/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "trailblazer-activity"
|
7
|
+
spec.version = Trailblazer::Activity::VERSION
|
8
|
+
spec.authors = ["Nick Sutterer"]
|
9
|
+
spec.email = ["apotonick@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{The main element for Trailblazer's BPMN-compliant workflows.}
|
12
|
+
spec.description = %q{The main element for Trailblazer's BPMN-compliant workflows. Used in Trailblazer's Operation to implement the Railway.}
|
13
|
+
spec.homepage = "http://trailblazer.to/gems/workflow"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
25
|
+
|
26
|
+
spec.add_dependency "hirb"
|
27
|
+
|
28
|
+
spec.required_ruby_version = '>= 2.0.0'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trailblazer-activity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Sutterer
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: hirb
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: The main element for Trailblazer's BPMN-compliant workflows. Used in
|
70
|
+
Trailblazer's Operation to implement the Railway.
|
71
|
+
email:
|
72
|
+
- apotonick@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- ".travis.yml"
|
79
|
+
- CHANGES.md
|
80
|
+
- Gemfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- lib/trailblazer-activity.rb
|
84
|
+
- lib/trailblazer/activity.rb
|
85
|
+
- lib/trailblazer/activity/graph.rb
|
86
|
+
- lib/trailblazer/activity/nested.rb
|
87
|
+
- lib/trailblazer/activity/version.rb
|
88
|
+
- lib/trailblazer/circuit.rb
|
89
|
+
- lib/trailblazer/circuit/present.rb
|
90
|
+
- lib/trailblazer/circuit/testing.rb
|
91
|
+
- lib/trailblazer/circuit/trace.rb
|
92
|
+
- lib/trailblazer/circuit/wrap.rb
|
93
|
+
- lib/trailblazer/container_chain.rb
|
94
|
+
- lib/trailblazer/context.rb
|
95
|
+
- lib/trailblazer/option.rb
|
96
|
+
- trailblazer-activity.gemspec
|
97
|
+
homepage: http://trailblazer.to/gems/workflow
|
98
|
+
licenses: []
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 2.0.0
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 2.6.8
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: The main element for Trailblazer's BPMN-compliant workflows.
|
120
|
+
test_files: []
|