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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +264 -0
  5. data/docs/API_V2.md +242 -0
  6. data/docs/ARCHITECTURE_V2.md +317 -0
  7. data/docs/EXECUTION_MODEL_V2.md +245 -0
  8. data/docs/IGNITER_CONCEPTS.md +81 -0
  9. data/examples/README.md +77 -0
  10. data/examples/basic_pricing.rb +27 -0
  11. data/examples/composition.rb +39 -0
  12. data/examples/diagnostics.rb +28 -0
  13. data/lib/igniter/compiler/compiled_graph.rb +78 -0
  14. data/lib/igniter/compiler/graph_compiler.rb +60 -0
  15. data/lib/igniter/compiler/validator.rb +205 -0
  16. data/lib/igniter/compiler.rb +10 -0
  17. data/lib/igniter/contract.rb +117 -0
  18. data/lib/igniter/diagnostics/report.rb +174 -0
  19. data/lib/igniter/diagnostics.rb +8 -0
  20. data/lib/igniter/dsl/contract_builder.rb +95 -0
  21. data/lib/igniter/dsl.rb +8 -0
  22. data/lib/igniter/errors.rb +53 -0
  23. data/lib/igniter/events/bus.rb +39 -0
  24. data/lib/igniter/events/event.rb +53 -0
  25. data/lib/igniter/events.rb +9 -0
  26. data/lib/igniter/extensions/auditing/timeline.rb +99 -0
  27. data/lib/igniter/extensions/auditing.rb +10 -0
  28. data/lib/igniter/extensions/introspection/graph_formatter.rb +73 -0
  29. data/lib/igniter/extensions/introspection/runtime_formatter.rb +102 -0
  30. data/lib/igniter/extensions/introspection.rb +11 -0
  31. data/lib/igniter/extensions/reactive/engine.rb +36 -0
  32. data/lib/igniter/extensions/reactive/matcher.rb +21 -0
  33. data/lib/igniter/extensions/reactive/reaction.rb +17 -0
  34. data/lib/igniter/extensions/reactive.rb +12 -0
  35. data/lib/igniter/extensions.rb +10 -0
  36. data/lib/igniter/model/composition_node.rb +22 -0
  37. data/lib/igniter/model/compute_node.rb +21 -0
  38. data/lib/igniter/model/graph.rb +15 -0
  39. data/lib/igniter/model/input_node.rb +27 -0
  40. data/lib/igniter/model/node.rb +22 -0
  41. data/lib/igniter/model/output_node.rb +21 -0
  42. data/lib/igniter/model.rb +13 -0
  43. data/lib/igniter/runtime/cache.rb +58 -0
  44. data/lib/igniter/runtime/execution.rb +142 -0
  45. data/lib/igniter/runtime/input_validator.rb +145 -0
  46. data/lib/igniter/runtime/invalidator.rb +52 -0
  47. data/lib/igniter/runtime/node_state.rb +31 -0
  48. data/lib/igniter/runtime/resolver.rb +114 -0
  49. data/lib/igniter/runtime/result.rb +105 -0
  50. data/lib/igniter/runtime.rb +14 -0
  51. data/lib/igniter/version.rb +5 -0
  52. data/lib/igniter.rb +20 -0
  53. data/sig/igniter.rbs +4 -0
  54. metadata +126 -0
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Introspection
6
+ class RuntimeFormatter
7
+ def self.states(execution)
8
+ new(execution).states
9
+ end
10
+
11
+ def self.explain_output(execution, output_name)
12
+ new(execution).explain_output(output_name)
13
+ end
14
+
15
+ def initialize(execution)
16
+ @execution = execution
17
+ end
18
+
19
+ def states
20
+ @execution.cache.to_h.each_with_object({}) do |(node_name, state), memo|
21
+ memo[node_name] = serialize_state(state)
22
+ end
23
+ end
24
+
25
+ def explain_output(output_name)
26
+ output = @execution.compiled_graph.fetch_output(output_name)
27
+ source = @execution.compiled_graph.fetch_node(output.source)
28
+
29
+ {
30
+ output_id: output.id,
31
+ output: output.name,
32
+ path: output.path,
33
+ source_id: source.id,
34
+ source: source.name,
35
+ source_path: source.path,
36
+ dependencies: dependency_tree(source)
37
+ }
38
+ end
39
+
40
+ private
41
+
42
+ def dependency_tree(node)
43
+ state = @execution.cache.fetch(node.name)
44
+ {
45
+ id: node.id,
46
+ name: node.name,
47
+ path: node.path,
48
+ kind: node.kind,
49
+ source_location: node.source_location,
50
+ status: state&.status,
51
+ invalidated_by: invalidation_details(state),
52
+ value: serialize_value(state&.value),
53
+ error: state&.error&.message,
54
+ dependencies: node.dependencies.map do |dependency_name|
55
+ dependency_tree(@execution.compiled_graph.fetch_node(dependency_name))
56
+ end
57
+ }
58
+ end
59
+
60
+ def serialize_state(state)
61
+ {
62
+ id: state.node.id,
63
+ path: state.node.path,
64
+ kind: state.node.kind,
65
+ source_location: state.node.source_location,
66
+ status: state.status,
67
+ version: state.version,
68
+ invalidated_by: invalidation_details(state),
69
+ value: serialize_value(state.value),
70
+ error: state.error&.message
71
+ }
72
+ end
73
+
74
+ def invalidation_details(state)
75
+ return nil unless state&.invalidated_by
76
+
77
+ invalidating_node = @execution.compiled_graph.fetch_node(state.invalidated_by)
78
+ {
79
+ node_id: invalidating_node.id,
80
+ node_name: invalidating_node.name,
81
+ node_path: invalidating_node.path
82
+ }
83
+ end
84
+
85
+ def serialize_value(value)
86
+ case value
87
+ when Igniter::Runtime::Result
88
+ {
89
+ type: :result,
90
+ graph: value.execution.compiled_graph.name,
91
+ execution_id: value.execution.events.execution_id
92
+ }
93
+ when Array
94
+ value.map { |item| serialize_value(item) }
95
+ else
96
+ value
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "introspection/graph_formatter"
4
+ require_relative "introspection/runtime_formatter"
5
+
6
+ module Igniter
7
+ module Extensions
8
+ module Introspection
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Reactive
6
+ class Engine
7
+ attr_reader :execution, :contract, :reactions, :errors
8
+
9
+ def initialize(execution:, contract:, reactions:)
10
+ @execution = execution
11
+ @contract = contract
12
+ @reactions = reactions
13
+ @errors = []
14
+ end
15
+
16
+ def call(event)
17
+ reactions.each do |reaction|
18
+ next unless Matcher.new(reaction, event).match?
19
+
20
+ reaction.action.call(
21
+ event: event,
22
+ contract: contract,
23
+ execution: execution
24
+ )
25
+ rescue StandardError => e
26
+ @errors << {
27
+ event: event,
28
+ reaction: reaction,
29
+ error: e
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Reactive
6
+ class Matcher
7
+ def initialize(reaction, event)
8
+ @reaction = reaction
9
+ @event = event
10
+ end
11
+
12
+ def match?
13
+ return false unless @reaction.event_type == @event.type
14
+ return true unless @reaction.path
15
+
16
+ @reaction.path == @event.path
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Reactive
6
+ class Reaction
7
+ attr_reader :event_type, :path, :action
8
+
9
+ def initialize(event_type:, path: nil, action:)
10
+ @event_type = event_type.to_sym
11
+ @path = path&.to_s
12
+ @action = action
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "reactive/reaction"
4
+ require_relative "reactive/matcher"
5
+ require_relative "reactive/engine"
6
+
7
+ module Igniter
8
+ module Extensions
9
+ module Reactive
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "extensions/auditing"
4
+ require_relative "extensions/reactive"
5
+ require_relative "extensions/introspection"
6
+
7
+ module Igniter
8
+ module Extensions
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class CompositionNode < Node
6
+ attr_reader :contract_class, :input_mapping
7
+
8
+ def initialize(id:, name:, contract_class:, input_mapping:, metadata: {})
9
+ super(
10
+ id: id,
11
+ kind: :composition,
12
+ name: name,
13
+ path: name,
14
+ dependencies: input_mapping.values,
15
+ metadata: metadata
16
+ )
17
+ @contract_class = contract_class
18
+ @input_mapping = input_mapping.transform_keys(&:to_sym).transform_values(&:to_sym).freeze
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class ComputeNode < Node
6
+ attr_reader :callable
7
+
8
+ def initialize(id:, name:, dependencies:, callable:, metadata: {})
9
+ super(
10
+ id: id,
11
+ kind: :compute,
12
+ name: name,
13
+ path: name,
14
+ dependencies: dependencies,
15
+ metadata: metadata
16
+ )
17
+ @callable = callable
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class Graph
6
+ attr_reader :name, :nodes, :metadata
7
+
8
+ def initialize(name:, nodes:, metadata: {})
9
+ @name = name
10
+ @nodes = nodes.freeze
11
+ @metadata = metadata.freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class InputNode < Node
6
+ def initialize(id:, name:, metadata: {})
7
+ super(id: id, kind: :input, name: name, path: name, metadata: metadata)
8
+ end
9
+
10
+ def type
11
+ metadata[:type]
12
+ end
13
+
14
+ def required?
15
+ metadata.fetch(:required, !metadata.key?(:default))
16
+ end
17
+
18
+ def default?
19
+ metadata.key?(:default)
20
+ end
21
+
22
+ def default
23
+ metadata[:default]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class Node
6
+ attr_reader :id, :kind, :name, :path, :dependencies, :metadata
7
+
8
+ def initialize(id:, kind:, name:, path:, dependencies: [], metadata: {})
9
+ @id = id
10
+ @kind = kind
11
+ @name = name.to_sym
12
+ @path = path.to_s
13
+ @dependencies = dependencies.map(&:to_sym).freeze
14
+ @metadata = metadata.freeze
15
+ end
16
+
17
+ def source_location
18
+ metadata[:source_location]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class OutputNode < Node
6
+ attr_reader :source
7
+
8
+ def initialize(id:, name:, source:, metadata: {})
9
+ super(
10
+ id: id,
11
+ kind: :output,
12
+ name: name,
13
+ path: "output.#{name}",
14
+ dependencies: [source],
15
+ metadata: metadata
16
+ )
17
+ @source = source.to_sym
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model/node"
4
+ require_relative "model/graph"
5
+ require_relative "model/input_node"
6
+ require_relative "model/compute_node"
7
+ require_relative "model/composition_node"
8
+ require_relative "model/output_node"
9
+
10
+ module Igniter
11
+ module Model
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Runtime
5
+ class Cache
6
+ def initialize
7
+ @states = {}
8
+ end
9
+
10
+ def fetch(node_name)
11
+ @states[node_name.to_sym]
12
+ end
13
+
14
+ def write(state)
15
+ current = fetch(state.node.name)
16
+ version = state.version || next_version(current)
17
+ @states[state.node.name] = NodeState.new(
18
+ node: state.node,
19
+ status: state.status,
20
+ value: state.value,
21
+ error: state.error,
22
+ version: version,
23
+ resolved_at: state.resolved_at,
24
+ invalidated_by: state.invalidated_by
25
+ )
26
+ end
27
+
28
+ def stale!(node, invalidated_by:)
29
+ current = fetch(node.name)
30
+ return unless current
31
+
32
+ @states[node.name] = NodeState.new(
33
+ node: node,
34
+ status: :stale,
35
+ value: current.value,
36
+ error: current.error,
37
+ version: current.version + 1,
38
+ resolved_at: current.resolved_at,
39
+ invalidated_by: invalidated_by
40
+ )
41
+ end
42
+
43
+ def values
44
+ @states.values
45
+ end
46
+
47
+ def to_h
48
+ @states.dup
49
+ end
50
+
51
+ private
52
+
53
+ def next_version(current)
54
+ current ? current.version + 1 : 1
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Runtime
5
+ class Execution
6
+ attr_reader :compiled_graph, :contract_instance, :inputs, :cache, :events, :audit
7
+
8
+ def initialize(compiled_graph:, contract_instance:, inputs:)
9
+ @compiled_graph = compiled_graph
10
+ @contract_instance = contract_instance
11
+ @input_validator = InputValidator.new(compiled_graph)
12
+ @inputs = @input_validator.normalize_initial_inputs(inputs)
13
+ @cache = Cache.new
14
+ @events = Events::Bus.new
15
+ @audit = Extensions::Auditing::Timeline.new(self)
16
+ @events.subscribe(@audit)
17
+ @resolver = Resolver.new(self)
18
+ @invalidator = Invalidator.new(self)
19
+ end
20
+
21
+ def resolve_output(name)
22
+ output = compiled_graph.fetch_output(name)
23
+ with_execution_lifecycle([output.source]) do
24
+ state = @resolver.resolve(output.source)
25
+ raise state.error if state.failed?
26
+
27
+ state.value
28
+ end
29
+ end
30
+
31
+ def resolve(name)
32
+ @resolver.resolve(name)
33
+ end
34
+
35
+ def resolve_all
36
+ output_sources = compiled_graph.outputs.map(&:source)
37
+
38
+ with_execution_lifecycle(output_sources) do
39
+ compiled_graph.outputs.each { |output_node| resolve(output_node.source) }
40
+ self
41
+ end
42
+ end
43
+
44
+ def update_inputs(new_inputs)
45
+ symbolize_keys(new_inputs).each do |name, value|
46
+ @input_validator.validate_update!(name, value)
47
+
48
+ @inputs[name] = value
49
+ input_node = compiled_graph.fetch_node(name)
50
+ cache.write(NodeState.new(node: input_node, status: :succeeded, value: value, invalidated_by: name))
51
+ @events.emit(:input_updated, node: input_node, status: :succeeded, payload: { value: value })
52
+ @invalidator.invalidate_from(name)
53
+ end
54
+
55
+ self
56
+ end
57
+
58
+ def success?
59
+ resolve_all
60
+ !cache.values.any?(&:failed?)
61
+ end
62
+
63
+ def failed?
64
+ resolve_all
65
+ cache.values.any?(&:failed?)
66
+ end
67
+
68
+ def states
69
+ Extensions::Introspection::RuntimeFormatter.states(self)
70
+ end
71
+
72
+ def explain_output(name)
73
+ Extensions::Introspection::RuntimeFormatter.explain_output(self, name)
74
+ end
75
+
76
+ def diagnostics
77
+ Diagnostics::Report.new(self)
78
+ end
79
+
80
+ def to_h
81
+ {
82
+ graph: compiled_graph.name,
83
+ execution_id: events.execution_id,
84
+ inputs: inputs.dup,
85
+ success: !cache.values.any?(&:failed?),
86
+ failed: cache.values.any?(&:failed?),
87
+ states: states,
88
+ event_count: events.events.size
89
+ }
90
+ end
91
+
92
+ def as_json(*)
93
+ to_h.merge(
94
+ events: events.events.map(&:as_json)
95
+ )
96
+ end
97
+
98
+ private
99
+
100
+ def with_execution_lifecycle(node_names)
101
+ if resolution_required_for_any?(node_names)
102
+ @events.emit(:execution_started, payload: { graph: compiled_graph.name, targets: node_names.map(&:to_sym) })
103
+ begin
104
+ result = yield
105
+ @events.emit(:execution_finished, payload: { graph: compiled_graph.name, targets: node_names.map(&:to_sym) })
106
+ result
107
+ rescue StandardError => e
108
+ @events.emit(
109
+ :execution_failed,
110
+ status: :failed,
111
+ payload: {
112
+ graph: compiled_graph.name,
113
+ targets: node_names.map(&:to_sym),
114
+ error: e.message
115
+ }
116
+ )
117
+ raise
118
+ end
119
+ else
120
+ yield
121
+ end
122
+ end
123
+
124
+ def resolution_required_for_any?(node_names)
125
+ node_names.any? do |node_name|
126
+ state = cache.fetch(node_name)
127
+ state.nil? || state.stale?
128
+ end
129
+ end
130
+
131
+ def symbolize_keys(hash)
132
+ hash.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = value }
133
+ end
134
+
135
+ public
136
+
137
+ def fetch_input!(name)
138
+ @input_validator.fetch_value!(name, @inputs)
139
+ end
140
+ end
141
+ end
142
+ end