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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +26 -8
- data/lib/phlex/reactive/component/dsl.rb +3 -1
- data/lib/phlex/reactive/param_schema.rb +34 -9
- data/lib/phlex/reactive/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: 7f176f438a8cddf912348d8c6d0ddf095fa3a6fa1ae2b52b109a8be514a48139
|
|
4
|
+
data.tar.gz: 49f4e78d5e21ea343fe5bfb6d77d1a3f58e3cb0d592a018536bc9ae52d5c827d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`, `:
|
|
726
|
-
`:date`, `:datetime`, `:decimal`. Anything
|
|
727
|
-
reaching your method. `:date`/`:datetime`
|
|
728
|
-
parse is dropped — the keyword default
|
|
729
|
-
`BigDecimal` (dropped on a non-numeric
|
|
730
|
-
declaration**: a typo'd type symbol
|
|
731
|
-
`Phlex::Reactive::UnknownParamType` at
|
|
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,
|
|
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
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
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
|
|