Class: Inquirex::Node

Inherits:
Object
  • Object
show all
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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id:, verb:, type: nil, question: nil, text: nil, options: nil, transitions: [], skip_if: nil, default: nil, required: true, widget_hints: nil, accumulations: []) ⇒ Node

Returns a new instance of Node.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/inquirex/node.rb', line 57

def initialize(id:,
  verb:,
  type: nil,
  question: nil,
  text: nil,
  options: nil,
  transitions: [],
  skip_if: nil,
  default: nil,
  required: true,
  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
  @widget_hints = widget_hints&.freeze
  @accumulations = accumulations.freeze
  extract_options(options)
  freeze
end

Instance Attribute Details

#accumulationsObject (readonly)

Returns the value of attribute accumulations.



43
44
45
# File 'lib/inquirex/node.rb', line 43

def accumulations
  @accumulations
end

#defaultObject? (readonly)

default value (pre-fill, user can change)

Returns:

  • (Object, nil)

    the current value of default



29
30
31
# File 'lib/inquirex/node.rb', line 29

def default
  @default
end

#idSymbol (readonly)

unique step identifier

Returns:

  • (Symbol)

    the current value of id



29
30
31
# File 'lib/inquirex/node.rb', line 29

def id
  @id
end

#option_labelsHash? (readonly)

key => display label mapping

Returns:

  • (Hash, nil)

    the current value of option_labels



29
30
31
# File 'lib/inquirex/node.rb', line 29

def option_labels
  @option_labels
end

#optionsArray? (readonly)

option keys for :enum/:multi_enum steps

Returns:

  • (Array, nil)

    the current value of options



29
30
31
# File 'lib/inquirex/node.rb', line 29

def options
  @options
end

#questionString? (readonly)

prompt text for collecting steps

Returns:

  • (String, nil)

    the current value of question



29
30
31
# File 'lib/inquirex/node.rb', line 29

def question
  @question
end

#requiredBoolean (readonly)

whether the user must answer (true by default); required false steps render a Skip control and accept Engine#skip

Returns:

  • (Boolean)

    the current value of required



29
30
31
# File 'lib/inquirex/node.rb', line 29

def required
  @required
end

#skip_ifRules::Base? (readonly)

rule to skip this step entirely

Returns:



29
30
31
# File 'lib/inquirex/node.rb', line 29

def skip_if
  @skip_if
end

#textString? (readonly)

display text for non-collecting steps

Returns:

  • (String, nil)

    the current value of text



29
30
31
# File 'lib/inquirex/node.rb', line 29

def text
  @text
end

#transitionsArray<Transition> (readonly)

ordered conditional next-step edges

Returns:

  • (Array<Transition>)

    the current value of transitions



29
30
31
# File 'lib/inquirex/node.rb', line 29

def transitions
  @transitions
end

#typeSymbol? (readonly)

input type for :ask/:confirm (nil for display verbs)

Returns:

  • (Symbol, nil)

    the current value of type



29
30
31
# File 'lib/inquirex/node.rb', line 29

def type
  @type
end

#verbSymbol (readonly)

DSL verb (:ask, :say, :header, :btw, :warning, :confirm)

Returns:

  • (Symbol)

    the current value of verb



29
30
31
# File 'lib/inquirex/node.rb', line 29

def verb
  @verb
end

#widget_hintsHash{Symbol => WidgetHint}? (readonly)

rendering hints per target

Returns:

  • (Hash{Symbol => WidgetHint}, nil)

    the current value of widget_hints



29
30
31
# File 'lib/inquirex/node.rb', line 29

def widget_hints
  @widget_hints
end

Class Method Details

.from_h(id, hash) ⇒ Node

Deserializes a node from a plain Hash.

Parameters:

  • id (Symbol, String)

    step id

  • hash (Hash)

    node attributes

Returns:



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/inquirex/node.rb', line 178

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]
  raw_options = 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) }
  widget_data = 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
  options = deserialize_options(raw_options)
  widget_hints = deserialize_widget_hints(widget_data)
  accumulations = deserialize_accumulations(accumulate_data)

  new(
    id:,
    verb:,
    type:,
    question:,
    text:,
    options:,
    transitions:,
    skip_if:,
    default:,
    required:,
    widget_hints:,
    accumulations:
  )
end

Instance Method Details

#collecting?Boolean

Returns true if this step collects input from the user.

Returns:

  • (Boolean)

    true if this step collects input from the user



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

def collecting?
  COLLECTING_VERBS.include?(@verb)
end

#display?Boolean

Returns true if this step only displays content (no input).

Returns:

  • (Boolean)

    true if this step only displays content (no input)



90
91
92
# File 'lib/inquirex/node.rb', line 90

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.

Parameters:

  • target (Symbol) (defaults to: :desktop)

    e.g. :desktop, :mobile, :tty

Returns:



116
117
118
# File 'lib/inquirex/node.rb', line 116

def effective_widget_hint_for(target: :desktop)
  widget_hint_for(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.

Parameters:

  • answers (Hash)

    current answer state

Returns:

  • (Symbol, nil)

    id of the next step, or nil if no transition matches (flow end)



124
125
126
127
# File 'lib/inquirex/node.rb', line 124

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.

Returns:

  • (Boolean)


99
100
101
# File 'lib/inquirex/node.rb', line 99

def required?
  @required
end

#skip?(answers) ⇒ Boolean

Whether this step should be skipped given current answers.

Parameters:

  • answers (Hash)

Returns:

  • (Boolean)


133
134
135
136
137
# File 'lib/inquirex/node.rb', line 133

def skip?(answers)
  return false if @skip_if.nil?

  @skip_if.evaluate(answers)
end

#to_hHash

Serializes the node to a plain Hash for JSON output. Lambda defaults/compute are stripped (server-side only).

Returns:

  • (Hash)


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/inquirex/node.rb', line 143

def to_h
  hash = { "verb" => @verb.to_s }

  if collecting?
    hash["type"] = @type.to_s if @type
    hash["question"] = @question if @question
    hash["options"] = serialize_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
  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.

Parameters:

  • target (Symbol) (defaults to: :desktop)

    e.g. :desktop, :mobile, :tty

Returns:



107
108
109
# File 'lib/inquirex/node.rb', line 107

def widget_hint_for(target: :desktop)
  @widget_hints&.fetch(target.to_sym, nil)
end