trailblazer-activity 0.2.1 → 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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +4 -0
  3. data/NOTES_ +36 -0
  4. data/README.md +7 -7
  5. data/Rakefile +1 -1
  6. data/lib/trailblazer/activity.rb +137 -96
  7. data/lib/trailblazer/activity/heritage.rb +30 -0
  8. data/lib/trailblazer/activity/introspection.rb +105 -0
  9. data/lib/trailblazer/activity/magnetic.rb +47 -0
  10. data/lib/trailblazer/activity/magnetic/builder.rb +161 -0
  11. data/lib/trailblazer/activity/magnetic/builder/block.rb +37 -0
  12. data/lib/trailblazer/activity/magnetic/builder/fast_track.rb +141 -0
  13. data/lib/trailblazer/activity/magnetic/builder/path.rb +98 -0
  14. data/lib/trailblazer/activity/magnetic/builder/railway.rb +123 -0
  15. data/lib/trailblazer/activity/magnetic/dsl.rb +90 -0
  16. data/lib/trailblazer/activity/magnetic/dsl/alterations.rb +44 -0
  17. data/lib/trailblazer/activity/magnetic/dsl/plus_poles.rb +59 -0
  18. data/lib/trailblazer/activity/magnetic/finalizer.rb +55 -0
  19. data/lib/trailblazer/activity/magnetic/generate.rb +62 -0
  20. data/lib/trailblazer/activity/present.rb +12 -19
  21. data/lib/trailblazer/activity/process.rb +16 -0
  22. data/lib/trailblazer/activity/schema/dependencies.rb +41 -0
  23. data/lib/trailblazer/activity/schema/sequence.rb +46 -0
  24. data/lib/trailblazer/activity/structures.rb +41 -0
  25. data/lib/trailblazer/activity/subprocess.rb +9 -1
  26. data/lib/trailblazer/activity/trace.rb +25 -16
  27. data/lib/trailblazer/activity/version.rb +1 -1
  28. data/lib/trailblazer/activity/wrap.rb +4 -13
  29. data/lib/trailblazer/circuit.rb +4 -35
  30. data/lib/trailblazer/wrap/call_task.rb +2 -2
  31. data/lib/trailblazer/wrap/runner.rb +7 -1
  32. data/lib/trailblazer/wrap/trace.rb +6 -5
  33. metadata +21 -10
  34. data/lib/trailblazer/activity/graph.rb +0 -157
  35. data/lib/trailblazer/circuit/testing.rb +0 -58
  36. data/lib/trailblazer/container_chain.rb +0 -45
  37. data/lib/trailblazer/context.rb +0 -68
  38. data/lib/trailblazer/option.rb +0 -78
  39. data/lib/trailblazer/wrap/inject.rb +0 -32
  40. data/lib/trailblazer/wrap/variable_mapping.rb +0 -92
@@ -1,157 +0,0 @@
1
- module Trailblazer
2
- # Note that Graph is a superset of a real directed graph. For instance, it might contain detached nodes.
3
- # == Design
4
- # * This class is designed to maintain a graph while building up a circuit step-wise.
5
- # * It can be imperformant as this all happens at compile-time.
6
- module Activity::Graph
7
- # Task => { name: "Nested{Task}", type: :subprocess, boundary_events: { Circuit::Left => {} } }
8
-
9
- # Edge keeps references to its peer nodes via the `:source` and `:target` options.
10
- class Edge
11
- def initialize(data)
12
- @data = data.freeze
13
- end
14
-
15
- def [](key)
16
- @data[key]
17
- end
18
-
19
- def to_h
20
- @data.to_h
21
- end
22
- end
23
-
24
- # Node does only save meta data, and has *no* references to edges.
25
- class Node < Edge
26
- end
27
-
28
- class Start < Node
29
- def initialize(data)
30
- yield self, data if block_given?
31
- super
32
- end
33
-
34
- # Builds a node from the provided `:target` arguments and attaches it via `:edge` to `:source`.
35
- # @param target: Array wrapped, options
36
- def attach!(target:raise, edge:raise, source:self)
37
- target = add!(target)
38
-
39
- connect!(target: target, edge: edge, source: source)
40
- end
41
-
42
- def connect!(target:raise, edge:raise, source:self)
43
- target = target.kind_of?(Node) ? target : (find_all(target)[0] || raise( "#{target} not found")) # FIXME: only needed for recompile_activity.
44
- source = source.kind_of?(Node) ? source : (find_all(source)[0] || raise( "#{source} not found"))
45
-
46
- connect_for!(source, edge, target)
47
- end
48
-
49
- def insert_before!(old_node, node:raise, outgoing:nil, incoming:raise)
50
- old_node = find_all(old_node)[0] || raise( "#{old_node} not found") unless old_node.kind_of?(Node) # FIXME: do we really need this?
51
- new_node = add!(node)
52
-
53
- incoming_edges = predecessors(old_node)
54
- rewired_edges = incoming_edges.find_all { |(node, edge)| incoming.(edge) }
55
-
56
- # rewire old_task's predecessors to new_task.
57
- rewired_edges.each { |left_node, edge| reconnect!(left_node, edge, new_node) }
58
-
59
- # connect new_task --> old_task.
60
- if outgoing
61
- node, edge = connect_for!(new_node, outgoing, old_node)
62
- end
63
-
64
- return new_node
65
- end
66
-
67
- def find_all(id=nil, &block)
68
- nodes = self[:graph].keys + self[:graph].values.collect(&:values).flatten
69
- nodes = nodes.uniq
70
-
71
- nodes.find_all(& block || ->(node) { node[:id] == id })
72
- end
73
-
74
- def predecessors(target_node)
75
- self[:graph].each_with_object([]) do |(node, connections), ary|
76
- connections.each { |edge, target| target == target_node && ary << [node, edge] }
77
- end
78
- end
79
-
80
- def successors(node)
81
- ( self[:graph][node] || [] ).collect { |edge, target| [target, edge] }
82
- end
83
-
84
- def to_h(include_leafs:true)
85
- hash = ::Hash[
86
- self[:graph].collect do |node, connections|
87
- connections = connections.collect { |edge, node| [ edge[:_wrapped], node[:_wrapped] ] }
88
-
89
- [ node[:_wrapped], ::Hash[connections] ]
90
- end
91
- ]
92
-
93
- if include_leafs == false
94
- hash = hash.select { |node, connections| connections.any? }
95
- end
96
-
97
- hash
98
- end
99
-
100
- # @private
101
- def Node(wrapped, id:raise("No ID was provided for #{wrapped}"), **options)
102
- Node.new( options.merge( id: id, _wrapped: wrapped ) )
103
- end
104
-
105
- private
106
-
107
- # Single entry point for adding nodes and edges to the graph.
108
- # @private
109
- # @return target Node
110
- # @return edge Edge the edge created connecting source and target.
111
- def connect_for!(source, edge_args, target)
112
- edge = Edge(source, edge_args, target)
113
-
114
- self[:graph][source][edge] = target
115
-
116
- return target, edge
117
- end
118
-
119
- # Removes edge.
120
- # @private
121
- def unconnect!(node, edge)
122
- self[:graph][node].delete(edge)
123
- end
124
-
125
- # @private
126
- # Create a Node and add it to the graph, without connecting it.
127
- def add!(node_args)
128
- new_node = Node(*node_args)
129
-
130
- raise IllegalNodeError.new("The ID `#{new_node[:id]}` has been added before.") if find_all( new_node[:id] ).any?
131
-
132
- self[:graph][new_node] = {}
133
- new_node
134
- end
135
-
136
- # @private
137
- def reconnect!(left_node, edge, new_node)
138
- unconnect!(left_node, edge) # dump the old edge.
139
- connect_for!(left_node, [ edge[:_wrapped], edge.to_h ], new_node)
140
- end
141
-
142
- # @private
143
- def Edge(source, (wrapped, options), target) # FIXME: test required id. test source and target
144
- id = "#{source[:id]}-#{wrapped}-#{target[:id]}"
145
- edge = Edge.new(options.merge( _wrapped: wrapped, id: id, source: source, target: target ))
146
- end
147
- end
148
-
149
- def self.Start(wrapped, graph:{}, **data, &block)
150
- block ||= ->(node, data) { data[:graph][node] = {} }
151
- Start.new( { _wrapped: wrapped, graph: graph }.merge(data), &block )
152
- end
153
-
154
- class IllegalNodeError < RuntimeError
155
- end
156
- end # Graph
157
- end
@@ -1,58 +0,0 @@
1
- # TODO: remove or move.
2
-
3
- module MiniTest::Assertions
4
- def assert_activity_inspect(text, subject)
5
- Trailblazer::Circuit::ActivityInspect(subject).must_equal text
6
- end
7
-
8
- def assert_event_inspect(text, subject)
9
- Trailblazer::Circuit::EndInspect(subject).must_equal(text)
10
- end
11
- end
12
-
13
-
14
- Trailblazer::Activity.infect_an_assertion :assert_activity_inspect, :must_inspect
15
- Trailblazer::Circuit::End.infect_an_assertion :assert_event_inspect, :must_inspect_end_fixme
16
-
17
- class Trailblazer::Circuit
18
- def self.EndInspect(event)
19
- event.instance_eval { "#<#{self.class.to_s.split("::").last}: #{@name} #{@options}>" }
20
- end
21
-
22
- def self.ActivityInspect(activity, strip: ["AlterTest::"])
23
- strip += ["Trailblazer::Circuit::"]
24
- stripped = ->(target) { strip_for(target, strip) }
25
-
26
- map, _ = activity.circuit.to_fields
27
-
28
- content = map.collect do |task, connections|
29
- bla =
30
- connections.collect do |direction, target|
31
- target_str = target.kind_of?(End) ? EndInspect(target) : stripped.(target)
32
- "#{stripped.(direction)}=>#{target_str}"
33
- end.join(", ")
34
- task_str = task.kind_of?(End) ? EndInspect(task) : stripped.(task)
35
- "#{task_str}=>{#{bla}}"
36
- end.join(", ")
37
- "{#{content}}"
38
- end
39
-
40
- def self.strip_for(target, strings)
41
- strings.each { |stripped| target = target.to_s.gsub(stripped, "") }
42
- target
43
- end
44
- end
45
-
46
- module Trailblazer::Activity::Inspect
47
- # The "matcher":
48
- # Finds out appropriate serializer and calls it.
49
- def self.call(inspected)
50
- Instance(inspected)
51
- end
52
-
53
- # Serializes an object instance with hex IDs.
54
- # <Trailblazer::Circuit::Start: @name=:default, @options={}>
55
- def self.Instance(object)
56
- object.inspect.gsub(/0x\w+/, "")
57
- end
58
- end
@@ -1,45 +0,0 @@
1
- # @private
2
- class Trailblazer::Context::ContainerChain # used to be called Resolver.
3
- # Keeps a list of containers. When looking up a key/value, containers are traversed in
4
- # the order they were added until key is found.
5
- #
6
- # Required Container interface: `#key?`, `#[]`.
7
- #
8
- # @note ContainerChain is an immutable data structure, it does not support writing.
9
- # @param containers Array of <Container> objects (splatted)
10
- def initialize(containers, to_hash: nil)
11
- @containers = containers
12
- @to_hash = to_hash
13
- end
14
-
15
- # @param name Symbol or String to lookup a value stored in one of the containers.
16
- def [](name)
17
- self.class.find(@containers, name)
18
- end
19
-
20
- # @private
21
- def key?(name)
22
- @containers.find { |container| container.key?(name) }
23
- end
24
-
25
- def self.find(containers, name)
26
- containers.find { |container| container.key?(name) && (return container[name]) }
27
- end
28
-
29
- def keys
30
- @containers.collect(&:keys).flatten
31
- end
32
-
33
- # @private
34
- def to_hash
35
- return @to_hash.(@containers) if @to_hash # FIXME: introduce pattern matching so we can have different "transformers" for each container type.
36
- @containers.each_with_object({}) { |container, hash| hash.merge!(container.to_hash) }
37
- end
38
- end
39
-
40
- # alternative implementation:
41
- # containers.reverse.each do |container| @mutable_options.merge!(container) end
42
- #
43
- # benchmark, merging in #initialize vs. this resolver.
44
- # merge 39.678k (± 9.1%) i/s - 198.700k in 5.056653s
45
- # resolver 68.928k (± 6.4%) i/s - 342.836k in 5.001610s
@@ -1,68 +0,0 @@
1
- # TODO: mark/make all but mutable_options as frozen.
2
- # The idea of Skill is to have a generic, ordered read/write interface that
3
- # collects mutable runtime-computed data while providing access to compile-time
4
- # information.
5
- # The runtime-data takes precedence over the class data.
6
- module Trailblazer
7
- # Holds local options (aka `mutable_options`) and "original" options from the "outer"
8
- # activity (aka wrapped_options).
9
-
10
- # only public creator: Build
11
- class Context # :data object:
12
- def initialize(wrapped_options, mutable_options)
13
- @wrapped_options, @mutable_options = wrapped_options, mutable_options
14
- end
15
-
16
- def [](name)
17
- ContainerChain.find( [@mutable_options, @wrapped_options], name )
18
- end
19
-
20
- def key?(name)
21
- @mutable_options.key?(name) || @wrapped_options.key?(name)
22
- end
23
-
24
- def []=(name, value)
25
- @mutable_options[name] = value
26
- end
27
-
28
- def merge(hash)
29
- original, mutable_options = decompose
30
-
31
- ctx = Trailblazer::Context( original, mutable_options.merge(hash) )
32
- end
33
-
34
- # Return the Context's two components. Used when computing the new output for
35
- # the next activity.
36
- def decompose
37
- [ @wrapped_options, @mutable_options ]
38
- end
39
-
40
- def key?(name)
41
- ContainerChain.find( [@mutable_options, @wrapped_options], name )
42
- end
43
-
44
-
45
- def keys
46
- @mutable_options.keys + @wrapped_options.keys # FIXME.
47
- end
48
-
49
-
50
-
51
- # TODO: maybe we shouldn't allow to_hash from context?
52
- # TODO: massive performance bottleneck. also, we could already "know" here what keys the
53
- # transformation wants.
54
- # FIXME: ToKeywordArguments()
55
- def to_hash
56
- {}.tap do |hash|
57
- # the "key" here is to call to_hash on all containers.
58
- [ @wrapped_options.to_hash, @mutable_options.to_hash ].each do |options|
59
- options.each { |k, v| hash[k.to_sym] = v }
60
- end
61
- end
62
- end
63
- end
64
-
65
- def self.Context(wrapped_options, mutable_options={})
66
- Context.new(wrapped_options, mutable_options)
67
- end
68
- end # Trailblazer
@@ -1,78 +0,0 @@
1
- module Trailblazer
2
- # @note This might go to trailblazer-args along with `Context` at some point.
3
- def self.Option(proc)
4
- Option.build(Option, proc)
5
- end
6
-
7
- class Option
8
- # Generic builder for a callable "option".
9
- # @param call_implementation [Class, Module] implements the process of calling the proc
10
- # while passing arguments/options to it in a specific style (e.g. kw args, step interface).
11
- # @return [Proc] when called, this proc will evaluate its option (at run-time).
12
- def self.build(call_implementation, proc)
13
- if proc.is_a? Symbol
14
- ->(*args) { call_implementation.evaluate_method(proc, *args) }
15
- else
16
- ->(*args) { call_implementation.evaluate_callable(proc, *args) }
17
- end
18
- end
19
-
20
- # A call implementation invoking `proc.(*args)` and plainly forwarding all arguments.
21
- # Override this for your own step strategy (see KW#call!).
22
- # @private
23
- def self.call!(proc, *args)
24
- proc.(*args)
25
- end
26
-
27
- # Note that both #evaluate_callable and #evaluate_method drop most of the args.
28
- # If you need those, override this class.
29
- # @private
30
- def self.evaluate_callable(proc, *args, **flow_options)
31
- call!(proc, *args)
32
- end
33
-
34
- # Make the context's instance method a "lambda" and reuse #call!.
35
- # @private
36
- def self.evaluate_method(proc, *args, exec_context:raise, **flow_options)
37
- call!(exec_context.method(proc), *args)
38
- end
39
-
40
- # Returns a {Proc} that, when called, invokes the `proc` argument with keyword arguments.
41
- # This is known as "step (call) interface".
42
- #
43
- # This is commonly used by `Operation::step` to wrap the argument and make it
44
- # callable in the circuit.
45
- #
46
- # my_proc = ->(options, **kws) { options["i got called"] = true }
47
- # task = Trailblazer::Option::KW(my_proc)
48
- # task.(options = {})
49
- # options["i got called"] #=> true
50
- #
51
- # Alternatively, you can pass a symbol and an `:exec_context`.
52
- #
53
- # my_proc = :some_method
54
- # task = Trailblazer::Option::KW(my_proc)
55
- #
56
- # class A
57
- # def some_method(options, **kws)
58
- # options["i got called"] = true
59
- # end
60
- # end
61
- #
62
- # task.(options = {}, exec_context: A.new)
63
- # options["i got called"] #=> true
64
- def self.KW(proc)
65
- Option.build(KW, proc)
66
- end
67
-
68
- # TODO: It would be cool if call! was typed and had `options SymbolizedHash` or something.
69
- class KW < Option
70
- # A different call implementation that calls `proc` with a "step interface".
71
- # your_code.(options, **options)
72
- # @private
73
- def self.call!(proc, options, *)
74
- proc.(options, **options.to_hash) # Step interface: (options, **)
75
- end
76
- end
77
- end
78
- end
@@ -1,32 +0,0 @@
1
- class Trailblazer::Activity
2
- module Wrap
3
- module Inject
4
- # Returns an Alteration wirings that, when applied, inserts the {ReverseMergeDefaults} task
5
- # before the {Wrap::Call} task. This is meant for macros and steps that accept a dependency
6
- # injection but need a default parameter to be set if not injected.
7
- # @returns Alteration
8
- def self.Defaults(default_dependencies)
9
- [
10
- [ :insert_before!, "task_wrap.call_task", node: [ ReverseMergeDefaults.new( default_dependencies ), id: "ReverseMergeDefaults#{default_dependencies}" ], incoming: Proc.new{ true }, outgoing: [ Trailblazer::Circuit::Right, {} ] ]
11
- ]
12
- end
13
-
14
- # @api private
15
- # @returns Task
16
- # @param Hash list of key/value that should be set if not already assigned/set before (or injected from the outside).
17
- class ReverseMergeDefaults
18
- def initialize(defaults)
19
- @defaults = defaults
20
- end
21
-
22
- def call((wrap_ctx, original_args), **circuit_options)
23
- ctx = original_args[0][0]
24
-
25
- @defaults.each { |k, v| ctx[k] ||= v }
26
-
27
- [ Trailblazer::Circuit::Right, [wrap_ctx, original_args] ]
28
- end
29
- end
30
- end # Inject
31
- end
32
- end
@@ -1,92 +0,0 @@
1
- class Trailblazer::Activity
2
- module Wrap
3
- # TaskWrap step to compute the incoming {Context} for the wrapped task.
4
- # This allows renaming, filtering, hiding, of the options passed into the wrapped task.
5
- #
6
- # Both Input and Output are typically to be added before and after task_wrap.call_task.
7
- #
8
- # @note Assumption: we always have :input _and_ :output, where :input produces a Context and :output decomposes it.
9
- class Input
10
- def initialize(filter)
11
- @filter = Trailblazer::Option(filter)
12
- end
13
-
14
- # `original_args` are the actual args passed to the wrapped task: [ [options, ..], circuit_options ]
15
- #
16
- def call((wrap_ctx, original_args), **circuit_options)
17
- # let user compute new ctx for the wrapped task.
18
- input_ctx = apply_filter(*original_args)
19
-
20
- # TODO: make this unnecessary.
21
- # wrap user's hash in Context if it's not one, already (in case user used options.merge).
22
- # DISCUSS: should we restrict user to .merge and options.Context?
23
- input_ctx = Trailblazer.Context({}, input_ctx) unless input_ctx.instance_of?(Trailblazer::Context)
24
-
25
- wrap_ctx = wrap_ctx.merge( vm_original_args: original_args )
26
-
27
- # decompose the original_args since we want to modify them.
28
- (original_ctx, original_flow_options), original_circuit_options = original_args
29
-
30
- # instead of the original Context, pass on the filtered `input_ctx` in the wrap.
31
- return Trailblazer::Circuit::Right, [ wrap_ctx, [[input_ctx, original_flow_options], original_circuit_options] ]
32
- end
33
-
34
- private
35
-
36
- def apply_filter((original_ctx, original_flow_options), original_circuit_options)
37
- @filter.( original_ctx, **original_circuit_options )
38
- end
39
- end
40
-
41
- # TaskWrap step to compute the outgoing {Context} from the wrapped task.
42
- # This allows renaming, filtering, hiding, of the options returned from the wrapped task.
43
- class Output
44
- def initialize(filter, strategy=CopyMutableToOriginal)
45
- @filter = Trailblazer::Option(filter)
46
- @strategy = strategy
47
- end
48
-
49
- # Runs the user filter and replaces the ctx in `wrap_ctx[:result_args]` with the filtered one.
50
- def call((wrap_ctx, original_args), **circuit_options)
51
- (original_ctx, original_flow_options), original_circuit_options = original_args
52
-
53
- returned_ctx, _ = wrap_ctx[:result_args] # this is the context returned from `call`ing the task.
54
-
55
- # returned_ctx is the Context object from the nested operation. In <=2.1, this might be a completely different one
56
- # than "ours" we created in Input. We now need to compile a list of all added values. This is time-intensive and should
57
- # be optimized by removing as many Context creations as possible (e.g. the one adding self[] stuff in Operation.__call__).
58
- _, mutable_data = returned_ctx.decompose # FIXME: this is a weak assumption. What if the task returns a deeply nested Context?
59
-
60
- # let user compute the output.
61
- output = apply_filter(mutable_data, original_flow_options, original_circuit_options)
62
-
63
- original_ctx = wrap_ctx[:vm_original_args][0][0]
64
-
65
- new_ctx = @strategy.( original_ctx, output ) # here, we compute the "new" options {Context}.
66
-
67
- wrap_ctx = wrap_ctx.merge( result_args: [new_ctx, original_flow_options] )
68
-
69
- # and then pass on the "new" context.
70
- return Trailblazer::Circuit::Right, [ wrap_ctx, original_args ]
71
- end
72
-
73
- private
74
-
75
- # @note API not stable
76
- def apply_filter(mutable_data, original_flow_options, original_circuit_options)
77
- @filter.(mutable_data, **original_circuit_options)
78
- end
79
-
80
- # "merge" Strategy
81
- class CopyMutableToOriginal
82
- # @param original Context
83
- # @param options Context The object returned from a (nested) {Activity}.
84
- def self.call(original, mutable)
85
- mutable.each { |k,v| original[k] = v }
86
-
87
- original
88
- end
89
- end
90
- end
91
- end # Wrap
92
- end