trailblazer-activity-dsl-linear 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/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
|