igniter 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +2 -2
- data/docs/API_V2.md +58 -0
- data/docs/DISTRIBUTED_CONTRACTS_V1.md +493 -0
- data/examples/README.md +3 -0
- data/examples/distributed_workflow.rb +52 -0
- data/examples/ringcentral_routing.rb +26 -35
- data/lib/igniter/compiler/compiled_graph.rb +20 -0
- data/lib/igniter/compiler/validation_pipeline.rb +3 -1
- data/lib/igniter/compiler/validators/await_validator.rb +53 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +43 -1
- data/lib/igniter/compiler/validators/remote_validator.rb +58 -0
- data/lib/igniter/compiler.rb +2 -0
- data/lib/igniter/contract.rb +75 -8
- data/lib/igniter/diagnostics/report.rb +102 -3
- data/lib/igniter/dsl/contract_builder.rb +109 -8
- data/lib/igniter/errors.rb +6 -1
- data/lib/igniter/extensions/introspection/graph_formatter.rb +4 -0
- data/lib/igniter/integrations/llm/config.rb +69 -0
- data/lib/igniter/integrations/llm/context.rb +74 -0
- data/lib/igniter/integrations/llm/executor.rb +159 -0
- data/lib/igniter/integrations/llm/providers/anthropic.rb +148 -0
- data/lib/igniter/integrations/llm/providers/base.rb +33 -0
- data/lib/igniter/integrations/llm/providers/ollama.rb +137 -0
- data/lib/igniter/integrations/llm/providers/openai.rb +153 -0
- data/lib/igniter/integrations/llm.rb +59 -0
- data/lib/igniter/integrations/rails/cable_adapter.rb +49 -0
- data/lib/igniter/integrations/rails/contract_job.rb +76 -0
- data/lib/igniter/integrations/rails/generators/contract/contract_generator.rb +22 -0
- data/lib/igniter/integrations/rails/generators/install/install_generator.rb +33 -0
- data/lib/igniter/integrations/rails/railtie.rb +25 -0
- data/lib/igniter/integrations/rails/webhook_concern.rb +49 -0
- data/lib/igniter/integrations/rails.rb +12 -0
- data/lib/igniter/model/await_node.rb +21 -0
- data/lib/igniter/model/branch_node.rb +9 -3
- data/lib/igniter/model/collection_node.rb +9 -3
- data/lib/igniter/model/remote_node.rb +26 -0
- data/lib/igniter/model.rb +2 -0
- data/lib/igniter/runtime/execution.rb +2 -2
- data/lib/igniter/runtime/input_validator.rb +5 -3
- data/lib/igniter/runtime/resolver.rb +91 -8
- data/lib/igniter/runtime/stores/active_record_store.rb +13 -1
- data/lib/igniter/runtime/stores/file_store.rb +50 -2
- data/lib/igniter/runtime/stores/memory_store.rb +55 -2
- data/lib/igniter/runtime/stores/redis_store.rb +13 -1
- data/lib/igniter/server/client.rb +123 -0
- data/lib/igniter/server/config.rb +27 -0
- data/lib/igniter/server/handlers/base.rb +105 -0
- data/lib/igniter/server/handlers/contracts_handler.rb +15 -0
- data/lib/igniter/server/handlers/event_handler.rb +28 -0
- data/lib/igniter/server/handlers/execute_handler.rb +37 -0
- data/lib/igniter/server/handlers/health_handler.rb +32 -0
- data/lib/igniter/server/handlers/status_handler.rb +27 -0
- data/lib/igniter/server/http_server.rb +109 -0
- data/lib/igniter/server/rack_app.rb +35 -0
- data/lib/igniter/server/registry.rb +56 -0
- data/lib/igniter/server/router.rb +75 -0
- data/lib/igniter/server.rb +67 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +4 -0
- metadata +36 -2
|
@@ -104,29 +104,26 @@ class CallConnectedContract < Igniter::Contract
|
|
|
104
104
|
active_calls.any?
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
map :call_inputs, from: :active_calls do |active_calls:|
|
|
108
|
-
active_calls.map do |call|
|
|
109
|
-
{
|
|
110
|
-
session_id: call.fetch("telephonySessionId"),
|
|
111
|
-
direction: call.fetch("direction"),
|
|
112
|
-
from: call.fetch("from"),
|
|
113
|
-
to: call.fetch("to"),
|
|
114
|
-
start_time: call.fetch("startTime")
|
|
115
|
-
}
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
107
|
collection :calls,
|
|
120
|
-
with: :
|
|
108
|
+
with: :active_calls,
|
|
121
109
|
each: CallEventContract,
|
|
122
110
|
key: :session_id,
|
|
123
|
-
mode: :collect
|
|
111
|
+
mode: :collect,
|
|
112
|
+
map_inputs: lambda { |item:|
|
|
113
|
+
{
|
|
114
|
+
session_id: item.fetch("telephonySessionId"),
|
|
115
|
+
direction: item.fetch("direction"),
|
|
116
|
+
from: item.fetch("from"),
|
|
117
|
+
to: item.fetch("to"),
|
|
118
|
+
start_time: item.fetch("startTime")
|
|
119
|
+
}
|
|
120
|
+
}
|
|
124
121
|
|
|
125
122
|
compute :call_summaries, with: :calls do |calls:|
|
|
126
123
|
calls.successes.values.map { |item| item.result.summary }
|
|
127
124
|
end
|
|
128
125
|
|
|
129
|
-
|
|
126
|
+
aggregate :routing_summary, with: %i[calls call_summaries extension_id telephony_status has_calls] do |calls:, call_summaries:, extension_id:, telephony_status:, has_calls:|
|
|
130
127
|
has_calls
|
|
131
128
|
{
|
|
132
129
|
extension_id: extension_id,
|
|
@@ -204,28 +201,22 @@ class RingcentralWebhookContract < Igniter::Contract
|
|
|
204
201
|
input :payload
|
|
205
202
|
|
|
206
203
|
scope :parse do
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
map :telephony_status, from: :body do |body:|
|
|
212
|
-
body["telephonyStatus"]
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
map :extension_id, from: :body do |body:|
|
|
216
|
-
body["extensionId"]
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
map :active_calls, from: :body do |body:|
|
|
220
|
-
body["activeCalls"] || []
|
|
221
|
-
end
|
|
204
|
+
project :body, from: :payload, key: :body, default: {}
|
|
205
|
+
project :telephony_status, from: :body, key: "telephonyStatus"
|
|
206
|
+
project :extension_id, from: :body, key: "extensionId"
|
|
207
|
+
project :active_calls, from: :body, key: "activeCalls", default: []
|
|
222
208
|
end
|
|
223
209
|
|
|
224
|
-
branch :status_route,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
210
|
+
branch :status_route,
|
|
211
|
+
with: :telephony_status,
|
|
212
|
+
depends_on: %i[extension_id active_calls],
|
|
213
|
+
map_inputs: lambda { |selector:, extension_id:, active_calls:|
|
|
214
|
+
{
|
|
215
|
+
extension_id: extension_id,
|
|
216
|
+
telephony_status: selector,
|
|
217
|
+
active_calls: active_calls
|
|
218
|
+
}
|
|
219
|
+
} do
|
|
229
220
|
on "CallConnected", contract: CallConnectedContract
|
|
230
221
|
on "NoCall", contract: NoCallContract
|
|
231
222
|
on "Ringing", contract: RingingContract
|
|
@@ -49,6 +49,10 @@ module Igniter
|
|
|
49
49
|
raise KeyError, "Unknown dependency '#{name}'"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def await_nodes
|
|
53
|
+
@nodes.select { |n| n.kind == :await }
|
|
54
|
+
end
|
|
55
|
+
|
|
52
56
|
def to_h
|
|
53
57
|
{
|
|
54
58
|
name: name,
|
|
@@ -66,16 +70,21 @@ module Igniter
|
|
|
66
70
|
end
|
|
67
71
|
if node.kind == :branch
|
|
68
72
|
base[:selector] = node.selector_dependency
|
|
73
|
+
base[:depends_on] = node.context_dependencies if node.context_dependencies.any?
|
|
69
74
|
base[:cases] = node.cases.map { |entry| { match: entry[:match], contract: entry[:contract].name } }
|
|
70
75
|
base[:default_contract] = node.default_contract.name
|
|
71
76
|
base[:inputs] = node.input_mapping
|
|
77
|
+
base[:mapper] = node.input_mapper.to_s if node.input_mapper?
|
|
72
78
|
end
|
|
73
79
|
if node.kind == :collection
|
|
74
80
|
base[:with] = node.source_dependency
|
|
81
|
+
base[:depends_on] = node.context_dependencies if node.context_dependencies.any?
|
|
75
82
|
base[:each] = node.contract_class.name
|
|
76
83
|
base[:key] = node.key_name
|
|
77
84
|
base[:mode] = node.mode
|
|
85
|
+
base[:mapper] = node.input_mapper.to_s if node.input_mapper?
|
|
78
86
|
end
|
|
87
|
+
base[:event] = node.event_name if node.kind == :await
|
|
79
88
|
base
|
|
80
89
|
end,
|
|
81
90
|
outputs: outputs.map do |output|
|
|
@@ -113,11 +122,20 @@ module Igniter
|
|
|
113
122
|
metadata: node.metadata.reject { |key, _| key == :source_location }
|
|
114
123
|
}
|
|
115
124
|
end,
|
|
125
|
+
awaits: nodes.select { |node| node.kind == :await }.map do |node|
|
|
126
|
+
{
|
|
127
|
+
name: node.name,
|
|
128
|
+
event: node.event_name,
|
|
129
|
+
metadata: node.metadata.reject { |key, _| key == :source_location }
|
|
130
|
+
}
|
|
131
|
+
end,
|
|
116
132
|
branches: nodes.select { |node| node.kind == :branch }.map do |node|
|
|
117
133
|
{
|
|
118
134
|
name: node.name,
|
|
119
135
|
with: node.selector_dependency,
|
|
136
|
+
depends_on: node.context_dependencies,
|
|
120
137
|
inputs: node.input_mapping,
|
|
138
|
+
map_inputs: (node.input_mapper if node.input_mapper?),
|
|
121
139
|
cases: node.cases.map { |entry| { on: entry[:match], contract: entry[:contract] } },
|
|
122
140
|
default: node.default_contract,
|
|
123
141
|
metadata: node.metadata.reject { |key, _| key == :source_location }
|
|
@@ -127,9 +145,11 @@ module Igniter
|
|
|
127
145
|
{
|
|
128
146
|
name: node.name,
|
|
129
147
|
with: node.source_dependency,
|
|
148
|
+
depends_on: node.context_dependencies,
|
|
130
149
|
each: node.contract_class,
|
|
131
150
|
key: node.key_name,
|
|
132
151
|
mode: node.mode,
|
|
152
|
+
map_inputs: (node.input_mapper if node.input_mapper?),
|
|
133
153
|
metadata: node.metadata.reject { |key, _| key == :source_location }
|
|
134
154
|
}
|
|
135
155
|
end,
|
|
@@ -8,7 +8,9 @@ module Igniter
|
|
|
8
8
|
Validators::OutputsValidator,
|
|
9
9
|
Validators::DependenciesValidator,
|
|
10
10
|
Validators::TypeCompatibilityValidator,
|
|
11
|
-
Validators::CallableValidator
|
|
11
|
+
Validators::CallableValidator,
|
|
12
|
+
Validators::AwaitValidator,
|
|
13
|
+
Validators::RemoteValidator
|
|
12
14
|
].freeze
|
|
13
15
|
|
|
14
16
|
def self.call(context, validators: DEFAULT_VALIDATORS)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class AwaitValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
await_nodes = @context.runtime_nodes.select { |n| n.kind == :await }
|
|
17
|
+
return if await_nodes.empty?
|
|
18
|
+
|
|
19
|
+
validate_correlation_keys_as_inputs!(await_nodes)
|
|
20
|
+
validate_unique_event_names!(await_nodes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_correlation_keys_as_inputs!(await_nodes) # rubocop:disable Metrics/AbcSize
|
|
26
|
+
correlation_keys = @context.graph.metadata[:correlation_keys] || []
|
|
27
|
+
return if correlation_keys.empty?
|
|
28
|
+
|
|
29
|
+
input_names = @context.runtime_nodes.select { |n| n.kind == :input }.map(&:name)
|
|
30
|
+
missing = correlation_keys.reject { |key| input_names.include?(key.to_sym) }
|
|
31
|
+
return if missing.empty?
|
|
32
|
+
|
|
33
|
+
raise @context.validation_error(
|
|
34
|
+
await_nodes.first,
|
|
35
|
+
"Correlation keys #{missing.inspect} must be declared as inputs"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_unique_event_names!(await_nodes)
|
|
40
|
+
event_names = await_nodes.map(&:event_name)
|
|
41
|
+
duplicates = event_names.select { |e| event_names.count(e) > 1 }.uniq
|
|
42
|
+
return if duplicates.empty?
|
|
43
|
+
|
|
44
|
+
node = await_nodes.find { |n| duplicates.include?(n.event_name) }
|
|
45
|
+
raise @context.validation_error(
|
|
46
|
+
node,
|
|
47
|
+
"Duplicate await event names: #{duplicates.inspect}"
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -12,8 +12,10 @@ module Igniter
|
|
|
12
12
|
@context = context
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def call
|
|
15
|
+
def call # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
16
16
|
@context.runtime_nodes.each do |node|
|
|
17
|
+
next if node.kind == :await
|
|
18
|
+
|
|
17
19
|
validate_composition_node!(node) if node.kind == :composition
|
|
18
20
|
validate_branch_node!(node) if node.kind == :branch
|
|
19
21
|
validate_collection_node!(node) if node.kind == :collection
|
|
@@ -40,6 +42,7 @@ module Igniter
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
validate_composition_input_mapping!(node, contract_class.compiled_graph)
|
|
45
|
+
validate_composition_cycle!(node)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def validate_composition_input_mapping!(node, child_graph)
|
|
@@ -100,6 +103,8 @@ module Igniter
|
|
|
100
103
|
end
|
|
101
104
|
|
|
102
105
|
def validate_branch_input_mapping!(node, child_graph)
|
|
106
|
+
return if node.input_mapper?
|
|
107
|
+
|
|
103
108
|
child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
|
|
104
109
|
child_input_names = child_input_nodes.map(&:name)
|
|
105
110
|
|
|
@@ -124,6 +129,43 @@ module Igniter
|
|
|
124
129
|
)
|
|
125
130
|
end
|
|
126
131
|
|
|
132
|
+
def validate_composition_cycle!(node)
|
|
133
|
+
child_contract = node.contract_class
|
|
134
|
+
return unless child_contract.respond_to?(:compiled_graph) && child_contract.compiled_graph
|
|
135
|
+
|
|
136
|
+
current_name = @context.graph.name
|
|
137
|
+
# Skip anonymous contracts to avoid false positives when multiple
|
|
138
|
+
# anonymous contracts share the same name "AnonymousContract"
|
|
139
|
+
return if current_name == "AnonymousContract"
|
|
140
|
+
|
|
141
|
+
validate_direct_cycle!(node, child_contract, current_name)
|
|
142
|
+
validate_grandchild_cycles!(node, child_contract, current_name)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def validate_direct_cycle!(node, child_contract, current_name)
|
|
146
|
+
return unless child_contract.compiled_graph.name == current_name
|
|
147
|
+
|
|
148
|
+
raise @context.validation_error(
|
|
149
|
+
node,
|
|
150
|
+
"Composition cycle: '#{node.name}' composes '#{child_contract.name}' " \
|
|
151
|
+
"which is the same contract ('#{current_name}')"
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def validate_grandchild_cycles!(node, child_contract, current_name) # rubocop:disable Metrics/AbcSize
|
|
156
|
+
child_contract.compiled_graph.nodes.select { |n| n.kind == :composition }.each do |grandchild|
|
|
157
|
+
next unless grandchild.contract_class.respond_to?(:compiled_graph)
|
|
158
|
+
next unless grandchild.contract_class.compiled_graph
|
|
159
|
+
next unless grandchild.contract_class.compiled_graph.name == current_name
|
|
160
|
+
|
|
161
|
+
raise @context.validation_error(
|
|
162
|
+
node,
|
|
163
|
+
"Composition cycle: '#{node.name}' -> '#{child_contract.name}' -> " \
|
|
164
|
+
"'#{grandchild.contract_class.name}' loops back to '#{current_name}'"
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
127
169
|
def validate_collection_node!(node)
|
|
128
170
|
unless node.contract_class.is_a?(Class) && node.contract_class <= Igniter::Contract
|
|
129
171
|
raise @context.validation_error(node, "Collection '#{node.name}' must reference an Igniter::Contract subclass")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Compiler
|
|
5
|
+
module Validators
|
|
6
|
+
class RemoteValidator
|
|
7
|
+
def self.call(context)
|
|
8
|
+
new(context).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(context)
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@context.runtime_nodes.each do |node|
|
|
17
|
+
next unless node.kind == :remote
|
|
18
|
+
|
|
19
|
+
validate_url!(node)
|
|
20
|
+
validate_contract_name!(node)
|
|
21
|
+
validate_dependencies!(node)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate_url!(node)
|
|
28
|
+
return if node.node_url.start_with?("http://", "https://")
|
|
29
|
+
|
|
30
|
+
raise @context.validation_error(
|
|
31
|
+
node,
|
|
32
|
+
"remote :#{node.name} has invalid node: URL '#{node.node_url}'. Must start with http:// or https://"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_contract_name!(node)
|
|
37
|
+
return unless node.contract_name.strip.empty?
|
|
38
|
+
|
|
39
|
+
raise @context.validation_error(
|
|
40
|
+
node,
|
|
41
|
+
"remote :#{node.name} requires a non-empty contract: name"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_dependencies!(node)
|
|
46
|
+
node.dependencies.each do |dep_name|
|
|
47
|
+
next if @context.dependency_resolvable?(dep_name)
|
|
48
|
+
|
|
49
|
+
raise @context.validation_error(
|
|
50
|
+
node,
|
|
51
|
+
"remote :#{node.name} depends on '#{dep_name}' which is not defined in the graph"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/igniter/compiler.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "compiler/validators/outputs_validator"
|
|
|
8
8
|
require_relative "compiler/validators/dependencies_validator"
|
|
9
9
|
require_relative "compiler/validators/callable_validator"
|
|
10
10
|
require_relative "compiler/validators/type_compatibility_validator"
|
|
11
|
+
require_relative "compiler/validators/await_validator"
|
|
12
|
+
require_relative "compiler/validators/remote_validator"
|
|
11
13
|
require_relative "compiler/validation_pipeline"
|
|
12
14
|
require_relative "compiler/validator"
|
|
13
15
|
require_relative "compiler/graph_compiler"
|
data/lib/igniter/contract.rb
CHANGED
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
module Igniter
|
|
4
4
|
class Contract
|
|
5
5
|
class << self
|
|
6
|
+
def correlate_by(*keys)
|
|
7
|
+
@correlation_keys = keys.map(&:to_sym).freeze
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def correlation_keys
|
|
11
|
+
@correlation_keys || []
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
def define(&block)
|
|
7
|
-
@compiled_graph = DSL::ContractBuilder.compile(
|
|
15
|
+
@compiled_graph = DSL::ContractBuilder.compile(
|
|
16
|
+
name: contract_name,
|
|
17
|
+
correlation_keys: correlation_keys,
|
|
18
|
+
&block
|
|
19
|
+
)
|
|
8
20
|
end
|
|
9
21
|
|
|
10
22
|
def run_with(runner:, max_workers: nil)
|
|
@@ -28,6 +40,45 @@ module Igniter
|
|
|
28
40
|
@compiled_graph = DSL::SchemaBuilder.compile(schema, name: contract_name)
|
|
29
41
|
end
|
|
30
42
|
|
|
43
|
+
def start(inputs = {}, store: nil, **keyword_inputs)
|
|
44
|
+
resolved_store = store || Igniter.execution_store
|
|
45
|
+
all_inputs = inputs.merge(keyword_inputs)
|
|
46
|
+
|
|
47
|
+
instance = new(all_inputs, runner: :store, store: resolved_store)
|
|
48
|
+
instance.resolve_all
|
|
49
|
+
|
|
50
|
+
correlation = correlation_keys.each_with_object({}) do |key, hash|
|
|
51
|
+
hash[key] = all_inputs[key] || all_inputs[key.to_s]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
resolved_store.save(instance.snapshot, correlation: correlation.compact, graph: contract_name)
|
|
55
|
+
instance
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def deliver_event(event_name, correlation:, payload:, store: nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
59
|
+
resolved_store = store || Igniter.execution_store
|
|
60
|
+
execution_id = resolved_store.find_by_correlation(
|
|
61
|
+
graph: contract_name,
|
|
62
|
+
correlation: correlation.transform_keys(&:to_sym)
|
|
63
|
+
)
|
|
64
|
+
unless execution_id
|
|
65
|
+
raise ResolutionError,
|
|
66
|
+
"No pending execution found for #{contract_name} with given correlation"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
instance = restore_from_store(execution_id, store: resolved_store)
|
|
70
|
+
|
|
71
|
+
await_node = instance.execution.compiled_graph.await_nodes
|
|
72
|
+
.find { |n| n.event_name == event_name.to_sym }
|
|
73
|
+
raise ResolutionError, "No await node found for event '#{event_name}' in #{contract_name}" unless await_node
|
|
74
|
+
|
|
75
|
+
instance.execution.resume(await_node.name, value: payload)
|
|
76
|
+
instance.resolve_all
|
|
77
|
+
|
|
78
|
+
resolved_store.save(instance.snapshot, correlation: correlation.transform_keys(&:to_sym), graph: contract_name)
|
|
79
|
+
instance
|
|
80
|
+
end
|
|
81
|
+
|
|
31
82
|
def restore(snapshot)
|
|
32
83
|
instance = new(
|
|
33
84
|
snapshot[:inputs] || snapshot["inputs"] || {},
|
|
@@ -103,6 +154,13 @@ module Igniter
|
|
|
103
154
|
react_to(:execution_failed, once_per_execution: true, &terminal_hook)
|
|
104
155
|
end
|
|
105
156
|
|
|
157
|
+
def present(output_name, with: nil, &block)
|
|
158
|
+
raise CompileError, "present requires a block or `with:`" unless block || with
|
|
159
|
+
raise CompileError, "present cannot use both a block and `with:`" if block && with
|
|
160
|
+
|
|
161
|
+
own_output_presenters[output_name.to_sym] = with || block
|
|
162
|
+
end
|
|
163
|
+
|
|
106
164
|
def compiled_graph
|
|
107
165
|
@compiled_graph || superclass_compiled_graph
|
|
108
166
|
end
|
|
@@ -116,8 +174,17 @@ module Igniter
|
|
|
116
174
|
@execution_options || superclass_execution_options || {}
|
|
117
175
|
end
|
|
118
176
|
|
|
177
|
+
def output_presenters
|
|
178
|
+
inherited = superclass.respond_to?(:output_presenters) ? superclass.output_presenters : {}
|
|
179
|
+
inherited.merge(own_output_presenters)
|
|
180
|
+
end
|
|
181
|
+
|
|
119
182
|
private
|
|
120
183
|
|
|
184
|
+
def own_output_presenters
|
|
185
|
+
@output_presenters ||= {}
|
|
186
|
+
end
|
|
187
|
+
|
|
121
188
|
def contract_name
|
|
122
189
|
name || "AnonymousContract"
|
|
123
190
|
end
|
|
@@ -143,9 +210,9 @@ module Igniter
|
|
|
143
210
|
end
|
|
144
211
|
end
|
|
145
212
|
|
|
146
|
-
attr_reader :execution, :result
|
|
213
|
+
attr_reader :execution, :result, :reactive
|
|
147
214
|
|
|
148
|
-
def initialize(inputs = nil, runner: nil, max_workers: nil, **keyword_inputs)
|
|
215
|
+
def initialize(inputs = nil, runner: nil, max_workers: nil, store: nil, **keyword_inputs)
|
|
149
216
|
graph = self.class.compiled_graph
|
|
150
217
|
raise CompileError, "#{self.class.name} has no compiled graph. Use `define`." unless graph
|
|
151
218
|
|
|
@@ -159,7 +226,7 @@ module Igniter
|
|
|
159
226
|
end
|
|
160
227
|
|
|
161
228
|
execution_options = self.class.execution_options.merge(
|
|
162
|
-
{ runner: runner, max_workers: max_workers }.compact
|
|
229
|
+
{ runner: runner, max_workers: max_workers, store: store }.compact
|
|
163
230
|
)
|
|
164
231
|
execution_options[:store] ||= Igniter.execution_store if execution_options[:runner]&.to_sym == :store
|
|
165
232
|
|
|
@@ -204,10 +271,6 @@ module Igniter
|
|
|
204
271
|
execution.audit.snapshot
|
|
205
272
|
end
|
|
206
273
|
|
|
207
|
-
def reactive
|
|
208
|
-
@reactive
|
|
209
|
-
end
|
|
210
|
-
|
|
211
274
|
def subscribe(subscriber = nil, &block)
|
|
212
275
|
execution.events.subscribe(subscriber, &block)
|
|
213
276
|
self
|
|
@@ -245,5 +308,9 @@ module Igniter
|
|
|
245
308
|
def failed?
|
|
246
309
|
execution.failed?
|
|
247
310
|
end
|
|
311
|
+
|
|
312
|
+
def pending?
|
|
313
|
+
execution.pending?
|
|
314
|
+
end
|
|
248
315
|
end
|
|
249
316
|
end
|
|
@@ -32,7 +32,7 @@ module Igniter
|
|
|
32
32
|
lines << "Diagnostics #{report[:graph]}"
|
|
33
33
|
lines << "Execution #{report[:execution_id]}"
|
|
34
34
|
lines << "Status: #{report[:status]}"
|
|
35
|
-
lines << format_outputs(
|
|
35
|
+
lines << format_outputs(presented_outputs)
|
|
36
36
|
lines << format_nodes(report[:nodes])
|
|
37
37
|
lines << format_collection_nodes(report[:collection_nodes])
|
|
38
38
|
lines << format_errors(report[:errors])
|
|
@@ -47,7 +47,7 @@ module Igniter
|
|
|
47
47
|
lines << ""
|
|
48
48
|
lines << "- Execution: `#{report[:execution_id]}`"
|
|
49
49
|
lines << "- Status: `#{report[:status]}`"
|
|
50
|
-
lines << "- Outputs: #{inline_hash(
|
|
50
|
+
lines << "- Outputs: #{inline_hash(presented_outputs)}"
|
|
51
51
|
lines << "- Nodes: total=#{report[:nodes][:total]}, succeeded=#{report[:nodes][:succeeded]}, failed=#{report[:nodes][:failed]}, stale=#{report[:nodes][:stale]}"
|
|
52
52
|
unless report[:collection_nodes].empty?
|
|
53
53
|
lines << "- Collections: #{report[:collection_nodes].map { |node| "#{node[:node_name]} total=#{node[:total]} succeeded=#{node[:succeeded]} failed=#{node[:failed]} status=#{node[:status]}" }.join('; ')}"
|
|
@@ -170,6 +170,13 @@ module Igniter
|
|
|
170
170
|
"Outputs: #{inline_hash(outputs)}"
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
def presented_outputs
|
|
174
|
+
@presented_outputs ||= execution.compiled_graph.outputs.each_with_object({}) do |output_node, memo|
|
|
175
|
+
raw_value = to_h[:outputs][output_node.name]
|
|
176
|
+
memo[output_node.name] = present_output(output_node.name, raw_value)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
173
180
|
def format_nodes(nodes)
|
|
174
181
|
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, stale=#{nodes[:stale]}"
|
|
175
182
|
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, pending=#{nodes[:pending]}, stale=#{nodes[:stale]}"
|
|
@@ -203,7 +210,27 @@ module Igniter
|
|
|
203
210
|
end
|
|
204
211
|
|
|
205
212
|
def inline_hash(hash)
|
|
206
|
-
hash.map { |key, value| "#{key}=#{value
|
|
213
|
+
hash.map { |key, value| "#{key}=#{inline_value(value)}" }.join(", ")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def present_output(output_name, raw_value)
|
|
217
|
+
presenter = execution.contract_instance.class.output_presenters[output_name.to_sym]
|
|
218
|
+
return raw_value unless presenter
|
|
219
|
+
|
|
220
|
+
if presenter.is_a?(Symbol) || presenter.is_a?(String)
|
|
221
|
+
execution.contract_instance.public_send(
|
|
222
|
+
presenter,
|
|
223
|
+
value: raw_value,
|
|
224
|
+
contract: execution.contract_instance,
|
|
225
|
+
execution: execution
|
|
226
|
+
)
|
|
227
|
+
else
|
|
228
|
+
presenter.call(
|
|
229
|
+
value: raw_value,
|
|
230
|
+
contract: execution.contract_instance,
|
|
231
|
+
execution: execution
|
|
232
|
+
)
|
|
233
|
+
end
|
|
207
234
|
end
|
|
208
235
|
|
|
209
236
|
def serialize_value(value)
|
|
@@ -221,6 +248,78 @@ module Igniter
|
|
|
221
248
|
end
|
|
222
249
|
end
|
|
223
250
|
|
|
251
|
+
def inline_value(value)
|
|
252
|
+
case value
|
|
253
|
+
when Hash
|
|
254
|
+
return summarize_serialized_collection_hash(value) if serialized_collection_hash?(value)
|
|
255
|
+
return summarize_serialized_collection_items_hash(value) if serialized_collection_items_hash?(value)
|
|
256
|
+
|
|
257
|
+
"{#{value.map { |key, nested| "#{key}: #{inline_value(nested)}" }.join(', ')}}"
|
|
258
|
+
when Array
|
|
259
|
+
"[#{value.map { |item| inline_value(item) }.join(', ')}]"
|
|
260
|
+
when Runtime::Result
|
|
261
|
+
summarize_nested_result(value)
|
|
262
|
+
when Runtime::CollectionResult
|
|
263
|
+
summarize_collection_result(value)
|
|
264
|
+
else
|
|
265
|
+
value.inspect
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def summarize_nested_result(result)
|
|
270
|
+
outputs = result.to_h.keys
|
|
271
|
+
"{graph=#{result.execution.compiled_graph.name.inspect}, status=#{nested_result_status(result).inspect}, outputs=#{outputs.inspect}}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def summarize_collection_result(result)
|
|
275
|
+
summary = result.summary
|
|
276
|
+
failed_keys = result.failures.keys
|
|
277
|
+
"{mode=#{result.mode.inspect}, total=#{summary[:total]}, succeeded=#{summary[:succeeded]}, failed=#{summary[:failed]}, status=#{summary[:status].inspect}, keys=#{result.keys.inspect}, failed_keys=#{failed_keys.inspect}}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def serialized_collection_hash?(value)
|
|
281
|
+
value.key?(:mode) && value.key?(:summary) && value.key?(:items)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def summarize_serialized_collection_hash(value)
|
|
285
|
+
summary = value[:summary] || {}
|
|
286
|
+
items = value[:items] || {}
|
|
287
|
+
failed_keys = items.each_with_object([]) do |(key, item), memo|
|
|
288
|
+
memo << key if item[:status] == :failed || item["status"] == :failed
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
"{mode=#{value[:mode].inspect}, total=#{summary[:total]}, succeeded=#{summary[:succeeded]}, failed=#{summary[:failed]}, status=#{summary[:status].inspect}, keys=#{items.keys.inspect}, failed_keys=#{failed_keys.inspect}}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def serialized_collection_items_hash?(value)
|
|
295
|
+
return false if value.empty?
|
|
296
|
+
|
|
297
|
+
value.values.all? do |item|
|
|
298
|
+
item.is_a?(Hash) && (item.key?(:key) || item.key?("key")) && (item.key?(:status) || item.key?("status"))
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def summarize_serialized_collection_items_hash(value)
|
|
303
|
+
failed_keys = value.each_with_object([]) do |(key, item), memo|
|
|
304
|
+
status = item[:status] || item["status"]
|
|
305
|
+
memo << key if status == :failed || status == "failed"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
total = value.size
|
|
309
|
+
failed = failed_keys.size
|
|
310
|
+
succeeded = total - failed
|
|
311
|
+
status = failed.zero? ? :succeeded : :partial_failure
|
|
312
|
+
|
|
313
|
+
"{mode=:collect, total=#{total}, succeeded=#{succeeded}, failed=#{failed}, status=#{status.inspect}, keys=#{value.keys.inspect}, failed_keys=#{failed_keys.inspect}}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def nested_result_status(result)
|
|
317
|
+
return :failed if result.failed?
|
|
318
|
+
return :pending if result.pending?
|
|
319
|
+
|
|
320
|
+
:succeeded
|
|
321
|
+
end
|
|
322
|
+
|
|
224
323
|
def summarize_collection_nodes
|
|
225
324
|
execution.cache.values.filter_map do |state|
|
|
226
325
|
next unless state.value.is_a?(Runtime::CollectionResult)
|