trailblazer-activity-dsl-linear 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CHANGES.md +3 -0
- data/DSL_IDEAS +91 -0
- data/Gemfile +13 -0
- data/README.md +23 -0
- data/Rakefile +13 -0
- data/lib/trailblazer-activity-dsl-linear.rb +2 -0
- data/lib/trailblazer/activity/dsl/linear.rb +163 -0
- data/lib/trailblazer/activity/dsl/linear/compiler.rb +70 -0
- data/lib/trailblazer/activity/dsl/linear/helper.rb +78 -0
- data/lib/trailblazer/activity/dsl/linear/normalizer.rb +220 -0
- data/lib/trailblazer/activity/dsl/linear/state.rb +58 -0
- data/lib/trailblazer/activity/dsl/linear/strategy.rb +92 -0
- data/lib/trailblazer/activity/dsl/linear/variable_mapping.rb +82 -0
- data/lib/trailblazer/activity/dsl/linear/version.rb +11 -0
- data/lib/trailblazer/activity/fast_track.rb +163 -0
- data/lib/trailblazer/activity/path.rb +179 -0
- data/lib/trailblazer/activity/railway.rb +172 -0
- data/trailblazer-activity-dsl-linear.gemspec +29 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d539047d4fc798ef33f6a128457db0197bbc196f6a03ea9a49e577c338224a8e
|
4
|
+
data.tar.gz: 6c461ee1e7f77eb07b428d34f5a0a6689ecc5584ea25c519e434c898b4dd9ed5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b29289e1248383a8a10a7d81428eb729dfaeeea2e6b6bc7a400d8aa399eff1875686042ef4c27dff715cae8f9a65933da9981d9adf089d0648497b58454c404
|
7
|
+
data.tar.gz: 942c8edbe8e4cc5710a9fdbea07e1714c25c3027d657c84836bb844febb79c16082ca63a1d6b5c802914f1be99353daa229f2a2d09fc9e45bac263dc9d3477b0
|
data/.gitignore
ADDED
data/CHANGES.md
ADDED
data/DSL_IDEAS
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
step a + b, Left => c # split
|
4
|
+
# OR
|
5
|
+
step a, Right => b, Left => c # split
|
6
|
+
|
7
|
+
step b + b2 + b3 + d # path
|
8
|
+
step c + c2 + d
|
9
|
+
step d
|
10
|
+
|
11
|
+
# Introspect adds Start and end?
|
12
|
+
|
13
|
+
|
14
|
+
step a + b + c + d # long path
|
15
|
+
step b, Left => b # loop
|
16
|
+
|
17
|
+
# railway
|
18
|
+
step a + b + c
|
19
|
+
|
20
|
+
|
21
|
+
start set_model + decide, A => stripe, B => int1, C => int2
|
22
|
+
path stripe + send_invoice, Left =>
|
23
|
+
|
24
|
+
|
25
|
+
railway model, build, validate, persist
|
26
|
+
railway model, build, validate, Fail(log), persist, Fail(log_again),
|
27
|
+
|
28
|
+
end(End.failure)
|
29
|
+
end(End.success)
|
30
|
+
path(a, decider,
|
31
|
+
{
|
32
|
+
A => railway(charge, invoice, {Left=>"End.failure", Right=>"End.success"}),
|
33
|
+
B => railway(int1, int_invoice, {Left=>"End.failure", Right=>"End.success"})
|
34
|
+
C => railway(int2, Right=>"int_invoice", {Left=>"End.failure", Right=>"End.success"})
|
35
|
+
}
|
36
|
+
)
|
37
|
+
|
38
|
+
path(charge =>{Left=>fail), invoice=>{Left=>fail})
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
magnetic_to: [*]
|
43
|
+
on: Track(*)
|
44
|
+
|
45
|
+
Path(
|
46
|
+
{a, :a, R+L-}
|
47
|
+
{b, :b, R+L-}
|
48
|
+
{c, :c, R+L-}, -=>a
|
49
|
+
)
|
50
|
+
|
51
|
+
Path(
|
52
|
+
+{a, :a, R+L-}, +=>b, -=>b
|
53
|
+
+{b, :b, R+L-} +=>c, -=>c
|
54
|
+
+{c, :c, R+L-}, -=>a +=>ES, -=>a
|
55
|
+
+{ES, :ES, }
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
# intermediate, e.g. from editor:
|
61
|
+
# allows {:replace} and {:implement}, wires correct signals
|
62
|
+
# to outgoing connections by semantic
|
63
|
+
A{ +-} => {+=>B, -=>B}
|
64
|
+
B{ +-} => {+=>C, -=>C}
|
65
|
+
C{ +-} => {+=>ES, -=>A}
|
66
|
+
ES{}
|
67
|
+
|
68
|
+
=> circuit
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
Path(
|
74
|
+
{a, :a, R+L-}, +=>b, -=>b
|
75
|
+
{b, :b, R+L-} +=>c, -=>c
|
76
|
+
{c, :c, R+L-}, -=>a +=>ES, -=>a
|
77
|
+
{ES, :ES, }
|
78
|
+
)
|
79
|
+
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
task [A, id: :a], [B, id: b], [C, id: c, outputs: {..}]
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
circuit(
|
88
|
+
A,
|
89
|
+
B,
|
90
|
+
C=>{..}
|
91
|
+
)
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in workflow.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "minitest-line"
|
7
|
+
|
8
|
+
gem "rubocop", require: false
|
9
|
+
|
10
|
+
# gem "trailblazer-context", path: "../trailblazer-context"
|
11
|
+
# gem "trailblazer-developer", path: "../trailblazer-developer"
|
12
|
+
gem "trailblazer-developer", github: "trailblazer/trailblazer-developer", branch: "exception-tracing"
|
13
|
+
gem "trailblazer-activity", path: "../trailblazer-activity"
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Trailblazer-Activity-DSL-Linear
|
2
|
+
|
3
|
+
_The popular Railway/Fasttrack DSL for building activities._
|
4
|
+
|
5
|
+
|
6
|
+
# Overview
|
7
|
+
|
8
|
+
This gem allows creating activities by leveraging a handy DSL. Built-in are the strategies `Path`, the popular `Railway` and `FastTrack`. The latter is used for `Trailblazer::Operation`.
|
9
|
+
|
10
|
+
Note that you don't need to use the DSL. You can simply create a InIm structure yourself, or use our online editor.
|
11
|
+
|
12
|
+
Full documentation can be found here: trailblazer.to/2.1/#dsl-linear
|
13
|
+
|
14
|
+
## Normalizer
|
15
|
+
|
16
|
+
Normalizers are itself linear activities (or "pipelines") that compute all options necessary for `DSL.insert_task`.
|
17
|
+
For example, `FailFast.normalizer` will process your options such as `fast_track: true` and add necessary connections and outputs.
|
18
|
+
|
19
|
+
The different "step types" (think of `step`, `fail`, and `pass`) are again implemented as different normalizers that "inherit" generic steps.
|
20
|
+
|
21
|
+
|
22
|
+
`:sequence_insert`
|
23
|
+
`:connections` are callables to find the connecting tasks
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rubocop/rake_task"
|
4
|
+
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.libs << "lib"
|
8
|
+
t.test_files = FileList["**/*_test.rb"]
|
9
|
+
end
|
10
|
+
|
11
|
+
RuboCop::RakeTask.new
|
12
|
+
|
13
|
+
task :default => :test
|
@@ -0,0 +1,163 @@
|
|
1
|
+
class Trailblazer::Activity
|
2
|
+
module DSL
|
3
|
+
# Implementing a specific DSL, simplified version of the {Magnetic DSL} from 2017.
|
4
|
+
#
|
5
|
+
# Produces {Implementation} and {Intermediate}.
|
6
|
+
module Linear
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# {Sequence} consists of rows.
|
10
|
+
# {Sequence row} consisting of {[magnetic_to, task, connections_searches, data]}.
|
11
|
+
class Sequence < Array
|
12
|
+
# Return {Sequence row} consisting of {[magnetic_to, task, connections_searches, data]}.
|
13
|
+
def self.create_row(task:, magnetic_to:, wirings:, **options)
|
14
|
+
[
|
15
|
+
magnetic_to,
|
16
|
+
task,
|
17
|
+
wirings,
|
18
|
+
options # {id: "Start.success"}
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
# @returns Sequence New sequence instance
|
23
|
+
# TODO: name it {apply_adds or something}
|
24
|
+
def self.insert_row(sequence, row:, insert:)
|
25
|
+
insert_function, *args = insert
|
26
|
+
|
27
|
+
insert_function.(sequence, [row], *args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.apply_adds(sequence, adds)
|
31
|
+
adds.each do |add|
|
32
|
+
sequence = insert_row(sequence, **add)
|
33
|
+
end
|
34
|
+
|
35
|
+
sequence
|
36
|
+
end
|
37
|
+
|
38
|
+
class IndexError < IndexError; end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sequence
|
42
|
+
module Search
|
43
|
+
module_function
|
44
|
+
|
45
|
+
# From this task onwards, find the next task that's "magnetic to" {target_color}.
|
46
|
+
# Note that we only go forward, no back-references are done here.
|
47
|
+
def Forward(output, target_color)
|
48
|
+
->(sequence, me) do
|
49
|
+
target_seq_row = sequence[sequence.index(me)+1..-1].find { |seq_row| seq_row[0] == target_color }
|
50
|
+
|
51
|
+
return output, target_seq_row
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def Noop(output)
|
56
|
+
->(sequence, me) do
|
57
|
+
return output, [nil,nil,nil,{}] # FIXME
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Find the seq_row with {id} and connect the current node to it.
|
62
|
+
def ById(output, id)
|
63
|
+
->(sequence, me) do
|
64
|
+
index = Insert.find_index(sequence, id) or return output, sequence[0] # FIXME # or raise "Couldn't find {#{id}}"
|
65
|
+
target_seq_row = sequence[index]
|
66
|
+
|
67
|
+
return output, target_seq_row
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end # Search
|
71
|
+
|
72
|
+
# Sequence
|
73
|
+
# Functions to mutate the Sequence by inserting, replacing, or deleting tasks.
|
74
|
+
# These functions are called in {insert_task}
|
75
|
+
module Insert
|
76
|
+
module_function
|
77
|
+
|
78
|
+
# Append {new_row} after {insert_id}.
|
79
|
+
def Append(sequence, new_rows, insert_id)
|
80
|
+
index, sequence = find(sequence, insert_id)
|
81
|
+
|
82
|
+
sequence.insert(index+1, *new_rows)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Insert {new_rows} before {insert_id}.
|
86
|
+
def Prepend(sequence, new_rows, insert_id)
|
87
|
+
index, sequence = find(sequence, insert_id)
|
88
|
+
|
89
|
+
sequence.insert(index, *new_rows)
|
90
|
+
end
|
91
|
+
|
92
|
+
def Replace(sequence, new_rows, insert_id)
|
93
|
+
index, sequence = find(sequence, insert_id)
|
94
|
+
|
95
|
+
sequence[index], _ = *new_rows # TODO: replace and insert remaining, if any.
|
96
|
+
sequence
|
97
|
+
end
|
98
|
+
|
99
|
+
def Delete(sequence, _, insert_id)
|
100
|
+
index, sequence = find(sequence, insert_id)
|
101
|
+
|
102
|
+
sequence.delete(sequence[index])
|
103
|
+
sequence
|
104
|
+
end
|
105
|
+
|
106
|
+
# @private
|
107
|
+
def find_index(sequence, insert_id)
|
108
|
+
sequence.find_index { |seq_row| seq_row[3][:id] == insert_id } # TODO: optimize id location!
|
109
|
+
end
|
110
|
+
|
111
|
+
def find(sequence, insert_id)
|
112
|
+
index = find_index(sequence, insert_id) or raise Sequence::IndexError.new(insert_id.inspect)
|
113
|
+
|
114
|
+
return index, sequence.clone # Ruby doesn't have an easy way to avoid mutating arrays :(
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def Merge(old_seq, new_seq, end_id: "End.success") # DISCUSS: also Insert
|
119
|
+
new_seq = strip_start_and_ends(new_seq, end_id: end_id)
|
120
|
+
|
121
|
+
seq = Insert.Prepend(old_seq, new_seq, end_id)
|
122
|
+
end
|
123
|
+
def strip_start_and_ends(seq, end_id:) # TODO: introduce Merge namespace?
|
124
|
+
cut_off_index = end_id.nil? ? seq.size : Insert.find_index(seq, end_id) # find the "first" end.
|
125
|
+
|
126
|
+
seq[1..cut_off_index-1]
|
127
|
+
end
|
128
|
+
|
129
|
+
module DSL
|
130
|
+
module_function
|
131
|
+
|
132
|
+
# Insert the task into the sequence using the {sequence_insert} strategy.
|
133
|
+
# @return Sequence sequence after applied insertion
|
134
|
+
# FIXME: DSL for strategies
|
135
|
+
def insert_task(sequence, sequence_insert:, **options)
|
136
|
+
new_row = Sequence.create_row(**options)
|
137
|
+
|
138
|
+
# {sequence_insert} is usually a function such as {Linear::Insert::Append} and its arguments.
|
139
|
+
seq = Sequence.insert_row(sequence, row: new_row, insert: sequence_insert)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Add one or several rows to the {sequence}.
|
143
|
+
# This is usually called from DSL methods such as {step}.
|
144
|
+
def apply_adds_from_dsl(sequence, sequence_insert:, adds:, **options)
|
145
|
+
# This is the ADDS for the actual task.
|
146
|
+
task_add = {row: Sequence.create_row(options), insert: sequence_insert} # Linear::Insert.method(:Prepend), end_id
|
147
|
+
|
148
|
+
Sequence.apply_adds(sequence, [task_add] + adds)
|
149
|
+
end
|
150
|
+
end # DSL
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
require "trailblazer/activity/dsl/linear/normalizer"
|
157
|
+
require "trailblazer/activity/dsl/linear/state"
|
158
|
+
require "trailblazer/activity/dsl/linear/strategy"
|
159
|
+
require "trailblazer/activity/dsl/linear/compiler"
|
160
|
+
require "trailblazer/activity/path"
|
161
|
+
require "trailblazer/activity/railway"
|
162
|
+
require "trailblazer/activity/fast_track"
|
163
|
+
require "trailblazer/activity/dsl/linear/helper" # FIXME
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL
|
4
|
+
module Linear
|
5
|
+
# Compile a {Schema} by computing {implementations} and {intermediate} from a {Sequence}.
|
6
|
+
module Compiler
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Default strategy to find out what's a stop event is to inspect the TaskRef's {data[:stop_event]}.
|
10
|
+
def find_stop_task_ids(intermediate_wiring)
|
11
|
+
intermediate_wiring.collect { |task_ref, outs| task_ref.data[:stop_event] ? task_ref.id : nil }.compact
|
12
|
+
end
|
13
|
+
|
14
|
+
# The first task in the wiring is the default start task.
|
15
|
+
def find_start_task_ids(intermediate_wiring)
|
16
|
+
[intermediate_wiring.first.first.id]
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(sequence, find_stops: method(:find_stop_task_ids), find_start: method(:find_start_task_ids))
|
20
|
+
_implementations, intermediate_wiring =
|
21
|
+
sequence.inject([[], []]) do |(implementations, intermediates), seq_row|
|
22
|
+
magnetic_to, task, connections, data = seq_row
|
23
|
+
id = data[:id]
|
24
|
+
|
25
|
+
# execute all {Search}s for one sequence row.
|
26
|
+
connections = find_connections(seq_row, connections, sequence)
|
27
|
+
|
28
|
+
# FIXME: {:extensions} should be initialized
|
29
|
+
implementations += [[id, Schema::Implementation::Task(task, connections.collect { |output, _| output }, data[:extensions] || []) ]]
|
30
|
+
|
31
|
+
intermediates += [
|
32
|
+
[
|
33
|
+
Schema::Intermediate::TaskRef(id, data),
|
34
|
+
# Compute outputs.
|
35
|
+
connections.collect { |output, target_id| Schema::Intermediate::Out(output.semantic, target_id) }
|
36
|
+
]
|
37
|
+
]
|
38
|
+
|
39
|
+
[implementations, intermediates]
|
40
|
+
end
|
41
|
+
|
42
|
+
start_task_ids = find_start.(intermediate_wiring)
|
43
|
+
stop_task_refs = find_stops.(intermediate_wiring)
|
44
|
+
|
45
|
+
intermediate = Schema::Intermediate.new(Hash[intermediate_wiring], stop_task_refs, start_task_ids)
|
46
|
+
implementation = Hash[_implementations]
|
47
|
+
|
48
|
+
Schema::Intermediate.(intermediate, implementation) # implemented in the generic {trailblazer-activity} gem.
|
49
|
+
end
|
50
|
+
|
51
|
+
# private
|
52
|
+
|
53
|
+
def find_connections(seq_row, strategies, sequence)
|
54
|
+
strategies.collect do |search|
|
55
|
+
output, target_seq_row = search.(sequence, seq_row) # invoke the node's "connection search" strategy.
|
56
|
+
|
57
|
+
target_seq_row = sequence[-1] if target_seq_row.nil? # connect to an End if target unknown. # DISCUSS: make this configurable, maybe?
|
58
|
+
|
59
|
+
[
|
60
|
+
output, # implementation
|
61
|
+
target_seq_row[3][:id], # intermediate # FIXME. this sucks.
|
62
|
+
target_seq_row # DISCUSS: needed?
|
63
|
+
]
|
64
|
+
end.compact
|
65
|
+
end
|
66
|
+
end # Compiler
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL::Linear # TODO: rename!
|
4
|
+
# @api private
|
5
|
+
OutputSemantic = Struct.new(:value)
|
6
|
+
Id = Struct.new(:value)
|
7
|
+
Track = Struct.new(:color, :adds)
|
8
|
+
Extension = Struct.new(:callable) do
|
9
|
+
def call(*args, &block)
|
10
|
+
callable.(*args, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Shortcut functions for the DSL.
|
15
|
+
module_function
|
16
|
+
|
17
|
+
# Output( Left, :failure )
|
18
|
+
# Output( :failure ) #=> Output::Semantic
|
19
|
+
def Output(signal, semantic=nil)
|
20
|
+
return OutputSemantic.new(signal) if semantic.nil?
|
21
|
+
|
22
|
+
Activity.Output(signal, semantic)
|
23
|
+
end
|
24
|
+
|
25
|
+
def End(semantic)
|
26
|
+
Activity.End(semantic)
|
27
|
+
end
|
28
|
+
|
29
|
+
def end_id(_end)
|
30
|
+
"End.#{_end.to_h[:semantic]}" # TODO: use everywhere
|
31
|
+
end
|
32
|
+
|
33
|
+
def Track(color)
|
34
|
+
Track.new(color, []).freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def Id(id)
|
38
|
+
Id.new(id).freeze
|
39
|
+
end
|
40
|
+
|
41
|
+
def Path(track_color: "track_#{rand}", end_id:, **options, &block)
|
42
|
+
# DISCUSS: here, we use the global normalizer and don't allow injection.
|
43
|
+
state = Activity::Path::DSL::State.new(Activity::Path::DSL.OptionsForState(track_name: track_color, end_id: end_id, **options)) # TODO: test injecting {:normalizers}.
|
44
|
+
|
45
|
+
# seq = block.call(state) # state changes.
|
46
|
+
state.instance_exec(&block)
|
47
|
+
|
48
|
+
seq = state.to_h[:sequence]
|
49
|
+
|
50
|
+
seq = Linear.strip_start_and_ends(seq, end_id: nil) # don't cut off end.
|
51
|
+
|
52
|
+
# Add the path before End.success - not sure this is bullet-proof.
|
53
|
+
adds = seq.collect do |row|
|
54
|
+
{
|
55
|
+
row: row,
|
56
|
+
insert: [Linear::Insert.method(:Prepend), "End.success"]
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
return Track.new(track_color, adds)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Computes the {:outputs} options for {activity}.
|
64
|
+
def Subprocess(activity)
|
65
|
+
{
|
66
|
+
task: activity,
|
67
|
+
outputs: Hash[activity.to_h[:outputs].collect { |output| [output.semantic, output] }]
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def normalize(options, local_keys) # TODO: test me.
|
72
|
+
locals = options.reject { |key, value| ! local_keys.include?(key) }
|
73
|
+
foreign = options.reject { |key, value| local_keys.include?(key) }
|
74
|
+
return foreign, locals
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|