inquirex 0.4.1 → 0.6.0
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 +18 -0
- data/README.md +203 -52
- data/docs/badges/coverage_badge.svg +2 -2
- data/examples/01_readme_example.rb +55 -0
- data/examples/02_readme_mermaid.png +0 -0
- data/examples/02_readme_mermaid.rb +36 -0
- data/examples/03_send_email_actions.rb +97 -0
- data/examples/README.md +33 -0
- data/justfile +3 -2
- 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 +6 -4
- data/lib/inquirex/engine.rb +97 -5
- 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 +35 -5
data/lib/inquirex/engine.rb
CHANGED
|
@@ -12,6 +12,23 @@ 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
|
+
# Exceptions raised by after_completion hooks, in the order they were
|
|
25
|
+
# raised. Hooks are isolated from one another, so a raising hook is
|
|
26
|
+
# recorded here rather than propagated — callers that care can inspect
|
|
27
|
+
# this after the flow finishes. Empty when every hook succeeded.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<StandardError>]
|
|
30
|
+
attr_reader :completion_hook_errors
|
|
31
|
+
|
|
15
32
|
# @param definition [Definition] the flow to run
|
|
16
33
|
# @param validator [Validation::Adapter] optional (default: NullAdapter)
|
|
17
34
|
def initialize(definition, validator: Validation::NullAdapter.new)
|
|
@@ -21,6 +38,9 @@ module Inquirex
|
|
|
21
38
|
@current_step_id = definition.start_step_id
|
|
22
39
|
@validator = validator
|
|
23
40
|
@totals = init_totals
|
|
41
|
+
@completion_metadata = nil
|
|
42
|
+
@after_completion_hooks = []
|
|
43
|
+
@completion_hook_errors = []
|
|
24
44
|
@history << @current_step_id
|
|
25
45
|
skip_display_steps_if_needed
|
|
26
46
|
end
|
|
@@ -101,18 +121,62 @@ module Inquirex
|
|
|
101
121
|
@answers
|
|
102
122
|
end
|
|
103
123
|
|
|
124
|
+
# Registers a hook to run when the flow finishes. The block receives the
|
|
125
|
+
# engine; front-ends typically use it to attach a rich
|
|
126
|
+
# completion_metadata (host, user, ips, terminal, ...). Optional — after
|
|
127
|
+
# all hooks run, the engine fills in a minimal CompletionMetadata when
|
|
128
|
+
# none of them provided one. Registering on an already-finished engine
|
|
129
|
+
# invokes the block immediately.
|
|
130
|
+
#
|
|
131
|
+
# Any number of hooks may be registered; they run in registration order.
|
|
132
|
+
# Each is isolated from the others — a hook that raises a StandardError
|
|
133
|
+
# has it recorded in #completion_hook_errors, and the remaining hooks
|
|
134
|
+
# still run. Non-StandardError exceptions (Interrupt, SignalException)
|
|
135
|
+
# propagate, as they should.
|
|
136
|
+
#
|
|
137
|
+
# @example Stamp renderer-specific completion metadata
|
|
138
|
+
# engine.after_completion do |eng|
|
|
139
|
+
# eng.completion_metadata = Inquirex::CompletionMetadata.new(
|
|
140
|
+
# engine: "inquirex-tty", engine_version: "0.5.0", hostname: Socket.gethostname
|
|
141
|
+
# )
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
144
|
+
# @yield [Engine] the engine, at completion time
|
|
145
|
+
# @return [Engine] self
|
|
146
|
+
def after_completion(&block)
|
|
147
|
+
raise ArgumentError, "after_completion requires a block" unless block
|
|
148
|
+
|
|
149
|
+
@after_completion_hooks << block
|
|
150
|
+
if finished?
|
|
151
|
+
invoke_completion_hook(block)
|
|
152
|
+
ensure_completion_metadata
|
|
153
|
+
end
|
|
154
|
+
self
|
|
155
|
+
end
|
|
156
|
+
|
|
104
157
|
# Serializable state snapshot for persistence or resumption.
|
|
105
158
|
#
|
|
106
159
|
# @return [Hash]
|
|
107
160
|
def to_state
|
|
108
161
|
{
|
|
109
|
-
current_step_id:
|
|
110
|
-
answers:
|
|
111
|
-
history:
|
|
112
|
-
totals:
|
|
162
|
+
current_step_id: @current_step_id,
|
|
163
|
+
answers: @answers,
|
|
164
|
+
history: @history,
|
|
165
|
+
totals: @totals,
|
|
166
|
+
completion_metadata: @completion_metadata&.to_h
|
|
113
167
|
}
|
|
114
168
|
end
|
|
115
169
|
|
|
170
|
+
# The collected answers with the completion metadata (when a renderer
|
|
171
|
+
# attached one) merged in under the :completion_metadata key.
|
|
172
|
+
#
|
|
173
|
+
# @return [Hash]
|
|
174
|
+
def answers_with_metadata
|
|
175
|
+
return @answers if @completion_metadata.nil?
|
|
176
|
+
|
|
177
|
+
@answers.merge(completion_metadata: @completion_metadata.to_h)
|
|
178
|
+
end
|
|
179
|
+
|
|
116
180
|
# Rebuilds an Engine from a previously saved state.
|
|
117
181
|
#
|
|
118
182
|
# @param definition [Definition] same definition used when state was captured
|
|
@@ -135,6 +199,9 @@ module Inquirex
|
|
|
135
199
|
@answers = state[:answers] || {}
|
|
136
200
|
@history = state[:history] || []
|
|
137
201
|
@totals = state[:totals] || init_totals
|
|
202
|
+
@completion_metadata = CompletionMetadata.from_h(state[:completion_metadata])
|
|
203
|
+
@after_completion_hooks = []
|
|
204
|
+
@completion_hook_errors = []
|
|
138
205
|
end
|
|
139
206
|
|
|
140
207
|
def init_totals
|
|
@@ -154,12 +221,37 @@ module Inquirex
|
|
|
154
221
|
node = @definition.step(@current_step_id)
|
|
155
222
|
next_id = node.next_step_id(@answers)
|
|
156
223
|
@current_step_id = next_id
|
|
157
|
-
return unless next_id
|
|
224
|
+
return run_after_completion_hooks unless next_id
|
|
158
225
|
|
|
159
226
|
@history << next_id
|
|
160
227
|
skip_if_needed
|
|
161
228
|
end
|
|
162
229
|
|
|
230
|
+
# Fires the moment the flow finishes: runs registered after_completion
|
|
231
|
+
# hooks in registration order, then guarantees completion_metadata exists
|
|
232
|
+
# — hooks may attach a rich version; absent that (including when the hook
|
|
233
|
+
# meant to supply it raised), a minimal core-stamped one is used.
|
|
234
|
+
def run_after_completion_hooks
|
|
235
|
+
@after_completion_hooks.each { |hook| invoke_completion_hook(hook) }
|
|
236
|
+
ensure_completion_metadata
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Runs one hook in isolation. A StandardError is recorded rather than
|
|
241
|
+
# propagated so that one misbehaving hook cannot silently cancel the
|
|
242
|
+
# hooks registered after it, nor abort the completion of the flow itself.
|
|
243
|
+
def invoke_completion_hook(hook)
|
|
244
|
+
hook.call(self)
|
|
245
|
+
rescue StandardError => e
|
|
246
|
+
@completion_hook_errors << e
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def ensure_completion_metadata
|
|
250
|
+
return if @completion_metadata
|
|
251
|
+
|
|
252
|
+
@completion_metadata = CompletionMetadata.new(engine: "inquirex", engine_version: VERSION)
|
|
253
|
+
end
|
|
254
|
+
|
|
163
255
|
# Auto-skips the current step if its skip_if rule is satisfied.
|
|
164
256
|
def skip_if_needed
|
|
165
257
|
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"
|
metadata
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inquirex
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ostruct
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.6'
|
|
12
26
|
description: Inquirex lets you define multi-step questionnaires as directed graphs
|
|
13
27
|
with conditional branching, using a conversational DSL (ask, say, mention) and an
|
|
14
28
|
AST-based rule system (contains, equals, greater_than, all, any). The engine walks
|
|
15
29
|
the graph, collects structured answers, and serializes everything to JSON — making
|
|
16
30
|
it the ideal backbone for cross-platform intake forms where the frontend is a chat
|
|
17
|
-
widget, a terminal, or a mobile app. Framework-agnostic,
|
|
18
|
-
immutable definitions.
|
|
31
|
+
widget, a terminal, or a mobile app. Framework-agnostic, no third-party dependencies,
|
|
32
|
+
thread-safe immutable definitions.
|
|
19
33
|
email:
|
|
20
34
|
- kigster@gmail.com
|
|
21
35
|
executables: []
|
|
@@ -32,13 +46,29 @@ files:
|
|
|
32
46
|
- README.md
|
|
33
47
|
- Rakefile
|
|
34
48
|
- docs/badges/coverage_badge.svg
|
|
49
|
+
- examples/01_readme_example.rb
|
|
50
|
+
- examples/02_readme_mermaid.png
|
|
51
|
+
- examples/02_readme_mermaid.rb
|
|
52
|
+
- examples/03_send_email_actions.rb
|
|
53
|
+
- examples/README.md
|
|
35
54
|
- justfile
|
|
36
55
|
- lefthook.yml
|
|
37
56
|
- lib/inquirex.rb
|
|
38
57
|
- lib/inquirex/accumulator.rb
|
|
58
|
+
- lib/inquirex/actions.rb
|
|
59
|
+
- lib/inquirex/actions/action.rb
|
|
60
|
+
- lib/inquirex/actions/base.rb
|
|
61
|
+
- lib/inquirex/actions/custom.rb
|
|
62
|
+
- lib/inquirex/actions/outbox.rb
|
|
63
|
+
- lib/inquirex/actions/runner.rb
|
|
64
|
+
- lib/inquirex/actions/send_email.rb
|
|
65
|
+
- lib/inquirex/actions/template.rb
|
|
66
|
+
- lib/inquirex/actions/webhook.rb
|
|
39
67
|
- lib/inquirex/answers.rb
|
|
68
|
+
- lib/inquirex/completion_metadata.rb
|
|
40
69
|
- lib/inquirex/definition.rb
|
|
41
70
|
- lib/inquirex/dsl.rb
|
|
71
|
+
- lib/inquirex/dsl/action_builder.rb
|
|
42
72
|
- lib/inquirex/dsl/flow_builder.rb
|
|
43
73
|
- lib/inquirex/dsl/rule_helpers.rb
|
|
44
74
|
- lib/inquirex/dsl/step_builder.rb
|
|
@@ -68,7 +98,7 @@ licenses:
|
|
|
68
98
|
- MIT
|
|
69
99
|
metadata:
|
|
70
100
|
allowed_push_host: https://rubygems.org
|
|
71
|
-
homepage_uri: https://
|
|
101
|
+
homepage_uri: https://qualified.at
|
|
72
102
|
source_code_uri: https://github.com/inquirex/inquirex
|
|
73
103
|
changelog_uri: https://github.com/inquirex/inquirex/blob/main/CHANGELOG.md
|
|
74
104
|
rubygems_mfa_required: 'true'
|