trailblazer-activity 0.1.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 +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: []
|