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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +296 -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 +127 -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 +269 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +90 -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 +153 -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 +152 -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 +186 -11
  35. data/lib/igniter/dsl/contract_builder.rb +271 -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 +33 -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 +46 -0
  51. data/lib/igniter/model/collection_node.rb +31 -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 +310 -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
@@ -7,16 +7,109 @@ module Igniter
7
7
  @compiled_graph = DSL::ContractBuilder.compile(name: contract_name, &block)
8
8
  end
9
9
 
10
- def react_to(event_type, path: nil, &block)
10
+ def run_with(runner:, max_workers: nil)
11
+ @execution_options = { runner: runner, max_workers: max_workers }.compact
12
+ end
13
+
14
+ def restore_from_store(execution_id, store: nil)
15
+ snapshot = (store || Igniter.execution_store).fetch(execution_id)
16
+ restore(snapshot)
17
+ end
18
+
19
+ def resume_from_store(execution_id, token:, value:, store: nil)
20
+ Runtime::JobWorker.new(self, store: store || Igniter.execution_store).resume(
21
+ execution_id: execution_id,
22
+ token: token,
23
+ value: value
24
+ )
25
+ end
26
+
27
+ def define_schema(schema)
28
+ @compiled_graph = DSL::SchemaBuilder.compile(schema, name: contract_name)
29
+ end
30
+
31
+ def restore(snapshot)
32
+ instance = new(
33
+ snapshot[:inputs] || snapshot["inputs"] || {},
34
+ runner: snapshot[:runner] || snapshot["runner"],
35
+ max_workers: snapshot[:max_workers] || snapshot["max_workers"]
36
+ )
37
+ instance.restore_execution(snapshot)
38
+ end
39
+
40
+ def react_to(event_type, path: nil, once_per_execution: false, &block)
11
41
  raise CompileError, "react_to requires a block" unless block
12
42
 
13
43
  reactions << Extensions::Reactive::Reaction.new(
14
44
  event_type: event_type,
15
45
  path: path,
16
- action: block
46
+ action: block,
47
+ once_per_execution: once_per_execution
17
48
  )
18
49
  end
19
50
 
51
+ def effect(path = nil, event_type: :node_succeeded, &block)
52
+ react_to(event_type, path: path, &block)
53
+ end
54
+
55
+ def on_success(target, &block)
56
+ graph = compiled_graph
57
+ if graph&.output?(target)
58
+ react_to(:execution_finished, once_per_execution: true) do |event:, contract:, execution:|
59
+ next if execution.cache.values.any?(&:failed?)
60
+ next if execution.cache.values.any?(&:pending?)
61
+
62
+ block.call(
63
+ event: event,
64
+ contract: contract,
65
+ execution: execution,
66
+ value: contract.result.public_send(target)
67
+ )
68
+ end
69
+ else
70
+ effect(target.to_s, event_type: :node_succeeded, &block)
71
+ end
72
+ end
73
+
74
+ def on_failure(&block)
75
+ react_to(:execution_failed, once_per_execution: true) do |event:, contract:, execution:|
76
+ block.call(
77
+ event: event,
78
+ contract: contract,
79
+ execution: execution,
80
+ status: :failed,
81
+ errors: terminal_errors(execution),
82
+ error: terminal_errors(execution).values.first
83
+ )
84
+ end
85
+ end
86
+
87
+ def on_exit(&block)
88
+ terminal_hook = proc do |event:, contract:, execution:|
89
+ status = event.type == :execution_failed ? :failed : :succeeded
90
+ errors = status == :failed ? terminal_errors(execution) : {}
91
+
92
+ block.call(
93
+ event: event,
94
+ contract: contract,
95
+ execution: execution,
96
+ status: status,
97
+ errors: errors,
98
+ error: errors.values.first
99
+ )
100
+ end
101
+
102
+ react_to(:execution_finished, once_per_execution: true, &terminal_hook)
103
+ react_to(:execution_failed, once_per_execution: true, &terminal_hook)
104
+ end
105
+
106
+ def present(output_name, with: nil, &block)
107
+ raise CompileError, "present requires a block or `with:`" unless block || with
108
+ raise CompileError, "present cannot use both a block and `with:`" if block && with
109
+
110
+ own_output_presenters[output_name.to_sym] = with || block
111
+ end
112
+
20
113
  def compiled_graph
21
114
  @compiled_graph || superclass_compiled_graph
22
115
  end
@@ -26,8 +119,21 @@ module Igniter
26
119
  @reactions ||= []
27
120
  end
28
121
 
122
+ def execution_options
123
+ @execution_options || superclass_execution_options || {}
124
+ end
125
+
126
+ def output_presenters
127
+ inherited = superclass.respond_to?(:output_presenters) ? superclass.output_presenters : {}
128
+ inherited.merge(own_output_presenters)
129
+ end
130
+
29
131
  private
30
132
 
133
+ def own_output_presenters
134
+ @output_presenters ||= {}
135
+ end
136
+
31
137
  def contract_name
32
138
  name || "AnonymousContract"
33
139
  end
@@ -37,18 +143,47 @@ module Igniter
37
143
 
38
144
  superclass.compiled_graph
39
145
  end
146
+
147
+ def superclass_execution_options
148
+ return unless superclass.respond_to?(:execution_options)
149
+
150
+ superclass.execution_options
151
+ end
152
+
153
+ def terminal_errors(execution)
154
+ execution.cache.values.each_with_object({}) do |state, memo|
155
+ next unless state.failed?
156
+
157
+ memo[state.node.name] = state.error
158
+ end
159
+ end
40
160
  end
41
161
 
42
162
  attr_reader :execution, :result
43
163
 
44
- def initialize(inputs = {})
164
+ def initialize(inputs = nil, runner: nil, max_workers: nil, **keyword_inputs)
45
165
  graph = self.class.compiled_graph
46
166
  raise CompileError, "#{self.class.name} has no compiled graph. Use `define`." unless graph
47
167
 
168
+ normalized_inputs =
169
+ if inputs.nil?
170
+ keyword_inputs
171
+ elsif keyword_inputs.empty?
172
+ inputs
173
+ else
174
+ inputs.to_h.merge(keyword_inputs)
175
+ end
176
+
177
+ execution_options = self.class.execution_options.merge(
178
+ { runner: runner, max_workers: max_workers }.compact
179
+ )
180
+ execution_options[:store] ||= Igniter.execution_store if execution_options[:runner]&.to_sym == :store
181
+
48
182
  @execution = Runtime::Execution.new(
49
183
  compiled_graph: graph,
50
184
  contract_instance: self,
51
- inputs: inputs
185
+ inputs: normalized_inputs,
186
+ **execution_options
52
187
  )
53
188
  @reactive = Extensions::Reactive::Engine.new(
54
189
  execution: @execution,
@@ -98,6 +233,15 @@ module Igniter
98
233
  Diagnostics::Report.new(execution)
99
234
  end
100
235
 
236
+ def snapshot
237
+ execution.snapshot
238
+ end
239
+
240
+ def restore_execution(snapshot)
241
+ execution.restore!(snapshot)
242
+ self
243
+ end
244
+
101
245
  def diagnostics_text
102
246
  diagnostics.to_text
103
247
  end
@@ -106,6 +250,10 @@ module Igniter
106
250
  diagnostics.to_markdown
107
251
  end
108
252
 
253
+ def explain_plan(output_names = nil)
254
+ execution.explain_plan(output_names)
255
+ end
256
+
109
257
  def success?
110
258
  execution.success?
111
259
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Igniter
6
+ module Diagnostics
7
+ module Auditing
8
+ module Report
9
+ # Generates a human-readable text tree of an audition step.
10
+ class ConsoleFormatter
11
+ def self.call(player)
12
+ new(player).call
13
+ end
14
+
15
+ def initialize(player)
16
+ @player = player
17
+ @buffer = StringIO.new
18
+ @definition_graph = player.definition_graph
19
+ @audited_graph = player.current_graph
20
+ end
21
+
22
+ def call
23
+ print_header
24
+
25
+ @definition_graph.root_nodes.each do |node_def|
26
+ build_node_tree(node_def, prefix: "")
27
+ end
28
+
29
+ @buffer.string
30
+ end
31
+
32
+ private
33
+
34
+ def print_header
35
+ trigger = @audited_graph.triggering_event
36
+ @buffer.puts "--- Audition for #{@player.record.contract_class_name} ---"
37
+ @buffer.puts "Step: #{@audited_graph.version} / #{@player.versions_count}"
38
+ @buffer.puts "Trigger: #{trigger[:type]} on '#{trigger[:path]}'" if trigger
39
+ @buffer.puts "-------------------------------------------------"
40
+ end
41
+
42
+ def build_node_tree(node_def, prefix:)
43
+ children_defs = @definition_graph.children_of(node_def)
44
+ is_last = (node_def.parent ? @definition_graph.children_of(node_def.parent) : @definition_graph.root_nodes).last == node_def
45
+
46
+ @buffer << prefix
47
+ @buffer << (is_last ? "└── " : "├── ")
48
+ @buffer << "#{node_def.name} [#{node_def.class.name.split('::').last}]"
49
+
50
+ append_node_details(node_def)
51
+
52
+ @buffer << "\n"
53
+
54
+ new_prefix = prefix + (is_last ? " " : "│ ")
55
+ children_defs.each do |child_def|
56
+ build_node_tree(child_def, prefix: new_prefix)
57
+ end
58
+ end
59
+
60
+ def append_node_details(node_def)
61
+ audited_node = @audited_graph.find(node_def.path)
62
+ if audited_node
63
+ status_icon = case audited_node.status.to_sym
64
+ when :success then "✓"
65
+ when :failure then "✗"
66
+ when :invalidated then "~"
67
+ else "?"
68
+ end
69
+ @buffer << " [#{status_icon}]"
70
+ @buffer << " => <#{audited_node.value_class}> #{audited_node.value.to_s.truncate(100)}"
71
+ @buffer << " (Errors: #{audited_node.errors.join(', ')})" if audited_node.status.to_sym == :failure
72
+ else
73
+ @buffer << " [Not Present in History]"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Diagnostics
5
+ module Auditing
6
+ module Report
7
+ class MarkdownFormatter
8
+ def self.call(player)
9
+ new(player).call
10
+ end
11
+
12
+ def initialize(player)
13
+ @player = player
14
+ @definition_graph = player.definition_graph
15
+ @audited_graph = player.current_graph
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Diagnostics
5
+ module Introspection
6
+ module Formatters
7
+ class MermaidFormatter
8
+ def self.call(structure)
9
+ new(structure).call
10
+ end
11
+
12
+ def initialize(structure)
13
+ @structure = structure
14
+ @output = StringIO.new
15
+ end
16
+
17
+ def call
18
+ # Устанавливаем направление диаграммы слева направо
19
+ @output.puts "graph LR"
20
+ @output.puts "classDef default fill:#fff,stroke:#333,stroke-width:2px;"
21
+
22
+ # Запускаем рекурсивную отрисовку
23
+ draw_nodes_and_links(@structure)
24
+
25
+ @output.string
26
+ end
27
+
28
+ private
29
+
30
+ def draw_nodes_and_links(nodes)
31
+ nodes.each do |node|
32
+ # 1. Если узел - это Namespace, создаем для него subgraph
33
+ if node[:type] == "Namespace"
34
+ @output.puts " subgraph #{node[:name]}"
35
+ # Рекурсивно отрисовываем всех детей внутри подграфа
36
+ draw_nodes_and_links(node[:children])
37
+ @output.puts " end"
38
+ else
39
+ # 2. Если это обычный узел, просто объявляем его
40
+ node_text = "\"#{node[:name]}<br>[#{node[:details]}]\""
41
+ @output.puts " #{node[:path]}(#{node_text})"
42
+
43
+ # У обычного узла тоже могут быть дети (например, проекции у композиции),
44
+ # их тоже нужно отрисовать.
45
+ draw_nodes_and_links(node[:children])
46
+ end
47
+
48
+ # 3. После объявления узла (или целого подграфа) рисуем его зависимости
49
+ node[:dependencies].each do |dep_path|
50
+ @output.puts " #{dep_path} --> #{node[:path]}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Diagnostics
5
+ module Introspection
6
+ module Formatters
7
+ class TextTreeFormatter
8
+ def self.call(structure)
9
+ new(structure).call
10
+ end
11
+
12
+ def initialize(structure)
13
+ @structure = structure
14
+ @output = StringIO.new
15
+ end
16
+
17
+ def call
18
+ @structure.each_with_index do |node, index|
19
+ is_last = index == @structure.size - 1
20
+ print_node(node, "", is_last)
21
+ end
22
+ @output.string
23
+ end
24
+
25
+ private
26
+
27
+ def print_node(node, prefix, is_last)
28
+ connector = is_last ? "└── " : "├── "
29
+ @output << "#{prefix}#{connector}#{node[:name]} [#{node[:type]}] #{node[:details]}\n"
30
+
31
+ children_prefix = prefix + (is_last ? " " : "│ ")
32
+ if node[:dependencies].any?
33
+ @output << "#{children_prefix} └─ depends_on: [#{node[:dependencies].join(', ')}]\n"
34
+ end
35
+
36
+ node[:children].each_with_index do |child, index|
37
+ print_node(child, children_prefix, index == node[:children].size - 1)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end