phlex-reactive 0.16.11 → 0.16.12

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: aa14c0a09a3ba2484361ba2b0d932e8445c8c186bbcae71d1defe50d16d7f53a
4
- data.tar.gz: dbc8eb7a7f4b352c8d859a62a692492522429cf6cd8723543d8b3f8731aec347
3
+ metadata.gz: 7f176f438a8cddf912348d8c6d0ddf095fa3a6fa1ae2b52b109a8be514a48139
4
+ data.tar.gz: 49f4e78d5e21ea343fe5bfb6d77d1a3f58e3cb0d592a018536bc9ae52d5c827d
5
5
  SHA512:
6
- metadata.gz: 4fae6bf61c3967ce6a802351bcdc8b355718cc0ead6ac74b55e11cf73dc2b4614a4ec29935a451abaa2ebe3e089ac5d313b938fa59e0bfab8c5dd5fedb8a4538
7
- data.tar.gz: f694fbbe45f6854ac2455c37e4515ee752e9d2df63fdd726d9ac660d9d9afec1da8f5c4ee604899c11c6675092b4de13f1a14919b3d08675d90adad6bfd8020d
6
+ metadata.gz: 6027a094e3dc14403d914c2f9406396b75fef0b4ea68615480b6d1c6a8e6e0b061c5f3e0df4b18ddc7e54642d54a0caf2490b5fe3d088b430864699106de6858
7
+ data.tar.gz: 87dd2ef191e0faf521bd869defc0cf645ed25466d1abadfae1db9641f74a6d9c8a6e9ca015834f81a29492e1c14c43eccc21d580478151f6d0d00506fa1473bd
data/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - **`:strict_integer` and `:strict_float` param types (#460).** `:integer` and
12
+ `:float` cast with `to_i`/`to_f`, so a cleared number field reaches the action
13
+ as `0` and `"12abc"` as `12`, and an action cannot tell a blank field from a
14
+ typed `0`. The two new built-ins follow the drop contract instead: a blank or
15
+ unparsable value is dropped, so the keyword default applies. `:strict_integer`
16
+ is `Integer(value, 10)`, so `"1.5"`, a JSON `1.0` and `"0x10"` drop and `"010"`
17
+ is `10`; `:strict_float` reads decimal notation, so it also drops hex and a
18
+ string too large or too small for a Float (`"1e400"`, `"1e-400"`). Both take a JSON number
19
+ as well as a string,
20
+ and work in nested and array schemas, where a dropped element follows the
21
+ existing array rule. `:integer` and `:float` are unchanged.
22
+
23
+ ### Fixed
24
+
25
+ - **The docs now say what `:integer` and `:float` do (#460).** The Actions &
26
+ events guide and the counter example said a non-numeric value is dropped; it
27
+ never was. Both pages, and the README, now describe `to_i`/`to_f` and point at
28
+ the strict types.
29
+
9
30
  ## [0.16.11] - 2026-10-19
10
31
 
11
32
  ### Added
data/README.md CHANGED
@@ -722,14 +722,32 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
722
722
  | `reply.pending(records, in:, job:) { … }` + `include Phlex::Reactive::Settles` / `reactive_settle` | For an action that **enqueues** work: mark targets pending, let your job settle them (row + count + empty-state) when it finishes. See [Async actions](#async-actions-replypending--reactive_settle). |
723
723
  | `Container.broadcast_collection_to(*keys, container:, in:, append:/prepend:/remove:)` | The broadcast-side `reply.append`/`reply.remove`: the row **plus** the count companion **plus** the empty-state toggle, to peers. |
724
724
 
725
- Param types: `:string` (default), `:integer`, `:float`, `:boolean`, `:file`,
726
- `:date`, `:datetime`, `:decimal`. Anything not in the schema is dropped before
727
- reaching your method. `:date`/`:datetime` parse ISO8601 (a value that won't
728
- parse is dropped — the keyword default applies); `:decimal` parses through
729
- `BigDecimal` (dropped on a non-numeric value). The schema is **compiled once at
730
- declaration**: a typo'd type symbol (`params: { count: :interger }`) raises
731
- `Phlex::Reactive::UnknownParamType` at class load, not silently coercing to a
732
- string at click time.
725
+ Param types: `:string` (default), `:integer`, `:float`, `:strict_integer`,
726
+ `:strict_float`, `:boolean`, `:file`, `:date`, `:datetime`, `:decimal`. Anything
727
+ not in the schema is dropped before reaching your method. `:date`/`:datetime`
728
+ parse ISO8601 (a value that won't parse is dropped — the keyword default
729
+ applies); `:decimal` parses through `BigDecimal` (dropped on a non-numeric
730
+ value). The schema is **compiled once at declaration**: a typo'd type symbol
731
+ (`params: { count: :interger }`) raises `Phlex::Reactive::UnknownParamType` at
732
+ class load, not silently coercing to a string at click time.
733
+
734
+ `:integer` and `:float` cast with `to_i`/`to_f` and never drop, so a cleared
735
+ number field arrives as `0` and `"12abc"` as `12`. When `0` means something,
736
+ declare `:strict_integer` (`Integer(value, 10)`) or `:strict_float`
737
+ (decimal notation only): a blank or unparsable value is dropped, so the keyword default
738
+ applies and a typed `0` stays `0`. `:strict_integer` also drops a fraction, even
739
+ a JSON `1.0`; `:strict_float` also drops hex and a string too large or too small
740
+ for a Float (`"1e400"`, `"1e-400"`). A bare `1e-400` in a JSON body is already
741
+ `0.0` when the parser hands it over, so it arrives as zero.
742
+
743
+ ```ruby
744
+ action :save, params: { quantity: :strict_integer, weight: :strict_float }
745
+
746
+ def save(quantity: nil, weight: nil) # nil when blank or unparsable; a typed 0 stays 0
747
+ authorize! @widget, :update?
748
+ @widget.update!(quantity:, weight:)
749
+ end
750
+ ```
733
751
 
734
752
  **Custom param types (`Phlex::Reactive.param_type`).** Register your own type in
735
753
  an initializer — the block receives the raw client value and returns the coerced
@@ -288,7 +288,9 @@ module Phlex
288
288
  #
289
289
  # Param types are coerced server-side; anything not in the schema is
290
290
  # dropped before reaching your method (no mass assignment):
291
- # * Scalars — :string (default), :integer, :float, :boolean
291
+ # * Scalars — :string (default), :integer, :float, :boolean,
292
+ # :strict_integer, :strict_float (drop a blank or unparsable value;
293
+ # :integer and :float read it as 0)
292
294
  # * Array of scalar — wrap the type in an array: [:integer]
293
295
  # * Array of hash (Rails nested attributes) — wrap a hash schema:
294
296
  # action :save, params: {
@@ -25,7 +25,8 @@ module Phlex
25
25
  #
26
26
  # A type is one of:
27
27
  # * a scalar symbol (:string/:integer/:float/:boolean/:file/:date/
28
- # :datetime/:decimal, or an app-registered type)
28
+ # :datetime/:decimal/:strict_integer/:strict_float,
29
+ # or an app-registered type)
29
30
  # * a Hash schema ({ id: :integer, ... }) — nested object
30
31
  # * a one-element Array ([:integer] / [{ ... }]) — array of that
31
32
  class ParamSchema
@@ -53,7 +54,7 @@ module Phlex
53
54
  # dups it so app registration doesn't mutate this template). Each entry
54
55
  # is a callable(value) -> coerced | DROP. The scalar casts here are the
55
56
  # EXACT ones the controller ran, so the existing request-spec matrix is
56
- # unchanged.
57
+ # unchanged (the strict pair came later and has no controller history).
57
58
  # rubocop:disable Style/ItBlockParameter, Style/SymbolProc
58
59
  # Explicit `(value)` params, NOT `it` or `&:to_s`: the date/datetime/decimal
59
60
  # casters wrap an INNER `parse_or_drop { ... }` block, and `it` there would
@@ -73,18 +74,42 @@ module Phlex
73
74
  file: nil,
74
75
  date: ->(value) { parse_or_drop { Date.iso8601(value.to_s) } },
75
76
  datetime: ->(value) { parse_or_drop { DateTime.iso8601(value.to_s) } },
76
- decimal: ->(value) { parse_or_drop { BigDecimal(value.to_s) } }
77
+ decimal: ->(value) { parse_or_drop { BigDecimal(value.to_s) } },
78
+ # Issue #460: :integer and :float above read a blank field as 0 and
79
+ # keep that meaning; these two drop it, so a typed 0 stays
80
+ # distinguishable. to_s first: a JSON body sends 12, not "12". Base 10
81
+ # so "0x10" isn't another number and "010" isn't octal.
82
+ strict_integer: ->(value) { parse_or_drop { Integer(value.to_s, 10) } },
83
+ strict_float: ->(value) { parse_or_drop { strict_float(value.to_s) } }
77
84
  }
78
85
  end
79
86
  # rubocop:enable Style/ItBlockParameter, Style/SymbolProc
80
87
 
81
88
  private
82
89
 
83
- # Run a parse that raises on bad input (Date/DateTime iso8601, BigDecimal)
84
- # and turn the failure into DROP — the keyword default then applies rather
85
- # than a fabricated value. ::ArgumentError covers BigDecimal("abc") and a
86
- # blank/garbage string; ::Date::Error (an ArgumentError subclass) covers
87
- # the iso8601 parsers; ::TypeError covers a nil slipping through.
90
+ # Float() alone is not strict: it reads "0x10" as 16.0 (:strict_integer's
91
+ # base 10 refuses the same string), takes "1e400" to Infinity and "1e-400"
92
+ # to a fabricated 0.0 without raising, and warns on stderr for both under
93
+ # -w, once per client value. So BigDecimal screens first: it refuses hex
94
+ # and never warns, and nothing out of range reaches Float(). Float() then
95
+ # stays the grammar, refusing the "1_" and "1d3" BigDecimal accepts.
96
+ #
97
+ # The zero check reads the mantissa, not BigDecimal#zero?: BigDecimal
98
+ # itself rounds "1e-999999999999999999999" to an exact zero.
99
+ def strict_float(text)
100
+ float = BigDecimal(text).to_f
101
+ return DROP unless float.finite? && (float.nonzero? || !text.match?(/\A[^e]*[1-9]/i))
102
+
103
+ Float(text)
104
+ end
105
+
106
+ # Run a parse that raises on bad input (Date/DateTime iso8601, BigDecimal,
107
+ # Integer, Float) and turn the failure into DROP — the keyword default then
108
+ # applies rather than a fabricated value. ::ArgumentError covers
109
+ # BigDecimal("abc") and a blank/garbage string; ::Date::Error (an
110
+ # ArgumentError subclass) covers the iso8601 parsers; ::TypeError covers a
111
+ # nil slipping through; ::FloatDomainError covers BigDecimal under an
112
+ # app's BigDecimal.mode exception flags ("Infinity", "1e400").
88
113
  #
89
114
  # MUST be top-level-qualified (::): the phlex gem defines Phlex::Argument
90
115
  # Error (a subclass of the stdlib one), and this file is nested under
@@ -93,7 +118,7 @@ module Phlex
93
118
  # parse failure would escape as a 500.
94
119
  def parse_or_drop
95
120
  yield
96
- rescue ::ArgumentError, ::TypeError
121
+ rescue ::ArgumentError, ::TypeError, ::FloatDomainError
97
122
  DROP
98
123
  end
99
124
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.16.11"
5
+ VERSION = "0.16.12"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.11
4
+ version: 0.16.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson