igniter 0.3.1 → 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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/docs/DISTRIBUTED_CONTRACTS_V1.md +493 -0
  3. data/examples/distributed_workflow.rb +52 -0
  4. data/lib/igniter/compiler/compiled_graph.rb +12 -0
  5. data/lib/igniter/compiler/validation_pipeline.rb +3 -1
  6. data/lib/igniter/compiler/validators/await_validator.rb +53 -0
  7. data/lib/igniter/compiler/validators/dependencies_validator.rb +41 -1
  8. data/lib/igniter/compiler/validators/remote_validator.rb +58 -0
  9. data/lib/igniter/compiler.rb +2 -0
  10. data/lib/igniter/contract.rb +59 -8
  11. data/lib/igniter/dsl/contract_builder.rb +42 -4
  12. data/lib/igniter/errors.rb +6 -1
  13. data/lib/igniter/integrations/llm/config.rb +69 -0
  14. data/lib/igniter/integrations/llm/context.rb +74 -0
  15. data/lib/igniter/integrations/llm/executor.rb +159 -0
  16. data/lib/igniter/integrations/llm/providers/anthropic.rb +148 -0
  17. data/lib/igniter/integrations/llm/providers/base.rb +33 -0
  18. data/lib/igniter/integrations/llm/providers/ollama.rb +137 -0
  19. data/lib/igniter/integrations/llm/providers/openai.rb +153 -0
  20. data/lib/igniter/integrations/llm.rb +59 -0
  21. data/lib/igniter/integrations/rails/cable_adapter.rb +49 -0
  22. data/lib/igniter/integrations/rails/contract_job.rb +76 -0
  23. data/lib/igniter/integrations/rails/generators/contract/contract_generator.rb +22 -0
  24. data/lib/igniter/integrations/rails/generators/install/install_generator.rb +33 -0
  25. data/lib/igniter/integrations/rails/railtie.rb +25 -0
  26. data/lib/igniter/integrations/rails/webhook_concern.rb +49 -0
  27. data/lib/igniter/integrations/rails.rb +12 -0
  28. data/lib/igniter/model/await_node.rb +21 -0
  29. data/lib/igniter/model/remote_node.rb +26 -0
  30. data/lib/igniter/model.rb +2 -0
  31. data/lib/igniter/runtime/execution.rb +2 -2
  32. data/lib/igniter/runtime/input_validator.rb +5 -3
  33. data/lib/igniter/runtime/resolver.rb +43 -1
  34. data/lib/igniter/runtime/stores/active_record_store.rb +13 -1
  35. data/lib/igniter/runtime/stores/file_store.rb +50 -2
  36. data/lib/igniter/runtime/stores/memory_store.rb +55 -2
  37. data/lib/igniter/runtime/stores/redis_store.rb +13 -1
  38. data/lib/igniter/server/client.rb +123 -0
  39. data/lib/igniter/server/config.rb +27 -0
  40. data/lib/igniter/server/handlers/base.rb +105 -0
  41. data/lib/igniter/server/handlers/contracts_handler.rb +15 -0
  42. data/lib/igniter/server/handlers/event_handler.rb +28 -0
  43. data/lib/igniter/server/handlers/execute_handler.rb +37 -0
  44. data/lib/igniter/server/handlers/health_handler.rb +32 -0
  45. data/lib/igniter/server/handlers/status_handler.rb +27 -0
  46. data/lib/igniter/server/http_server.rb +109 -0
  47. data/lib/igniter/server/rack_app.rb +35 -0
  48. data/lib/igniter/server/registry.rb +56 -0
  49. data/lib/igniter/server/router.rb +75 -0
  50. data/lib/igniter/server.rb +67 -0
  51. data/lib/igniter/version.rb +1 -1
  52. data/lib/igniter.rb +4 -0
  53. metadata +36 -2
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Igniter
6
+ module Rails
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+ desc "Creates an Igniter initializer in your application."
11
+
12
+ def copy_initializer
13
+ template "igniter.rb.tt", "config/initializers/igniter.rb"
14
+ end
15
+
16
+ def create_contracts_directory
17
+ empty_directory "app/contracts"
18
+ create_file "app/contracts/.keep"
19
+ end
20
+
21
+ def show_readme
22
+ say "", :green
23
+ say "✓ Igniter installed!", :green
24
+ say ""
25
+ say "Next steps:"
26
+ say " 1. Configure your store in config/initializers/igniter.rb"
27
+ say " 2. Generate a contract: rails g igniter:contract YourContractName"
28
+ say ""
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ initializer "igniter.configure_store" do
7
+ # Auto-configure store based on available adapters unless already set
8
+ next if Igniter.instance_variable_defined?(:@execution_store)
9
+
10
+ Igniter.execution_store =
11
+ if defined?(Redis) && ::Rails.application.config.respond_to?(:redis)
12
+ Igniter::Runtime::Stores::RedisStore.new(::Redis.current)
13
+ else
14
+ Igniter::Runtime::Stores::MemoryStore.new
15
+ end
16
+ end
17
+
18
+ initializer "igniter.load_contracts" do
19
+ ::Rails.autoloaders.main.on_load("ApplicationContract") do
20
+ # Hook point for future eager loading of compiled contracts
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Rails
5
+ # Controller mixin for delivering external events to running contracts.
6
+ #
7
+ # Usage:
8
+ # class WebhooksController < ApplicationController
9
+ # include Igniter::Rails::WebhookHandler
10
+ #
11
+ # def stripe
12
+ # deliver_event_for(
13
+ # OrderContract,
14
+ # event: :stripe_payment_succeeded,
15
+ # correlation_from: { order_id: params[:metadata][:order_id] },
16
+ # payload: params.to_unsafe_h
17
+ # )
18
+ # end
19
+ # end
20
+ module WebhookHandler
21
+ def deliver_event_for(contract_class, event:, correlation_from:, payload: nil, store: nil) # rubocop:disable Metrics/MethodLength
22
+ payload_data = payload || (respond_to?(:params) ? params.to_unsafe_h : {})
23
+ correlation = extract_correlation(correlation_from)
24
+
25
+ contract_class.deliver_event(
26
+ event,
27
+ correlation: correlation,
28
+ payload: payload_data,
29
+ store: store || Igniter.execution_store
30
+ )
31
+
32
+ head :ok
33
+ rescue Igniter::ResolutionError => e
34
+ render json: { error: e.message }, status: :unprocessable_entity
35
+ end
36
+
37
+ private
38
+
39
+ def extract_correlation(source)
40
+ case source
41
+ when Hash then source.transform_keys(&:to_sym)
42
+ when Symbol then { source => params[source] }
43
+ when Array then source.each_with_object({}) { |k, h| h[k.to_sym] = params[k] }
44
+ else source.to_h.transform_keys(&:to_sym)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter"
4
+ require_relative "rails/railtie" if defined?(::Rails::Railtie)
5
+ require_relative "rails/contract_job"
6
+ require_relative "rails/webhook_concern"
7
+ require_relative "rails/cable_adapter"
8
+
9
+ module Igniter
10
+ module Rails
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class AwaitNode < Node
6
+ attr_reader :event_name
7
+
8
+ def initialize(id:, name:, event_name:, path: nil, metadata: {})
9
+ super(
10
+ id: id,
11
+ kind: :await,
12
+ name: name,
13
+ path: path || name.to_s,
14
+ dependencies: [],
15
+ metadata: metadata
16
+ )
17
+ @event_name = event_name.to_sym
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ # Represents a node that executes a contract on a remote igniter-server node.
6
+ # The result is the outputs hash returned by the remote contract.
7
+ class RemoteNode < Node
8
+ attr_reader :contract_name, :node_url, :input_mapping, :timeout
9
+
10
+ def initialize(id:, name:, contract_name:, node_url:, input_mapping:, timeout: 30, path: nil, metadata: {})
11
+ super(
12
+ id: id,
13
+ kind: :remote,
14
+ name: name,
15
+ path: path || name.to_s,
16
+ dependencies: input_mapping.values.map(&:to_sym),
17
+ metadata: metadata
18
+ )
19
+ @contract_name = contract_name.to_s
20
+ @node_url = node_url.to_s
21
+ @input_mapping = input_mapping.transform_keys(&:to_sym).transform_values(&:to_sym).freeze
22
+ @timeout = Integer(timeout)
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/igniter/model.rb CHANGED
@@ -8,6 +8,8 @@ require_relative "model/composition_node"
8
8
  require_relative "model/branch_node"
9
9
  require_relative "model/collection_node"
10
10
  require_relative "model/output_node"
11
+ require_relative "model/await_node"
12
+ require_relative "model/remote_node"
11
13
 
12
14
  module Igniter
13
15
  module Model
@@ -11,10 +11,10 @@ module Igniter
11
11
  @runner_strategy = runner
12
12
  @max_workers = max_workers
13
13
  @store = store
14
- @input_validator = InputValidator.new(compiled_graph)
14
+ @events = Events::Bus.new
15
+ @input_validator = InputValidator.new(compiled_graph, execution_id: @events.execution_id)
15
16
  @inputs = @input_validator.normalize_initial_inputs(inputs)
16
17
  @cache = Cache.new
17
- @events = Events::Bus.new
18
18
  @audit = Extensions::Auditing::Timeline.new(self)
19
19
  @events.subscribe(@audit)
20
20
  @resolver = Resolver.new(self)
@@ -3,8 +3,9 @@
3
3
  module Igniter
4
4
  module Runtime
5
5
  class InputValidator
6
- def initialize(compiled_graph)
6
+ def initialize(compiled_graph, execution_id: nil)
7
7
  @compiled_graph = compiled_graph
8
+ @execution_id = execution_id
8
9
  end
9
10
 
10
11
  def normalize_initial_inputs(raw_inputs)
@@ -106,7 +107,7 @@ module Igniter
106
107
  hash.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = value }
107
108
  end
108
109
 
109
- def input_error(input_node, message)
110
+ def input_error(input_node, message) # rubocop:disable Metrics/MethodLength
110
111
  InputError.new(
111
112
  message,
112
113
  context: {
@@ -114,7 +115,8 @@ module Igniter
114
115
  node_id: input_node.id,
115
116
  node_name: input_node.name,
116
117
  node_path: input_node.path,
117
- source_location: input_node.source_location
118
+ source_location: input_node.source_location,
119
+ execution_id: @execution_id
118
120
  }
119
121
  )
120
122
  end
@@ -25,6 +25,10 @@ module Igniter
25
25
  resolve_branch(node)
26
26
  when :collection
27
27
  resolve_collection(node)
28
+ when :await
29
+ resolve_await(node)
30
+ when :remote
31
+ resolve_remote(node)
28
32
  else
29
33
  raise ResolutionError, "Unsupported node kind: #{node.kind}"
30
34
  end
@@ -59,6 +63,43 @@ module Igniter
59
63
  NodeState.new(node: node, status: :succeeded, value: @execution.fetch_input!(node.name))
60
64
  end
61
65
 
66
+ def resolve_await(node)
67
+ deferred = Runtime::DeferredResult.build(
68
+ payload: { event: node.event_name },
69
+ source_node: node.name,
70
+ waiting_on: node.name
71
+ )
72
+ raise PendingDependencyError.new(deferred, "Waiting for external event '#{node.event_name}'")
73
+ end
74
+
75
+ def resolve_remote(node) # rubocop:disable Metrics/MethodLength
76
+ unless defined?(Igniter::Server::Client)
77
+ raise ResolutionError,
78
+ "remote: nodes require `require 'igniter/server'` (server integration not loaded)"
79
+ end
80
+
81
+ inputs = node.input_mapping.each_with_object({}) do |(child_input, dep_name), memo|
82
+ memo[child_input] = resolve_dependency_value(dep_name)
83
+ end
84
+
85
+ client = Igniter::Server::Client.new(node.node_url, timeout: node.timeout)
86
+ response = client.execute(node.contract_name, inputs: inputs)
87
+
88
+ case response[:status]
89
+ when :succeeded
90
+ NodeState.new(node: node, status: :succeeded, value: response[:outputs])
91
+ when :failed
92
+ error_message = response.dig(:error, :message) || response.dig(:error, "message")
93
+ raise ResolutionError,
94
+ "Remote #{node.contract_name}@#{node.node_url}: #{error_message}"
95
+ else
96
+ raise ResolutionError,
97
+ "Remote #{node.contract_name}@#{node.node_url}: unexpected status '#{response[:status]}'"
98
+ end
99
+ rescue Igniter::Server::Client::ConnectionError => e
100
+ raise ResolutionError, "Cannot reach #{node.node_url}: #{e.message}"
101
+ end
102
+
62
103
  def resolve_compute(node)
63
104
  dependencies = node.dependencies.each_with_object({}) do |dependency_name, memo|
64
105
  memo[dependency_name] = resolve_dependency_value(dependency_name)
@@ -310,7 +351,8 @@ module Igniter
310
351
  node_id: node.id,
311
352
  node_name: node.name,
312
353
  node_path: node.path,
313
- source_location: node.source_location
354
+ source_location: node.source_location,
355
+ execution_id: @execution.events.execution_id
314
356
  }
315
357
  )
316
358
  end
@@ -12,7 +12,7 @@ module Igniter
12
12
  @snapshot_column = snapshot_column.to_sym
13
13
  end
14
14
 
15
- def save(snapshot)
15
+ def save(snapshot, correlation: nil, graph: nil) # rubocop:disable Lint/UnusedMethodArgument
16
16
  execution_id = snapshot[:execution_id] || snapshot["execution_id"]
17
17
  record = @record_class.find_or_initialize_by(@execution_id_column => execution_id)
18
18
  record.public_send(:"#{@snapshot_column}=", JSON.generate(snapshot))
@@ -20,6 +20,18 @@ module Igniter
20
20
  execution_id
21
21
  end
22
22
 
23
+ def find_by_correlation(graph:, correlation:)
24
+ raise NotImplementedError, "find_by_correlation is not implemented for ActiveRecordStore"
25
+ end
26
+
27
+ def list_all(graph: nil)
28
+ raise NotImplementedError, "list_all is not implemented for ActiveRecordStore"
29
+ end
30
+
31
+ def list_pending(graph: nil)
32
+ raise NotImplementedError, "list_pending is not implemented for ActiveRecordStore"
33
+ end
34
+
23
35
  def fetch(execution_id)
24
36
  record = @record_class.find_by(@execution_id_column => execution_id)
25
37
  raise Igniter::ResolutionError, "No execution snapshot found for '#{execution_id}'" unless record
@@ -12,12 +12,51 @@ module Igniter
12
12
  FileUtils.mkdir_p(@root)
13
13
  end
14
14
 
15
- def save(snapshot)
15
+ def save(snapshot, correlation: nil, graph: nil)
16
16
  execution_id = snapshot[:execution_id] || snapshot["execution_id"]
17
- File.write(path_for(execution_id), JSON.pretty_generate(snapshot))
17
+ data = snapshot.merge(
18
+ _graph: graph,
19
+ _correlation: correlation&.transform_keys(&:to_s)
20
+ ).compact
21
+ File.write(path_for(execution_id), JSON.pretty_generate(data))
18
22
  execution_id
19
23
  end
20
24
 
25
+ def find_by_correlation(graph:, correlation:)
26
+ normalized = correlation.transform_keys(&:to_s)
27
+ each_snapshot do |data|
28
+ next unless data["_graph"] == graph
29
+
30
+ stored_corr = data["_correlation"] || {}
31
+ return data["execution_id"] if stored_corr == normalized
32
+ end
33
+ nil
34
+ end
35
+
36
+ def list_all(graph: nil)
37
+ results = []
38
+ each_snapshot do |data|
39
+ next if graph && data["_graph"] != graph
40
+
41
+ results << data["execution_id"]
42
+ end
43
+ results
44
+ end
45
+
46
+ def list_pending(graph: nil)
47
+ results = []
48
+ each_snapshot do |data|
49
+ next if graph && data["_graph"] != graph
50
+
51
+ states = data["states"] || {}
52
+ pending = states.any? do |_name, state|
53
+ (state["status"] || state[:status]).to_s == "pending"
54
+ end
55
+ results << data["execution_id"] if pending
56
+ end
57
+ results
58
+ end
59
+
21
60
  def fetch(execution_id)
22
61
  JSON.parse(File.read(path_for(execution_id)))
23
62
  rescue Errno::ENOENT
@@ -37,6 +76,15 @@ module Igniter
37
76
  def path_for(execution_id)
38
77
  File.join(@root, "#{execution_id}.json")
39
78
  end
79
+
80
+ def each_snapshot(&block)
81
+ Dir.glob(File.join(@root, "*.json")).each do |file|
82
+ data = JSON.parse(File.read(file))
83
+ block.call(data)
84
+ rescue JSON::ParserError
85
+ next
86
+ end
87
+ end
40
88
  end
41
89
  end
42
90
  end
@@ -6,15 +6,68 @@ module Igniter
6
6
  class MemoryStore
7
7
  def initialize
8
8
  @snapshots = {}
9
+ @correlation_index = {}
9
10
  @mutex = Mutex.new
10
11
  end
11
12
 
12
- def save(snapshot)
13
+ def save(snapshot, correlation: nil, graph: nil) # rubocop:disable Metrics/MethodLength
13
14
  execution_id = snapshot[:execution_id] || snapshot["execution_id"]
14
- @mutex.synchronize { @snapshots[execution_id] = deep_copy(snapshot) }
15
+ @mutex.synchronize do
16
+ @snapshots[execution_id] = deep_copy(snapshot)
17
+ if graph
18
+ @correlation_index[execution_id] = {
19
+ graph: graph,
20
+ correlation: (correlation || {}).transform_keys(&:to_sym)
21
+ }
22
+ end
23
+ end
15
24
  execution_id
16
25
  end
17
26
 
27
+ def find_by_correlation(graph:, correlation:)
28
+ normalized = correlation.transform_keys(&:to_sym)
29
+ @mutex.synchronize do
30
+ @correlation_index.each do |execution_id, entry|
31
+ next unless entry[:graph] == graph
32
+ return execution_id if entry[:correlation] == normalized
33
+ end
34
+ nil
35
+ end
36
+ end
37
+
38
+ def list_all(graph: nil)
39
+ @mutex.synchronize do
40
+ if graph
41
+ @correlation_index.select { |_id, entry| entry[:graph] == graph }.keys
42
+ else
43
+ @snapshots.keys
44
+ end
45
+ end
46
+ end
47
+
48
+ def list_pending(graph: nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
49
+ ids = @mutex.synchronize do
50
+ if graph
51
+ @correlation_index.select { |_id, entry| entry[:graph] == graph }.keys
52
+ else
53
+ @snapshots.keys
54
+ end
55
+ end
56
+
57
+ @mutex.synchronize do
58
+ ids.select do |id|
59
+ snapshot = @snapshots[id]
60
+ next false unless snapshot
61
+
62
+ states = snapshot[:states] || snapshot["states"] || {}
63
+ states.any? do |_name, state|
64
+ status = state[:status] || state["status"]
65
+ status.to_s == "pending"
66
+ end
67
+ end
68
+ end
69
+ end
70
+
18
71
  def fetch(execution_id)
19
72
  @mutex.synchronize { deep_copy(@snapshots.fetch(execution_id)) }
20
73
  rescue KeyError
@@ -11,12 +11,24 @@ module Igniter
11
11
  @namespace = namespace
12
12
  end
13
13
 
14
- def save(snapshot)
14
+ def save(snapshot, correlation: nil, graph: nil) # rubocop:disable Lint/UnusedMethodArgument
15
15
  execution_id = snapshot[:execution_id] || snapshot["execution_id"]
16
16
  @redis.set(redis_key(execution_id), JSON.generate(snapshot))
17
17
  execution_id
18
18
  end
19
19
 
20
+ def find_by_correlation(graph:, correlation:)
21
+ raise NotImplementedError, "find_by_correlation is not implemented for RedisStore"
22
+ end
23
+
24
+ def list_all(graph: nil)
25
+ raise NotImplementedError, "list_all is not implemented for RedisStore"
26
+ end
27
+
28
+ def list_pending(graph: nil)
29
+ raise NotImplementedError, "list_pending is not implemented for RedisStore"
30
+ end
31
+
20
32
  def fetch(execution_id)
21
33
  payload = @redis.get(redis_key(execution_id))
22
34
  raise Igniter::ResolutionError, "No execution snapshot found for '#{execution_id}'" unless payload
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Igniter
8
+ module Server
9
+ # HTTP client for calling remote igniter-server nodes.
10
+ # Uses only stdlib (Net::HTTP + JSON), no external gems required.
11
+ class Client
12
+ class Error < Igniter::Server::Error; end
13
+ class ConnectionError < Error; end
14
+ class RemoteError < Error; end
15
+
16
+ def initialize(base_url, timeout: 30)
17
+ @base_url = base_url.chomp("/")
18
+ @timeout = timeout
19
+ end
20
+
21
+ # Execute a contract on the remote node synchronously.
22
+ #
23
+ # Returns a symbolized hash:
24
+ # { status: :succeeded, execution_id: "uuid", outputs: { result: 42 } }
25
+ # { status: :failed, execution_id: "uuid", error: { message: "..." } }
26
+ # { status: :pending, execution_id: "uuid", waiting_for: ["event"] }
27
+ def execute(contract_name, inputs: {})
28
+ response = post(
29
+ "/v1/contracts/#{uri_encode(contract_name)}/execute",
30
+ { inputs: inputs }
31
+ )
32
+ symbolize_response(response)
33
+ end
34
+
35
+ # Deliver an event to a pending distributed workflow on the remote node.
36
+ def deliver_event(contract_name, event:, correlation:, payload: {})
37
+ response = post(
38
+ "/v1/contracts/#{uri_encode(contract_name)}/events",
39
+ { event: event, correlation: correlation, payload: payload }
40
+ )
41
+ symbolize_response(response)
42
+ end
43
+
44
+ # Fetch execution status by ID.
45
+ def status(execution_id)
46
+ symbolize_response(get("/v1/executions/#{uri_encode(execution_id)}"))
47
+ end
48
+
49
+ # Check remote node health.
50
+ def health
51
+ get("/v1/health")
52
+ end
53
+
54
+ private
55
+
56
+ def post(path, body)
57
+ uri = build_uri(path)
58
+ http = build_http(uri)
59
+ req = Net::HTTP::Post.new(uri.path, json_headers)
60
+ req.body = JSON.generate(body)
61
+ parse_response(http.request(req))
62
+ rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, SocketError, Net::OpenTimeout => e
63
+ raise ConnectionError, "Cannot connect to #{@base_url}: #{e.message}"
64
+ end
65
+
66
+ def get(path)
67
+ uri = build_uri(path)
68
+ http = build_http(uri)
69
+ req = Net::HTTP::Get.new(uri.path, json_headers)
70
+ parse_response(http.request(req))
71
+ rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, SocketError, Net::OpenTimeout => e
72
+ raise ConnectionError, "Cannot connect to #{@base_url}: #{e.message}"
73
+ end
74
+
75
+ def build_uri(path)
76
+ URI.parse("#{@base_url}#{path}")
77
+ end
78
+
79
+ def build_http(uri)
80
+ http = Net::HTTP.new(uri.host, uri.port)
81
+ http.use_ssl = uri.scheme == "https"
82
+ http.read_timeout = @timeout
83
+ http.open_timeout = 10
84
+ http
85
+ end
86
+
87
+ def json_headers
88
+ { "Content-Type" => "application/json", "Accept" => "application/json" }
89
+ end
90
+
91
+ def parse_response(response)
92
+ body = begin
93
+ JSON.parse(response.body.to_s)
94
+ rescue JSON::ParserError
95
+ {}
96
+ end
97
+ raise RemoteError, "Remote error #{response.code}: #{body["error"]}" unless response.is_a?(Net::HTTPSuccess)
98
+
99
+ body
100
+ end
101
+
102
+ def symbolize_response(hash)
103
+ {
104
+ status: hash["status"]&.to_sym,
105
+ execution_id: hash["execution_id"],
106
+ outputs: symbolize_keys(hash["outputs"] || {}),
107
+ waiting_for: hash["waiting_for"] || [],
108
+ error: hash["error"]
109
+ }
110
+ end
111
+
112
+ def symbolize_keys(hash)
113
+ return hash unless hash.is_a?(Hash)
114
+
115
+ hash.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v }
116
+ end
117
+
118
+ def uri_encode(str)
119
+ URI.encode_uri_component(str.to_s)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ class Config
6
+ attr_accessor :host, :port, :store, :logger
7
+ attr_reader :registry
8
+
9
+ def initialize
10
+ @host = "0.0.0.0"
11
+ @port = 4567
12
+ @store = Igniter::Runtime::Stores::MemoryStore.new
13
+ @registry = Registry.new
14
+ @logger = nil
15
+ end
16
+
17
+ def register(name, contract_class)
18
+ @registry.register(name, contract_class)
19
+ self
20
+ end
21
+
22
+ def contracts=(hash)
23
+ hash.each { |name, klass| register(name.to_s, klass) }
24
+ end
25
+ end
26
+ end
27
+ end