Class: Inquirex::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/inquirex/engine.rb,
lib/inquirex/engine/state_serializer.rb

Overview

Runtime session that drives flow navigation. Holds the definition, collected answers, and current position in the flow graph.

Collecting steps (ask, confirm): call engine.answer(value) Display steps (say, header, btw, warning): call engine.advance

Validates each answer via an optional Validation::Adapter, then advances using node transitions. Skips steps whose skip_if rule evaluates to true.

Defined Under Namespace

Modules: StateSerializer

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(definition, validator: Validation::NullAdapter.new) ⇒ Engine

Returns a new instance of Engine.

Parameters:

  • definition (Definition)

    the flow to run

  • validator (Validation::Adapter) (defaults to: Validation::NullAdapter.new)

    optional (default: NullAdapter)



43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/inquirex/engine.rb', line 43

def initialize(definition, validator: Validation::NullAdapter.new)
  @definition = definition
  @answers = {}
  @history = []
  @current_step_id = definition.start_step_id
  @validator = validator
  @totals = init_totals
  @completion_metadata = nil
  @after_completion_hooks = []
  @completion_hook_errors = []
  @suggestions = {}
  @history << @current_step_id
  skip_display_steps_if_needed
end

Instance Attribute Details

#answersObject (readonly)

Returns the value of attribute answers.



13
14
15
# File 'lib/inquirex/engine.rb', line 13

def answers
  @answers
end

#completion_hook_errorsArray<StandardError> (readonly)

Exceptions raised by after_completion hooks, in the order they were raised. Hooks are isolated from one another, so a raising hook is recorded here rather than propagated — callers that care can inspect this after the flow finishes. Empty when every hook succeeded.

Returns:

  • (Array<StandardError>)


39
40
41
# File 'lib/inquirex/engine.rb', line 39

def completion_hook_errors
  @completion_hook_errors
end

#completion_metadataCompletionMetadata?

Metadata describing how and where the flow was completed. Rendering front-ends (TTY, web, chat widget) attach a rich version from an after_completion hook; when no hook provides one, the engine stamps a minimal core version the moment the flow finishes. Only :engine and :engine_version are required members; everything else is free-form.

Returns:



22
23
24
# File 'lib/inquirex/engine.rb', line 22

def 
  @completion_metadata
end

#current_step_idObject (readonly)

Returns the value of attribute current_step_id.



13
14
15
# File 'lib/inquirex/engine.rb', line 13

def current_step_id
  @current_step_id
end

#definitionObject (readonly)

Returns the value of attribute definition.



13
14
15
# File 'lib/inquirex/engine.rb', line 13

def definition
  @definition
end

#historyObject (readonly)

Returns the value of attribute history.



13
14
15
# File 'lib/inquirex/engine.rb', line 13

def history
  @history
end

#suggestionsHash{Symbol => Array} (readonly)

Answer suggestions produced by prefill! for multi-select steps, keyed by step id. A suggestion pre-populates the step's choices in a renderer but — unlike an answer — never satisfies skip_if rules: multi-select extraction is treated as a hint the user confirms and may extend, not a deterministic fact. Cleared per step once the user answers it.

Returns:

  • (Hash{Symbol => Array})


31
32
33
# File 'lib/inquirex/engine.rb', line 31

def suggestions
  @suggestions
end

#totalsObject (readonly)

Returns the value of attribute totals.



13
14
15
# File 'lib/inquirex/engine.rb', line 13

def totals
  @totals
end

Class Method Details

.from_state(definition, state_hash, validator: Validation::NullAdapter.new) ⇒ Engine

Rebuilds an Engine from a previously saved state.

Parameters:

  • definition (Definition)

    same definition used when state was captured

  • state_hash (Hash)

    hash with state keys (may be strings from JSON)

  • validator (Validation::Adapter) (defaults to: Validation::NullAdapter.new)

Returns:



214
215
216
217
218
219
# File 'lib/inquirex/engine.rb', line 214

def self.from_state(definition, state_hash, validator: Validation::NullAdapter.new)
  state = StateSerializer.symbolize_state(state_hash)
  engine = allocate
  engine.send(:restore_state, definition, state, validator)
  engine
end

Instance Method Details

#advanceObject

Advances past the current non-collecting step (say/header/btw/warning).

Raises:



102
103
104
105
106
# File 'lib/inquirex/engine.rb', line 102

def advance
  raise Errors::AlreadyFinishedError, "Flow is already finished" if finished?

  advance_step
end

#after_completion {|Engine| ... } ⇒ Engine

Registers a hook to run when the flow finishes. The block receives the engine; front-ends typically use it to attach a rich completion_metadata (host, user, ips, terminal, ...). Optional — after all hooks run, the engine fills in a minimal CompletionMetadata when none of them provided one. Registering on an already-finished engine invokes the block immediately.

Any number of hooks may be registered; they run in registration order. Each is isolated from the others — a hook that raises a StandardError has it recorded in #completion_hook_errors, and the remaining hooks still run. Non-StandardError exceptions (Interrupt, SignalException) propagate, as they should.

Examples:

Stamp renderer-specific completion metadata

engine.after_completion do |eng|
  eng. = Inquirex::CompletionMetadata.new(
    engine: "inquirex-tty", engine_version: "0.5.0", hostname: Socket.gethostname
  )
end

Yields:

  • (Engine)

    the engine, at completion time

Returns:

Raises:

  • (ArgumentError)


173
174
175
176
177
178
179
180
181
182
# File 'lib/inquirex/engine.rb', line 173

def after_completion(&block)
  raise ArgumentError, "after_completion requires a block" unless block

  @after_completion_hooks << block
  if finished?
    invoke_completion_hook(block)
    
  end
  self
end

#answer(value) ⇒ Object

Submits an answer for the current collecting step (ask/confirm). Validates, stores, and advances to the next step.

Parameters:

  • value (Object)

    user's answer for the current step

Raises:



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/inquirex/engine.rb', line 85

def answer(value)
  raise Errors::AlreadyFinishedError, "Flow is already finished" if finished?
  raise Errors::NonCollectingStepError, "Step #{@current_step_id} is a display step; use #advance instead" \
    unless current_step.collecting?

  result = @validator.validate(current_step, value)
  raise Errors::ValidationError, "Validation failed: #{result.errors.join(", ")}" unless result.valid?

  @answers[@current_step_id] = value
  @suggestions.delete(@current_step_id)
  apply_accumulations(current_step, value)
  advance_step
end

#answers_with_metadataHash

The collected answers with the completion metadata (when a renderer attached one) merged in under the :completion_metadata key.

Returns:

  • (Hash)


202
203
204
205
206
# File 'lib/inquirex/engine.rb', line 202

def 
  return @answers if @completion_metadata.nil?

  @answers.merge(completion_metadata: @completion_metadata.to_h)
end

#current_stepNode?

Returns current step node, or nil if flow is finished.

Returns:

  • (Node, nil)

    current step node, or nil if flow is finished



67
68
69
70
71
# File 'lib/inquirex/engine.rb', line 67

def current_step
  return nil if finished?

  @definition.step(@current_step_id)
end

#finished?Boolean

Returns true when there is no current step (flow ended).

Returns:

  • (Boolean)

    true when there is no current step (flow ended)



74
75
76
# File 'lib/inquirex/engine.rb', line 74

def finished?
  @current_step_id.nil?
end

#prefill!(hash) ⇒ Hash

Merges a hash of { step_id => value } into the top-level answers without clobbering answers the user has already provided. Used by LLM clarify steps to populate downstream answers from free-text extraction so that skip_if not_empty(:id) rules on later steps will fire.

Nil/empty values in the hash are ignored so that "unknown" LLM outputs don't spuriously satisfy not_empty rules.

If the engine's current step becomes skippable as a result of the prefill, it auto-advances past it.

Parameters:

  • hash (Hash)

    answers keyed by step id

Returns:

  • (Hash)

    the updated answers



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/inquirex/engine.rb', line 121

def prefill!(hash)
  return @answers unless hash.is_a?(Hash)

  hash.each do |key, value|
    next if value.nil?
    next if value.respond_to?(:empty?) && value.empty?

    sym = key.to_sym
    if multi_select_step?(sym)
      # Multi-select extraction is a hint, not a fact: the user may have
      # more selections in mind than the text revealed. Record it as a
      # suggestion so renderers pre-check the choices while the question
      # is still asked; skip_if rules see no answer and do not fire.
      @suggestions[sym] = Array(value) unless @answers.key?(sym)
    else
      @answers[sym] = value unless @answers.key?(sym)
    end
  end
  skip_if_needed unless finished?
  @answers
end

#suggestion_for(step_id) ⇒ Array?

The prefill suggestion for a step, or nil when none was recorded.

Parameters:

  • step_id (Symbol, String)

    step id

Returns:

  • (Array, nil)

    suggested selections for a multi-select step



147
148
149
# File 'lib/inquirex/engine.rb', line 147

def suggestion_for(step_id)
  @suggestions[step_id.to_sym]
end

#to_stateHash

Serializable state snapshot for persistence or resumption.

Returns:

  • (Hash)


187
188
189
190
191
192
193
194
195
196
# File 'lib/inquirex/engine.rb', line 187

def to_state
  {
    current_step_id:     @current_step_id,
    answers:             @answers,
    history:             @history,
    totals:              @totals,
    suggestions:         @suggestions,
    completion_metadata: @completion_metadata&.to_h
  }
end

#total(name) ⇒ Numeric

Convenience accessor for a single accumulator's running total.

Parameters:

  • name (Symbol)

    accumulator name (e.g. :price)

Returns:

  • (Numeric)


62
63
64
# File 'lib/inquirex/engine.rb', line 62

def total(name)
  @totals[name.to_sym] || 0
end