trailblazer-activity-dsl-linear 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,3 @@
1
+ # 0.1.0
2
+
3
+ * This code is extracted, refactored and heavy-metaly simplified from the original `trailblazer-activity` gem.
@@ -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"
@@ -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
@@ -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,2 @@
1
+ require "trailblazer/activity/dsl/linear/version"
2
+ require "trailblazer/activity/dsl/linear"
@@ -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