igniter 0.2.0 → 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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +238 -1
  5. data/docs/BACKLOG.md +166 -0
  6. data/docs/BRANCHES_V1.md +213 -0
  7. data/docs/COLLECTIONS_V1.md +303 -0
  8. data/docs/EXECUTION_MODEL_V2.md +79 -0
  9. data/docs/PATTERNS.md +222 -0
  10. data/docs/STORE_ADAPTERS.md +126 -0
  11. data/examples/README.md +124 -0
  12. data/examples/async_store.rb +47 -0
  13. data/examples/collection.rb +43 -0
  14. data/examples/collection_partial_failure.rb +50 -0
  15. data/examples/marketing_ergonomics.rb +57 -0
  16. data/examples/ringcentral_routing.rb +278 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +82 -0
  18. data/lib/igniter/compiler/graph_compiler.rb +12 -2
  19. data/lib/igniter/compiler/type_resolver.rb +54 -0
  20. data/lib/igniter/compiler/validation_context.rb +61 -0
  21. data/lib/igniter/compiler/validation_pipeline.rb +30 -0
  22. data/lib/igniter/compiler/validator.rb +1 -187
  23. data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
  24. data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
  25. data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
  26. data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
  27. data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
  28. data/lib/igniter/compiler.rb +8 -0
  29. data/lib/igniter/contract.rb +136 -4
  30. data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
  31. data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
  32. data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
  33. data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
  34. data/lib/igniter/diagnostics/report.rb +84 -8
  35. data/lib/igniter/dsl/contract_builder.rb +208 -5
  36. data/lib/igniter/dsl/schema_builder.rb +73 -0
  37. data/lib/igniter/dsl.rb +1 -0
  38. data/lib/igniter/errors.rb +11 -0
  39. data/lib/igniter/events/bus.rb +5 -0
  40. data/lib/igniter/events/event.rb +29 -0
  41. data/lib/igniter/executor.rb +74 -0
  42. data/lib/igniter/executor_registry.rb +44 -0
  43. data/lib/igniter/extensions/auditing/timeline.rb +4 -0
  44. data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
  45. data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
  46. data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
  47. data/lib/igniter/extensions/introspection.rb +1 -0
  48. data/lib/igniter/extensions/reactive/engine.rb +49 -2
  49. data/lib/igniter/extensions/reactive/reaction.rb +3 -2
  50. data/lib/igniter/model/branch_node.rb +40 -0
  51. data/lib/igniter/model/collection_node.rb +25 -0
  52. data/lib/igniter/model/composition_node.rb +2 -2
  53. data/lib/igniter/model/compute_node.rb +58 -2
  54. data/lib/igniter/model/input_node.rb +2 -2
  55. data/lib/igniter/model/output_node.rb +24 -4
  56. data/lib/igniter/model.rb +2 -0
  57. data/lib/igniter/runtime/cache.rb +64 -25
  58. data/lib/igniter/runtime/collection_result.rb +111 -0
  59. data/lib/igniter/runtime/deferred_result.rb +40 -0
  60. data/lib/igniter/runtime/execution.rb +261 -11
  61. data/lib/igniter/runtime/input_validator.rb +2 -24
  62. data/lib/igniter/runtime/invalidator.rb +1 -1
  63. data/lib/igniter/runtime/job_worker.rb +18 -0
  64. data/lib/igniter/runtime/node_state.rb +20 -0
  65. data/lib/igniter/runtime/planner.rb +126 -0
  66. data/lib/igniter/runtime/resolver.rb +269 -15
  67. data/lib/igniter/runtime/result.rb +14 -2
  68. data/lib/igniter/runtime/runner_factory.rb +20 -0
  69. data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
  70. data/lib/igniter/runtime/runners/store_runner.rb +29 -0
  71. data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
  72. data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
  73. data/lib/igniter/runtime/stores/file_store.rb +43 -0
  74. data/lib/igniter/runtime/stores/memory_store.rb +40 -0
  75. data/lib/igniter/runtime/stores/redis_store.rb +44 -0
  76. data/lib/igniter/runtime.rb +12 -0
  77. data/lib/igniter/type_system.rb +44 -0
  78. data/lib/igniter/version.rb +1 -1
  79. data/lib/igniter.rb +23 -0
  80. metadata +43 -2
@@ -11,9 +11,12 @@ module Igniter
11
11
  @name = name
12
12
  @nodes = []
13
13
  @sequence = 0
14
+ @scope_stack = []
14
15
  end
15
16
 
16
17
  UNDEFINED_INPUT_DEFAULT = :__igniter_undefined__
18
+ UNDEFINED_CONST_VALUE = :__igniter_const_undefined__
19
+ UNDEFINED_GUARD_MATCHER = :__igniter_guard_matcher_undefined__
17
20
 
18
21
  def input(name, type: nil, required: nil, default: UNDEFINED_INPUT_DEFAULT, **metadata)
19
22
  input_metadata = with_source_location(metadata)
@@ -25,33 +28,114 @@ module Igniter
25
28
  Model::InputNode.new(
26
29
  id: next_id,
27
30
  name: name,
31
+ path: scoped_path(name),
28
32
  metadata: input_metadata
29
33
  )
30
34
  )
31
35
  end
32
36
 
33
- def compute(name, depends_on:, call: nil, **metadata, &block)
34
- callable = call || block
37
+ def compute(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
38
+ callable, resolved_metadata = resolve_compute_callable(call: call, executor: executor, metadata: metadata, block: block)
35
39
  raise CompileError, "compute :#{name} requires a callable" unless callable
36
- raise CompileError, "compute :#{name} cannot accept both `call:` and a block" if call && block
37
40
 
38
41
  add_node(
39
42
  Model::ComputeNode.new(
40
43
  id: next_id,
41
44
  name: name,
42
- dependencies: Array(depends_on),
45
+ dependencies: normalize_dependencies(depends_on: depends_on, with: with),
43
46
  callable: callable,
44
- metadata: with_source_location(metadata)
47
+ path: scoped_path(name),
48
+ metadata: with_source_location(resolved_metadata)
45
49
  )
46
50
  )
47
51
  end
48
52
 
53
+ def const(name, value = UNDEFINED_CONST_VALUE, **metadata, &block)
54
+ raise CompileError, "const :#{name} cannot accept both a value and a block" if !block.nil? && value != UNDEFINED_CONST_VALUE
55
+ raise CompileError, "const :#{name} requires a value or a block" if block.nil? && value == UNDEFINED_CONST_VALUE
56
+
57
+ callable = if block
58
+ block
59
+ else
60
+ proc { value }
61
+ end
62
+
63
+ compute(name, with: [], call: callable, **metadata.merge(kind: :const))
64
+ end
65
+
66
+ def lookup(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
67
+ compute(name, depends_on: depends_on, with: with, call: call, executor: executor, **{ category: :lookup }.merge(metadata), &block)
68
+ end
69
+
70
+ def map(name, from:, call: nil, executor: nil, **metadata, &block)
71
+ compute(name, with: from, call: call, executor: executor, **{ category: :map }.merge(metadata), &block)
72
+ end
73
+
74
+ def guard(name, depends_on: nil, with: nil, call: nil, executor: nil, message: nil,
75
+ eq: UNDEFINED_GUARD_MATCHER, in: UNDEFINED_GUARD_MATCHER, matches: UNDEFINED_GUARD_MATCHER,
76
+ **metadata, &block)
77
+ matcher_options = {
78
+ eq: eq,
79
+ in: binding.local_variable_get(:in),
80
+ matches: matches
81
+ }.reject { |_key, value| value == UNDEFINED_GUARD_MATCHER }
82
+
83
+ if matcher_options.any?
84
+ raise CompileError, "guard :#{name} cannot combine matcher options with `call:`, `executor:`, or a block" if call || executor || block
85
+ raise CompileError, "guard :#{name} supports only one matcher option at a time" if matcher_options.size > 1
86
+
87
+ dependencies = normalize_dependencies(depends_on: depends_on, with: with)
88
+ raise CompileError, "guard :#{name} with matcher options requires exactly one dependency" unless dependencies.size == 1
89
+
90
+ dependency = dependencies.first
91
+ matcher_name, matcher_value = matcher_options.first
92
+
93
+ call = build_guard_matcher(matcher_name, matcher_value, dependency)
94
+ end
95
+
96
+ compute(
97
+ name,
98
+ depends_on: depends_on,
99
+ with: with,
100
+ call: call,
101
+ executor: executor,
102
+ **metadata.merge(kind: :guard, guard: true, guard_message: message || "Guard '#{name}' failed"),
103
+ &block
104
+ )
105
+ end
106
+
107
+ def export(*names, from:, **metadata)
108
+ names.each do |name|
109
+ output(name, from: "#{from}.#{name}", **metadata)
110
+ end
111
+ end
112
+
113
+ def expose(*sources, as: nil, **metadata)
114
+ raise CompileError, "expose cannot use `as:` with multiple sources" if as && sources.size != 1
115
+
116
+ sources.each do |source|
117
+ output(as || source, from: source, **metadata)
118
+ end
119
+ end
120
+
121
+ def scope(name, &block)
122
+ raise CompileError, "scope requires a block" unless block
123
+
124
+ @scope_stack << name.to_s
125
+ instance_eval(&block)
126
+ ensure
127
+ @scope_stack.pop
128
+ end
129
+
130
+ alias namespace scope
131
+
49
132
  def output(name, from: nil, **metadata)
50
133
  add_node(
51
134
  Model::OutputNode.new(
52
135
  id: next_id,
53
136
  name: name,
54
137
  source: (from || name),
138
+ path: scoped_output_path(name),
55
139
  metadata: with_source_location(metadata)
56
140
  )
57
141
  )
@@ -66,6 +150,42 @@ module Igniter
66
150
  name: name,
67
151
  contract_class: contract,
68
152
  input_mapping: inputs,
153
+ path: scoped_path(name),
154
+ metadata: with_source_location(metadata)
155
+ )
156
+ )
157
+ end
158
+
159
+ def branch(name, with:, inputs:, **metadata, &block)
160
+ raise CompileError, "branch :#{name} requires a block" unless block
161
+ raise CompileError, "branch :#{name} requires an `inputs:` hash" unless inputs.is_a?(Hash)
162
+
163
+ definition = BranchBuilder.build(&block)
164
+
165
+ add_node(
166
+ Model::BranchNode.new(
167
+ id: next_id,
168
+ name: name,
169
+ selector_dependency: with,
170
+ cases: definition[:cases],
171
+ default_contract: definition[:default_contract],
172
+ input_mapping: inputs,
173
+ path: scoped_path(name),
174
+ metadata: with_source_location(metadata)
175
+ )
176
+ )
177
+ end
178
+
179
+ def collection(name, with:, each:, key:, mode: :collect, **metadata)
180
+ add_node(
181
+ Model::CollectionNode.new(
182
+ id: next_id,
183
+ name: name,
184
+ source_dependency: with,
185
+ contract_class: each,
186
+ key_name: key,
187
+ mode: mode,
188
+ path: scoped_path(name),
69
189
  metadata: with_source_location(metadata)
70
190
  )
71
191
  )
@@ -90,6 +210,89 @@ module Igniter
90
210
  def with_source_location(metadata)
91
211
  metadata.merge(source_location: caller_locations(2, 1).first&.to_s)
92
212
  end
213
+
214
+ def resolve_compute_callable(call:, executor:, metadata:, block:)
215
+ raise CompileError, "compute cannot accept both `call:` and `executor:`" if call && executor
216
+ raise CompileError, "compute cannot accept both `call:` and a block" if call && block
217
+ raise CompileError, "compute cannot accept both `executor:` and a block" if executor && block
218
+
219
+ if executor
220
+ definition = Igniter.executor_registry.fetch(executor)
221
+ return [definition.executor_class, definition.metadata.merge(metadata).merge(executor_key: definition.key)]
222
+ end
223
+
224
+ [call || block, metadata]
225
+ end
226
+
227
+ def normalize_dependencies(depends_on:, with:)
228
+ raise CompileError, "Use either `depends_on:` or `with:`, not both" if depends_on && with
229
+
230
+ dependencies = depends_on || with
231
+ Array(dependencies)
232
+ end
233
+
234
+ def build_guard_matcher(matcher_name, matcher_value, dependency)
235
+ case matcher_name
236
+ when :eq
237
+ proc do |**values|
238
+ values.fetch(dependency) == matcher_value
239
+ end
240
+ when :in
241
+ allowed_values = Array(matcher_value)
242
+ proc do |**values|
243
+ allowed_values.include?(values.fetch(dependency))
244
+ end
245
+ when :matches
246
+ matcher = matcher_value
247
+ raise CompileError, "`matches:` expects a Regexp" unless matcher.is_a?(Regexp)
248
+
249
+ proc do |**values|
250
+ values.fetch(dependency).to_s.match?(matcher)
251
+ end
252
+ else
253
+ raise CompileError, "Unsupported guard matcher: #{matcher_name}"
254
+ end
255
+ end
256
+
257
+ def scoped_path(name)
258
+ return name.to_s if @scope_stack.empty?
259
+
260
+ "#{@scope_stack.join('.')}.#{name}"
261
+ end
262
+
263
+ def scoped_output_path(name)
264
+ return "output.#{name}" if @scope_stack.empty?
265
+
266
+ "#{@scope_stack.join('.')}.output.#{name}"
267
+ end
268
+
269
+ class BranchBuilder
270
+ def self.build(&block)
271
+ new.tap { |builder| builder.instance_eval(&block) }.to_h
272
+ end
273
+
274
+ def initialize
275
+ @cases = []
276
+ @default_contract = nil
277
+ end
278
+
279
+ def on(match, contract:)
280
+ @cases << { match: match, contract: contract }
281
+ end
282
+
283
+ def default(contract:)
284
+ raise CompileError, "branch can define only one `default`" if @default_contract
285
+
286
+ @default_contract = contract
287
+ end
288
+
289
+ def to_h
290
+ {
291
+ cases: @cases,
292
+ default_contract: @default_contract
293
+ }
294
+ end
295
+ end
93
296
  end
94
297
  end
95
298
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module DSL
5
+ class SchemaBuilder
6
+ def self.compile(schema, name: nil)
7
+ new(schema, name: name).compile
8
+ end
9
+
10
+ def initialize(schema, name: nil)
11
+ @schema = symbolize(schema)
12
+ @name = name || @schema[:name] || "AnonymousContract"
13
+ end
14
+
15
+ def compile
16
+ schema = @schema
17
+
18
+ ContractBuilder.compile(name: @name) do
19
+ Array(schema[:inputs]).each do |input_config|
20
+ config = input_config
21
+ input(
22
+ config.fetch(:name),
23
+ type: config[:type],
24
+ required: config[:required],
25
+ default: config.fetch(:default, ContractBuilder::UNDEFINED_INPUT_DEFAULT),
26
+ **config.fetch(:metadata, {})
27
+ )
28
+ end
29
+
30
+ Array(schema[:compositions]).each do |composition_config|
31
+ config = composition_config
32
+ compose(
33
+ config.fetch(:name),
34
+ contract: config.fetch(:contract),
35
+ inputs: config.fetch(:inputs),
36
+ **config.fetch(:metadata, {})
37
+ )
38
+ end
39
+
40
+ Array(schema[:computes]).each do |compute_config|
41
+ config = compute_config
42
+ options = {
43
+ depends_on: Array(config.fetch(:depends_on)).map(&:to_sym)
44
+ }
45
+ options[:call] = config[:call] if config.key?(:call)
46
+ options[:executor] = config[:executor] if config.key?(:executor)
47
+ compute(config.fetch(:name), **options, **config.fetch(:metadata, {}))
48
+ end
49
+
50
+ Array(schema[:outputs]).each do |output_config|
51
+ config = output_config
52
+ output(config.fetch(:name), from: config[:from], **config.fetch(:metadata, {}))
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def symbolize(value)
60
+ case value
61
+ when Hash
62
+ value.each_with_object({}) do |(key, nested), memo|
63
+ memo[key.to_sym] = symbolize(nested)
64
+ end
65
+ when Array
66
+ value.map { |item| symbolize(item) }
67
+ else
68
+ value
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/igniter/dsl.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "dsl/contract_builder"
4
+ require_relative "dsl/schema_builder"
4
5
 
5
6
  module Igniter
6
7
  module DSL
@@ -48,6 +48,17 @@ module Igniter
48
48
  class ValidationError < CompileError; end
49
49
  class CycleError < ValidationError; end
50
50
  class InputError < Error; end
51
+ class CollectionInputError < Error; end
52
+ class CollectionKeyError < Error; end
51
53
  class ResolutionError < Error; end
52
54
  class CompositionError < Error; end
55
+ class BranchSelectionError < Error; end
56
+ class PendingDependencyError < Error
57
+ attr_reader :deferred_result
58
+
59
+ def initialize(deferred_result, message = "Dependency is pending", context: {})
60
+ @deferred_result = deferred_result
61
+ super(message, context: context)
62
+ end
63
+ end
53
64
  end
@@ -34,6 +34,11 @@ module Igniter
34
34
  def subscribe(subscriber = nil, &block)
35
35
  @subscribers << (subscriber || block)
36
36
  end
37
+
38
+ def restore!(events:, execution_id: nil)
39
+ @execution_id = execution_id if execution_id
40
+ @events = Array(events).map { |event| event.is_a?(Event) ? event : Event.from_h(event) }
41
+ end
37
42
  end
38
43
  end
39
44
  end
@@ -16,6 +16,20 @@ module Igniter
16
16
  :timestamp,
17
17
  keyword_init: true
18
18
  ) do
19
+ def self.from_h(data)
20
+ new(
21
+ event_id: value_from(data, :event_id),
22
+ type: value_from(data, :type).to_sym,
23
+ execution_id: value_from(data, :execution_id),
24
+ node_id: value_from(data, :node_id),
25
+ node_name: value_from(data, :node_name)&.to_sym,
26
+ path: value_from(data, :path),
27
+ status: value_from(data, :status)&.to_sym,
28
+ payload: value_from(data, :payload) || {},
29
+ timestamp: parse_timestamp(value_from(data, :timestamp))
30
+ )
31
+ end
32
+
19
33
  def to_h
20
34
  {
21
35
  event_id: event_id,
@@ -36,6 +50,21 @@ module Igniter
36
50
 
37
51
  private
38
52
 
53
+ def self.parse_timestamp(value)
54
+ case value
55
+ when Time
56
+ value
57
+ when String
58
+ Time.iso8601(value)
59
+ else
60
+ value
61
+ end
62
+ end
63
+
64
+ def self.value_from(data, key)
65
+ data[key] || data[key.to_s]
66
+ end
67
+
39
68
  def serialize_value(value)
40
69
  case value
41
70
  when Time
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ class Executor
5
+ class << self
6
+ def inherited(subclass)
7
+ super
8
+ subclass.instance_variable_set(:@executor_inputs, executor_inputs.transform_values(&:dup))
9
+ subclass.instance_variable_set(:@executor_metadata, executor_metadata.dup)
10
+ end
11
+
12
+ def input(name, required: true, type: nil, **metadata)
13
+ executor_inputs[name.to_sym] = metadata.merge(required: required, type: type).compact
14
+ end
15
+
16
+ def executor_inputs
17
+ @executor_inputs ||= {}
18
+ end
19
+
20
+ def executor_metadata
21
+ @executor_metadata ||= {}
22
+ end
23
+
24
+ def executor_key(value = nil)
25
+ metadata_value(:key, value)
26
+ end
27
+
28
+ def label(value = nil)
29
+ metadata_value(:label, value)
30
+ end
31
+
32
+ def category(value = nil)
33
+ metadata_value(:category, value)
34
+ end
35
+
36
+ def summary(value = nil)
37
+ metadata_value(:summary, value)
38
+ end
39
+
40
+ def tags(*values)
41
+ return Array(executor_metadata[:tags]).freeze if values.empty?
42
+
43
+ executor_metadata[:tags] = values.flatten.compact.map(&:to_sym).freeze
44
+ end
45
+
46
+ def output_schema(value = nil)
47
+ metadata_value(:output_schema, value)
48
+ end
49
+
50
+ def call(**dependencies)
51
+ new.call(**dependencies)
52
+ end
53
+
54
+ private
55
+
56
+ def metadata_value(key, value)
57
+ return executor_metadata[key] if value.nil?
58
+
59
+ executor_metadata[key] = value
60
+ end
61
+ end
62
+
63
+ attr_reader :execution, :contract
64
+
65
+ def initialize(execution: nil, contract: nil)
66
+ @execution = execution
67
+ @contract = contract
68
+ end
69
+
70
+ def defer(token: nil, payload: {})
71
+ Runtime::DeferredResult.build(token: token, payload: payload)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ class ExecutorRegistry
5
+ Definition = Struct.new(:key, :executor_class, :metadata, keyword_init: true)
6
+
7
+ def initialize
8
+ @definitions = {}
9
+ end
10
+
11
+ def register(key, executor_class, **metadata)
12
+ normalized_key = key.to_s
13
+ raise CompileError, "executor registry key cannot be empty" if normalized_key.empty?
14
+
15
+ unless executor_class.is_a?(Class) && executor_class <= Igniter::Executor
16
+ raise CompileError, "Executor registry key '#{normalized_key}' must reference an Igniter::Executor subclass"
17
+ end
18
+
19
+ @definitions[normalized_key] = Definition.new(
20
+ key: normalized_key,
21
+ executor_class: executor_class,
22
+ metadata: executor_class.executor_metadata.merge(metadata).merge(key: normalized_key)
23
+ )
24
+ end
25
+
26
+ def fetch(key)
27
+ @definitions.fetch(key.to_s)
28
+ rescue KeyError
29
+ raise CompileError, "Unknown executor registry key: #{key}"
30
+ end
31
+
32
+ def registered?(key)
33
+ @definitions.key?(key.to_s)
34
+ end
35
+
36
+ def definitions
37
+ @definitions.values.sort_by(&:key)
38
+ end
39
+
40
+ def clear
41
+ @definitions.clear
42
+ end
43
+ end
44
+ end
@@ -19,6 +19,10 @@ module Igniter
19
19
  @events.dup
20
20
  end
21
21
 
22
+ def restore!(events)
23
+ @events = Array(events).map { |event| event.is_a?(Igniter::Events::Event) ? event : Igniter::Events::Event.from_h(event) }
24
+ end
25
+
22
26
  def snapshot
23
27
  {
24
28
  execution_id: execution.events.execution_id,
@@ -23,9 +23,31 @@ module Igniter
23
23
  @graph.nodes.each do |node|
24
24
  line = "- #{node.kind} #{node.path}"
25
25
  line += " depends_on=#{node.dependencies.join(',')}" if node.dependencies.any?
26
+ if node.kind == :compute
27
+ line += " callable=#{node.callable_name}"
28
+ line += " guard=true" if node.guard?
29
+ line += " const=true" if node.const?
30
+ line += " executor_key=#{node.executor_key}" if node.executor_key
31
+ line += " label=#{node.executor_label}" if node.executor_label
32
+ line += " category=#{node.executor_category}" if node.executor_category
33
+ line += " tags=#{node.executor_tags.join(',')}" if node.executor_tags.any?
34
+ line += " summary=#{node.executor_summary}" if node.executor_summary
35
+ end
26
36
  if node.kind == :composition
27
37
  line += " contract=#{node.contract_class.name || 'AnonymousContract'}"
28
38
  end
39
+ if node.kind == :branch
40
+ cases = node.cases.map { |entry| "#{entry[:match].inspect}:#{entry[:contract].name || 'AnonymousContract'}" }
41
+ line += " selector=#{node.selector_dependency}"
42
+ line += " cases=#{cases.join('|')}"
43
+ line += " default=#{node.default_contract.name || 'AnonymousContract'}"
44
+ end
45
+ if node.kind == :collection
46
+ line += " with=#{node.source_dependency}"
47
+ line += " each=#{node.contract_class.name || 'AnonymousContract'}"
48
+ line += " key=#{node.key_name}"
49
+ line += " mode=#{node.mode}"
50
+ end
29
51
  lines << line
30
52
  end
31
53
  lines << "Outputs:"
@@ -43,11 +65,11 @@ module Igniter
43
65
  end
44
66
  @graph.outputs.each do |output|
45
67
  lines << %( #{output_id(output)}["output: #{output.name}"])
46
- lines << %( #{node_id(@graph.fetch_node(output.source))} --> #{output_id(output)})
68
+ lines << %( #{node_id(@graph.fetch_node(output.source_root))} --> #{output_id(output)})
47
69
  end
48
70
  @graph.nodes.each do |node|
49
71
  node.dependencies.each do |dependency_name|
50
- dependency_node = @graph.fetch_node(dependency_name)
72
+ dependency_node = @graph.fetch_dependency(dependency_name)
51
73
  lines << %( #{node_id(dependency_node)} --> #{node_id(node)})
52
74
  end
53
75
  end
@@ -57,6 +79,8 @@ module Igniter
57
79
  private
58
80
 
59
81
  def node_id(node)
82
+ return "output_#{node.name}" if node.kind == :output
83
+
60
84
  "node_#{node.name}"
61
85
  end
62
86
 
@@ -65,7 +89,9 @@ module Igniter
65
89
  end
66
90
 
67
91
  def node_label(node)
68
- "#{node.kind}: #{node.name}"
92
+ return "#{node.kind}: #{node.path}" unless node.kind == :compute
93
+
94
+ "#{node.kind}: #{node.path}\\n#{node.callable_name}"
69
95
  end
70
96
  end
71
97
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Introspection
6
+ class PlanFormatter
7
+ def self.to_text(execution, output_names = nil)
8
+ new(execution, output_names).to_text
9
+ end
10
+
11
+ def initialize(execution, output_names = nil)
12
+ @execution = execution
13
+ @plan = execution.plan(output_names)
14
+ end
15
+
16
+ def to_text
17
+ lines = []
18
+ lines << "Plan #{@execution.compiled_graph.name}"
19
+ lines << "Targets: #{format_list(@plan[:targets])}"
20
+ lines << "Ready: #{format_list(@plan[:ready])}"
21
+ lines << "Blocked: #{format_list(@plan[:blocked])}"
22
+ lines << "Nodes:"
23
+
24
+ @plan[:nodes].each_value do |entry|
25
+ lines << format_node(entry)
26
+ end
27
+
28
+ lines.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def format_node(entry)
34
+ line = "- #{entry[:kind]} #{entry[:path]} status=#{entry[:status]}"
35
+ line += " ready=true" if entry[:ready]
36
+ line += " blocked=true" if entry[:blocked]
37
+ line += " waiting_on=#{format_list(entry[:waiting_on])}" if entry[:waiting_on].any?
38
+
39
+ dependency_summary = entry[:dependencies].map do |dependency|
40
+ "#{dependency[:name]}(#{dependency[:status]})"
41
+ end
42
+ line += " deps=#{dependency_summary.join(',')}" if dependency_summary.any?
43
+ line
44
+ end
45
+
46
+ def format_list(values)
47
+ array = Array(values)
48
+ return "none" if array.empty?
49
+
50
+ array.join(",")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end