trailblazer-activity-dsl-linear 0.2.5 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7b19b59ebf1ee26717edb3a9d5e797eec4a4796bc567bdc21ee92ba13fd4a04
4
- data.tar.gz: 782a283ddf31bdebc6575323c7fcdae0958f301802793c5d193b2c42494f1076
3
+ metadata.gz: fb92b49935156ee66dcf347cfe7b803cc3be0222ebf84ee676923e48bfb20ea2
4
+ data.tar.gz: 802be96cb11290483f4b326d9251933b7783f230dbf8cf6f6bf6781b64bb6a4e
5
5
  SHA512:
6
- metadata.gz: bc097663260f945545c68f4f7f9c8e6023631b5ff67552ec0baf0ee39acdf3b6c8517c31b31aecadcaf8d61b09e04ee31c033d0ae80b5019f5cc0b3392da64b1
7
- data.tar.gz: cafc5c1e5dd50768986066988efac3b4fcbc37a5da13e10b2ca542fec63eb7193dee59a6379437bdd3ac69ea3a2e492065c4c076755e58432b11461e215fb01b
6
+ metadata.gz: 3b408a672b9ad2ccbd09352ede7f47938f3506c687a7204ac762da44abd949a9788f61fd7dce096e41c222dafcbc6ee67c4ee304aede850f7f7ffcae90b42137
7
+ data.tar.gz: 4f13684ec5f770e321f713747460b095033ff39e2c1e9b67f9afc82ac69bec057d4f5e32f4342d88fb763344be9f710aa42e04026fcc44dd824d66a7517e4c69
data/CHANGES.md CHANGED
@@ -1,3 +1,30 @@
1
+ # 0.3.0
2
+
3
+ * Fix circuit interface callable to make `step task: :instance_method` use circuit signature.
4
+
5
+ # 0.2.9
6
+
7
+ * The `Path()` helper, when used with `:end_task` will now automatically _append_ the end task (or terminus) to `End.success`.
8
+ It used to be placed straight after the last path's element, which made it hard to later insert more steps into that very path.
9
+
10
+ # 0.2.8
11
+
12
+ * Add `:inherit` option so `step` can override an existing step while inheriting the original `:extensions` and `:connections` (which are the `Outputs`). This is great to customize "template" activities.
13
+ * Add `Track(:color, wrap_around: true)` option and `Search::WrapAround` so you can find a certain track color (or the beginning of a Path) even when the path was positioned before the actual step in the `Sequence`.
14
+ Note that this feature is still experimental and might get removed.
15
+
16
+ # 0.2.7
17
+
18
+ * `Did you mean ?` suggestions on Linear::Sequence::IndexError.
19
+ * Introduce `Linear::Helper` module for strategy extensions in third-party gems.
20
+ * Convenient way to patch Subprocess itself using `patch` option.
21
+ * Allow multiple `Path()` macro per step.
22
+ * Small fix for defining instance methods as steps using circuit interface.
23
+
24
+ # 0.2.6
25
+
26
+ * Added `@fields` to `Linear::State` to save arbitrary data on the activity/strategy level.
27
+
1
28
  # 0.2.5
2
29
 
3
30
  * Patching now requires the [`:patch` option for `Subprocess`](http://2019.trailblazer.to/2.1/docs/activity.html#activity-dsl-options-patching
data/Gemfile CHANGED
@@ -10,3 +10,4 @@ gem "rubocop", require: false
10
10
  # gem "trailblazer-context", path: "../trailblazer-context"
11
11
  # gem "trailblazer-developer", path: "../trailblazer-developer"
12
12
  # gem "trailblazer-activity", path: "../trailblazer-activity"
13
+ # gem "trailblazer-activity", path: "../circuit"
data/README.md CHANGED
@@ -1,29 +1,93 @@
1
- # Trailblazer-Activity-DSL-Linear
1
+ # Activity-DSL-Linear
2
2
 
3
- _The popular Railway/Fasttrack DSL for building activities._
3
+ The `activity-dsl-linear` gem brings:
4
+ - [Path](https://2019.trailblazer.to/2.1/docs/activity.html#activity-strategy-path)
5
+ - [Railway](https://2019.trailblazer.to/2.1/docs/activity.html#activity-strategy-railway)
6
+ - [Fasttrack](https://2019.trailblazer.to/2.1/docs/activity.html#activity-strategy-fasttrack)
7
+
8
+ DSLs strategies for buildig activities. It is build around [`activity`](https://github.com/trailblazer/trailblazer-activity) gem.
4
9
 
10
+ Please find the [full documentation on the Trailblazer website](https://2019.trailblazer.to/2.1/docs/activity.html#activity-strategy). [Note that the docs are WIP.]
5
11
 
6
- # Overview
12
+ ## Example
7
13
 
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`.
14
+ The `activity-dsl-linear` gem provides three default patterns to model processes: `Path`, `Railway` and `FastTrack`. Here's an example of what a railway activity could look like, along with some more complex connections (you can read more about Railway strategy in the [docs](https://2019.trailblazer.to/2.1/docs/activity.html#activity-strategy-railway)).
9
15
 
10
- Note that you don't need to use the DSL. You can simply create a InIm structure yourself, or use our online editor.
16
+ ```ruby
17
+ require "trailblazer-activity"
18
+ require "trailblazer-activity-dsl-linear"
11
19
 
12
- Full documentation can be found here: trailblazer.to/2.1/#dsl-linear
20
+ class Memo::Update < Trailblazer::Activity::Railway
21
+ # here goes your business logic
22
+ #
23
+ def find_model(ctx, id:, **)
24
+ ctx[:model] = Memo.find_by(id: id)
25
+ end
13
26
 
14
- ## Normalizer
27
+ def validate(ctx, params:, **)
28
+ return true if params[:body].is_a?(String) && params[:body].size > 10
29
+ ctx[:errors] = "body not long enough"
30
+ false
31
+ end
15
32
 
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.
33
+ def save(ctx, model:, params:, **)
34
+ model.update_attributes(params)
35
+ end
18
36
 
19
- The different "step types" (think of `step`, `fail`, and `pass`) are again implemented as different normalizers that "inherit" generic steps.
37
+ def log_error(ctx, params:, **)
38
+ ctx[:log] = "Some idiot wrote #{params.inspect}"
39
+ end
20
40
 
41
+ # here comes the DSL describing the layout of the activity
42
+ #
43
+ step :find_model
44
+ step :validate, Output(:failure) => End(:validation_error)
45
+ step :save
46
+ fail :log_error
47
+ end
48
+ ```
21
49
 
22
- `:sequence_insert`
23
- `:connections` are callables to find the connecting tasks
50
+ Visually, this would translate to the following circuit.
51
+
52
+ <img src="http://trailblazer.to/images/2.1/activity-readme-example.png">
53
+
54
+ You can run the activity by invoking its `call` method.
55
+
56
+ ```ruby
57
+ ctx = { id: 1, params: { body: "Awesome!" } }
58
+
59
+ signal, (ctx, *) = Update.( [ctx, {}] )
60
+
61
+ pp ctx #=>
62
+ {:id=>1,
63
+ :params=>{:body=>"Awesome!"},
64
+ :model=>#<Memo body=nil>,
65
+ :errors=>"body not long enough"}
66
+
67
+ pp signal #=> #<struct Trailblazer::Activity::End semantic=:validation_error>
68
+ ```
69
+
70
+ With Activity, modeling business processes turns out to be ridiculously simple: You define what should happen and when, and Trailblazer makes sure _that_ it happens.
71
+
72
+ ## Features
73
+
74
+ * Activities can model any process with arbitrary flow and connections.
75
+ * Nesting and compositions are allowed and encouraged (via Trailblazer's [`dsl-linear`](https://github.com/trailblazer/trailblazer-activity-dsl-linear) gem).
76
+ * Different step interfaces, manual processing of DSL options, etc is all possible.
77
+ * Steps can be any kind of callable objects.
78
+ * Tracing! (via Trailblazer's [`developer`](https://github.com/trailblazer/trailblazer-developer) gem)
79
+
80
+ ## Operation
81
+
82
+ Trailblazer's [`Operation`](https://2019.trailblazer.to/2.1/docs/operation.html#operation-overview) internally uses an activity to model the processes.
83
+
84
+ ## Workflow
85
+ Activities can be formed into bigger compounds and using workflow, you can build long-running processes such as a moderated blog post or a parcel delivery. Also, you don't have to use the DSL but can use the [`editor`](https://2019.trailblazer.to/2.1/docs/pro.html#pro-editor)instead(cool for more complex, long-running flows). Here comes a sample screenshot.
86
+
87
+ <img src="http://2019.trailblazer.to/2.1/dist/img/flow.png">
24
88
 
25
89
  ## License
26
90
 
27
91
  © Copyright 2018, Trailblazer GmbH
28
92
 
29
- Licensed under the LGPLv3 license. We also offer a [commercial-friendly license](http://trailblazer.to/pro).
93
+ Licensed under the LGPLv3 license. We also offer a commercial-friendly [license](https://2019.trailblazer.to/2.1/docs/pro.html#pro-license).
@@ -37,7 +37,17 @@ class Trailblazer::Activity
37
37
  sequence
38
38
  end
39
39
 
40
- class IndexError < IndexError; end
40
+ class IndexError < IndexError
41
+ attr_reader :step_id
42
+
43
+ def initialize(sequence, step_id)
44
+ @step_id = step_id
45
+ valid_ids = sequence.collect{ |row| row[3][:id].inspect }
46
+
47
+ message = %{#{@step_id.inspect} is not a valid step ID. Did you mean any of these ?\n#{valid_ids.join("\n")}}
48
+ super(message)
49
+ end
50
+ end
41
51
  end
42
52
 
43
53
  # Sequence
@@ -48,7 +58,21 @@ class Trailblazer::Activity
48
58
  # Note that we only go forward, no back-references are done here.
49
59
  def Forward(output, target_color)
50
60
  ->(sequence, me) do
51
- target_seq_row = sequence[sequence.index(me)+1..-1].find { |seq_row| seq_row[0] == target_color }
61
+ target_seq_row = find_in_range(sequence[sequence.index(me)+1..-1], target_color)
62
+
63
+ return output, target_seq_row
64
+ end
65
+ end
66
+
67
+ # Tries to find a track colored step by doing a Forward-search, first, then wraps around going
68
+ # through all steps from sequence start to self.
69
+ def WrapAround(output, target_color)
70
+ ->(sequence, me) do
71
+ my_index = sequence.index(me)
72
+ # First, try all elements after me, then go through the elements preceding myself.
73
+ wrapped_range = sequence[my_index+1..-1] + sequence[0..my_index-1]
74
+
75
+ target_seq_row = find_in_range(wrapped_range, target_color)
52
76
 
53
77
  return output, target_seq_row
54
78
  end
@@ -69,6 +93,11 @@ class Trailblazer::Activity
69
93
  return output, target_seq_row
70
94
  end
71
95
  end
96
+
97
+ # @private
98
+ def find_in_range(range, target_color)
99
+ target_seq_row = range.find { |seq_row| seq_row[0] == target_color }
100
+ end
72
101
  end # Search
73
102
 
74
103
  # Sequence
@@ -111,7 +140,7 @@ class Trailblazer::Activity
111
140
  end
112
141
 
113
142
  def find(sequence, insert_id)
114
- index = find_index(sequence, insert_id) or raise Sequence::IndexError.new(insert_id.inspect)
143
+ index = find_index(sequence, insert_id) or raise Sequence::IndexError.new(sequence, insert_id)
115
144
 
116
145
  return index, sequence.clone # Ruby doesn't have an easy way to avoid mutating arrays :(
117
146
  end
@@ -157,10 +186,10 @@ end
157
186
 
158
187
  require "trailblazer/activity/dsl/linear/normalizer"
159
188
  require "trailblazer/activity/dsl/linear/state"
189
+ require "trailblazer/activity/dsl/linear/helper"
160
190
  require "trailblazer/activity/dsl/linear/strategy"
161
191
  require "trailblazer/activity/dsl/linear/compiler"
162
192
  require "trailblazer/activity/path"
163
193
  require "trailblazer/activity/railway"
164
194
  require "trailblazer/activity/fast_track"
165
- require "trailblazer/activity/dsl/linear/helper" # FIXME
166
195
  require "trailblazer/activity/dsl/linear/variable_mapping"
@@ -1,125 +1,150 @@
1
1
  module Trailblazer
2
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:"path_end_#{rand}", connect_to:nil, **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
- _end_id =
51
- if connect_to
52
- end_id
53
- else
54
- nil
3
+ module DSL
4
+ module Linear
5
+ module Helper
6
+ # @api private
7
+ OutputSemantic = Struct.new(:value)
8
+ Id = Struct.new(:value)
9
+ Track = Struct.new(:color, :adds, :options)
10
+ Extension = Struct.new(:callable) do
11
+ def call(*args, &block)
12
+ callable.(*args, &block)
13
+ end
14
+ end
15
+
16
+ def self.included(base)
17
+ base.extend ClassMethods
55
18
  end
56
19
 
57
- seq = strip_start_and_ends(seq, end_id: _end_id) # don't cut off end unless {:connect_to} is set.
20
+ # Shortcut functions for the DSL.
21
+ module ClassMethods
22
+ # Output( Left, :failure )
23
+ # Output( :failure ) #=> Output::Semantic
24
+ def Output(signal, semantic=nil)
25
+ return OutputSemantic.new(signal) if semantic.nil?
26
+
27
+ Activity.Output(signal, semantic)
28
+ end
29
+
30
+ def End(semantic)
31
+ Activity.End(semantic)
32
+ end
33
+
34
+ def end_id(_end)
35
+ "End.#{_end.to_h[:semantic]}" # TODO: use everywhere
36
+ end
37
+
38
+ def Track(color, wrap_around: false)
39
+ Track.new(color, [], wrap_around: wrap_around).freeze
40
+ end
41
+
42
+ def Id(id)
43
+ Id.new(id).freeze
44
+ end
45
+
46
+ def Path(track_color: "track_#{rand}", end_id:"path_end_#{rand}", connect_to:nil, **options, &block)
47
+ # DISCUSS: here, we use the global normalizer and don't allow injection.
48
+ state = Activity::Path::DSL::State.new(Activity::Path::DSL.OptionsForState(track_name: track_color, end_id: end_id, **options)) # TODO: test injecting {:normalizers}.
58
49
 
59
- if connect_to
60
- output, _ = seq[-1][2][0].(seq, seq[-1]) # FIXME: the Forward() proc contains the row's Output, and the only current way to retrieve it is calling the search strategy. It should be Forward#to_h
50
+ # seq = block.call(state) # state changes.
51
+ state.instance_exec(&block)
61
52
 
62
- searches = [Search.ById(output, connect_to.value)]
53
+ seq = state.to_h[:sequence]
63
54
 
64
- row = seq[-1]
65
- row = row[0..1] + [searches] + [row[3]] # FIXME: not mutating an array is so hard: we only want to replace the "searches" element, index 2
55
+ _end_id =
56
+ if connect_to
57
+ end_id
58
+ else
59
+ nil
60
+ end
66
61
 
67
- seq = seq[0..-2] + [row]
68
- end
62
+ seq = strip_start_and_ends(seq, end_id: _end_id) # don't cut off end unless {:connect_to} is set.
69
63
 
70
- # Add the path before End.success - not sure this is bullet-proof.
71
- adds = seq.collect do |row|
72
- {
73
- row: row,
74
- insert: [Insert.method(:Prepend), "End.success"]
75
- }
76
- end
64
+ if connect_to
65
+ output, _ = seq[-1][2][0].(seq, seq[-1]) # FIXME: the Forward() proc contains the row's Output, and the only current way to retrieve it is calling the search strategy. It should be Forward#to_h
77
66
 
78
- # Connect the Output() => Track(path_track)
79
- return Track.new(track_color, adds)
80
- end
67
+ searches = [Search.ById(output, connect_to.value)]
81
68
 
82
- # Computes the {:outputs} options for {activity}.
83
- def Subprocess(activity, patch: {})
84
- patch.each do |path, patch|
85
- activity = Patch.(activity, path, patch) # TODO: test if multiple patches works!
86
- end
69
+ row = seq[-1]
70
+ row = row[0..1] + [searches] + [row[3]] # FIXME: not mutating an array is so hard: we only want to replace the "searches" element, index 2
87
71
 
88
- {
89
- task: activity,
90
- outputs: Hash[activity.to_h[:outputs].collect { |output| [output.semantic, output] }]
91
- }
92
- end
72
+ seq = seq[0..-2] + [row]
73
+ end
93
74
 
94
- module Patch
95
- module_function
75
+ # Add the path elements before {End.success}.
76
+ # Termini (or :stop_event) are to be placed after {End.success}.
77
+ adds = seq.collect do |row|
78
+ options = row[3]
96
79
 
97
- def call(activity, path, customization)
98
- task_id, *path = path
80
+ # the terminus of the path goes _after_ {End.success} into the "end group".
81
+ insert_method = options[:stop_event] ? Insert.method(:Append) : Insert.method(:Prepend)
99
82
 
83
+ {
84
+ row: row,
85
+ insert: [insert_method, "End.success"]
86
+ }
87
+ end
100
88
 
101
- patch =
102
- if task_id
103
- segment_activity = Introspect::Graph(activity).find(task_id).task
104
- patched_segment_activity = call(segment_activity, path, customization)
89
+ # Connect the Output() => Track(path_track)
90
+ return Track.new(track_color, adds, {})
91
+ end
92
+
93
+ # Computes the {:outputs} options for {activity}.
94
+ def Subprocess(activity, patch: {})
95
+ activity = Patch.customize(activity, options: patch)
96
+
97
+ {
98
+ task: activity,
99
+ outputs: Hash[activity.to_h[:outputs].collect { |output| [output.semantic, output] }]
100
+ }
101
+ end
102
+
103
+ module Patch
104
+ module_function
105
+
106
+ def customize(activity, options:)
107
+ options = options.is_a?(Proc) ?
108
+ { [] => options } : # hash-wrapping with empty path, for patching given activity itself
109
+ options
110
+
111
+ options.each do |path, patch|
112
+ activity = call(activity, path, patch) # TODO: test if multiple patches works!
113
+ end
105
114
 
106
- # Replace the patched subprocess.
107
- -> { step Subprocess(patched_segment_activity), replace: task_id, id: task_id }
108
- else
109
- customization # apply the *actual* patch from the Subprocess() call.
115
+ activity
116
+ end
117
+
118
+ def call(activity, path, customization)
119
+ task_id, *path = path
120
+
121
+ patch =
122
+ if task_id
123
+ segment_activity = Introspect::Graph(activity).find(task_id).task
124
+ patched_segment_activity = call(segment_activity, path, customization)
125
+
126
+ # Replace the patched subprocess.
127
+ -> { step Subprocess(patched_segment_activity), replace: task_id, id: task_id }
128
+ else
129
+ customization # apply the *actual* patch from the Subprocess() call.
130
+ end
131
+
132
+ patched_activity = Class.new(activity)
133
+ patched_activity.class_exec(&patch)
134
+ patched_activity
135
+ end
110
136
  end
111
137
 
112
- patched_activity = Class.new(activity)
113
- patched_activity.class_exec(&patch)
114
- patched_activity
115
- end
116
- end
117
-
118
- def normalize(options, local_keys) # TODO: test me.
119
- locals = options.reject { |key, value| ! local_keys.include?(key) }
120
- foreign = options.reject { |key, value| local_keys.include?(key) }
121
- return foreign, locals
122
- end
123
- end
124
- end
138
+ def normalize(options, local_keys) # TODO: test me.
139
+ locals = options.reject { |key, value| ! local_keys.include?(key) }
140
+ foreign = options.reject { |key, value| local_keys.include?(key) }
141
+ return foreign, locals
142
+ end
143
+ end
144
+ end # Helper
145
+
146
+ include Helper # Introduce Helper constants in DSL::Linear scope
147
+ end # Linear
148
+ end # DSL
149
+ end # Activity
125
150
  end
@@ -20,6 +20,7 @@ module Trailblazer
20
20
  "activity.normalize_id" => method(:normalize_id),
21
21
  "activity.normalize_override" => method(:normalize_override),
22
22
  "activity.wrap_task_with_step_interface" => method(:wrap_task_with_step_interface), # last
23
+ "activity.inherit_option" => method(:inherit_option),
23
24
  },
24
25
 
25
26
  Linear::Insert.method(:Append), "Start.default"
@@ -53,16 +54,26 @@ module Trailblazer
53
54
  # Specific to the "step DSL": if the first argument is a callable, wrap it in a {step_interface_builder}
54
55
  # since its interface expects the step interface, but the circuit will call it with circuit interface.
55
56
  def normalize_step_interface((ctx, flow_options), *)
56
- options = ctx[:options] # either a <#task> or {} from macro
57
-
58
- unless options.is_a?(::Hash)
59
- # task = wrap_with_step_interface(task: options, step_interface_builder: ctx[:user_options][:step_interface_builder]) # TODO: make this optional with appropriate wiring.
60
- task = options
61
-
62
- ctx = ctx.merge(options: {task: task, wrap_task: true})
63
- end
64
-
65
- return Trailblazer::Activity::Right, [ctx, flow_options]
57
+ options = case ctx[:options]
58
+ when Hash
59
+ # Circuit Interface
60
+ task = ctx[:options].fetch(:task)
61
+
62
+ if task.is_a?(Symbol)
63
+ # step task: :find
64
+ { options: { task: Trailblazer::Option( task ) } }
65
+ else
66
+ # step task: Callable, ... (Subprocess, Proc, macros etc)
67
+ { task: task }
68
+ end
69
+ else
70
+ # Step Interface
71
+ # step :find, ...
72
+ # step Callable, ... (Method, Proc etc)
73
+ { options: { task: ctx[:options], wrap_task: true } }
74
+ end
75
+
76
+ return Trailblazer::Activity::Right, [ctx.merge(options), flow_options]
66
77
  end
67
78
 
68
79
  def wrap_task_with_step_interface((ctx, flow_options), **)
@@ -122,7 +133,7 @@ module Trailblazer
122
133
 
123
134
  ctx[:wirings] =
124
135
  connections.collect do |semantic, (search_strategy_builder, *search_args)|
125
- output = outputs[semantic] || raise("No `#{semantic}` output found for #{outputs.inspect}")
136
+ output = outputs[semantic] || raise("No `#{semantic}` output found for #{ctx[:id].inspect} and outputs #{outputs.inspect}")
126
137
 
127
138
  search_strategy_builder.( # return proc to be called when compiling Seq, e.g. {ById(output, :id)}
128
139
  output,
@@ -157,6 +168,8 @@ module Trailblazer
157
168
  _adds = [add_end(cfg, magnetic_to: end_id, id: end_id)] unless end_exists
158
169
 
159
170
  [output_to_id(ctx, output, end_id), _adds]
171
+ else
172
+ raise cfg.inspect
160
173
  end
161
174
 
162
175
  connections = connections.merge(new_connections)
@@ -168,8 +181,10 @@ module Trailblazer
168
181
  return Trailblazer::Activity::Right, [new_ctx, flow_options]
169
182
  end
170
183
 
171
- def output_to_track(ctx, output, target)
172
- {output.value => [Linear::Search.method(:Forward), target.color]}
184
+ def output_to_track(ctx, output, track)
185
+ search_strategy = track.options[:wrap_around] ? :WrapAround : :Forward
186
+
187
+ {output.value => [Linear::Search.method(search_strategy), track.color]}
173
188
  end
174
189
 
175
190
  def output_to_id(ctx, output, target)
@@ -219,9 +234,28 @@ module Trailblazer
219
234
  return Trailblazer::Activity::Right, [ctx.merge(new_ctx), flow_options]
220
235
  end
221
236
 
237
+ # Currently, the {:inherit} option copies over {:connections} from the original step
238
+ # and merges them with the (prolly) connections passed from the user.
239
+ def inherit_option((ctx, flow_options), *)
240
+ return Trailblazer::Activity::Right, [ctx, flow_options] unless ctx[:inherit]
241
+
242
+ sequence = ctx[:sequence]
243
+ id = ctx[:id]
244
+
245
+ index = Linear::Insert.find_index(sequence, id)
246
+ row = sequence[index] # from this row we're inheriting options.
247
+
248
+ extensions = (row[3][:extensions]||[]) + (ctx[:extensions]||[]) # FIXME: DEFAULTING, FOR FUCK'S SAKE
249
+
250
+ ctx = ctx.merge(connections: row[3][:connections], extensions: extensions) # "inherit"
251
+
252
+ return Trailblazer::Activity::Right, [ctx, flow_options]
253
+ end
254
+
222
255
  # TODO: make this extendable!
223
256
  def cleanup_options((ctx, flow_options), *)
224
- new_ctx = ctx.reject { |k, v| [:connections, :outputs, :end_id, :step_interface_builder, :failure_end, :track_name, :sequence].include?(k) }
257
+ # new_ctx = ctx.reject { |k, v| [:connections, :outputs, :end_id, :step_interface_builder, :failure_end, :track_name, :sequence].include?(k) }
258
+ new_ctx = ctx.reject { |k, v| [:outputs, :end_id, :step_interface_builder, :failure_end, :track_name, :sequence].include?(k) }
225
259
 
226
260
  return Trailblazer::Activity::Right, [new_ctx, flow_options]
227
261
  end
@@ -3,28 +3,40 @@ module Trailblazer
3
3
  module DSL
4
4
  module Linear
5
5
  # A {State} instance is kept per DSL client, which usually is a subclass of {Path}, {Railway}, etc.
6
+ # State doesn't have any immutable features - all write operations to it must guarantee they only replace
7
+ # instance variables.
8
+ #
9
+ # @private
10
+ #
11
+ # DISCUSS: why do we have this structure? It doesn't cover "immutable copying", that has to be done by its clients.
12
+ # also, copy with to_h
6
13
  class State
7
14
  # remembers how to call normalizers (e.g. track_color), TaskBuilder
8
15
  # remembers sequence
9
- def initialize(normalizers:, initial_sequence:, **normalizer_options)
16
+ def initialize(normalizers:, initial_sequence:, fields: {}.freeze, **normalizer_options)
10
17
  @normalizer = normalizers # compiled normalizers.
11
18
  @sequence = initial_sequence
12
19
  @normalizer_options = normalizer_options
20
+ @fields = fields
13
21
  end
14
22
 
15
23
  # Called to "inherit" a state.
16
24
  def copy
17
- self.class.new(normalizers: @normalizer, initial_sequence: @sequence, **@normalizer_options)
25
+ self.class.new(normalizers: @normalizer, initial_sequence: @sequence, fields: @fields, **@normalizer_options)
18
26
  end
19
27
 
20
28
  def to_h
21
- {sequence: @sequence, normalizers: @normalizer, normalizer_options: @normalizer_options} # FIXME.
29
+ {sequence: @sequence, normalizers: @normalizer, normalizer_options: @normalizer_options, fields: @fields} # FIXME.
22
30
  end
23
31
 
24
32
  def update_sequence(&block)
25
33
  @sequence = yield(to_h)
26
34
  end
27
35
 
36
+ def update_options(fields)
37
+ @fields = fields
38
+ end
39
+
28
40
  # Compiles and maintains all final normalizers for a specific DSL.
29
41
  class Normalizer
30
42
  def compile_normalizer(normalizer_sequence)
@@ -1,5 +1,3 @@
1
- require "forwardable"
2
-
3
1
  module Trailblazer
4
2
  class Activity
5
3
  module DSL
@@ -29,7 +27,7 @@ module Trailblazer
29
27
  options = options.merge(dsl_track: type)
30
28
 
31
29
  # {#update_sequence} is the only way to mutate the state instance.
32
- state.update_sequence do |sequence:, normalizers:, normalizer_options:|
30
+ state.update_sequence do |sequence:, normalizers:, normalizer_options:, fields:|
33
31
  # Compute the sequence rows.
34
32
  options = normalizers.(type, normalizer_options: normalizer_options, options: task, user_options: options.merge(sequence: sequence))
35
33
 
@@ -58,23 +56,29 @@ module Trailblazer
58
56
 
59
57
  private def forward_block(args, block)
60
58
  options = args[1]
61
- if options.is_a?(Hash) # FIXME: doesn't account {task: <>} and repeats logic from Normalizer.
62
- output, proxy = (options.find { |k,v| v.is_a?(BlockProxy) } or return args)
59
+
60
+ return args unless options.is_a?(Hash)
61
+
62
+ # FIXME: doesn't account {task: <>} and repeats logic from Normalizer.
63
+
64
+ # DISCUSS: THIS SHOULD BE DONE IN DSL.Path() which is stateful! the block forwarding should be the only thing happening here!
65
+ evaluated_options =
66
+ options.find_all { |k,v| v.is_a?(BlockProxy) }.collect do |output, proxy|
63
67
  shared_options = {step_interface_builder: @state.instance_variable_get(:@normalizer_options)[:step_interface_builder]} # FIXME: how do we know what to pass on and what not?
64
- return args[0], options.merge(output => Linear.Path(**shared_options, **proxy.options, &block))
68
+
69
+ [output, Linear.Path(**shared_options, **proxy.options, &(proxy.block || block))] # FIXME: the || sucks.
65
70
  end
66
71
 
67
- args
68
- end
72
+ evaluated_options = Hash[evaluated_options]
69
73
 
70
- extend Forwardable
71
- def_delegators Linear, :Output, :End, :Track, :Id, :Subprocess
74
+ return args[0], options.merge(evaluated_options)
75
+ end
72
76
 
73
- def Path(options) # we can't access {block} here, syntactically.
74
- BlockProxy.new(options)
77
+ def Path(options, &block) # syntactically, we can't access the {do ... end} block here.
78
+ BlockProxy.new(options, block)
75
79
  end
76
80
 
77
- BlockProxy = Struct.new(:options)
81
+ BlockProxy = Struct.new(:options, :block)
78
82
 
79
83
  private def merge!(activity)
80
84
  old_seq = @state.instance_variable_get(:@sequence) # TODO: fixme
@@ -3,7 +3,7 @@ module Trailblazer
3
3
  module Activity
4
4
  module DSL
5
5
  module Linear
6
- VERSION = "0.2.5"
6
+ VERSION = "0.3.0"
7
7
  end
8
8
  end
9
9
  end
@@ -154,6 +154,7 @@ module Trailblazer
154
154
  end
155
155
  end
156
156
 
157
+ include Activity::DSL::Linear::Helper
157
158
  extend Activity::DSL::Linear::Strategy
158
159
 
159
160
  initialize!(Railway::DSL::State.new(DSL.OptionsForState()))
@@ -177,6 +177,7 @@ module Trailblazer
177
177
 
178
178
  end # DSL
179
179
 
180
+ include DSL::Linear::Helper
180
181
  extend DSL::Linear::Strategy
181
182
 
182
183
  initialize!(Path::DSL::State.new(DSL.OptionsForState()))
@@ -90,6 +90,8 @@ module Trailblazer
90
90
  )
91
91
  end
92
92
 
93
+ # Add {:failure} output to {:outputs}.
94
+ # TODO: assert that failure_outputs doesn't override existing {:outputs}
93
95
  def normalize_path_outputs((ctx, flow_options), *)
94
96
  outputs = failure_outputs.merge(ctx[:outputs])
95
97
  ctx = ctx.merge(outputs: outputs)
@@ -132,7 +134,6 @@ module Trailblazer
132
134
  pass: Linear::Normalizer.activity_normalizer( Railway::DSL.normalizer_for_pass ),
133
135
  )
134
136
 
135
-
136
137
  def self.OptionsForState(normalizers: Normalizers, failure_end: Activity::End.new(semantic: :failure), **options)
137
138
  options = Path::DSL.OptionsForState(options).
138
139
  merge(normalizers: normalizers, failure_end: failure_end)
@@ -157,6 +158,7 @@ module Trailblazer
157
158
  end
158
159
  end
159
160
 
161
+ include DSL::Linear::Helper
160
162
  extend DSL::Linear::Strategy
161
163
 
162
164
  initialize!(Railway::DSL::State.new(DSL.OptionsForState()))
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailblazer-activity-dsl-linear
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-08 00:00:00.000000000 Z
11
+ date: 2020-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: trailblazer-activity
@@ -135,8 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
137
  requirements: []
138
- rubyforge_project:
139
- rubygems_version: 2.7.3
138
+ rubygems_version: 3.0.8
140
139
  signing_key:
141
140
  specification_version: 4
142
141
  summary: Simple DSL to define Trailblazer activities.