inquirex 0.9.4 → 0.9.5
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/lib/inquirex/dsl/step_builder.rb +96 -1
- data/lib/inquirex/node.rb +79 -0
- data/lib/inquirex/safe_source/vocabulary.rb +16 -0
- data/lib/inquirex/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8104ced622e0b1641020699aa84277e7f5ab39e583fb258018c50853ebeaef95
|
|
4
|
+
data.tar.gz: 2ebcb19ca70047b6dec0db437b0e4e5247619dfa1c9503d956648526825bdfb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1960f28308bceb5164274a111725019a1596fa2938800535cf7452355b72a6e644c5a35b1573c2d1aac433e643da604a09af801dc63d114694e0a41ead88216
|
|
7
|
+
data.tar.gz: e633b465d42e02fb5563f5ad9bbc887d3de07cae1005753e9c1c457f72802cd9015d986b90a5ef6b7af20bd448290366ab3af4471ff26ed0f3115ee0acb5c3ba
|
|
@@ -18,6 +18,7 @@ module Inquirex
|
|
|
18
18
|
@skip_if = nil
|
|
19
19
|
@default = nil
|
|
20
20
|
@required = true
|
|
21
|
+
@requiredness = nil
|
|
21
22
|
@compute = nil
|
|
22
23
|
@widget_hints = {}
|
|
23
24
|
@accumulations = []
|
|
@@ -138,8 +139,34 @@ module Inquirex
|
|
|
138
139
|
# end
|
|
139
140
|
#
|
|
140
141
|
# @param value [Boolean] false to make the step skippable
|
|
142
|
+
# @raise [Errors::DefinitionError] if the step also declares `optional` to the contrary
|
|
141
143
|
def required(value = true)
|
|
142
|
-
|
|
144
|
+
assign_requiredness(:required, value ? true : false)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Declares this step skippable — the inverse of {#required}, and the form
|
|
148
|
+
# most flows want. Since steps are required by default, `required true` is
|
|
149
|
+
# a no-op and the keyword only ever appears as `required false`; `optional`
|
|
150
|
+
# says the same thing without the double negative.
|
|
151
|
+
#
|
|
152
|
+
# Sugar only: both spellings serialize to the single wire field
|
|
153
|
+
# `"required": false`, so a definition round-trips identically however it
|
|
154
|
+
# was authored and no consumer has to learn a second key.
|
|
155
|
+
#
|
|
156
|
+
# @example Equivalent to `required false`, and easier to read
|
|
157
|
+
# ask :dependents do
|
|
158
|
+
# type :integer
|
|
159
|
+
# question "How many dependents?"
|
|
160
|
+
# optional
|
|
161
|
+
# default 0
|
|
162
|
+
# transition to: :done
|
|
163
|
+
# end
|
|
164
|
+
#
|
|
165
|
+
# @param value [Boolean] true (the default) to make the step skippable
|
|
166
|
+
# @return [void]
|
|
167
|
+
# @raise [Errors::DefinitionError] if the step also declares `required` to the contrary
|
|
168
|
+
def optional(value = true)
|
|
169
|
+
assign_requiredness(:optional, value ? false : true)
|
|
143
170
|
end
|
|
144
171
|
|
|
145
172
|
# Registers a compute block: auto-calculates a value from answers, not shown to user.
|
|
@@ -150,6 +177,51 @@ module Inquirex
|
|
|
150
177
|
@compute = block
|
|
151
178
|
end
|
|
152
179
|
|
|
180
|
+
# Declares the inclusive lower bound of a numeric step.
|
|
181
|
+
#
|
|
182
|
+
# Only meaningful for `:integer`, `:decimal` and `:currency`; declaring it
|
|
183
|
+
# on any other type raises at build time rather than being ignored.
|
|
184
|
+
#
|
|
185
|
+
# @example A headcount that cannot be negative
|
|
186
|
+
# ask :employees do
|
|
187
|
+
# type :integer
|
|
188
|
+
# question "How many employees?"
|
|
189
|
+
# min 0
|
|
190
|
+
# max 500
|
|
191
|
+
# transition to: :done
|
|
192
|
+
# end
|
|
193
|
+
#
|
|
194
|
+
# @param value [Numeric] inclusive lower bound
|
|
195
|
+
# @return [void]
|
|
196
|
+
def min(value)
|
|
197
|
+
@min = value
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Declares the inclusive upper bound of a numeric step.
|
|
201
|
+
#
|
|
202
|
+
# @param value [Numeric] inclusive upper bound
|
|
203
|
+
# @return [void]
|
|
204
|
+
# @see #min
|
|
205
|
+
def max(value)
|
|
206
|
+
@max = value
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Declares the increment a numeric step's stepper arrows move by.
|
|
210
|
+
#
|
|
211
|
+
# Defaults, when unset, to 1 for `:integer` and 0.01 for `:decimal` and
|
|
212
|
+
# `:currency` — the renderer applies that, not this builder, so an unset
|
|
213
|
+
# value stays absent from the wire format.
|
|
214
|
+
#
|
|
215
|
+
# Spelled `step_size` rather than `step` because a step *is* the unit of
|
|
216
|
+
# a flow; `steps.employees.step` would read as a nested flow step.
|
|
217
|
+
#
|
|
218
|
+
# @param value [Numeric] stepper increment
|
|
219
|
+
# @return [void]
|
|
220
|
+
# @see #min
|
|
221
|
+
def step_size(value)
|
|
222
|
+
@step_size = value
|
|
223
|
+
end
|
|
224
|
+
|
|
153
225
|
# Builds the Node for the given step id.
|
|
154
226
|
#
|
|
155
227
|
# @param id [Symbol]
|
|
@@ -166,6 +238,9 @@ module Inquirex
|
|
|
166
238
|
skip_if: @skip_if,
|
|
167
239
|
default: @default,
|
|
168
240
|
required: @required,
|
|
241
|
+
min: @min,
|
|
242
|
+
max: @max,
|
|
243
|
+
step_size: @step_size,
|
|
169
244
|
widget_hints: resolve_widget_hints,
|
|
170
245
|
accumulations: @accumulations
|
|
171
246
|
)
|
|
@@ -173,6 +248,26 @@ module Inquirex
|
|
|
173
248
|
|
|
174
249
|
private
|
|
175
250
|
|
|
251
|
+
# Records requiredness from either spelling, rejecting a step that says
|
|
252
|
+
# both things at once. `required false` alongside `optional` agrees and is
|
|
253
|
+
# allowed — harmlessly redundant — but `required` with `optional` is a
|
|
254
|
+
# contradiction, and silently letting the last call win would hide it.
|
|
255
|
+
#
|
|
256
|
+
# @param keyword [Symbol] :required or :optional, for the error message
|
|
257
|
+
# @param value [Boolean] the resulting requiredness
|
|
258
|
+
# @return [void]
|
|
259
|
+
# @raise [Errors::DefinitionError] on contradictory declarations
|
|
260
|
+
def assign_requiredness(keyword, value)
|
|
261
|
+
if !@requiredness.nil? && @requiredness != value
|
|
262
|
+
raise Errors::DefinitionError,
|
|
263
|
+
"Step declares both required and optional with conflicting values " \
|
|
264
|
+
"(#{keyword} would make it #{value ? "required" : "optional"}); use one"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
@requiredness = value
|
|
268
|
+
@required = value
|
|
269
|
+
end
|
|
270
|
+
|
|
176
271
|
def pick_accumulator_shape(lookup:, per_selection:, per_unit:, flat:)
|
|
177
272
|
provided = { lookup:, per_selection:, per_unit:, flat: }.compact
|
|
178
273
|
if provided.size != 1
|
data/lib/inquirex/node.rb
CHANGED
|
@@ -40,6 +40,12 @@ module Inquirex
|
|
|
40
40
|
enum multi_enum date email phone
|
|
41
41
|
].freeze
|
|
42
42
|
|
|
43
|
+
# Types for which {#min}, {#max} and {#step_size} carry meaning. Declaring
|
|
44
|
+
# a bound on anything else is a definition error rather than a silent
|
|
45
|
+
# no-op — a bounded `:string` almost always means the author picked the
|
|
46
|
+
# wrong type, and failing loudly is cheaper than shipping it.
|
|
47
|
+
BOUNDED_TYPES = %i[integer decimal currency].freeze
|
|
48
|
+
|
|
43
49
|
attr_reader :id,
|
|
44
50
|
:verb,
|
|
45
51
|
:type,
|
|
@@ -51,6 +57,9 @@ module Inquirex
|
|
|
51
57
|
:skip_if,
|
|
52
58
|
:default,
|
|
53
59
|
:required,
|
|
60
|
+
:min,
|
|
61
|
+
:max,
|
|
62
|
+
:step_size,
|
|
54
63
|
:widget_hints,
|
|
55
64
|
:accumulations
|
|
56
65
|
|
|
@@ -64,6 +73,9 @@ module Inquirex
|
|
|
64
73
|
skip_if: nil,
|
|
65
74
|
default: nil,
|
|
66
75
|
required: true,
|
|
76
|
+
min: nil,
|
|
77
|
+
max: nil,
|
|
78
|
+
step_size: nil,
|
|
67
79
|
widget_hints: nil,
|
|
68
80
|
accumulations: [])
|
|
69
81
|
@id = id.to_sym
|
|
@@ -75,12 +87,47 @@ module Inquirex
|
|
|
75
87
|
@skip_if = skip_if
|
|
76
88
|
@default = default
|
|
77
89
|
@required = required ? true : false
|
|
90
|
+
@min = coerce_bound(min)
|
|
91
|
+
@max = coerce_bound(max)
|
|
92
|
+
@step_size = coerce_bound(step_size)
|
|
78
93
|
@widget_hints = widget_hints&.freeze
|
|
79
94
|
@accumulations = accumulations.freeze
|
|
80
95
|
extract_options(options)
|
|
96
|
+
validate_bounds!
|
|
81
97
|
freeze
|
|
82
98
|
end
|
|
83
99
|
|
|
100
|
+
# Whether this step declares any numeric bound at all.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def bounded?
|
|
104
|
+
!@min.nil? || !@max.nil?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Pulls a number inside this step's declared bounds.
|
|
108
|
+
#
|
|
109
|
+
# The wire format carries `min`/`max` so a renderer can *present* them, but
|
|
110
|
+
# an HTML number input does not stop a visitor typing past them and an LLM
|
|
111
|
+
# extraction has no notion of them at all. Every path that stores an answer
|
|
112
|
+
# for a bounded step goes through here, so the bound holds regardless of
|
|
113
|
+
# which client produced the value.
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# node.clamp(900) # => 10, for a step declaring `max 10`
|
|
117
|
+
#
|
|
118
|
+
# @param value [Object] candidate answer
|
|
119
|
+
# @return [Object] the clamped Numeric, or the value unchanged when it is
|
|
120
|
+
# not numeric or the step declares no bounds
|
|
121
|
+
def clamp(value)
|
|
122
|
+
return value unless bounded?
|
|
123
|
+
return value unless value.is_a?(Numeric)
|
|
124
|
+
|
|
125
|
+
clamped = value
|
|
126
|
+
clamped = @min if @min && clamped < @min
|
|
127
|
+
clamped = @max if @max && clamped > @max
|
|
128
|
+
clamped
|
|
129
|
+
end
|
|
130
|
+
|
|
84
131
|
# @return [Boolean] true if this step collects input from the user
|
|
85
132
|
def collecting?
|
|
86
133
|
COLLECTING_VERBS.include?(@verb)
|
|
@@ -179,6 +226,9 @@ module Inquirex
|
|
|
179
226
|
hash["skip_if"] = @skip_if.to_h if @skip_if
|
|
180
227
|
hash["default"] = @default unless @default.nil? || @default.is_a?(Proc)
|
|
181
228
|
hash["required"] = false unless @required
|
|
229
|
+
hash["min"] = @min unless @min.nil?
|
|
230
|
+
hash["max"] = @max unless @max.nil?
|
|
231
|
+
hash["step_size"] = @step_size unless @step_size.nil?
|
|
182
232
|
elsif @text
|
|
183
233
|
hash["text"] = @text
|
|
184
234
|
end
|
|
@@ -215,6 +265,9 @@ module Inquirex
|
|
|
215
265
|
default = hash["default"] || hash[:default]
|
|
216
266
|
# Fetch chain (not ||) so an explicit false survives; absent key means required.
|
|
217
267
|
required = hash.fetch("required") { hash.fetch(:required, true) }
|
|
268
|
+
min = hash["min"] || hash[:min]
|
|
269
|
+
max = hash["max"] || hash[:max]
|
|
270
|
+
step_size = hash["step_size"] || hash[:step_size]
|
|
218
271
|
widget_data = hash["widget"] || hash[:widget]
|
|
219
272
|
accumulate_data = hash["accumulate"] || hash[:accumulate]
|
|
220
273
|
|
|
@@ -235,6 +288,9 @@ module Inquirex
|
|
|
235
288
|
skip_if:,
|
|
236
289
|
default:,
|
|
237
290
|
required:,
|
|
291
|
+
min:,
|
|
292
|
+
max:,
|
|
293
|
+
step_size:,
|
|
238
294
|
widget_hints:,
|
|
239
295
|
accumulations:
|
|
240
296
|
)
|
|
@@ -242,6 +298,29 @@ module Inquirex
|
|
|
242
298
|
|
|
243
299
|
private
|
|
244
300
|
|
|
301
|
+
# Numeric bounds arrive as Integers from Ruby DSL and as either Integer or
|
|
302
|
+
# Float from parsed JSON. Anything else — a String "10", a Symbol — is a
|
|
303
|
+
# definition error, not something to coerce quietly into 0.
|
|
304
|
+
def coerce_bound(value)
|
|
305
|
+
return nil if value.nil?
|
|
306
|
+
raise Errors::DefinitionError, "step #{@id}: numeric bound must be a number, got #{value.inspect}" unless value.is_a?(Numeric)
|
|
307
|
+
|
|
308
|
+
value
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def validate_bounds!
|
|
312
|
+
return if @min.nil? && @max.nil? && @step_size.nil?
|
|
313
|
+
|
|
314
|
+
if @type && !BOUNDED_TYPES.include?(@type)
|
|
315
|
+
raise Errors::DefinitionError,
|
|
316
|
+
"step #{@id}: min/max/step_size only apply to #{BOUNDED_TYPES.join(", ")} steps, not #{@type}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
return unless @min && @max && @min > @max
|
|
320
|
+
|
|
321
|
+
raise Errors::DefinitionError, "step #{@id}: min (#{@min}) is greater than max (#{@max})"
|
|
322
|
+
end
|
|
323
|
+
|
|
245
324
|
def extract_options(raw)
|
|
246
325
|
case raw
|
|
247
326
|
when Hash
|
|
@@ -278,6 +278,22 @@ module Inquirex
|
|
|
278
278
|
# `{ optional: :literal }` mirrors `required(value = true)`: both bare
|
|
279
279
|
# `required` and `required false` are real DSL.
|
|
280
280
|
allow :step, :required, positional: { optional: :literal }
|
|
281
|
+
# `optional` is the inverse spelling of `required` and carries exactly
|
|
282
|
+
# the same authority — it sets the same field and serializes to the
|
|
283
|
+
# same `"required": false`. (The `optional:` key below is the argument
|
|
284
|
+
# schema's own word for "this positional may be omitted", unrelated to
|
|
285
|
+
# the verb being allowed here: both bare `optional` and `optional true`
|
|
286
|
+
# are real DSL.)
|
|
287
|
+
allow :step, :optional, positional: { optional: :literal }
|
|
288
|
+
# Numeric bounds are inert metadata in exactly the sense `required`
|
|
289
|
+
# is: Node stores them, `Node#clamp` reads them, and they serialize to
|
|
290
|
+
# plain numbers. `:literal` is the same value kind `default` already
|
|
291
|
+
# accepts, so nothing executable becomes expressible — a non-Numeric
|
|
292
|
+
# literal is refused by `Node#coerce_bound` at build time rather than
|
|
293
|
+
# by the scanner.
|
|
294
|
+
allow :step, :min, positional: %i[literal]
|
|
295
|
+
allow :step, :max, positional: %i[literal]
|
|
296
|
+
allow :step, :step_size, positional: %i[literal]
|
|
281
297
|
allow :step, :skip_if, positional: %i[rule]
|
|
282
298
|
allow :step, :transition, keywords: TRANSITION_KEYWORDS
|
|
283
299
|
allow :step,
|
data/lib/inquirex/version.rb
CHANGED