igniter 0.2.0 → 0.3.1
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 +21 -0
- data/README.md +224 -1
- data/docs/API_V2.md +296 -1
- data/docs/BACKLOG.md +166 -0
- data/docs/BRANCHES_V1.md +213 -0
- data/docs/COLLECTIONS_V1.md +303 -0
- data/docs/EXECUTION_MODEL_V2.md +79 -0
- data/docs/PATTERNS.md +222 -0
- data/docs/STORE_ADAPTERS.md +126 -0
- data/examples/README.md +127 -0
- data/examples/async_store.rb +47 -0
- data/examples/collection.rb +43 -0
- data/examples/collection_partial_failure.rb +50 -0
- data/examples/marketing_ergonomics.rb +57 -0
- data/examples/ringcentral_routing.rb +269 -0
- data/lib/igniter/compiler/compiled_graph.rb +90 -0
- data/lib/igniter/compiler/graph_compiler.rb +12 -2
- data/lib/igniter/compiler/type_resolver.rb +54 -0
- data/lib/igniter/compiler/validation_context.rb +61 -0
- data/lib/igniter/compiler/validation_pipeline.rb +30 -0
- data/lib/igniter/compiler/validator.rb +1 -187
- data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +153 -0
- data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
- data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
- data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
- data/lib/igniter/compiler.rb +8 -0
- data/lib/igniter/contract.rb +152 -4
- data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
- data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
- data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
- data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
- data/lib/igniter/diagnostics/report.rb +186 -11
- data/lib/igniter/dsl/contract_builder.rb +271 -5
- data/lib/igniter/dsl/schema_builder.rb +73 -0
- data/lib/igniter/dsl.rb +1 -0
- data/lib/igniter/errors.rb +11 -0
- data/lib/igniter/events/bus.rb +5 -0
- data/lib/igniter/events/event.rb +29 -0
- data/lib/igniter/executor.rb +74 -0
- data/lib/igniter/executor_registry.rb +44 -0
- data/lib/igniter/extensions/auditing/timeline.rb +4 -0
- data/lib/igniter/extensions/introspection/graph_formatter.rb +33 -3
- data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
- data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
- data/lib/igniter/extensions/introspection.rb +1 -0
- data/lib/igniter/extensions/reactive/engine.rb +49 -2
- data/lib/igniter/extensions/reactive/reaction.rb +3 -2
- data/lib/igniter/model/branch_node.rb +46 -0
- data/lib/igniter/model/collection_node.rb +31 -0
- data/lib/igniter/model/composition_node.rb +2 -2
- data/lib/igniter/model/compute_node.rb +58 -2
- data/lib/igniter/model/input_node.rb +2 -2
- data/lib/igniter/model/output_node.rb +24 -4
- data/lib/igniter/model.rb +2 -0
- data/lib/igniter/runtime/cache.rb +64 -25
- data/lib/igniter/runtime/collection_result.rb +111 -0
- data/lib/igniter/runtime/deferred_result.rb +40 -0
- data/lib/igniter/runtime/execution.rb +261 -11
- data/lib/igniter/runtime/input_validator.rb +2 -24
- data/lib/igniter/runtime/invalidator.rb +1 -1
- data/lib/igniter/runtime/job_worker.rb +18 -0
- data/lib/igniter/runtime/node_state.rb +20 -0
- data/lib/igniter/runtime/planner.rb +126 -0
- data/lib/igniter/runtime/resolver.rb +310 -15
- data/lib/igniter/runtime/result.rb +14 -2
- data/lib/igniter/runtime/runner_factory.rb +20 -0
- data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
- data/lib/igniter/runtime/runners/store_runner.rb +29 -0
- data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
- data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
- data/lib/igniter/runtime/stores/file_store.rb +43 -0
- data/lib/igniter/runtime/stores/memory_store.rb +40 -0
- data/lib/igniter/runtime/stores/redis_store.rb +44 -0
- data/lib/igniter/runtime.rb +12 -0
- data/lib/igniter/type_system.rb +44 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +23 -0
- metadata +43 -2
data/lib/igniter/dsl.rb
CHANGED
data/lib/igniter/errors.rb
CHANGED
|
@@ -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
|
data/lib/igniter/events/bus.rb
CHANGED
|
@@ -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
|
data/lib/igniter/events/event.rb
CHANGED
|
@@ -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,35 @@ 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 += " depends_on=#{node.context_dependencies.join(',')}" if node.context_dependencies.any?
|
|
43
|
+
line += " cases=#{cases.join('|')}"
|
|
44
|
+
line += " default=#{node.default_contract.name || 'AnonymousContract'}"
|
|
45
|
+
line += " mapper=#{node.input_mapper}" if node.input_mapper?
|
|
46
|
+
end
|
|
47
|
+
if node.kind == :collection
|
|
48
|
+
line += " with=#{node.source_dependency}"
|
|
49
|
+
line += " depends_on=#{node.context_dependencies.join(',')}" if node.context_dependencies.any?
|
|
50
|
+
line += " each=#{node.contract_class.name || 'AnonymousContract'}"
|
|
51
|
+
line += " key=#{node.key_name}"
|
|
52
|
+
line += " mode=#{node.mode}"
|
|
53
|
+
line += " mapper=#{node.input_mapper}" if node.input_mapper?
|
|
54
|
+
end
|
|
29
55
|
lines << line
|
|
30
56
|
end
|
|
31
57
|
lines << "Outputs:"
|
|
@@ -43,11 +69,11 @@ module Igniter
|
|
|
43
69
|
end
|
|
44
70
|
@graph.outputs.each do |output|
|
|
45
71
|
lines << %( #{output_id(output)}["output: #{output.name}"])
|
|
46
|
-
lines << %( #{node_id(@graph.fetch_node(output.
|
|
72
|
+
lines << %( #{node_id(@graph.fetch_node(output.source_root))} --> #{output_id(output)})
|
|
47
73
|
end
|
|
48
74
|
@graph.nodes.each do |node|
|
|
49
75
|
node.dependencies.each do |dependency_name|
|
|
50
|
-
dependency_node = @graph.
|
|
76
|
+
dependency_node = @graph.fetch_dependency(dependency_name)
|
|
51
77
|
lines << %( #{node_id(dependency_node)} --> #{node_id(node)})
|
|
52
78
|
end
|
|
53
79
|
end
|
|
@@ -57,6 +83,8 @@ module Igniter
|
|
|
57
83
|
private
|
|
58
84
|
|
|
59
85
|
def node_id(node)
|
|
86
|
+
return "output_#{node.name}" if node.kind == :output
|
|
87
|
+
|
|
60
88
|
"node_#{node.name}"
|
|
61
89
|
end
|
|
62
90
|
|
|
@@ -65,7 +93,9 @@ module Igniter
|
|
|
65
93
|
end
|
|
66
94
|
|
|
67
95
|
def node_label(node)
|
|
68
|
-
"#{node.kind}: #{node.
|
|
96
|
+
return "#{node.kind}: #{node.path}" unless node.kind == :compute
|
|
97
|
+
|
|
98
|
+
"#{node.kind}: #{node.path}\\n#{node.callable_name}"
|
|
69
99
|
end
|
|
70
100
|
end
|
|
71
101
|
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
|
|
@@ -24,9 +24,9 @@ module Igniter
|
|
|
24
24
|
|
|
25
25
|
def explain_output(output_name)
|
|
26
26
|
output = @execution.compiled_graph.fetch_output(output_name)
|
|
27
|
-
source = @execution.compiled_graph.fetch_node(output.
|
|
27
|
+
source = @execution.compiled_graph.fetch_node(output.source_root)
|
|
28
28
|
|
|
29
|
-
{
|
|
29
|
+
explanation = {
|
|
30
30
|
output_id: output.id,
|
|
31
31
|
output: output.name,
|
|
32
32
|
path: output.path,
|
|
@@ -35,6 +35,13 @@ module Igniter
|
|
|
35
35
|
source_path: source.path,
|
|
36
36
|
dependencies: dependency_tree(source)
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
if output.composition_output?
|
|
40
|
+
explanation[:child_output] = output.child_output_name
|
|
41
|
+
explanation[:child_output_path] = output.source.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
explanation
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
private
|
|
@@ -52,7 +59,7 @@ module Igniter
|
|
|
52
59
|
value: serialize_value(state&.value),
|
|
53
60
|
error: state&.error&.message,
|
|
54
61
|
dependencies: node.dependencies.map do |dependency_name|
|
|
55
|
-
dependency_tree(@execution.compiled_graph.
|
|
62
|
+
dependency_tree(@execution.compiled_graph.fetch_dependency(dependency_name))
|
|
56
63
|
end
|
|
57
64
|
}
|
|
58
65
|
end
|
|
@@ -84,6 +91,14 @@ module Igniter
|
|
|
84
91
|
|
|
85
92
|
def serialize_value(value)
|
|
86
93
|
case value
|
|
94
|
+
when Igniter::Runtime::DeferredResult
|
|
95
|
+
{
|
|
96
|
+
type: :deferred,
|
|
97
|
+
token: value.token,
|
|
98
|
+
payload: value.payload,
|
|
99
|
+
source_node: value.source_node,
|
|
100
|
+
waiting_on: value.waiting_on
|
|
101
|
+
}
|
|
87
102
|
when Igniter::Runtime::Result
|
|
88
103
|
{
|
|
89
104
|
type: :result,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Igniter
|
|
4
6
|
module Extensions
|
|
5
7
|
module Reactive
|
|
@@ -11,16 +13,21 @@ module Igniter
|
|
|
11
13
|
@contract = contract
|
|
12
14
|
@reactions = reactions
|
|
13
15
|
@errors = []
|
|
16
|
+
@fired_reactions = Set.new
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def call(event)
|
|
17
20
|
reactions.each do |reaction|
|
|
18
21
|
next unless Matcher.new(reaction, event).match?
|
|
22
|
+
next if already_fired?(reaction, event)
|
|
19
23
|
|
|
20
|
-
reaction
|
|
24
|
+
mark_fired(reaction, event)
|
|
25
|
+
call_action(
|
|
26
|
+
reaction.action,
|
|
21
27
|
event: event,
|
|
22
28
|
contract: contract,
|
|
23
|
-
execution: execution
|
|
29
|
+
execution: execution,
|
|
30
|
+
value: value_for(event)
|
|
24
31
|
)
|
|
25
32
|
rescue StandardError => e
|
|
26
33
|
@errors << {
|
|
@@ -30,6 +37,46 @@ module Igniter
|
|
|
30
37
|
}
|
|
31
38
|
end
|
|
32
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def call_action(action, **kwargs)
|
|
44
|
+
parameters = action.parameters
|
|
45
|
+
accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
|
|
46
|
+
|
|
47
|
+
if accepts_any_keywords
|
|
48
|
+
action.call(**kwargs)
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
accepted_keywords = parameters.select { |kind, _name| %i[key keyreq].include?(kind) }.map(&:last)
|
|
53
|
+
filtered_kwargs = kwargs.slice(*accepted_keywords)
|
|
54
|
+
action.call(**filtered_kwargs)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def value_for(event)
|
|
58
|
+
return nil unless %i[node_succeeded node_resumed node_pending].include?(event.type)
|
|
59
|
+
return nil unless event.node_name
|
|
60
|
+
|
|
61
|
+
state = execution.cache.fetch(event.node_name)
|
|
62
|
+
state&.value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def already_fired?(reaction, event)
|
|
66
|
+
return false unless reaction.once_per_execution
|
|
67
|
+
|
|
68
|
+
@fired_reactions.include?(reaction_key(reaction, event))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def mark_fired(reaction, event)
|
|
72
|
+
return unless reaction.once_per_execution
|
|
73
|
+
|
|
74
|
+
@fired_reactions << reaction_key(reaction, event)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def reaction_key(reaction, event)
|
|
78
|
+
[reaction.object_id, event.execution_id]
|
|
79
|
+
end
|
|
33
80
|
end
|
|
34
81
|
end
|
|
35
82
|
end
|
|
@@ -4,12 +4,13 @@ module Igniter
|
|
|
4
4
|
module Extensions
|
|
5
5
|
module Reactive
|
|
6
6
|
class Reaction
|
|
7
|
-
attr_reader :event_type, :path, :action
|
|
7
|
+
attr_reader :event_type, :path, :action, :once_per_execution
|
|
8
8
|
|
|
9
|
-
def initialize(event_type:, path: nil, action:)
|
|
9
|
+
def initialize(event_type:, path: nil, action:, once_per_execution: false)
|
|
10
10
|
@event_type = event_type.to_sym
|
|
11
11
|
@path = path&.to_s
|
|
12
12
|
@action = action
|
|
13
|
+
@once_per_execution = once_per_execution
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Model
|
|
5
|
+
class BranchNode < Node
|
|
6
|
+
attr_reader :selector_dependency, :cases, :default_contract, :input_mapping, :context_dependencies, :input_mapper
|
|
7
|
+
|
|
8
|
+
def initialize(id:, name:, selector_dependency:, cases:, default_contract:, input_mapping:, context_dependencies: [], input_mapper: nil, path: nil, metadata: {})
|
|
9
|
+
dependencies = ([selector_dependency] + input_mapping.values + context_dependencies).uniq
|
|
10
|
+
|
|
11
|
+
super(
|
|
12
|
+
id: id,
|
|
13
|
+
kind: :branch,
|
|
14
|
+
name: name,
|
|
15
|
+
path: (path || name),
|
|
16
|
+
dependencies: dependencies,
|
|
17
|
+
metadata: metadata
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@selector_dependency = selector_dependency.to_sym
|
|
21
|
+
@cases = cases.map { |entry| normalize_case(entry) }.freeze
|
|
22
|
+
@default_contract = default_contract
|
|
23
|
+
@input_mapping = input_mapping.transform_keys(&:to_sym).transform_values(&:to_sym).freeze
|
|
24
|
+
@context_dependencies = Array(context_dependencies).map(&:to_sym).freeze
|
|
25
|
+
@input_mapper = input_mapper
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def possible_contracts
|
|
29
|
+
(cases.map { |entry| entry[:contract] } + [default_contract]).uniq
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def input_mapper?
|
|
33
|
+
!input_mapper.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def normalize_case(entry)
|
|
39
|
+
{
|
|
40
|
+
match: entry.fetch(:match),
|
|
41
|
+
contract: entry.fetch(:contract)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Model
|
|
5
|
+
class CollectionNode < Node
|
|
6
|
+
attr_reader :source_dependency, :contract_class, :key_name, :mode, :context_dependencies, :input_mapper
|
|
7
|
+
|
|
8
|
+
def initialize(id:, name:, source_dependency:, contract_class:, key_name:, mode:, context_dependencies: [], input_mapper: nil, path: nil, metadata: {})
|
|
9
|
+
super(
|
|
10
|
+
id: id,
|
|
11
|
+
kind: :collection,
|
|
12
|
+
name: name,
|
|
13
|
+
path: (path || name),
|
|
14
|
+
dependencies: [source_dependency, *context_dependencies],
|
|
15
|
+
metadata: metadata
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@source_dependency = source_dependency.to_sym
|
|
19
|
+
@contract_class = contract_class
|
|
20
|
+
@key_name = key_name.to_sym
|
|
21
|
+
@mode = mode.to_sym
|
|
22
|
+
@context_dependencies = Array(context_dependencies).map(&:to_sym)
|
|
23
|
+
@input_mapper = input_mapper
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def input_mapper?
|
|
27
|
+
!input_mapper.nil?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,12 +5,12 @@ module Igniter
|
|
|
5
5
|
class CompositionNode < Node
|
|
6
6
|
attr_reader :contract_class, :input_mapping
|
|
7
7
|
|
|
8
|
-
def initialize(id:, name:, contract_class:, input_mapping:, metadata: {})
|
|
8
|
+
def initialize(id:, name:, contract_class:, input_mapping:, path: nil, metadata: {})
|
|
9
9
|
super(
|
|
10
10
|
id: id,
|
|
11
11
|
kind: :composition,
|
|
12
12
|
name: name,
|
|
13
|
-
path: name,
|
|
13
|
+
path: (path || name),
|
|
14
14
|
dependencies: input_mapping.values,
|
|
15
15
|
metadata: metadata
|
|
16
16
|
)
|
|
@@ -5,17 +5,73 @@ module Igniter
|
|
|
5
5
|
class ComputeNode < Node
|
|
6
6
|
attr_reader :callable
|
|
7
7
|
|
|
8
|
-
def initialize(id:, name:, dependencies:, callable:, metadata: {})
|
|
8
|
+
def initialize(id:, name:, dependencies:, callable:, path: nil, metadata: {})
|
|
9
9
|
super(
|
|
10
10
|
id: id,
|
|
11
11
|
kind: :compute,
|
|
12
12
|
name: name,
|
|
13
|
-
path: name,
|
|
13
|
+
path: (path || name),
|
|
14
14
|
dependencies: dependencies,
|
|
15
15
|
metadata: metadata
|
|
16
16
|
)
|
|
17
17
|
@callable = callable
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def callable_name
|
|
21
|
+
return "const" if const?
|
|
22
|
+
return "guard" if guard?
|
|
23
|
+
|
|
24
|
+
case callable
|
|
25
|
+
when Proc
|
|
26
|
+
"proc"
|
|
27
|
+
when Symbol, String
|
|
28
|
+
callable.to_s
|
|
29
|
+
when Class
|
|
30
|
+
callable.name || "AnonymousClass"
|
|
31
|
+
else
|
|
32
|
+
callable.class.name || "AnonymousCallable"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def executor_key
|
|
37
|
+
metadata[:executor_key] || executor_metadata[:key]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def executor_label
|
|
41
|
+
metadata[:label] || executor_metadata[:label]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def executor_category
|
|
45
|
+
metadata[:category] || executor_metadata[:category]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def executor_tags
|
|
49
|
+
Array(metadata[:tags] || executor_metadata[:tags]).freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def executor_summary
|
|
53
|
+
metadata[:summary] || executor_metadata[:summary]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def type
|
|
57
|
+
metadata[:type] || executor_metadata[:type]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def const?
|
|
61
|
+
metadata[:kind] == :const
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def guard?
|
|
65
|
+
metadata[:guard] == true || metadata[:kind] == :guard
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def executor_metadata
|
|
71
|
+
return {} unless callable.is_a?(Class) && callable <= Igniter::Executor
|
|
72
|
+
|
|
73
|
+
callable.executor_metadata
|
|
74
|
+
end
|
|
19
75
|
end
|
|
20
76
|
end
|
|
21
77
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Igniter
|
|
4
4
|
module Model
|
|
5
5
|
class InputNode < Node
|
|
6
|
-
def initialize(id:, name:, metadata: {})
|
|
7
|
-
super(id: id, kind: :input, name: name, path: name, metadata: metadata)
|
|
6
|
+
def initialize(id:, name:, path: nil, metadata: {})
|
|
7
|
+
super(id: id, kind: :input, name: name, path: (path || name), metadata: metadata)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def type
|