Class: Inquirex::Node
- Inherits:
-
Object
- Object
- Inquirex::Node
- Defined in:
- lib/inquirex/node.rb
Overview
A single step in the flow. The verb determines the role of this node:
Collecting verbs (user provides input):
:ask — typed question with one of the 11 data types
:confirm — yes/no boolean gate (shorthand for ask with type :boolean)
Display verbs (no input, auto-advance):
:say — informational message
:header — section heading or title card
:btw — admonition, sidebar, or contextual note
:warning — important alert
Constant Summary collapse
- VERBS =
Valid DSL verbs and which ones collect input from the user.
%i[ask say header btw warning confirm].freeze
- COLLECTING_VERBS =
Verbs that collect input from the user.
%i[ask confirm].freeze
- DISPLAY_VERBS =
Verbs that only display content and auto-advance.
%i[say header btw warning].freeze
- TYPES =
Valid data types for :ask steps.
%i[ string text integer decimal currency boolean enum multi_enum date email phone ].freeze
- BOUNDED_TYPES =
Types for which #min, #max and #step_size carry meaning. Declaring a bound on anything else is a definition error rather than a silent no-op — a bounded
:stringalmost always means the author picked the wrong type, and failing loudly is cheaper than shipping it. %i[integer decimal currency].freeze
Instance Attribute Summary collapse
-
#accumulations ⇒ Object
readonly
Returns the value of attribute accumulations.
-
#default ⇒ Object?
readonly
default value (pre-fill, user can change).
-
#id ⇒ Symbol
readonly
unique step identifier.
-
#max ⇒ Object
readonly
Returns the value of attribute max.
-
#min ⇒ Object
readonly
Returns the value of attribute min.
-
#option_labels ⇒ Hash?
readonly
key => display label mapping.
-
#options ⇒ Array?
readonly
option keys for :enum/:multi_enum steps.
-
#question ⇒ String?
readonly
prompt text for collecting steps.
-
#required ⇒ Boolean
readonly
whether the user must answer (true by default);
required falsesteps render a Skip control and accept Engine#skip. -
#skip_if ⇒ Rules::Base?
readonly
rule to skip this step entirely.
-
#step_size ⇒ Object
readonly
Returns the value of attribute step_size.
-
#text ⇒ String?
readonly
display text for non-collecting steps.
-
#transitions ⇒ Array<Transition>
readonly
ordered conditional next-step edges.
-
#type ⇒ Symbol?
readonly
input type for :ask/:confirm (nil for display verbs).
-
#verb ⇒ Symbol
readonly
DSL verb (:ask, :say, :header, :btw, :warning, :confirm).
-
#widget_hints ⇒ Hash{Symbol => WidgetHint}?
readonly
rendering hints per target.
Class Method Summary collapse
-
.from_h(id, hash) ⇒ Node
Deserializes a node from a plain Hash.
Instance Method Summary collapse
-
#bounded? ⇒ Boolean
Whether this step declares any numeric bound at all.
-
#clamp(value) ⇒ Object
Pulls a number inside this step's declared bounds.
-
#collecting? ⇒ Boolean
True if this step collects input from the user.
-
#display? ⇒ Boolean
True if this step only displays content (no input).
-
#effective_widget_hint_for(target: :desktop) ⇒ WidgetHint?
Returns the explicit hint for the target, falling back to the registry default for this node's type when no explicit hint is set.
-
#initialize(id:, verb:, type: nil, question: nil, text: nil, options: nil, transitions: [], skip_if: nil, default: nil, required: true, min: nil, max: nil, step_size: nil, widget_hints: nil, accumulations: []) ⇒ Node
constructor
A new instance of Node.
-
#next_step_id(answers) ⇒ Symbol?
Resolves the next step id from current answers by evaluating transitions in order.
-
#required? ⇒ Boolean
Whether the user must answer this step.
-
#resolve_option(raw) ⇒ String, ...
Canonicalizes a raw value against this step's options: an exact value match wins, then a case-insensitive value match, then a case-insensitive label match — LLM extractions and humans often answer with the friendly label ("US citizen or permanent resident") when the form value is the canonical key ("us_person").
-
#skip?(answers) ⇒ Boolean
Whether this step should be skipped given current answers.
-
#to_h ⇒ Hash
Serializes the node to a plain Hash for JSON output.
-
#widget_hint_for(target: :desktop) ⇒ WidgetHint?
Returns the explicit widget hint for the given target, or nil.
Constructor Details
#initialize(id:, verb:, type: nil, question: nil, text: nil, options: nil, transitions: [], skip_if: nil, default: nil, required: true, min: nil, max: nil, step_size: nil, widget_hints: nil, accumulations: []) ⇒ Node
Returns a new instance of Node.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/inquirex/node.rb', line 66 def initialize(id:, verb:, type: nil, question: nil, text: nil, options: nil, transitions: [], skip_if: nil, default: nil, required: true, min: nil, max: nil, step_size: nil, widget_hints: nil, accumulations: []) @id = id.to_sym @verb = verb.to_sym @type = type&.to_sym @question = question @text = text @transitions = transitions.freeze @skip_if = skip_if @default = default @required = required ? true : false @min = coerce_bound(min) @max = coerce_bound(max) @step_size = coerce_bound(step_size) @widget_hints = &.freeze @accumulations = accumulations.freeze () validate_bounds! freeze end |
Instance Attribute Details
#accumulations ⇒ Object (readonly)
Returns the value of attribute accumulations.
49 50 51 |
# File 'lib/inquirex/node.rb', line 49 def accumulations @accumulations end |
#default ⇒ Object? (readonly)
default value (pre-fill, user can change)
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def default @default end |
#id ⇒ Symbol (readonly)
unique step identifier
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def id @id end |
#max ⇒ Object (readonly)
Returns the value of attribute max.
49 50 51 |
# File 'lib/inquirex/node.rb', line 49 def max @max end |
#min ⇒ Object (readonly)
Returns the value of attribute min.
49 50 51 |
# File 'lib/inquirex/node.rb', line 49 def min @min end |
#option_labels ⇒ Hash? (readonly)
key => display label mapping
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def option_labels @option_labels end |
#options ⇒ Array? (readonly)
option keys for :enum/:multi_enum steps
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def @options end |
#question ⇒ String? (readonly)
prompt text for collecting steps
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def question @question end |
#required ⇒ Boolean (readonly)
whether the user must answer (true by default);
required false steps render a Skip control and accept Engine#skip
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def required @required end |
#skip_if ⇒ Rules::Base? (readonly)
rule to skip this step entirely
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def skip_if @skip_if end |
#step_size ⇒ Object (readonly)
Returns the value of attribute step_size.
49 50 51 |
# File 'lib/inquirex/node.rb', line 49 def step_size @step_size end |
#text ⇒ String? (readonly)
display text for non-collecting steps
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def text @text end |
#transitions ⇒ Array<Transition> (readonly)
ordered conditional next-step edges
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def transitions @transitions end |
#type ⇒ Symbol? (readonly)
input type for :ask/:confirm (nil for display verbs)
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def type @type end |
#verb ⇒ Symbol (readonly)
DSL verb (:ask, :say, :header, :btw, :warning, :confirm)
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def verb @verb end |
#widget_hints ⇒ Hash{Symbol => WidgetHint}? (readonly)
rendering hints per target
29 30 31 |
# File 'lib/inquirex/node.rb', line 29 def @widget_hints end |
Class Method Details
.from_h(id, hash) ⇒ Node
Deserializes a node from a plain Hash.
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/inquirex/node.rb', line 257 def self.from_h(id, hash) verb = hash["verb"] || hash[:verb] type = hash["type"] || hash[:type] question = hash["question"] || hash[:question] text = hash["text"] || hash[:text] = hash["options"] || hash[:options] transitions_data = hash["transitions"] || hash[:transitions] || [] skip_if_data = hash["skip_if"] || hash[:skip_if] default = hash["default"] || hash[:default] # Fetch chain (not ||) so an explicit false survives; absent key means required. required = hash.fetch("required") { hash.fetch(:required, true) } min = hash["min"] || hash[:min] max = hash["max"] || hash[:max] step_size = hash["step_size"] || hash[:step_size] = hash["widget"] || hash[:widget] accumulate_data = hash["accumulate"] || hash[:accumulate] transitions = transitions_data.map { |t| Transition.from_h(t) } skip_if = skip_if_data ? Rules::Base.from_h(skip_if_data) : nil = () = () accumulations = deserialize_accumulations(accumulate_data) new( id:, verb:, type:, question:, text:, options:, transitions:, skip_if:, default:, required:, min:, max:, step_size:, widget_hints:, accumulations: ) end |
Instance Method Details
#bounded? ⇒ Boolean
Whether this step declares any numeric bound at all.
103 104 105 |
# File 'lib/inquirex/node.rb', line 103 def bounded? !@min.nil? || !@max.nil? end |
#clamp(value) ⇒ Object
Pulls a number inside this step's declared bounds.
The wire format carries min/max so a renderer can present them, but
an HTML number input does not stop a visitor typing past them and an LLM
extraction has no notion of them at all. Every path that stores an answer
for a bounded step goes through here, so the bound holds regardless of
which client produced the value.
121 122 123 124 125 126 127 128 129 |
# File 'lib/inquirex/node.rb', line 121 def clamp(value) return value unless bounded? return value unless value.is_a?(Numeric) clamped = value clamped = @min if @min && clamped < @min clamped = @max if @max && clamped > @max clamped end |
#collecting? ⇒ Boolean
Returns true if this step collects input from the user.
132 133 134 |
# File 'lib/inquirex/node.rb', line 132 def collecting? COLLECTING_VERBS.include?(@verb) end |
#display? ⇒ Boolean
Returns true if this step only displays content (no input).
166 167 168 |
# File 'lib/inquirex/node.rb', line 166 def display? DISPLAY_VERBS.include?(@verb) end |
#effective_widget_hint_for(target: :desktop) ⇒ WidgetHint?
Returns the explicit hint for the target, falling back to the registry default for this node's type when no explicit hint is set.
192 193 194 |
# File 'lib/inquirex/node.rb', line 192 def (target: :desktop) (target:) || WidgetRegistry.default_hint_for(@type, context: target) end |
#next_step_id(answers) ⇒ Symbol?
Resolves the next step id from current answers by evaluating transitions in order.
200 201 202 203 |
# File 'lib/inquirex/node.rb', line 200 def next_step_id(answers) match = @transitions.find { |t| t.applies?(answers) } match&.target end |
#required? ⇒ Boolean
Whether the user must answer this step. True by default; steps declared
with required false render a Skip control and accept Engine#skip.
Only meaningful for collecting steps.
175 176 177 |
# File 'lib/inquirex/node.rb', line 175 def required? @required end |
#resolve_option(raw) ⇒ String, ...
Canonicalizes a raw value against this step's options: an exact value match wins, then a case-insensitive value match, then a case-insensitive label match — LLM extractions and humans often answer with the friendly label ("US citizen or permanent resident") when the form value is the canonical key ("us_person"). Matching is always resolved TO the form value, never the label. Steps without options return the value unchanged; an unmatchable value returns nil rather than polluting answers with junk that would satisfy not_empty rules.
155 156 157 158 159 160 161 162 163 |
# File 'lib/inquirex/node.rb', line 155 def resolve_option(raw) return raw if @options.nil? || @options.empty? return nil if raw.nil? candidate = raw.to_s @options.find { |value| value == candidate } || @options.find { |value| value.casecmp?(candidate) } || @option_labels&.find { |_value, label| label.casecmp?(candidate) }&.first end |
#skip?(answers) ⇒ Boolean
Whether this step should be skipped given current answers.
209 210 211 212 213 |
# File 'lib/inquirex/node.rb', line 209 def skip?(answers) return false if @skip_if.nil? @skip_if.evaluate(answers) end |
#to_h ⇒ Hash
Serializes the node to a plain Hash for JSON output. Lambda defaults/compute are stripped (server-side only).
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/inquirex/node.rb', line 219 def to_h hash = { "verb" => @verb.to_s } if collecting? hash["type"] = @type.to_s if @type hash["question"] = @question if @question hash["options"] = if @options hash["skip_if"] = @skip_if.to_h if @skip_if hash["default"] = @default unless @default.nil? || @default.is_a?(Proc) hash["required"] = false unless @required hash["min"] = @min unless @min.nil? hash["max"] = @max unless @max.nil? hash["step_size"] = @step_size unless @step_size.nil? elsif @text hash["text"] = @text end hash["transitions"] = @transitions.map(&:to_h) unless @transitions.empty? if @widget_hints && !@widget_hints.empty? hash["widget"] = @widget_hints.transform_keys(&:to_s) .transform_values(&:to_h) end unless @accumulations.empty? hash["accumulate"] = @accumulations.to_h do |acc| [acc.target.to_s, acc.to_h] end end hash end |
#widget_hint_for(target: :desktop) ⇒ WidgetHint?
Returns the explicit widget hint for the given target, or nil.
183 184 185 |
# File 'lib/inquirex/node.rb', line 183 def (target: :desktop) @widget_hints&.fetch(target.to_sym, nil) end |