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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5f521bddfb7ad29c91aa0d99b19615ea43462739ba5da7b0a8ba4ce081fc411
4
- data.tar.gz: 9096cdb056678b643213b17193b7228f984babce96f6bd04f22ce631542438f0
3
+ metadata.gz: 8104ced622e0b1641020699aa84277e7f5ab39e583fb258018c50853ebeaef95
4
+ data.tar.gz: 2ebcb19ca70047b6dec0db437b0e4e5247619dfa1c9503d956648526825bdfb3
5
5
  SHA512:
6
- metadata.gz: 982bfd2e952d710be958ce3a2aba4fc0eccdcacfef4a0d761c6afae09e0a7a4fbfadce93e52ce22c736844cd659da58ec4dbdfb654fb7d6b2988783b885d7f5f
7
- data.tar.gz: cf62d36c583af4fba6462ca8629187895d14a626ae157df99e5f6a58b0878a1c8b4b97152391dab61767b24449be77d36d0233badd7a17b54c037a3bf85fa1d6
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
- @required = value
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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Inquirex
4
- VERSION = "0.9.4"
4
+ VERSION = "0.9.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inquirex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.4
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul