Class: Inquirex::Engine
- Inherits:
-
Object
- Object
- Inquirex::Engine
- 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
-
#answers ⇒ Object
readonly
Returns the value of attribute answers.
-
#completion_hook_errors ⇒ Array<StandardError>
readonly
Exceptions raised by after_completion hooks, in the order they were raised.
-
#completion_metadata ⇒ CompletionMetadata?
Metadata describing how and where the flow was completed.
-
#current_step_id ⇒ Object
readonly
Returns the value of attribute current_step_id.
-
#definition ⇒ Object
readonly
Returns the value of attribute definition.
-
#history ⇒ Object
readonly
Returns the value of attribute history.
-
#skipped ⇒ Array<Symbol>
readonly
Step ids the user explicitly skipped via #skip, in the order they were skipped.
-
#suggestions ⇒ Hash{Symbol => Array}
readonly
Answer suggestions produced by prefill! for multi-select steps, keyed by step id.
-
#totals ⇒ Object
readonly
Returns the value of attribute totals.
Class Method Summary collapse
-
.from_state(definition, state_hash, validator: Validation::NullAdapter.new) ⇒ Engine
Rebuilds an Engine from a previously saved state.
Instance Method Summary collapse
-
#advance ⇒ Object
Advances past the current non-collecting step (say/header/btw/warning).
-
#after_completion {|Engine| ... } ⇒ Engine
Registers a hook to run when the flow finishes.
-
#answer(value) ⇒ Object
Submits an answer for the current collecting step (ask/confirm).
-
#answers_with_metadata ⇒ Hash
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.
-
#current_step ⇒ Node?
Current step node, or nil if flow is finished.
-
#finished? ⇒ Boolean
True when there is no current step (flow ended).
-
#initialize(definition, validator: Validation::NullAdapter.new) ⇒ Engine
constructor
A new instance of Engine.
-
#prefill!(hash) ⇒ Hash
Merges a hash of { step_id => value } into the top-level answers without clobbering answers the user has already provided.
-
#skip ⇒ void
Skips the current optional collecting step at the user's request — the engine-side handler for a widget's Skip button.
-
#skipped?(step_id) ⇒ Boolean
Whether the user explicitly skipped the given step via #skip.
-
#suggestion_for(step_id) ⇒ Array?
The prefill suggestion for a step, or nil when none was recorded.
-
#to_state ⇒ Hash
Serializable state snapshot for persistence or resumption.
-
#total(name) ⇒ Numeric
Convenience accessor for a single accumulator's running total.
Constructor Details
#initialize(definition, validator: Validation::NullAdapter.new) ⇒ Engine
Returns a new instance of Engine.
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
#answers ⇒ Object (readonly)
Returns the value of attribute answers.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def answers @answers end |
#completion_hook_errors ⇒ Array<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.
49 50 51 |
# File 'lib/inquirex/engine.rb', line 49 def completion_hook_errors @completion_hook_errors end |
#completion_metadata ⇒ CompletionMetadata?
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.
23 24 25 |
# File 'lib/inquirex/engine.rb', line 23 def @completion_metadata end |
#current_step_id ⇒ Object (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 |
#definition ⇒ Object (readonly)
Returns the value of attribute definition.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def definition @definition end |
#history ⇒ Object (readonly)
Returns the value of attribute history.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def history @history end |
#skipped ⇒ Array<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.
41 42 43 |
# File 'lib/inquirex/engine.rb', line 41 def skipped @skipped end |
#suggestions ⇒ Hash{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.
32 33 34 |
# File 'lib/inquirex/engine.rb', line 32 def suggestions @suggestions end |
#totals ⇒ Object (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.
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
#advance ⇒ Object
Advances past the current non-collecting step (say/header/btw/warning).
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.
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.
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_metadata ⇒ Hash
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.
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_step ⇒ Node?
Returns 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).
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.
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 |
#skip ⇒ void
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.
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.
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.
205 206 207 |
# File 'lib/inquirex/engine.rb', line 205 def suggestion_for(step_id) @suggestions[step_id.to_sym] end |
#to_state ⇒ Hash
Serializable state snapshot for persistence or resumption.
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.
73 74 75 |
# File 'lib/inquirex/engine.rb', line 73 def total(name) @totals[name.to_sym] || 0 end |