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.
@@ -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