igniter 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +238 -1
  5. data/docs/BACKLOG.md +166 -0
  6. data/docs/BRANCHES_V1.md +213 -0
  7. data/docs/COLLECTIONS_V1.md +303 -0
  8. data/docs/EXECUTION_MODEL_V2.md +79 -0
  9. data/docs/PATTERNS.md +222 -0
  10. data/docs/STORE_ADAPTERS.md +126 -0
  11. data/examples/README.md +124 -0
  12. data/examples/async_store.rb +47 -0
  13. data/examples/collection.rb +43 -0
  14. data/examples/collection_partial_failure.rb +50 -0
  15. data/examples/marketing_ergonomics.rb +57 -0
  16. data/examples/ringcentral_routing.rb +278 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +82 -0
  18. data/lib/igniter/compiler/graph_compiler.rb +12 -2
  19. data/lib/igniter/compiler/type_resolver.rb +54 -0
  20. data/lib/igniter/compiler/validation_context.rb +61 -0
  21. data/lib/igniter/compiler/validation_pipeline.rb +30 -0
  22. data/lib/igniter/compiler/validator.rb +1 -187
  23. data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
  24. data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
  25. data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
  26. data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
  27. data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
  28. data/lib/igniter/compiler.rb +8 -0
  29. data/lib/igniter/contract.rb +136 -4
  30. data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
  31. data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
  32. data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
  33. data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
  34. data/lib/igniter/diagnostics/report.rb +84 -8
  35. data/lib/igniter/dsl/contract_builder.rb +208 -5
  36. data/lib/igniter/dsl/schema_builder.rb +73 -0
  37. data/lib/igniter/dsl.rb +1 -0
  38. data/lib/igniter/errors.rb +11 -0
  39. data/lib/igniter/events/bus.rb +5 -0
  40. data/lib/igniter/events/event.rb +29 -0
  41. data/lib/igniter/executor.rb +74 -0
  42. data/lib/igniter/executor_registry.rb +44 -0
  43. data/lib/igniter/extensions/auditing/timeline.rb +4 -0
  44. data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
  45. data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
  46. data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
  47. data/lib/igniter/extensions/introspection.rb +1 -0
  48. data/lib/igniter/extensions/reactive/engine.rb +49 -2
  49. data/lib/igniter/extensions/reactive/reaction.rb +3 -2
  50. data/lib/igniter/model/branch_node.rb +40 -0
  51. data/lib/igniter/model/collection_node.rb +25 -0
  52. data/lib/igniter/model/composition_node.rb +2 -2
  53. data/lib/igniter/model/compute_node.rb +58 -2
  54. data/lib/igniter/model/input_node.rb +2 -2
  55. data/lib/igniter/model/output_node.rb +24 -4
  56. data/lib/igniter/model.rb +2 -0
  57. data/lib/igniter/runtime/cache.rb +64 -25
  58. data/lib/igniter/runtime/collection_result.rb +111 -0
  59. data/lib/igniter/runtime/deferred_result.rb +40 -0
  60. data/lib/igniter/runtime/execution.rb +261 -11
  61. data/lib/igniter/runtime/input_validator.rb +2 -24
  62. data/lib/igniter/runtime/invalidator.rb +1 -1
  63. data/lib/igniter/runtime/job_worker.rb +18 -0
  64. data/lib/igniter/runtime/node_state.rb +20 -0
  65. data/lib/igniter/runtime/planner.rb +126 -0
  66. data/lib/igniter/runtime/resolver.rb +269 -15
  67. data/lib/igniter/runtime/result.rb +14 -2
  68. data/lib/igniter/runtime/runner_factory.rb +20 -0
  69. data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
  70. data/lib/igniter/runtime/runners/store_runner.rb +29 -0
  71. data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
  72. data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
  73. data/lib/igniter/runtime/stores/file_store.rb +43 -0
  74. data/lib/igniter/runtime/stores/memory_store.rb +40 -0
  75. data/lib/igniter/runtime/stores/redis_store.rb +44 -0
  76. data/lib/igniter/runtime.rb +12 -0
  77. data/lib/igniter/type_system.rb +44 -0
  78. data/lib/igniter/version.rb +1 -1
  79. data/lib/igniter.rb +23 -0
  80. metadata +43 -2
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+ require "igniter"
5
+
6
+ class TechnicianAvailabilityContract < Igniter::Contract
7
+ define do
8
+ input :technician_id
9
+ input :active
10
+
11
+ guard :active_technician, with: :active, eq: true, message: "Technician inactive"
12
+
13
+ compute :summary, with: %i[technician_id active_technician] do |technician_id:, active_technician:|
14
+ active_technician
15
+ { id: technician_id, status: "available" }
16
+ end
17
+
18
+ output :summary
19
+ end
20
+ end
21
+
22
+ class TechnicianAvailabilityBatchContract < Igniter::Contract
23
+ define do
24
+ input :technician_inputs, type: :array
25
+
26
+ collection :technicians,
27
+ with: :technician_inputs,
28
+ each: TechnicianAvailabilityContract,
29
+ key: :technician_id,
30
+ mode: :collect
31
+
32
+ output :technicians
33
+ end
34
+ end
35
+
36
+ contract = TechnicianAvailabilityBatchContract.new(
37
+ technician_inputs: [
38
+ { technician_id: 1, active: true },
39
+ { technician_id: 2, active: false },
40
+ { technician_id: 3, active: true }
41
+ ]
42
+ )
43
+
44
+ result = contract.result.technicians
45
+
46
+ puts "summary=#{result.summary.inspect}"
47
+ puts "items_summary=#{result.items_summary.inspect}"
48
+ puts "failed_items=#{result.failed_items.inspect}"
49
+ puts "---"
50
+ puts contract.diagnostics_text
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+ require "igniter"
5
+
6
+ OUTBOX = []
7
+
8
+ class MarketingQuoteContract < Igniter::Contract
9
+ define do
10
+ input :service, type: :string
11
+ input :zip_code, type: :string
12
+
13
+ const :vendor_id, "eLocal"
14
+
15
+ scope :routing do
16
+ map :trade_name, from: :service do |service:|
17
+ %w[heating cooling ventilation air_conditioning].include?(service.downcase) ? "HVAC" : service
18
+ end
19
+ end
20
+
21
+ scope :pricing do
22
+ lookup :trade, with: :trade_name do |trade_name:|
23
+ { name: trade_name, base_bid: trade_name == "HVAC" ? 45.0 : 25.0 }
24
+ end
25
+ end
26
+
27
+ namespace :validation do
28
+ guard :zip_supported, with: :zip_code, in: %w[60601 10001], message: "Unsupported zip"
29
+ end
30
+
31
+ compute :quote, with: %i[vendor_id trade zip_supported zip_code] do |vendor_id:, trade:, zip_supported:, zip_code:|
32
+ zip_supported
33
+ {
34
+ vendor_id: vendor_id,
35
+ trade: trade[:name],
36
+ zip_code: zip_code,
37
+ bid: trade[:base_bid]
38
+ }
39
+ end
40
+
41
+ expose :quote, as: :response
42
+ end
43
+
44
+ on_success :response do |value:, **|
45
+ OUTBOX << {
46
+ vendor_id: value[:vendor_id],
47
+ zip_code: value[:zip_code]
48
+ }
49
+ end
50
+ end
51
+
52
+ contract = MarketingQuoteContract.new(service: "heating", zip_code: "60601")
53
+
54
+ puts contract.explain_plan
55
+ puts "---"
56
+ puts "response=#{contract.result.response.inspect}"
57
+ puts "outbox=#{OUTBOX.inspect}"
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+ require "igniter"
5
+ require "pp"
6
+
7
+ class InboundCallContract < Igniter::Contract
8
+ define do
9
+ input :session_id, type: :string
10
+ input :direction, type: :string
11
+ input :from, type: :string
12
+ input :to, type: :string
13
+ input :start_time, type: :string
14
+
15
+ compute :summary, with: %i[session_id from to start_time] do |session_id:, from:, to:, start_time:|
16
+ {
17
+ session_id: session_id,
18
+ direction: "Inbound",
19
+ customer_phone: from,
20
+ lookup_phone: from,
21
+ dialed_phone: to,
22
+ started_at: start_time
23
+ }
24
+ end
25
+
26
+ output :summary
27
+ end
28
+ end
29
+
30
+ class OutboundCallContract < Igniter::Contract
31
+ define do
32
+ input :session_id, type: :string
33
+ input :direction, type: :string
34
+ input :from, type: :string
35
+ input :to, type: :string
36
+ input :start_time, type: :string
37
+
38
+ compute :summary, with: %i[session_id from to start_time] do |session_id:, from:, to:, start_time:|
39
+ {
40
+ session_id: session_id,
41
+ direction: "Outbound",
42
+ customer_phone: to,
43
+ lookup_phone: to,
44
+ operator_phone: from,
45
+ started_at: start_time
46
+ }
47
+ end
48
+
49
+ output :summary
50
+ end
51
+ end
52
+
53
+ class UnknownDirectionContract < Igniter::Contract
54
+ define do
55
+ input :session_id, type: :string
56
+ input :direction, type: :string
57
+ input :from, type: :string
58
+ input :to, type: :string
59
+ input :start_time, type: :string
60
+
61
+ compute :summary, with: %i[session_id direction] do |session_id:, direction:|
62
+ {
63
+ session_id: session_id,
64
+ direction: direction,
65
+ ignored: true
66
+ }
67
+ end
68
+
69
+ output :summary
70
+ end
71
+ end
72
+
73
+ class CallEventContract < Igniter::Contract
74
+ define do
75
+ input :session_id, type: :string
76
+ input :direction, type: :string
77
+ input :from, type: :string
78
+ input :to, type: :string
79
+ input :start_time, type: :string
80
+
81
+ branch :call_context, with: :direction, inputs: {
82
+ session_id: :session_id,
83
+ direction: :direction,
84
+ from: :from,
85
+ to: :to,
86
+ start_time: :start_time
87
+ } do
88
+ on "Inbound", contract: InboundCallContract
89
+ on "Outbound", contract: OutboundCallContract
90
+ default contract: UnknownDirectionContract
91
+ end
92
+
93
+ export :summary, from: :call_context
94
+ end
95
+ end
96
+
97
+ class CallConnectedContract < Igniter::Contract
98
+ define do
99
+ input :extension_id, type: :integer
100
+ input :active_calls, type: :array
101
+ input :telephony_status, type: :string
102
+
103
+ guard :has_calls, with: :active_calls, message: "No active calls" do |active_calls:|
104
+ active_calls.any?
105
+ end
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
+ collection :calls,
120
+ with: :call_inputs,
121
+ each: CallEventContract,
122
+ key: :session_id,
123
+ mode: :collect
124
+
125
+ compute :call_summaries, with: :calls do |calls:|
126
+ calls.successes.values.map { |item| item.result.summary }
127
+ end
128
+
129
+ compute :routing_summary, with: %i[calls call_summaries extension_id telephony_status has_calls] do |calls:, call_summaries:, extension_id:, telephony_status:, has_calls:|
130
+ has_calls
131
+ {
132
+ extension_id: extension_id,
133
+ telephony_status: telephony_status,
134
+ total_calls: calls.summary[:total],
135
+ succeeded_calls: calls.summary[:succeeded],
136
+ failed_calls: calls.summary[:failed],
137
+ inbound_calls: call_summaries.count { |item| item[:direction] == "Inbound" },
138
+ outbound_calls: call_summaries.count { |item| item[:direction] == "Outbound" },
139
+ ignored_calls: call_summaries.count { |item| item[:ignored] }
140
+ }
141
+ end
142
+
143
+ output :calls
144
+ output :routing_summary
145
+ end
146
+ end
147
+
148
+ class NoCallContract < Igniter::Contract
149
+ define do
150
+ input :extension_id, type: :integer
151
+ input :telephony_status, type: :string
152
+ input :active_calls, type: :array
153
+
154
+ compute :routing_summary, with: %i[extension_id telephony_status] do |extension_id:, telephony_status:|
155
+ {
156
+ extension_id: extension_id,
157
+ telephony_status: telephony_status,
158
+ clear_operator: true
159
+ }
160
+ end
161
+
162
+ output :routing_summary
163
+ end
164
+ end
165
+
166
+ class RingingContract < Igniter::Contract
167
+ define do
168
+ input :extension_id, type: :integer
169
+ input :telephony_status, type: :string
170
+ input :active_calls, type: :array
171
+
172
+ compute :routing_summary, with: %i[extension_id telephony_status] do |extension_id:, telephony_status:|
173
+ {
174
+ extension_id: extension_id,
175
+ telephony_status: telephony_status,
176
+ ringing: true
177
+ }
178
+ end
179
+
180
+ output :routing_summary
181
+ end
182
+ end
183
+
184
+ class UnknownStatusContract < Igniter::Contract
185
+ define do
186
+ input :extension_id, type: :integer
187
+ input :telephony_status, type: :string
188
+ input :active_calls, type: :array
189
+
190
+ compute :routing_summary, with: %i[extension_id telephony_status] do |extension_id:, telephony_status:|
191
+ {
192
+ extension_id: extension_id,
193
+ telephony_status: telephony_status,
194
+ ignored_status: true
195
+ }
196
+ end
197
+
198
+ output :routing_summary
199
+ end
200
+ end
201
+
202
+ class RingcentralWebhookContract < Igniter::Contract
203
+ define do
204
+ input :payload
205
+
206
+ scope :parse do
207
+ map :body, from: :payload do |payload:|
208
+ payload.fetch("body", {})
209
+ end
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
222
+ end
223
+
224
+ branch :status_route, with: :telephony_status, inputs: {
225
+ extension_id: :extension_id,
226
+ telephony_status: :telephony_status,
227
+ active_calls: :active_calls
228
+ } do
229
+ on "CallConnected", contract: CallConnectedContract
230
+ on "NoCall", contract: NoCallContract
231
+ on "Ringing", contract: RingingContract
232
+ default contract: UnknownStatusContract
233
+ end
234
+
235
+ export :routing_summary, from: :status_route
236
+ output :status_route
237
+ end
238
+ end
239
+
240
+ payload = {
241
+ "body" => {
242
+ "extensionId" => 62872332031,
243
+ "telephonyStatus" => "CallConnected",
244
+ "activeCalls" => [
245
+ {
246
+ "from" => "+18009066027",
247
+ "to" => "+16199627154",
248
+ "direction" => "Outbound",
249
+ "startTime" => "2024-04-01T16:18:13.553Z",
250
+ "telephonySessionId" => "s-outbound-1"
251
+ },
252
+ {
253
+ "from" => "+13125550100",
254
+ "to" => "+18009066027",
255
+ "direction" => "Inbound",
256
+ "startTime" => "2024-04-01T16:19:10.000Z",
257
+ "telephonySessionId" => "s-inbound-2"
258
+ },
259
+ {
260
+ "from" => "+13125550199",
261
+ "to" => "+18009066027",
262
+ "direction" => "Parked",
263
+ "startTime" => "2024-04-01T16:20:00.000Z",
264
+ "telephonySessionId" => "s-unknown-3"
265
+ }
266
+ ]
267
+ }
268
+ }
269
+
270
+ contract = RingcentralWebhookContract.new(payload: payload)
271
+
272
+ puts contract.explain_plan
273
+ puts "---"
274
+ puts "routing_summary=#{contract.result.routing_summary.inspect}"
275
+ puts "status_route_branch=#{contract.events.find { |event| event.type == :branch_selected }.payload[:matched_case]}"
276
+ puts "child_collection_summary=#{contract.result.status_route.calls.summary.inspect}"
277
+ puts "child_diagnostics_status=#{contract.result.status_route.execution.diagnostics.to_h[:status]}"
278
+ pp contract.result.status_route.calls.as_json
@@ -38,6 +38,17 @@ module Igniter
38
38
  @outputs_by_name.fetch(name.to_sym)
39
39
  end
40
40
 
41
+ def output?(name)
42
+ @outputs_by_name.key?(name.to_sym)
43
+ end
44
+
45
+ def fetch_dependency(name)
46
+ return fetch_node(name) if node?(name)
47
+ return fetch_output(name) if output?(name)
48
+
49
+ raise KeyError, "Unknown dependency '#{name}'"
50
+ end
51
+
41
52
  def to_h
42
53
  {
43
54
  name: name,
@@ -53,6 +64,18 @@ module Igniter
53
64
  base[:contract] = node.contract_class.name
54
65
  base[:inputs] = node.input_mapping
55
66
  end
67
+ if node.kind == :branch
68
+ base[:selector] = node.selector_dependency
69
+ base[:cases] = node.cases.map { |entry| { match: entry[:match], contract: entry[:contract].name } }
70
+ base[:default_contract] = node.default_contract.name
71
+ base[:inputs] = node.input_mapping
72
+ end
73
+ if node.kind == :collection
74
+ base[:with] = node.source_dependency
75
+ base[:each] = node.contract_class.name
76
+ base[:key] = node.key_name
77
+ base[:mode] = node.mode
78
+ end
56
79
  base
57
80
  end,
58
81
  outputs: outputs.map do |output|
@@ -70,6 +93,65 @@ module Igniter
70
93
  Extensions::Introspection::GraphFormatter.to_text(self)
71
94
  end
72
95
 
96
+ def to_schema
97
+ {
98
+ name: name,
99
+ inputs: nodes.select { |node| node.kind == :input }.map do |node|
100
+ {
101
+ name: node.name,
102
+ type: node.type,
103
+ required: node.required?,
104
+ default: (node.default if node.default?),
105
+ metadata: node.metadata.reject { |key, _| key == :source_location || key == :type || key == :required || key == :default }
106
+ }.compact
107
+ end,
108
+ compositions: nodes.select { |node| node.kind == :composition }.map do |node|
109
+ {
110
+ name: node.name,
111
+ contract: node.contract_class,
112
+ inputs: node.input_mapping,
113
+ metadata: node.metadata.reject { |key, _| key == :source_location }
114
+ }
115
+ end,
116
+ branches: nodes.select { |node| node.kind == :branch }.map do |node|
117
+ {
118
+ name: node.name,
119
+ with: node.selector_dependency,
120
+ inputs: node.input_mapping,
121
+ cases: node.cases.map { |entry| { on: entry[:match], contract: entry[:contract] } },
122
+ default: node.default_contract,
123
+ metadata: node.metadata.reject { |key, _| key == :source_location }
124
+ }
125
+ end,
126
+ collections: nodes.select { |node| node.kind == :collection }.map do |node|
127
+ {
128
+ name: node.name,
129
+ with: node.source_dependency,
130
+ each: node.contract_class,
131
+ key: node.key_name,
132
+ mode: node.mode,
133
+ metadata: node.metadata.reject { |key, _| key == :source_location }
134
+ }
135
+ end,
136
+ computes: nodes.select { |node| node.kind == :compute }.map do |node|
137
+ {
138
+ name: node.name,
139
+ depends_on: node.dependencies,
140
+ executor: node.executor_key,
141
+ call: (node.callable unless node.executor_key),
142
+ metadata: node.metadata.reject { |key, _| %i[source_location executor_key].include?(key) }
143
+ }.compact
144
+ end,
145
+ outputs: outputs.map do |output|
146
+ {
147
+ name: output.name,
148
+ from: output.source,
149
+ metadata: output.metadata.reject { |key, _| %i[source_location type].include?(key) }
150
+ }.compact
151
+ end
152
+ }
153
+ end
154
+
73
155
  def to_mermaid
74
156
  Extensions::Introspection::GraphFormatter.to_mermaid(self)
75
157
  end
@@ -18,6 +18,7 @@ module Igniter
18
18
  def call
19
19
  validator = Validator.call(@graph)
20
20
  @nodes_by_name = validator.runtime_nodes_by_name
21
+ @outputs_by_name = validator.outputs.each_with_object({}) { |output, memo| memo[output.name] = output }
21
22
 
22
23
  CompiledGraph.new(
23
24
  name: @graph.name,
@@ -36,7 +37,9 @@ module Igniter
36
37
 
37
38
  def build_dependents
38
39
  runtime_nodes.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |node, memo|
39
- node.dependencies.each { |dependency_name| memo[dependency_name] << node.name }
40
+ node.dependencies.each do |dependency_name|
41
+ memo[dependency_source_name(dependency_name)] << node.name
42
+ end
40
43
  end
41
44
  end
42
45
 
@@ -46,10 +49,17 @@ module Igniter
46
49
 
47
50
  def tsort_each_child(node, &block)
48
51
  node.dependencies.each do |dependency_name|
49
- block.call(@nodes_by_name.fetch(dependency_name))
52
+ block.call(@nodes_by_name.fetch(dependency_source_name(dependency_name)))
50
53
  end
51
54
  end
52
55
 
56
+ def dependency_source_name(dependency_name)
57
+ output = @outputs_by_name[dependency_name.to_sym]
58
+ return output.source_root if output
59
+
60
+ dependency_name
61
+ end
62
+
53
63
  def tsort
54
64
  super
55
65
  rescue TSort::Cyclic => e
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ class TypeResolver
6
+ def self.call(graph, name)
7
+ new(graph).call(name)
8
+ end
9
+
10
+ def initialize(graph)
11
+ @graph = graph
12
+ end
13
+
14
+ def call(name)
15
+ if @graph.output?(name)
16
+ resolve_output(@graph.fetch_output(name))
17
+ else
18
+ resolve_node(@graph.fetch_node(name))
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def resolve_output(output)
25
+ return output.type if output.type
26
+
27
+ if output.composition_output?
28
+ composition = @graph.fetch_node(output.source_root)
29
+ child_graph = composition.contract_class.compiled_graph
30
+ self.class.call(child_graph, output.child_output_name)
31
+ else
32
+ resolve_node(@graph.fetch_node(output.source))
33
+ end
34
+ end
35
+
36
+ def resolve_node(node)
37
+ case node.kind
38
+ when :input
39
+ node.type
40
+ when :compute
41
+ node.type
42
+ when :composition
43
+ :result
44
+ when :branch
45
+ :result
46
+ when :collection
47
+ :array
48
+ else
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ class ValidationContext
6
+ attr_reader :graph, :runtime_nodes_by_name, :outputs_by_name
7
+
8
+ def initialize(graph)
9
+ @graph = graph
10
+ @runtime_nodes_by_name = {}
11
+ @outputs_by_name = {}
12
+ end
13
+
14
+ def runtime_nodes
15
+ @runtime_nodes ||= graph.nodes.reject { |node| node.kind == :output }
16
+ end
17
+
18
+ def outputs
19
+ @outputs ||= graph.nodes.select { |node| node.kind == :output }
20
+ end
21
+
22
+ def build_indexes!
23
+ runtime_nodes.each { |node| @runtime_nodes_by_name[node.name] = node }
24
+ outputs.each { |output| @outputs_by_name[output.name] = output }
25
+ end
26
+
27
+ def dependency_resolvable?(dependency_name)
28
+ runtime_nodes_by_name.key?(dependency_name.to_sym) || outputs_by_name.key?(dependency_name.to_sym)
29
+ end
30
+
31
+ def node?(name)
32
+ runtime_nodes_by_name.key?(name.to_sym)
33
+ end
34
+
35
+ def output?(name)
36
+ outputs_by_name.key?(name.to_sym)
37
+ end
38
+
39
+ def fetch_node(name)
40
+ runtime_nodes_by_name.fetch(name.to_sym)
41
+ end
42
+
43
+ def fetch_output(name)
44
+ outputs_by_name.fetch(name.to_sym)
45
+ end
46
+
47
+ def validation_error(node, message)
48
+ ValidationError.new(
49
+ message,
50
+ context: {
51
+ graph: graph.name,
52
+ node_id: node.id,
53
+ node_name: node.name,
54
+ node_path: node.path,
55
+ source_location: node.source_location
56
+ }
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ class ValidationPipeline
6
+ DEFAULT_VALIDATORS = [
7
+ Validators::UniquenessValidator,
8
+ Validators::OutputsValidator,
9
+ Validators::DependenciesValidator,
10
+ Validators::TypeCompatibilityValidator,
11
+ Validators::CallableValidator
12
+ ].freeze
13
+
14
+ def self.call(context, validators: DEFAULT_VALIDATORS)
15
+ new(context, validators: validators).call
16
+ end
17
+
18
+ def initialize(context, validators:)
19
+ @context = context
20
+ @validators = validators
21
+ end
22
+
23
+ def call
24
+ @context.build_indexes!
25
+ @validators.each { |validator| validator.call(@context) }
26
+ @context
27
+ end
28
+ end
29
+ end
30
+ end