igniter 0.3.0 → 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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +2 -2
  4. data/docs/API_V2.md +58 -0
  5. data/docs/DISTRIBUTED_CONTRACTS_V1.md +493 -0
  6. data/examples/README.md +3 -0
  7. data/examples/distributed_workflow.rb +52 -0
  8. data/examples/ringcentral_routing.rb +26 -35
  9. data/lib/igniter/compiler/compiled_graph.rb +20 -0
  10. data/lib/igniter/compiler/validation_pipeline.rb +3 -1
  11. data/lib/igniter/compiler/validators/await_validator.rb +53 -0
  12. data/lib/igniter/compiler/validators/dependencies_validator.rb +43 -1
  13. data/lib/igniter/compiler/validators/remote_validator.rb +58 -0
  14. data/lib/igniter/compiler.rb +2 -0
  15. data/lib/igniter/contract.rb +75 -8
  16. data/lib/igniter/diagnostics/report.rb +102 -3
  17. data/lib/igniter/dsl/contract_builder.rb +109 -8
  18. data/lib/igniter/errors.rb +6 -1
  19. data/lib/igniter/extensions/introspection/graph_formatter.rb +4 -0
  20. data/lib/igniter/integrations/llm/config.rb +69 -0
  21. data/lib/igniter/integrations/llm/context.rb +74 -0
  22. data/lib/igniter/integrations/llm/executor.rb +159 -0
  23. data/lib/igniter/integrations/llm/providers/anthropic.rb +148 -0
  24. data/lib/igniter/integrations/llm/providers/base.rb +33 -0
  25. data/lib/igniter/integrations/llm/providers/ollama.rb +137 -0
  26. data/lib/igniter/integrations/llm/providers/openai.rb +153 -0
  27. data/lib/igniter/integrations/llm.rb +59 -0
  28. data/lib/igniter/integrations/rails/cable_adapter.rb +49 -0
  29. data/lib/igniter/integrations/rails/contract_job.rb +76 -0
  30. data/lib/igniter/integrations/rails/generators/contract/contract_generator.rb +22 -0
  31. data/lib/igniter/integrations/rails/generators/install/install_generator.rb +33 -0
  32. data/lib/igniter/integrations/rails/railtie.rb +25 -0
  33. data/lib/igniter/integrations/rails/webhook_concern.rb +49 -0
  34. data/lib/igniter/integrations/rails.rb +12 -0
  35. data/lib/igniter/model/await_node.rb +21 -0
  36. data/lib/igniter/model/branch_node.rb +9 -3
  37. data/lib/igniter/model/collection_node.rb +9 -3
  38. data/lib/igniter/model/remote_node.rb +26 -0
  39. data/lib/igniter/model.rb +2 -0
  40. data/lib/igniter/runtime/execution.rb +2 -2
  41. data/lib/igniter/runtime/input_validator.rb +5 -3
  42. data/lib/igniter/runtime/resolver.rb +91 -8
  43. data/lib/igniter/runtime/stores/active_record_store.rb +13 -1
  44. data/lib/igniter/runtime/stores/file_store.rb +50 -2
  45. data/lib/igniter/runtime/stores/memory_store.rb +55 -2
  46. data/lib/igniter/runtime/stores/redis_store.rb +13 -1
  47. data/lib/igniter/server/client.rb +123 -0
  48. data/lib/igniter/server/config.rb +27 -0
  49. data/lib/igniter/server/handlers/base.rb +105 -0
  50. data/lib/igniter/server/handlers/contracts_handler.rb +15 -0
  51. data/lib/igniter/server/handlers/event_handler.rb +28 -0
  52. data/lib/igniter/server/handlers/execute_handler.rb +37 -0
  53. data/lib/igniter/server/handlers/health_handler.rb +32 -0
  54. data/lib/igniter/server/handlers/status_handler.rb +27 -0
  55. data/lib/igniter/server/http_server.rb +109 -0
  56. data/lib/igniter/server/rack_app.rb +35 -0
  57. data/lib/igniter/server/registry.rb +56 -0
  58. data/lib/igniter/server/router.rb +75 -0
  59. data/lib/igniter/server.rb +67 -0
  60. data/lib/igniter/version.rb +1 -1
  61. data/lib/igniter.rb +4 -0
  62. metadata +36 -2
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Igniter
6
+ module Server
7
+ module Handlers
8
+ class Base
9
+ def initialize(registry, store)
10
+ @registry = registry
11
+ @store = store
12
+ end
13
+
14
+ def call(params:, body:)
15
+ handle(params: params, body: body)
16
+ rescue Igniter::Server::Registry::RegistryError => e
17
+ json_error(e.message, status: 404)
18
+ rescue Igniter::Error => e
19
+ json_error(e.message, status: 422)
20
+ rescue StandardError => e
21
+ json_error("Internal server error: #{e.message}", status: 500)
22
+ end
23
+
24
+ private
25
+
26
+ def handle(params:, body:)
27
+ raise NotImplementedError, "#{self.class}#handle must be implemented"
28
+ end
29
+
30
+ def json_ok(data)
31
+ { status: 200, body: JSON.generate(data), headers: json_ct }
32
+ end
33
+
34
+ def json_error(message, status: 422)
35
+ { status: status, body: JSON.generate({ error: message }), headers: json_ct }
36
+ end
37
+
38
+ def json_ct
39
+ { "Content-Type" => "application/json" }
40
+ end
41
+
42
+ # Serialize a contract execution result into the standard API response hash.
43
+ # Reads the cache directly to avoid calling contract.pending?/failed? which
44
+ # internally re-invoke resolve_all and would re-raise on failed nodes.
45
+ def serialize_execution(contract, contract_class) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
46
+ execution_id = contract.execution.events.execution_id
47
+ cache_values = contract.execution.cache.values
48
+
49
+ if cache_values.any?(&:pending?)
50
+ pending_nodes = cache_values.select(&:pending?)
51
+ waiting_for = pending_nodes.filter_map { |s| s.value.payload[:event]&.to_s }
52
+ { execution_id: execution_id, status: "pending", waiting_for: waiting_for }
53
+ elsif cache_values.any?(&:failed?)
54
+ error_state = cache_values.find(&:failed?)
55
+ { execution_id: execution_id, status: "failed",
56
+ error: serialize_error(error_state&.error) }
57
+ else
58
+ outputs = serialize_outputs(contract, contract_class)
59
+ { execution_id: execution_id, status: "succeeded", outputs: outputs }
60
+ end
61
+ end
62
+
63
+ def serialize_outputs(contract, contract_class)
64
+ contract_class.compiled_graph.outputs.each_with_object({}) do |output, memo|
65
+ memo[output.name] = to_json_value(contract.result.public_send(output.name))
66
+ rescue StandardError
67
+ nil
68
+ end
69
+ end
70
+
71
+ def serialize_error(error)
72
+ return { message: "Unknown error" } unless error
73
+
74
+ {
75
+ type: error.class.name,
76
+ message: error.message,
77
+ node: error.context[:node_name]
78
+ }
79
+ end
80
+
81
+ # Recursively convert a value into JSON-safe primitives.
82
+ def to_json_value(value) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
83
+ case value
84
+ when Hash
85
+ value.each_with_object({}) { |(k, v), h| h[k.to_s] = to_json_value(v) }
86
+ when Array
87
+ value.map { |v| to_json_value(v) }
88
+ when String, Integer, Float, TrueClass, FalseClass, NilClass
89
+ value
90
+ when Symbol
91
+ value.to_s
92
+ else
93
+ value.respond_to?(:to_h) ? to_json_value(value.to_h) : value.to_s
94
+ end
95
+ end
96
+
97
+ def symbolize_inputs(hash)
98
+ return {} unless hash.is_a?(Hash)
99
+
100
+ hash.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ class ContractsHandler < Base
7
+ private
8
+
9
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument
10
+ json_ok(@registry.introspect)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ class EventHandler < Base
7
+ private
8
+
9
+ def handle(params:, body:) # rubocop:disable Metrics/MethodLength
10
+ contract_class = @registry.fetch(params[:name])
11
+ event = body["event"]&.to_sym
12
+ correlation = symbolize_inputs(body["correlation"] || {})
13
+ payload = body["payload"] || {}
14
+
15
+ raise Igniter::Error, "event name is required" unless event
16
+
17
+ contract = contract_class.deliver_event(event,
18
+ correlation: correlation,
19
+ payload: payload,
20
+ store: @store)
21
+ contract.resolve_all unless contract.success? || contract.failed?
22
+
23
+ json_ok(serialize_execution(contract, contract_class))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ class ExecuteHandler < Base
7
+ private
8
+
9
+ def handle(params:, body:)
10
+ contract_class = @registry.fetch(params[:name])
11
+ inputs = symbolize_inputs(body["inputs"] || {})
12
+ contract = run_contract(contract_class, inputs)
13
+ json_ok(serialize_execution(contract, contract_class))
14
+ end
15
+
16
+ def run_contract(contract_class, inputs) # rubocop:disable Metrics/MethodLength
17
+ if use_distributed_start?(contract_class)
18
+ contract_class.start(inputs, store: @store)
19
+ else
20
+ contract = contract_class.new(inputs)
21
+ begin
22
+ contract.resolve_all
23
+ rescue Igniter::Error
24
+ nil # failed state is already written to cache; serialize_execution handles it
25
+ end
26
+ contract
27
+ end
28
+ end
29
+
30
+ # Use .start for contracts that declare correlate_by (distributed workflows).
31
+ def use_distributed_start?(contract_class)
32
+ contract_class.respond_to?(:correlation_keys) && !contract_class.correlation_keys.empty?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ class HealthHandler < Base
7
+ def initialize(registry, store, node_url: nil)
8
+ super(registry, store)
9
+ @node_url = node_url
10
+ end
11
+
12
+ private
13
+
14
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument,Metrics/MethodLength
15
+ pending_count = begin
16
+ @store.list_pending.size
17
+ rescue StandardError
18
+ 0
19
+ end
20
+
21
+ json_ok({
22
+ status: "ok",
23
+ node: @node_url,
24
+ contracts: @registry.names,
25
+ store: @store.class.name.split("::").last,
26
+ pending: pending_count
27
+ })
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ module Handlers
6
+ class StatusHandler < Base
7
+ private
8
+
9
+ def handle(params:, body:) # rubocop:disable Lint/UnusedMethodArgument,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
10
+ execution_id = params[:id]
11
+ snapshot = @store.fetch(execution_id)
12
+ states = snapshot[:states] || snapshot["states"] || {}
13
+
14
+ pending = states.values.any? { |s| (s[:status] || s["status"]).to_s == "pending" }
15
+ failed = states.values.any? { |s| (s[:status] || s["status"]).to_s == "failed" }
16
+
17
+ status = if pending then "pending"
18
+ elsif failed then "failed"
19
+ else "succeeded"
20
+ end
21
+
22
+ json_ok({ execution_id: execution_id, status: status })
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Igniter
6
+ module Server
7
+ # Pure-Ruby HTTP/1.1 server built on TCPServer (stdlib, zero external deps).
8
+ # Spawns one thread per connection. Intended for development and orchestration use.
9
+ # For production, use RackApp with Puma via `Igniter::Server.rack_app`.
10
+ class HttpServer
11
+ CRLF = "\r\n"
12
+ STATUS_MESSAGES = {
13
+ 200 => "OK",
14
+ 400 => "Bad Request",
15
+ 404 => "Not Found",
16
+ 422 => "Unprocessable Entity",
17
+ 500 => "Internal Server Error"
18
+ }.freeze
19
+
20
+ def initialize(config)
21
+ @config = config
22
+ @router = Router.new(config)
23
+ end
24
+
25
+ def start # rubocop:disable Metrics/MethodLength
26
+ @tcp_server = TCPServer.new(@config.host, @config.port)
27
+ @running = true
28
+
29
+ trap("INT") { stop }
30
+ trap("TERM") { stop }
31
+
32
+ log("igniter-server listening on http://#{@config.host}:#{@config.port}")
33
+
34
+ loop do
35
+ break unless @running
36
+
37
+ client = accept_connection
38
+ Thread.new(client) { |conn| handle_connection(conn) } if client
39
+ end
40
+ rescue IOError
41
+ # Server socket closed via stop
42
+ end
43
+
44
+ def stop
45
+ @running = false
46
+ @tcp_server&.close
47
+ end
48
+
49
+ private
50
+
51
+ def accept_connection
52
+ @tcp_server.accept_nonblock
53
+ rescue IO::WaitReadable
54
+ IO.select([@tcp_server], nil, nil, 0.5)
55
+ nil
56
+ rescue IOError
57
+ nil
58
+ end
59
+
60
+ def handle_connection(socket) # rubocop:disable Metrics/MethodLength
61
+ request_line = socket.gets&.chomp
62
+ return unless request_line&.include?(" ")
63
+
64
+ http_method, path, = request_line.split(" ", 3)
65
+ headers = read_headers(socket)
66
+ body = read_body(socket, headers["content-length"].to_i)
67
+
68
+ result = @router.call(http_method, path, body)
69
+ write_response(socket, result)
70
+ rescue StandardError => e
71
+ log("Connection error: #{e.message}")
72
+ ensure
73
+ socket.close rescue nil # rubocop:disable Style/RescueModifier
74
+ end
75
+
76
+ def read_headers(socket)
77
+ headers = {}
78
+ while (line = socket.gets&.chomp) && !line.empty?
79
+ name, value = line.split(": ", 2)
80
+ headers[name.downcase] = value if name
81
+ end
82
+ headers
83
+ end
84
+
85
+ def read_body(socket, length)
86
+ length.positive? ? socket.read(length).to_s : ""
87
+ end
88
+
89
+ def write_response(socket, result)
90
+ body = result[:body].to_s
91
+ code = result[:status].to_i
92
+ phrase = STATUS_MESSAGES.fetch(code, "Unknown")
93
+
94
+ response = "HTTP/1.1 #{code} #{phrase}#{CRLF}"
95
+ response += "Content-Type: application/json#{CRLF}"
96
+ response += "Content-Length: #{body.bytesize}#{CRLF}"
97
+ response += "Connection: close#{CRLF}"
98
+ response += CRLF
99
+ response += body
100
+
101
+ socket.write(response)
102
+ end
103
+
104
+ def log(message)
105
+ @config.logger&.puts(message) || $stdout.puts(message)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ # Rack-compatible application adapter.
6
+ # Allows igniter-server to run under any Rack-compatible server (Puma, Unicorn, etc.).
7
+ #
8
+ # Usage in config.ru:
9
+ # require "igniter/server"
10
+ # Igniter::Server.configure { |c| c.register "MyContract", MyContract }
11
+ # run Igniter::Server.rack_app
12
+ class RackApp
13
+ def initialize(config)
14
+ @router = Router.new(config)
15
+ end
16
+
17
+ def call(env) # rubocop:disable Metrics/MethodLength
18
+ method = env["REQUEST_METHOD"]
19
+ path = env["PATH_INFO"]
20
+ body_str = env["rack.input"].read
21
+
22
+ result = @router.call(method, path, body_str)
23
+
24
+ [
25
+ result[:status],
26
+ result[:headers].merge("Content-Length" => result[:body].bytesize.to_s),
27
+ [result[:body]]
28
+ ]
29
+ rescue StandardError => e
30
+ error_body = JSON.generate({ error: "Internal server error: #{e.message}" })
31
+ [500, { "Content-Type" => "application/json" }, [error_body]]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Server
5
+ class Registry
6
+ class RegistryError < Igniter::Server::Error; end
7
+
8
+ def initialize
9
+ @contracts = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def register(name, contract_class)
14
+ unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
15
+ raise RegistryError, "'#{name}' must be an Igniter::Contract subclass"
16
+ end
17
+
18
+ @mutex.synchronize { @contracts[name.to_s] = contract_class }
19
+ self
20
+ end
21
+
22
+ def fetch(name)
23
+ @mutex.synchronize do
24
+ @contracts.fetch(name.to_s) do
25
+ raise RegistryError, "Contract '#{name}' is not registered. " \
26
+ "Available: #{@contracts.keys.inspect}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def all
32
+ @mutex.synchronize { @contracts.dup }
33
+ end
34
+
35
+ def names
36
+ @mutex.synchronize { @contracts.keys }
37
+ end
38
+
39
+ def registered?(name)
40
+ @mutex.synchronize { @contracts.key?(name.to_s) }
41
+ end
42
+
43
+ # Returns a description of each registered contract for introspection.
44
+ def introspect
45
+ all.map do |name, klass|
46
+ graph = klass.compiled_graph
47
+ {
48
+ name: name,
49
+ inputs: graph.nodes.select { |n| n.kind == :input }.map(&:name),
50
+ outputs: graph.outputs.map(&:name)
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Igniter
6
+ module Server
7
+ # Transport-agnostic HTTP router.
8
+ # Receives (method, path, body_string) and returns { status:, body:, headers: }.
9
+ # Used by both HttpServer (TCPServer) and RackApp.
10
+ class Router
11
+ ROUTES = [
12
+ { method: "GET", pattern: %r{\A/v1/health\z}, handler: :health },
13
+ { method: "GET", pattern: %r{\A/v1/contracts\z}, handler: :contracts },
14
+ { method: "POST", pattern: %r{\A/v1/contracts/(?<name>[^/]+)/execute\z}, handler: :execute },
15
+ { method: "POST", pattern: %r{\A/v1/contracts/(?<name>[^/]+)/events\z}, handler: :event },
16
+ { method: "GET", pattern: %r{\A/v1/executions/(?<id>[^/]+)\z}, handler: :status }
17
+ ].freeze
18
+
19
+ def initialize(config)
20
+ @config = config
21
+ end
22
+
23
+ # Main dispatch entry point — called by both WEBrick and Rack adapters.
24
+ def call(http_method, path, body_str) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
25
+ method_uc = http_method.to_s.upcase
26
+ ROUTES.each do |route|
27
+ next unless route[:method] == method_uc
28
+
29
+ match = route[:pattern].match(path)
30
+ next unless match
31
+
32
+ params = match.named_captures.transform_keys(&:to_sym)
33
+ body = parse_body(body_str)
34
+
35
+ handler = build_handler(route[:handler])
36
+ return handler.call(params: params, body: body)
37
+ end
38
+
39
+ not_found_response(path)
40
+ rescue JSON::ParserError => e
41
+ { status: 400, body: JSON.generate({ error: "Invalid JSON: #{e.message}" }), headers: json_ct }
42
+ end
43
+
44
+ private
45
+
46
+ def build_handler(key)
47
+ registry = @config.registry
48
+ store = @config.store
49
+ node_url = "http://#{@config.host}:#{@config.port}"
50
+
51
+ case key
52
+ when :health then Handlers::HealthHandler.new(registry, store, node_url: node_url)
53
+ when :contracts then Handlers::ContractsHandler.new(registry, store)
54
+ when :execute then Handlers::ExecuteHandler.new(registry, store)
55
+ when :event then Handlers::EventHandler.new(registry, store)
56
+ when :status then Handlers::StatusHandler.new(registry, store)
57
+ end
58
+ end
59
+
60
+ def parse_body(str)
61
+ return {} if str.nil? || str.strip.empty?
62
+
63
+ JSON.parse(str)
64
+ end
65
+
66
+ def not_found_response(path)
67
+ { status: 404, body: JSON.generate({ error: "Not found: #{path}" }), headers: json_ct }
68
+ end
69
+
70
+ def json_ct
71
+ { "Content-Type" => "application/json" }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter"
4
+ require "json"
5
+
6
+ # Define the top-level Server module and Error class first,
7
+ # so subfiles can inherit from Igniter::Server::Error.
8
+ module Igniter
9
+ module Server
10
+ class Error < Igniter::Error; end
11
+ end
12
+ end
13
+
14
+ require_relative "server/registry"
15
+ require_relative "server/config"
16
+ require_relative "server/router"
17
+ require_relative "server/http_server"
18
+ require_relative "server/rack_app"
19
+ require_relative "server/client"
20
+ require_relative "server/handlers/base"
21
+ require_relative "server/handlers/health_handler"
22
+ require_relative "server/handlers/contracts_handler"
23
+ require_relative "server/handlers/execute_handler"
24
+ require_relative "server/handlers/event_handler"
25
+ require_relative "server/handlers/status_handler"
26
+
27
+ module Igniter
28
+ module Server
29
+ class << self
30
+ def config
31
+ @config ||= Config.new
32
+ end
33
+
34
+ def configure
35
+ yield config
36
+ self
37
+ end
38
+
39
+ # Start the built-in HTTP server (blocking).
40
+ def start(**options)
41
+ apply_options!(options)
42
+ HttpServer.new(config).start
43
+ end
44
+
45
+ # Return a Rack-compatible application for use with Puma/Unicorn/etc.
46
+ def rack_app
47
+ RackApp.new(config)
48
+ end
49
+
50
+ # Reset configuration (useful in tests).
51
+ def reset!
52
+ @config = nil
53
+ end
54
+
55
+ private
56
+
57
+ def apply_options!(options) # rubocop:disable Metrics/AbcSize
58
+ config.port = options[:port] if options[:port]
59
+ config.host = options[:host] if options[:host]
60
+ config.store = options[:store] if options[:store]
61
+ return unless options[:contracts].is_a?(Hash)
62
+
63
+ options[:contracts].each { |name, klass| config.register(name.to_s, klass) }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Igniter
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/igniter.rb CHANGED
@@ -39,5 +39,9 @@ module Igniter
39
39
  def compile_schema(schema, name: nil)
40
40
  DSL::SchemaBuilder.compile(schema, name: name)
41
41
  end
42
+
43
+ def configure
44
+ yield self
45
+ end
42
46
  end
43
47
  end