trailblazer-activity 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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