trailblazer-activity 0.1.6 → 0.2.0

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