inquirex 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +88 -6
- data/lib/.DS_Store +0 -0
- data/lib/inquirex/accumulator.rb +29 -0
- data/lib/inquirex/actions/action.rb +68 -0
- data/lib/inquirex/actions/base.rb +41 -0
- data/lib/inquirex/actions/custom.rb +31 -0
- data/lib/inquirex/actions/outbox.rb +57 -0
- data/lib/inquirex/actions/runner.rb +52 -0
- data/lib/inquirex/actions/send_email.rb +174 -0
- data/lib/inquirex/actions/template.rb +95 -0
- data/lib/inquirex/actions/webhook.rb +139 -0
- data/lib/inquirex/actions.rb +57 -0
- data/lib/inquirex/answers.rb +13 -0
- data/lib/inquirex/completion_metadata.rb +82 -0
- data/lib/inquirex/definition.rb +67 -5
- data/lib/inquirex/dsl/action_builder.rb +53 -0
- data/lib/inquirex/dsl/flow_builder.rb +49 -6
- data/lib/inquirex/engine/state_serializer.rb +7 -4
- data/lib/inquirex/engine.rb +136 -6
- data/lib/inquirex/errors.rb +5 -0
- data/lib/inquirex/graph/mermaid_exporter.rb +2 -0
- data/lib/inquirex/node.rb +2 -0
- data/lib/inquirex/rules/all.rb +12 -0
- data/lib/inquirex/rules/any.rb +11 -0
- data/lib/inquirex/rules/base.rb +6 -0
- data/lib/inquirex/rules/contains.rb +12 -0
- data/lib/inquirex/rules/equals.rb +12 -0
- data/lib/inquirex/rules/greater_than.rb +12 -0
- data/lib/inquirex/rules/less_than.rb +12 -0
- data/lib/inquirex/rules/not_empty.rb +12 -0
- data/lib/inquirex/validation/adapter.rb +1 -0
- data/lib/inquirex/validation/null_adapter.rb +5 -0
- data/lib/inquirex/version.rb +1 -1
- data/lib/inquirex/widget_registry.rb +1 -0
- data/lib/inquirex.rb +13 -0
- metadata +30 -18
- data/.relaxed_rubocop.yml +0 -153
- data/.rub +0 -0
- data/.ruby-version +0 -1
- data/.secrets.baseline +0 -127
- data/Brewfile +0 -7
- data/Rakefile +0 -36
- data/docs/badges/coverage_badge.svg +0 -21
- data/examples/01_readme_example.rb +0 -55
- data/examples/02_readme_mermaid.png +0 -0
- data/examples/02_readme_mermaid.rb +0 -36
- data/examples/README.md +0 -33
- data/justfile +0 -70
- data/lefthook.yml +0 -35
- data/sig/inquirex.rbs +0 -4
data/lib/inquirex/engine.rb
CHANGED
|
@@ -12,6 +12,32 @@ module Inquirex
|
|
|
12
12
|
class Engine
|
|
13
13
|
attr_reader :definition, :answers, :history, :current_step_id, :totals
|
|
14
14
|
|
|
15
|
+
# Metadata describing how and where the flow was completed. Rendering
|
|
16
|
+
# front-ends (TTY, web, chat widget) attach a rich version from an
|
|
17
|
+
# after_completion hook; when no hook provides one, the engine stamps a
|
|
18
|
+
# minimal core version the moment the flow finishes. Only :engine and
|
|
19
|
+
# :engine_version are required members; everything else is free-form.
|
|
20
|
+
#
|
|
21
|
+
# @return [CompletionMetadata, nil] nil until the flow finishes
|
|
22
|
+
attr_accessor :completion_metadata
|
|
23
|
+
|
|
24
|
+
# Answer suggestions produced by prefill! for multi-select steps, keyed by
|
|
25
|
+
# step id. A suggestion pre-populates the step's choices in a renderer but
|
|
26
|
+
# — unlike an answer — never satisfies skip_if rules: multi-select
|
|
27
|
+
# extraction is treated as a hint the user confirms and may extend, not a
|
|
28
|
+
# deterministic fact. Cleared per step once the user answers it.
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash{Symbol => Array}]
|
|
31
|
+
attr_reader :suggestions
|
|
32
|
+
|
|
33
|
+
# Exceptions raised by after_completion hooks, in the order they were
|
|
34
|
+
# raised. Hooks are isolated from one another, so a raising hook is
|
|
35
|
+
# recorded here rather than propagated — callers that care can inspect
|
|
36
|
+
# this after the flow finishes. Empty when every hook succeeded.
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<StandardError>]
|
|
39
|
+
attr_reader :completion_hook_errors
|
|
40
|
+
|
|
15
41
|
# @param definition [Definition] the flow to run
|
|
16
42
|
# @param validator [Validation::Adapter] optional (default: NullAdapter)
|
|
17
43
|
def initialize(definition, validator: Validation::NullAdapter.new)
|
|
@@ -21,6 +47,10 @@ module Inquirex
|
|
|
21
47
|
@current_step_id = definition.start_step_id
|
|
22
48
|
@validator = validator
|
|
23
49
|
@totals = init_totals
|
|
50
|
+
@completion_metadata = nil
|
|
51
|
+
@after_completion_hooks = []
|
|
52
|
+
@completion_hook_errors = []
|
|
53
|
+
@suggestions = {}
|
|
24
54
|
@history << @current_step_id
|
|
25
55
|
skip_display_steps_if_needed
|
|
26
56
|
end
|
|
@@ -61,6 +91,7 @@ module Inquirex
|
|
|
61
91
|
raise Errors::ValidationError, "Validation failed: #{result.errors.join(", ")}" unless result.valid?
|
|
62
92
|
|
|
63
93
|
@answers[@current_step_id] = value
|
|
94
|
+
@suggestions.delete(@current_step_id)
|
|
64
95
|
apply_accumulations(current_step, value)
|
|
65
96
|
advance_step
|
|
66
97
|
end
|
|
@@ -95,24 +126,85 @@ module Inquirex
|
|
|
95
126
|
next if value.respond_to?(:empty?) && value.empty?
|
|
96
127
|
|
|
97
128
|
sym = key.to_sym
|
|
98
|
-
|
|
129
|
+
if multi_select_step?(sym)
|
|
130
|
+
# Multi-select extraction is a hint, not a fact: the user may have
|
|
131
|
+
# more selections in mind than the text revealed. Record it as a
|
|
132
|
+
# suggestion so renderers pre-check the choices while the question
|
|
133
|
+
# is still asked; skip_if rules see no answer and do not fire.
|
|
134
|
+
@suggestions[sym] = Array(value) unless @answers.key?(sym)
|
|
135
|
+
else
|
|
136
|
+
@answers[sym] = value unless @answers.key?(sym)
|
|
137
|
+
end
|
|
99
138
|
end
|
|
100
139
|
skip_if_needed unless finished?
|
|
101
140
|
@answers
|
|
102
141
|
end
|
|
103
142
|
|
|
143
|
+
# The prefill suggestion for a step, or nil when none was recorded.
|
|
144
|
+
#
|
|
145
|
+
# @param step_id [Symbol, String] step id
|
|
146
|
+
# @return [Array, nil] suggested selections for a multi-select step
|
|
147
|
+
def suggestion_for(step_id)
|
|
148
|
+
@suggestions[step_id.to_sym]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Registers a hook to run when the flow finishes. The block receives the
|
|
152
|
+
# engine; front-ends typically use it to attach a rich
|
|
153
|
+
# completion_metadata (host, user, ips, terminal, ...). Optional — after
|
|
154
|
+
# all hooks run, the engine fills in a minimal CompletionMetadata when
|
|
155
|
+
# none of them provided one. Registering on an already-finished engine
|
|
156
|
+
# invokes the block immediately.
|
|
157
|
+
#
|
|
158
|
+
# Any number of hooks may be registered; they run in registration order.
|
|
159
|
+
# Each is isolated from the others — a hook that raises a StandardError
|
|
160
|
+
# has it recorded in #completion_hook_errors, and the remaining hooks
|
|
161
|
+
# still run. Non-StandardError exceptions (Interrupt, SignalException)
|
|
162
|
+
# propagate, as they should.
|
|
163
|
+
#
|
|
164
|
+
# @example Stamp renderer-specific completion metadata
|
|
165
|
+
# engine.after_completion do |eng|
|
|
166
|
+
# eng.completion_metadata = Inquirex::CompletionMetadata.new(
|
|
167
|
+
# engine: "inquirex-tty", engine_version: "0.5.0", hostname: Socket.gethostname
|
|
168
|
+
# )
|
|
169
|
+
# end
|
|
170
|
+
#
|
|
171
|
+
# @yield [Engine] the engine, at completion time
|
|
172
|
+
# @return [Engine] self
|
|
173
|
+
def after_completion(&block)
|
|
174
|
+
raise ArgumentError, "after_completion requires a block" unless block
|
|
175
|
+
|
|
176
|
+
@after_completion_hooks << block
|
|
177
|
+
if finished?
|
|
178
|
+
invoke_completion_hook(block)
|
|
179
|
+
ensure_completion_metadata
|
|
180
|
+
end
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
104
184
|
# Serializable state snapshot for persistence or resumption.
|
|
105
185
|
#
|
|
106
186
|
# @return [Hash]
|
|
107
187
|
def to_state
|
|
108
188
|
{
|
|
109
|
-
current_step_id:
|
|
110
|
-
answers:
|
|
111
|
-
history:
|
|
112
|
-
totals:
|
|
189
|
+
current_step_id: @current_step_id,
|
|
190
|
+
answers: @answers,
|
|
191
|
+
history: @history,
|
|
192
|
+
totals: @totals,
|
|
193
|
+
suggestions: @suggestions,
|
|
194
|
+
completion_metadata: @completion_metadata&.to_h
|
|
113
195
|
}
|
|
114
196
|
end
|
|
115
197
|
|
|
198
|
+
# The collected answers with the completion metadata (when a renderer
|
|
199
|
+
# attached one) merged in under the :completion_metadata key.
|
|
200
|
+
#
|
|
201
|
+
# @return [Hash]
|
|
202
|
+
def answers_with_metadata
|
|
203
|
+
return @answers if @completion_metadata.nil?
|
|
204
|
+
|
|
205
|
+
@answers.merge(completion_metadata: @completion_metadata.to_h)
|
|
206
|
+
end
|
|
207
|
+
|
|
116
208
|
# Rebuilds an Engine from a previously saved state.
|
|
117
209
|
#
|
|
118
210
|
# @param definition [Definition] same definition used when state was captured
|
|
@@ -135,6 +227,19 @@ module Inquirex
|
|
|
135
227
|
@answers = state[:answers] || {}
|
|
136
228
|
@history = state[:history] || []
|
|
137
229
|
@totals = state[:totals] || init_totals
|
|
230
|
+
@completion_metadata = CompletionMetadata.from_h(state[:completion_metadata])
|
|
231
|
+
@after_completion_hooks = []
|
|
232
|
+
@completion_hook_errors = []
|
|
233
|
+
@suggestions = state[:suggestions] || {}
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Whether +step_id+ names a multi-select step in the definition. Unknown
|
|
237
|
+
# ids (e.g. an extract schema field that matches no step) are not.
|
|
238
|
+
#
|
|
239
|
+
# @param step_id [Symbol]
|
|
240
|
+
# @return [Boolean]
|
|
241
|
+
def multi_select_step?(step_id)
|
|
242
|
+
@definition.step_ids.include?(step_id) && @definition.step(step_id).type == :multi_enum
|
|
138
243
|
end
|
|
139
244
|
|
|
140
245
|
def init_totals
|
|
@@ -154,12 +259,37 @@ module Inquirex
|
|
|
154
259
|
node = @definition.step(@current_step_id)
|
|
155
260
|
next_id = node.next_step_id(@answers)
|
|
156
261
|
@current_step_id = next_id
|
|
157
|
-
return unless next_id
|
|
262
|
+
return run_after_completion_hooks unless next_id
|
|
158
263
|
|
|
159
264
|
@history << next_id
|
|
160
265
|
skip_if_needed
|
|
161
266
|
end
|
|
162
267
|
|
|
268
|
+
# Fires the moment the flow finishes: runs registered after_completion
|
|
269
|
+
# hooks in registration order, then guarantees completion_metadata exists
|
|
270
|
+
# — hooks may attach a rich version; absent that (including when the hook
|
|
271
|
+
# meant to supply it raised), a minimal core-stamped one is used.
|
|
272
|
+
def run_after_completion_hooks
|
|
273
|
+
@after_completion_hooks.each { |hook| invoke_completion_hook(hook) }
|
|
274
|
+
ensure_completion_metadata
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Runs one hook in isolation. A StandardError is recorded rather than
|
|
279
|
+
# propagated so that one misbehaving hook cannot silently cancel the
|
|
280
|
+
# hooks registered after it, nor abort the completion of the flow itself.
|
|
281
|
+
def invoke_completion_hook(hook)
|
|
282
|
+
hook.call(self)
|
|
283
|
+
rescue StandardError => e
|
|
284
|
+
@completion_hook_errors << e
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def ensure_completion_metadata
|
|
288
|
+
return if @completion_metadata
|
|
289
|
+
|
|
290
|
+
@completion_metadata = CompletionMetadata.new(engine: "inquirex", engine_version: VERSION)
|
|
291
|
+
end
|
|
292
|
+
|
|
163
293
|
# Auto-skips the current step if its skip_if rule is satisfied.
|
|
164
294
|
def skip_if_needed
|
|
165
295
|
return if finished?
|
data/lib/inquirex/errors.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Inquirex
|
|
4
|
+
# Namespace for all Inquirex exception classes.
|
|
4
5
|
module Errors
|
|
5
6
|
# Base exception for all Inquirex errors.
|
|
6
7
|
class Error < StandardError; end
|
|
@@ -26,5 +27,9 @@ module Inquirex
|
|
|
26
27
|
|
|
27
28
|
# Raised when serializing or deserializing a Definition to/from JSON fails.
|
|
28
29
|
class SerializationError < Error; end
|
|
30
|
+
|
|
31
|
+
# Raised when a post-completion action cannot execute structurally,
|
|
32
|
+
# e.g. send_email is used without the mail gem installed.
|
|
33
|
+
class ActionError < Error; end
|
|
29
34
|
end
|
|
30
35
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Inquirex
|
|
4
|
+
# Namespace for flow graph export utilities (Mermaid, ...).
|
|
4
5
|
module Graph
|
|
5
6
|
# Exports a flow Definition to Mermaid flowchart syntax for visualization.
|
|
6
7
|
# Node labels show verb + truncated question/text; edges show condition labels.
|
|
7
8
|
class MermaidExporter
|
|
9
|
+
# Maximum characters of a node label before it is truncated with "...".
|
|
8
10
|
MAX_LABEL_LENGTH = 50
|
|
9
11
|
|
|
10
12
|
attr_reader :definition
|
data/lib/inquirex/node.rb
CHANGED
|
@@ -27,7 +27,9 @@ module Inquirex
|
|
|
27
27
|
class Node
|
|
28
28
|
# Valid DSL verbs and which ones collect input from the user.
|
|
29
29
|
VERBS = %i[ask say header btw warning confirm].freeze
|
|
30
|
+
# Verbs that collect input from the user.
|
|
30
31
|
COLLECTING_VERBS = %i[ask confirm].freeze
|
|
32
|
+
# Verbs that only display content and auto-advance.
|
|
31
33
|
DISPLAY_VERBS = %i[say header btw warning].freeze
|
|
32
34
|
|
|
33
35
|
# Valid data types for :ask steps.
|
data/lib/inquirex/rules/all.rb
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Inquirex
|
|
4
|
+
# Namespace for rule AST nodes used in transitions and visibility conditions.
|
|
4
5
|
module Rules
|
|
5
6
|
# Composite rule: logical AND of multiple sub-rules. All must be true.
|
|
6
7
|
class All < Base
|
|
7
8
|
attr_reader :rules
|
|
8
9
|
|
|
10
|
+
# @param rules [Array<Rules::Base>] sub-rules that must all hold
|
|
9
11
|
def initialize(*rules)
|
|
10
12
|
super()
|
|
11
13
|
@rules = rules.flatten.freeze
|
|
12
14
|
freeze
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# True when every sub-rule evaluates true (vacuously true when empty).
|
|
18
|
+
#
|
|
19
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
20
|
+
# @return [Boolean]
|
|
15
21
|
def evaluate(answers)
|
|
16
22
|
@rules.all? { |rule| rule.evaluate(answers) }
|
|
17
23
|
end
|
|
18
24
|
|
|
25
|
+
# @return [Hash{String => Object}] wire format with nested rule hashes
|
|
19
26
|
def to_h
|
|
20
27
|
{ "op" => "all", "rules" => @rules.map(&:to_h) }
|
|
21
28
|
end
|
|
22
29
|
|
|
30
|
+
# @return [String] human-readable form, sub-rules joined with AND
|
|
23
31
|
def to_s
|
|
24
32
|
"(#{@rules.join(" AND ")})"
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
# Deserializes an All rule, recursively rehydrating its sub-rules.
|
|
36
|
+
#
|
|
37
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
38
|
+
# @return [All]
|
|
27
39
|
def self.from_h(hash)
|
|
28
40
|
raw_rules = hash["rules"] || hash[:rules] || []
|
|
29
41
|
new(*raw_rules.map { |r| Base.from_h(r) })
|
data/lib/inquirex/rules/any.rb
CHANGED
|
@@ -6,24 +6,35 @@ module Inquirex
|
|
|
6
6
|
class Any < Base
|
|
7
7
|
attr_reader :rules
|
|
8
8
|
|
|
9
|
+
# @param rules [Array<Rules::Base>] sub-rules, at least one of which must hold
|
|
9
10
|
def initialize(*rules)
|
|
10
11
|
super()
|
|
11
12
|
@rules = rules.flatten.freeze
|
|
12
13
|
freeze
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
# True when at least one sub-rule evaluates true (false when empty).
|
|
17
|
+
#
|
|
18
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
19
|
+
# @return [Boolean]
|
|
15
20
|
def evaluate(answers)
|
|
16
21
|
@rules.any? { |rule| rule.evaluate(answers) }
|
|
17
22
|
end
|
|
18
23
|
|
|
24
|
+
# @return [Hash{String => Object}] wire format with nested rule hashes
|
|
19
25
|
def to_h
|
|
20
26
|
{ "op" => "any", "rules" => @rules.map(&:to_h) }
|
|
21
27
|
end
|
|
22
28
|
|
|
29
|
+
# @return [String] human-readable form, sub-rules joined with OR
|
|
23
30
|
def to_s
|
|
24
31
|
"(#{@rules.join(" OR ")})"
|
|
25
32
|
end
|
|
26
33
|
|
|
34
|
+
# Deserializes an Any rule, recursively rehydrating its sub-rules.
|
|
35
|
+
#
|
|
36
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
37
|
+
# @return [Any]
|
|
27
38
|
def self.from_h(hash)
|
|
28
39
|
raw_rules = hash["rules"] || hash[:rules] || []
|
|
29
40
|
new(*raw_rules.map { |r| Base.from_h(r) })
|
data/lib/inquirex/rules/base.rb
CHANGED
|
@@ -30,6 +30,12 @@ module Inquirex
|
|
|
30
30
|
|
|
31
31
|
# Deserializes a rule from a plain Hash (e.g. parsed from JSON).
|
|
32
32
|
#
|
|
33
|
+
# @example Rehydrate a rule from its wire format
|
|
34
|
+
# rule = Inquirex::Rules::Base.from_h(
|
|
35
|
+
# "op" => "equals", "field" => "filing_status", "value" => "single"
|
|
36
|
+
# )
|
|
37
|
+
# rule.evaluate(filing_status: "single") # => true
|
|
38
|
+
#
|
|
33
39
|
# @param hash [Hash] rule hash with string or symbol keys
|
|
34
40
|
# @return [Rules::Base] the deserialized rule
|
|
35
41
|
# @raise [Inquirex::Errors::SerializationError] on unknown operator
|
|
@@ -7,6 +7,8 @@ module Inquirex
|
|
|
7
7
|
class Contains < Base
|
|
8
8
|
attr_reader :field, :value
|
|
9
9
|
|
|
10
|
+
# @param field [Symbol, String] step id whose answer is inspected
|
|
11
|
+
# @param value [Object] value the answer must include
|
|
10
12
|
def initialize(field, value)
|
|
11
13
|
super()
|
|
12
14
|
@field = field.to_sym
|
|
@@ -14,18 +16,28 @@ module Inquirex
|
|
|
14
16
|
freeze
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# True when the field's answer, coerced to an Array, includes the value.
|
|
20
|
+
#
|
|
21
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
22
|
+
# @return [Boolean]
|
|
17
23
|
def evaluate(answers)
|
|
18
24
|
Array(answers[@field]).include?(@value)
|
|
19
25
|
end
|
|
20
26
|
|
|
27
|
+
# @return [Hash{String => Object}] wire format, same shape .from_h accepts
|
|
21
28
|
def to_h
|
|
22
29
|
{ "op" => "contains", "field" => @field.to_s, "value" => @value }
|
|
23
30
|
end
|
|
24
31
|
|
|
32
|
+
# @return [String] human-readable form, e.g. "Business in income_types"
|
|
25
33
|
def to_s
|
|
26
34
|
"#{@value} in #{@field}"
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# Deserializes a Contains rule from a plain Hash.
|
|
38
|
+
#
|
|
39
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
40
|
+
# @return [Contains]
|
|
29
41
|
def self.from_h(hash)
|
|
30
42
|
field = hash["field"] || hash[:field]
|
|
31
43
|
value = hash["value"] || hash[:value]
|
|
@@ -6,6 +6,8 @@ module Inquirex
|
|
|
6
6
|
class Equals < Base
|
|
7
7
|
attr_reader :field, :value
|
|
8
8
|
|
|
9
|
+
# @param field [Symbol, String] step id whose answer is compared
|
|
10
|
+
# @param value [Object] value the answer must equal
|
|
9
11
|
def initialize(field, value)
|
|
10
12
|
super()
|
|
11
13
|
@field = field.to_sym
|
|
@@ -13,18 +15,28 @@ module Inquirex
|
|
|
13
15
|
freeze
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# True when the field's answer equals the value (Ruby ==).
|
|
19
|
+
#
|
|
20
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
21
|
+
# @return [Boolean]
|
|
16
22
|
def evaluate(answers)
|
|
17
23
|
answers[@field] == @value
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# @return [Hash{String => Object}] wire format, same shape .from_h accepts
|
|
20
27
|
def to_h
|
|
21
28
|
{ "op" => "equals", "field" => @field.to_s, "value" => @value }
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
# @return [String] human-readable form, e.g. "filing_status == single"
|
|
24
32
|
def to_s
|
|
25
33
|
"#{@field} == #{@value}"
|
|
26
34
|
end
|
|
27
35
|
|
|
36
|
+
# Deserializes an Equals rule from a plain Hash.
|
|
37
|
+
#
|
|
38
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
39
|
+
# @return [Equals]
|
|
28
40
|
def self.from_h(hash)
|
|
29
41
|
field = hash["field"] || hash[:field]
|
|
30
42
|
value = hash["value"] || hash[:value]
|
|
@@ -6,6 +6,8 @@ module Inquirex
|
|
|
6
6
|
class GreaterThan < Base
|
|
7
7
|
attr_reader :field, :value
|
|
8
8
|
|
|
9
|
+
# @param field [Symbol, String] step id whose answer is compared
|
|
10
|
+
# @param value [Integer] threshold the answer must exceed
|
|
9
11
|
def initialize(field, value)
|
|
10
12
|
super()
|
|
11
13
|
@field = field.to_sym
|
|
@@ -13,18 +15,28 @@ module Inquirex
|
|
|
13
15
|
freeze
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# True when the field's answer, coerced to an Integer, exceeds the threshold.
|
|
19
|
+
#
|
|
20
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
21
|
+
# @return [Boolean]
|
|
16
22
|
def evaluate(answers)
|
|
17
23
|
answers[@field].to_i > @value
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# @return [Hash{String => Object}] wire format, same shape .from_h accepts
|
|
20
27
|
def to_h
|
|
21
28
|
{ "op" => "greater_than", "field" => @field.to_s, "value" => @value }
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
# @return [String] human-readable form, e.g. "dependents > 2"
|
|
24
32
|
def to_s
|
|
25
33
|
"#{@field} > #{@value}"
|
|
26
34
|
end
|
|
27
35
|
|
|
36
|
+
# Deserializes a GreaterThan rule from a plain Hash.
|
|
37
|
+
#
|
|
38
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
39
|
+
# @return [GreaterThan]
|
|
28
40
|
def self.from_h(hash)
|
|
29
41
|
field = hash["field"] || hash[:field]
|
|
30
42
|
value = (hash["value"] || hash[:value]).to_i
|
|
@@ -6,6 +6,8 @@ module Inquirex
|
|
|
6
6
|
class LessThan < Base
|
|
7
7
|
attr_reader :field, :value
|
|
8
8
|
|
|
9
|
+
# @param field [Symbol, String] step id whose answer is compared
|
|
10
|
+
# @param value [Integer] threshold the answer must stay below
|
|
9
11
|
def initialize(field, value)
|
|
10
12
|
super()
|
|
11
13
|
@field = field.to_sym
|
|
@@ -13,18 +15,28 @@ module Inquirex
|
|
|
13
15
|
freeze
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
# True when the field's answer, coerced to an Integer, is below the threshold.
|
|
19
|
+
#
|
|
20
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
21
|
+
# @return [Boolean]
|
|
16
22
|
def evaluate(answers)
|
|
17
23
|
answers[@field].to_i < @value
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# @return [Hash{String => Object}] wire format, same shape .from_h accepts
|
|
20
27
|
def to_h
|
|
21
28
|
{ "op" => "less_than", "field" => @field.to_s, "value" => @value }
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
# @return [String] human-readable form, e.g. "dependents < 2"
|
|
24
32
|
def to_s
|
|
25
33
|
"#{@field} < #{@value}"
|
|
26
34
|
end
|
|
27
35
|
|
|
36
|
+
# Deserializes a LessThan rule from a plain Hash.
|
|
37
|
+
#
|
|
38
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
39
|
+
# @return [LessThan]
|
|
28
40
|
def self.from_h(hash)
|
|
29
41
|
field = hash["field"] || hash[:field]
|
|
30
42
|
value = (hash["value"] || hash[:value]).to_i
|
|
@@ -6,12 +6,18 @@ module Inquirex
|
|
|
6
6
|
class NotEmpty < Base
|
|
7
7
|
attr_reader :field
|
|
8
8
|
|
|
9
|
+
# @param field [Symbol, String] step id whose answer is checked for presence
|
|
9
10
|
def initialize(field)
|
|
10
11
|
super()
|
|
11
12
|
@field = field.to_sym
|
|
12
13
|
freeze
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
# True when the field's answer is neither nil nor empty
|
|
17
|
+
# (empty String, Array, Hash, etc. all evaluate false).
|
|
18
|
+
#
|
|
19
|
+
# @param answers [Hash{Symbol => Object}] answer context, step_id => value
|
|
20
|
+
# @return [Boolean]
|
|
15
21
|
def evaluate(answers)
|
|
16
22
|
val = answers[@field]
|
|
17
23
|
return false if val.nil?
|
|
@@ -20,14 +26,20 @@ module Inquirex
|
|
|
20
26
|
true
|
|
21
27
|
end
|
|
22
28
|
|
|
29
|
+
# @return [Hash{String => Object}] wire format, same shape .from_h accepts
|
|
23
30
|
def to_h
|
|
24
31
|
{ "op" => "not_empty", "field" => @field.to_s }
|
|
25
32
|
end
|
|
26
33
|
|
|
34
|
+
# @return [String] human-readable form, e.g. "income_types is not empty"
|
|
27
35
|
def to_s
|
|
28
36
|
"#{@field} is not empty"
|
|
29
37
|
end
|
|
30
38
|
|
|
39
|
+
# Deserializes a NotEmpty rule from a plain Hash.
|
|
40
|
+
#
|
|
41
|
+
# @param hash [Hash] rule hash with string or symbol keys
|
|
42
|
+
# @return [NotEmpty]
|
|
31
43
|
def self.from_h(hash)
|
|
32
44
|
field = hash["field"] || hash[:field]
|
|
33
45
|
new(field)
|
|
@@ -5,6 +5,11 @@ module Inquirex
|
|
|
5
5
|
# No-op validator: always accepts any input.
|
|
6
6
|
# Used by default when no validation adapter is configured.
|
|
7
7
|
class NullAdapter < Adapter
|
|
8
|
+
# Accepts any input unconditionally.
|
|
9
|
+
#
|
|
10
|
+
# @param _node [Node] the current step (ignored)
|
|
11
|
+
# @param _input [Object] value submitted by the user (ignored)
|
|
12
|
+
# @return [Result] always valid, with no errors
|
|
8
13
|
def validate(_node, _input)
|
|
9
14
|
Result.new(valid: true)
|
|
10
15
|
end
|
data/lib/inquirex/version.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Inquirex
|
|
|
10
10
|
# Adapters (TTY, JS, Rails) use this to pick an appropriate renderer when
|
|
11
11
|
# the DSL author has not specified an explicit `widget` hint.
|
|
12
12
|
module WidgetRegistry
|
|
13
|
+
# Every widget type an adapter may be asked to render, across all contexts.
|
|
13
14
|
WIDGET_TYPES = %i[
|
|
14
15
|
text_input
|
|
15
16
|
textarea
|
data/lib/inquirex.rb
CHANGED
|
@@ -24,16 +24,29 @@ require_relative "inquirex/accumulator"
|
|
|
24
24
|
require_relative "inquirex/node"
|
|
25
25
|
require_relative "inquirex/definition"
|
|
26
26
|
require_relative "inquirex/answers"
|
|
27
|
+
require_relative "inquirex/completion_metadata"
|
|
27
28
|
|
|
28
29
|
# Validation
|
|
29
30
|
require_relative "inquirex/validation/adapter"
|
|
30
31
|
require_relative "inquirex/validation/null_adapter"
|
|
31
32
|
|
|
33
|
+
# Post-completion actions
|
|
34
|
+
require_relative "inquirex/actions"
|
|
35
|
+
require_relative "inquirex/actions/template"
|
|
36
|
+
require_relative "inquirex/actions/outbox"
|
|
37
|
+
require_relative "inquirex/actions/base"
|
|
38
|
+
require_relative "inquirex/actions/send_email"
|
|
39
|
+
require_relative "inquirex/actions/webhook"
|
|
40
|
+
require_relative "inquirex/actions/custom"
|
|
41
|
+
require_relative "inquirex/actions/action"
|
|
42
|
+
require_relative "inquirex/actions/runner"
|
|
43
|
+
|
|
32
44
|
# DSL
|
|
33
45
|
require_relative "inquirex/dsl"
|
|
34
46
|
require_relative "inquirex/dsl/rule_helpers"
|
|
35
47
|
require_relative "inquirex/dsl/step_builder"
|
|
36
48
|
require_relative "inquirex/dsl/flow_builder"
|
|
49
|
+
require_relative "inquirex/dsl/action_builder"
|
|
37
50
|
|
|
38
51
|
# Engine
|
|
39
52
|
require_relative "inquirex/engine/state_serializer"
|