igniter 0.2.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 +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/docs/API_V2.md +242 -0
- data/docs/ARCHITECTURE_V2.md +317 -0
- data/docs/EXECUTION_MODEL_V2.md +245 -0
- data/docs/IGNITER_CONCEPTS.md +81 -0
- data/examples/README.md +77 -0
- data/examples/basic_pricing.rb +27 -0
- data/examples/composition.rb +39 -0
- data/examples/diagnostics.rb +28 -0
- data/lib/igniter/compiler/compiled_graph.rb +78 -0
- data/lib/igniter/compiler/graph_compiler.rb +60 -0
- data/lib/igniter/compiler/validator.rb +205 -0
- data/lib/igniter/compiler.rb +10 -0
- data/lib/igniter/contract.rb +117 -0
- data/lib/igniter/diagnostics/report.rb +174 -0
- data/lib/igniter/diagnostics.rb +8 -0
- data/lib/igniter/dsl/contract_builder.rb +95 -0
- data/lib/igniter/dsl.rb +8 -0
- data/lib/igniter/errors.rb +53 -0
- data/lib/igniter/events/bus.rb +39 -0
- data/lib/igniter/events/event.rb +53 -0
- data/lib/igniter/events.rb +9 -0
- data/lib/igniter/extensions/auditing/timeline.rb +99 -0
- data/lib/igniter/extensions/auditing.rb +10 -0
- data/lib/igniter/extensions/introspection/graph_formatter.rb +73 -0
- data/lib/igniter/extensions/introspection/runtime_formatter.rb +102 -0
- data/lib/igniter/extensions/introspection.rb +11 -0
- data/lib/igniter/extensions/reactive/engine.rb +36 -0
- data/lib/igniter/extensions/reactive/matcher.rb +21 -0
- data/lib/igniter/extensions/reactive/reaction.rb +17 -0
- data/lib/igniter/extensions/reactive.rb +12 -0
- data/lib/igniter/extensions.rb +10 -0
- data/lib/igniter/model/composition_node.rb +22 -0
- data/lib/igniter/model/compute_node.rb +21 -0
- data/lib/igniter/model/graph.rb +15 -0
- data/lib/igniter/model/input_node.rb +27 -0
- data/lib/igniter/model/node.rb +22 -0
- data/lib/igniter/model/output_node.rb +21 -0
- data/lib/igniter/model.rb +13 -0
- data/lib/igniter/runtime/cache.rb +58 -0
- data/lib/igniter/runtime/execution.rb +142 -0
- data/lib/igniter/runtime/input_validator.rb +145 -0
- data/lib/igniter/runtime/invalidator.rb +52 -0
- data/lib/igniter/runtime/node_state.rb +31 -0
- data/lib/igniter/runtime/resolver.rb +114 -0
- data/lib/igniter/runtime/result.rb +105 -0
- data/lib/igniter/runtime.rb +14 -0
- data/lib/igniter/version.rb +5 -0
- data/lib/igniter.rb +20 -0
- data/sig/igniter.rbs +4 -0
- metadata +126 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Diagnostics
|
|
5
|
+
class Report
|
|
6
|
+
def initialize(execution)
|
|
7
|
+
@execution = execution
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_h
|
|
11
|
+
safely_resolve_execution
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
graph: execution.compiled_graph.name,
|
|
15
|
+
execution_id: execution.events.execution_id,
|
|
16
|
+
status: status,
|
|
17
|
+
outputs: serialize_outputs,
|
|
18
|
+
errors: serialize_errors,
|
|
19
|
+
nodes: summarize_nodes,
|
|
20
|
+
events: summarize_events
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def as_json(*)
|
|
25
|
+
to_h
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_text
|
|
29
|
+
report = to_h
|
|
30
|
+
lines = []
|
|
31
|
+
lines << "Diagnostics #{report[:graph]}"
|
|
32
|
+
lines << "Execution #{report[:execution_id]}"
|
|
33
|
+
lines << "Status: #{report[:status]}"
|
|
34
|
+
lines << format_outputs(report[:outputs])
|
|
35
|
+
lines << format_nodes(report[:nodes])
|
|
36
|
+
lines << format_errors(report[:errors])
|
|
37
|
+
lines << format_events(report[:events])
|
|
38
|
+
lines.compact.join("\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_markdown
|
|
42
|
+
report = to_h
|
|
43
|
+
lines = []
|
|
44
|
+
lines << "# Diagnostics #{report[:graph]}"
|
|
45
|
+
lines << ""
|
|
46
|
+
lines << "- Execution: `#{report[:execution_id]}`"
|
|
47
|
+
lines << "- Status: `#{report[:status]}`"
|
|
48
|
+
lines << "- Outputs: #{inline_hash(report[:outputs])}"
|
|
49
|
+
lines << "- Nodes: total=#{report[:nodes][:total]}, succeeded=#{report[:nodes][:succeeded]}, failed=#{report[:nodes][:failed]}, stale=#{report[:nodes][:stale]}"
|
|
50
|
+
lines << "- Events: total=#{report[:events][:total]}, latest=#{report[:events][:latest_type] || 'none'}"
|
|
51
|
+
|
|
52
|
+
unless report[:errors].empty?
|
|
53
|
+
lines << ""
|
|
54
|
+
lines << "## Errors"
|
|
55
|
+
report[:errors].each do |error|
|
|
56
|
+
lines << "- `#{error[:node_name]}`: #{error[:message]}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
lines.join("\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
attr_reader :execution
|
|
66
|
+
|
|
67
|
+
def execution_result
|
|
68
|
+
@execution_result ||= Runtime::Result.new(execution)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def safely_resolve_execution
|
|
72
|
+
execution.resolve_all
|
|
73
|
+
rescue Igniter::Error
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def status
|
|
78
|
+
return :failed if execution.cache.values.any?(&:failed?)
|
|
79
|
+
return :stale if execution.cache.values.any?(&:stale?)
|
|
80
|
+
|
|
81
|
+
:succeeded
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def serialize_errors
|
|
85
|
+
execution_result.errors.map do |node_name, error|
|
|
86
|
+
{
|
|
87
|
+
node_name: node_name,
|
|
88
|
+
type: error.class.name,
|
|
89
|
+
message: error.message,
|
|
90
|
+
context: error.respond_to?(:context) ? error.context : {}
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def serialize_outputs
|
|
96
|
+
execution.compiled_graph.outputs.each_with_object({}) do |output_node, memo|
|
|
97
|
+
state = execution.cache.fetch(output_node.source)
|
|
98
|
+
memo[output_node.name] = serialize_output_state(state)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def serialize_output_state(state)
|
|
103
|
+
return nil unless state
|
|
104
|
+
return { error: state.error.message, status: state.status } if state.failed?
|
|
105
|
+
|
|
106
|
+
serialize_value(state.value)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def summarize_nodes
|
|
110
|
+
states = execution.states
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
total: states.size,
|
|
114
|
+
succeeded: states.values.count { |state| state[:status] == :succeeded },
|
|
115
|
+
failed: states.values.count { |state| state[:status] == :failed },
|
|
116
|
+
stale: states.values.count { |state| state[:status] == :stale },
|
|
117
|
+
failed_nodes: states.filter_map do |node_name, state|
|
|
118
|
+
next unless state[:status] == :failed
|
|
119
|
+
|
|
120
|
+
{ node_name: node_name, path: state[:path], error: state[:error] }
|
|
121
|
+
end
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def summarize_events
|
|
126
|
+
events = execution.events.events
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
total: events.size,
|
|
130
|
+
latest_type: events.last&.type,
|
|
131
|
+
latest_at: events.last&.timestamp,
|
|
132
|
+
by_type: events.each_with_object(Hash.new(0)) { |event, memo| memo[event.type] += 1 }
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def format_outputs(outputs)
|
|
137
|
+
"Outputs: #{inline_hash(outputs)}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def format_nodes(nodes)
|
|
141
|
+
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, stale=#{nodes[:stale]}"
|
|
142
|
+
return line if nodes[:failed_nodes].empty?
|
|
143
|
+
|
|
144
|
+
failures = nodes[:failed_nodes].map { |node| "#{node[:node_name]}(#{node[:error]})" }.join(", ")
|
|
145
|
+
"#{line}\nFailed Nodes: #{failures}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def format_errors(errors)
|
|
149
|
+
return "Errors: none" if errors.empty?
|
|
150
|
+
|
|
151
|
+
"Errors: #{errors.map { |error| "#{error[:node_name]}=#{error[:type]}" }.join(', ')}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def format_events(events)
|
|
155
|
+
"Events: total=#{events[:total]}, latest=#{events[:latest_type] || 'none'}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def inline_hash(hash)
|
|
159
|
+
hash.map { |key, value| "#{key}=#{value.inspect}" }.join(", ")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def serialize_value(value)
|
|
163
|
+
case value
|
|
164
|
+
when Runtime::Result
|
|
165
|
+
value.to_h
|
|
166
|
+
when Array
|
|
167
|
+
value.map { |item| serialize_value(item) }
|
|
168
|
+
else
|
|
169
|
+
value
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module DSL
|
|
5
|
+
class ContractBuilder
|
|
6
|
+
def self.compile(name: "AnonymousContract", &block)
|
|
7
|
+
new(name: name).tap { |builder| builder.instance_eval(&block) }.compile
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(name:)
|
|
11
|
+
@name = name
|
|
12
|
+
@nodes = []
|
|
13
|
+
@sequence = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
UNDEFINED_INPUT_DEFAULT = :__igniter_undefined__
|
|
17
|
+
|
|
18
|
+
def input(name, type: nil, required: nil, default: UNDEFINED_INPUT_DEFAULT, **metadata)
|
|
19
|
+
input_metadata = with_source_location(metadata)
|
|
20
|
+
input_metadata[:type] = type if type
|
|
21
|
+
input_metadata[:required] = required unless required.nil?
|
|
22
|
+
input_metadata[:default] = default unless default == UNDEFINED_INPUT_DEFAULT
|
|
23
|
+
|
|
24
|
+
add_node(
|
|
25
|
+
Model::InputNode.new(
|
|
26
|
+
id: next_id,
|
|
27
|
+
name: name,
|
|
28
|
+
metadata: input_metadata
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def compute(name, depends_on:, call: nil, **metadata, &block)
|
|
34
|
+
callable = call || block
|
|
35
|
+
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
|
+
|
|
38
|
+
add_node(
|
|
39
|
+
Model::ComputeNode.new(
|
|
40
|
+
id: next_id,
|
|
41
|
+
name: name,
|
|
42
|
+
dependencies: Array(depends_on),
|
|
43
|
+
callable: callable,
|
|
44
|
+
metadata: with_source_location(metadata)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def output(name, from: nil, **metadata)
|
|
50
|
+
add_node(
|
|
51
|
+
Model::OutputNode.new(
|
|
52
|
+
id: next_id,
|
|
53
|
+
name: name,
|
|
54
|
+
source: (from || name),
|
|
55
|
+
metadata: with_source_location(metadata)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def compose(name, contract:, inputs:, **metadata)
|
|
61
|
+
raise CompileError, "compose :#{name} requires an `inputs:` hash" unless inputs.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
add_node(
|
|
64
|
+
Model::CompositionNode.new(
|
|
65
|
+
id: next_id,
|
|
66
|
+
name: name,
|
|
67
|
+
contract_class: contract,
|
|
68
|
+
input_mapping: inputs,
|
|
69
|
+
metadata: with_source_location(metadata)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def compile
|
|
75
|
+
Compiler::GraphCompiler.call(Model::Graph.new(name: @name, nodes: @nodes))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def add_node(node)
|
|
81
|
+
@nodes << node
|
|
82
|
+
node
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def next_id
|
|
86
|
+
@sequence += 1
|
|
87
|
+
"#{@name}:#{@sequence}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def with_source_location(metadata)
|
|
91
|
+
metadata.merge(source_location: caller_locations(2, 1).first&.to_s)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/igniter/dsl.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :context
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, context: {})
|
|
8
|
+
@context = context.compact.freeze
|
|
9
|
+
super(format_message(message, @context))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def graph
|
|
13
|
+
context[:graph]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def node_id
|
|
17
|
+
context[:node_id]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def node_name
|
|
21
|
+
context[:node_name]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def node_path
|
|
25
|
+
context[:node_path]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def source_location
|
|
29
|
+
context[:source_location]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def format_message(message, context)
|
|
35
|
+
details = []
|
|
36
|
+
details << "graph=#{context[:graph]}" if context[:graph]
|
|
37
|
+
details << "node=#{context[:node_name]}" if context[:node_name]
|
|
38
|
+
details << "path=#{context[:node_path]}" if context[:node_path]
|
|
39
|
+
details << "location=#{context[:source_location]}" if context[:source_location]
|
|
40
|
+
|
|
41
|
+
return message if details.empty?
|
|
42
|
+
|
|
43
|
+
"#{message} [#{details.join(', ')}]"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class CompileError < Error; end
|
|
48
|
+
class ValidationError < CompileError; end
|
|
49
|
+
class CycleError < ValidationError; end
|
|
50
|
+
class InputError < Error; end
|
|
51
|
+
class ResolutionError < Error; end
|
|
52
|
+
class CompositionError < Error; end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Events
|
|
7
|
+
class Bus
|
|
8
|
+
attr_reader :execution_id, :events
|
|
9
|
+
|
|
10
|
+
def initialize(execution_id: SecureRandom.uuid)
|
|
11
|
+
@execution_id = execution_id
|
|
12
|
+
@events = []
|
|
13
|
+
@subscribers = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def emit(type, node: nil, status: nil, payload: {})
|
|
17
|
+
event = Event.new(
|
|
18
|
+
event_id: SecureRandom.uuid,
|
|
19
|
+
type: type,
|
|
20
|
+
execution_id: execution_id,
|
|
21
|
+
node_id: node&.id,
|
|
22
|
+
node_name: node&.name,
|
|
23
|
+
path: node&.path,
|
|
24
|
+
status: status,
|
|
25
|
+
payload: payload,
|
|
26
|
+
timestamp: Time.now.utc
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@events << event
|
|
30
|
+
@subscribers.each { |subscriber| subscriber.call(event) }
|
|
31
|
+
event
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def subscribe(subscriber = nil, &block)
|
|
35
|
+
@subscribers << (subscriber || block)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Events
|
|
7
|
+
Event = Struct.new(
|
|
8
|
+
:event_id,
|
|
9
|
+
:type,
|
|
10
|
+
:execution_id,
|
|
11
|
+
:node_id,
|
|
12
|
+
:node_name,
|
|
13
|
+
:path,
|
|
14
|
+
:status,
|
|
15
|
+
:payload,
|
|
16
|
+
:timestamp,
|
|
17
|
+
keyword_init: true
|
|
18
|
+
) do
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
event_id: event_id,
|
|
22
|
+
type: type,
|
|
23
|
+
execution_id: execution_id,
|
|
24
|
+
node_id: node_id,
|
|
25
|
+
node_name: node_name,
|
|
26
|
+
path: path,
|
|
27
|
+
status: status,
|
|
28
|
+
payload: payload,
|
|
29
|
+
timestamp: timestamp
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def as_json(*)
|
|
34
|
+
to_h.transform_values { |value| serialize_value(value) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def serialize_value(value)
|
|
40
|
+
case value
|
|
41
|
+
when Time
|
|
42
|
+
value.iso8601
|
|
43
|
+
when Hash
|
|
44
|
+
value.each_with_object({}) { |(key, nested_value), memo| memo[key] = serialize_value(nested_value) }
|
|
45
|
+
when Array
|
|
46
|
+
value.map { |item| serialize_value(item) }
|
|
47
|
+
else
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Auditing
|
|
6
|
+
class Timeline
|
|
7
|
+
attr_reader :execution
|
|
8
|
+
|
|
9
|
+
def initialize(execution)
|
|
10
|
+
@execution = execution
|
|
11
|
+
@events = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(event)
|
|
15
|
+
@events << event
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def events
|
|
19
|
+
@events.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def snapshot
|
|
23
|
+
{
|
|
24
|
+
execution_id: execution.events.execution_id,
|
|
25
|
+
graph: execution.compiled_graph.name,
|
|
26
|
+
event_count: @events.size,
|
|
27
|
+
events: @events.map { |event| serialize_event(event) },
|
|
28
|
+
states: serialize_states,
|
|
29
|
+
children: child_snapshots
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def serialize_event(event)
|
|
36
|
+
{
|
|
37
|
+
event_id: event.event_id,
|
|
38
|
+
type: event.type,
|
|
39
|
+
execution_id: event.execution_id,
|
|
40
|
+
node_id: event.node_id,
|
|
41
|
+
node_name: event.node_name,
|
|
42
|
+
path: event.path,
|
|
43
|
+
status: event.status,
|
|
44
|
+
payload: serialize_payload(event.payload),
|
|
45
|
+
timestamp: event.timestamp
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def serialize_states
|
|
50
|
+
execution.cache.to_h.each_with_object({}) do |(node_name, state), memo|
|
|
51
|
+
memo[node_name] = {
|
|
52
|
+
path: state.node.path,
|
|
53
|
+
kind: state.node.kind,
|
|
54
|
+
status: state.status,
|
|
55
|
+
version: state.version,
|
|
56
|
+
invalidated_by: state.invalidated_by,
|
|
57
|
+
resolved_at: state.resolved_at,
|
|
58
|
+
value: serialize_value(state.value),
|
|
59
|
+
error: state.error&.message
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def child_snapshots
|
|
65
|
+
execution.cache.values.filter_map do |state|
|
|
66
|
+
next unless state.value.is_a?(Igniter::Runtime::Result)
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
node_name: state.node.name,
|
|
70
|
+
path: state.node.path,
|
|
71
|
+
snapshot: state.value.execution.audit.snapshot
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def serialize_payload(payload)
|
|
77
|
+
payload.each_with_object({}) do |(key, value), memo|
|
|
78
|
+
memo[key] = serialize_value(value)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def serialize_value(value)
|
|
83
|
+
case value
|
|
84
|
+
when Igniter::Runtime::Result
|
|
85
|
+
{
|
|
86
|
+
type: :result,
|
|
87
|
+
execution_id: value.execution.events.execution_id,
|
|
88
|
+
graph: value.execution.compiled_graph.name
|
|
89
|
+
}
|
|
90
|
+
when Array
|
|
91
|
+
value.map { |item| serialize_value(item) }
|
|
92
|
+
else
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Introspection
|
|
6
|
+
class GraphFormatter
|
|
7
|
+
def self.to_text(graph)
|
|
8
|
+
new(graph).to_text
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.to_mermaid(graph)
|
|
12
|
+
new(graph).to_mermaid
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(graph)
|
|
16
|
+
@graph = graph
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_text
|
|
20
|
+
lines = []
|
|
21
|
+
lines << "Graph #{@graph.name}"
|
|
22
|
+
lines << "Nodes:"
|
|
23
|
+
@graph.nodes.each do |node|
|
|
24
|
+
line = "- #{node.kind} #{node.path}"
|
|
25
|
+
line += " depends_on=#{node.dependencies.join(',')}" if node.dependencies.any?
|
|
26
|
+
if node.kind == :composition
|
|
27
|
+
line += " contract=#{node.contract_class.name || 'AnonymousContract'}"
|
|
28
|
+
end
|
|
29
|
+
lines << line
|
|
30
|
+
end
|
|
31
|
+
lines << "Outputs:"
|
|
32
|
+
@graph.outputs.each do |output|
|
|
33
|
+
lines << "- #{output.path} -> #{output.source}"
|
|
34
|
+
end
|
|
35
|
+
lines.join("\n")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_mermaid
|
|
39
|
+
lines = []
|
|
40
|
+
lines << "graph TD"
|
|
41
|
+
@graph.nodes.each do |node|
|
|
42
|
+
lines << %( #{node_id(node)}["#{node_label(node)}"])
|
|
43
|
+
end
|
|
44
|
+
@graph.outputs.each do |output|
|
|
45
|
+
lines << %( #{output_id(output)}["output: #{output.name}"])
|
|
46
|
+
lines << %( #{node_id(@graph.fetch_node(output.source))} --> #{output_id(output)})
|
|
47
|
+
end
|
|
48
|
+
@graph.nodes.each do |node|
|
|
49
|
+
node.dependencies.each do |dependency_name|
|
|
50
|
+
dependency_node = @graph.fetch_node(dependency_name)
|
|
51
|
+
lines << %( #{node_id(dependency_node)} --> #{node_id(node)})
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def node_id(node)
|
|
60
|
+
"node_#{node.name}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def output_id(output)
|
|
64
|
+
"output_#{output.name}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def node_label(node)
|
|
68
|
+
"#{node.kind}: #{node.name}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|