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 Optional steps (declared required false): engine.skip is also allowed

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)



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/inquirex/engine.rb', line 53

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 = {}
  @skipped = []
  @history << @current_step_id
  skip_display_steps_if_needed
end

Instance Attribute Details

#answersObject (readonly)

Returns the value of attribute answers.



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

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>)


49
50
51
# File 'lib/inquirex/engine.rb', line 49

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:



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

def 
  @completion_metadata
end

#current_step_idObject (readonly)

Returns the value of attribute current_step_id.



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

def current_step_id
  @current_step_id
end

#definitionObject (readonly)

Returns the value of attribute definition.



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

def definition
  @definition
end

#historyObject (readonly)

Returns the value of attribute history.



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

def history
  @history
end

#skippedArray<Symbol> (readonly)

Step ids the user explicitly skipped via #skip, in the order they were skipped. Distinguishes default-by-skip values in answers from values the user actually provided. Steps elided automatically by their skip_if rule are NOT listed here — they were never presented, so the user cannot have declined them.

Returns:

  • (Array<Symbol>)


41
42
43
# File 'lib/inquirex/engine.rb', line 41

def skipped
  @skipped
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})


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

def suggestions
  @suggestions
end

#totalsObject (readonly)

Returns the value of attribute totals.



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

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:



277
278
279
280
281
282
# File 'lib/inquirex/engine.rb', line 277

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:



113
114
115
116
117
# File 'lib/inquirex/engine.rb', line 113

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)


231
232
233
234
235
236
237
238
239
240
# File 'lib/inquirex/engine.rb', line 231

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:



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/inquirex/engine.rb', line 96

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, and the user-skipped step ids (when any) under the :skipped key — so post-completion actions and API consumers can tell default-by-skip values apart from answers the user actually provided.

Returns:

  • (Hash)


264
265
266
267
268
269
# File 'lib/inquirex/engine.rb', line 264

def 
  extra = {}
  extra[:completion_metadata] = @completion_metadata.to_h if @completion_metadata
  extra[:skipped] = @skipped.dup unless @skipped.empty?
  extra.empty? ? @answers : @answers.merge(extra)
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



78
79
80
81
82
# File 'lib/inquirex/engine.rb', line 78

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)



85
86
87
# File 'lib/inquirex/engine.rb', line 85

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



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/inquirex/engine.rb', line 179

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

#skipvoid

This method returns an undefined value.

Skips the current optional collecting step at the user's request — the engine-side handler for a widget's Skip button. Only steps declared with required false may be skipped.

When the step has a default, the default is recorded into the answers and contributes to accumulators exactly as if the user had submitted it; the step id lands in #skipped so consumers can tell the value apart from one the user actually provided. Without a default, no answers entry is written (rules read a missing key as nil, so branching is unaffected). Advances through transitions exactly like #answer.

Examples:

Optional question, skipped by the user

engine.current_step.required?  # => false
engine.skip
engine.answers[:dependents]    # => 0 (the step's default)
engine.skipped                 # => [:dependents]

Raises:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/inquirex/engine.rb', line 140

def skip
  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?
  raise Errors::RequiredStepError, "Step #{@current_step_id} is required and cannot be skipped" \
    if current_step.required?

  node = current_step
  default = resolve_default(node)
  unless default.nil?
    @answers[@current_step_id] = default
    apply_accumulations(node, default)
  end
  @skipped << @current_step_id unless @skipped.include?(@current_step_id)
  @suggestions.delete(@current_step_id)
  advance_step
end

#skipped?(step_id) ⇒ Boolean

Whether the user explicitly skipped the given step via #skip.

Parameters:

  • step_id (Symbol, String)

    step id

Returns:

  • (Boolean)


162
163
164
# File 'lib/inquirex/engine.rb', line 162

def skipped?(step_id)
  @skipped.include?(step_id.to_sym)
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



205
206
207
# File 'lib/inquirex/engine.rb', line 205

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

#to_stateHash

Serializable state snapshot for persistence or resumption.

Returns:

  • (Hash)


245
246
247
248
249
250
251
252
253
254
255
# File 'lib/inquirex/engine.rb', line 245

def to_state
  {
    current_step_id:     @current_step_id,
    answers:             @answers,
    history:             @history,
    totals:              @totals,
    suggestions:         @suggestions,
    skipped:             @skipped,
    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)


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

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