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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.0
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3.3
8
+ - 2.4.0
9
+ matrix:
10
+ include:
11
+ - rvm: jruby-9.1.7.0
12
+ env: JRUBY_OPTS="--profile.api"
13
+ before_install: gem install bundler
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
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in workflow.gemspec
4
+ gemspec
5
+
6
+ gem "minitest-line"
7
+ gem "benchmark-ips"
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,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -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,5 @@
1
+ module Trailblazer
2
+ class Activity
3
+ VERSION = "0.1.0"
4
+ end
5
+ 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: []