concerns_on_rails 1.28.9 → 1.30.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +164 -0
  3. data/README.md +169 -43
  4. data/lib/concerns_on_rails/controllers/cacheable.rb +44 -10
  5. data/lib/concerns_on_rails/controllers/cursor_paginatable.rb +43 -7
  6. data/lib/concerns_on_rails/controllers/filterable.rb +159 -10
  7. data/lib/concerns_on_rails/controllers/idempotentable.rb +76 -18
  8. data/lib/concerns_on_rails/controllers/includable.rb +8 -3
  9. data/lib/concerns_on_rails/controllers/localizable.rb +42 -6
  10. data/lib/concerns_on_rails/controllers/paginatable.rb +7 -13
  11. data/lib/concerns_on_rails/controllers/throttleable.rb +20 -3
  12. data/lib/concerns_on_rails/controllers/timezoneable.rb +24 -1
  13. data/lib/concerns_on_rails/controllers/webhook_verifiable.rb +79 -24
  14. data/lib/concerns_on_rails/models/activatable.rb +43 -14
  15. data/lib/concerns_on_rails/models/addressable.rb +83 -13
  16. data/lib/concerns_on_rails/models/aliasable.rb +43 -0
  17. data/lib/concerns_on_rails/models/anonymizable.rb +230 -11
  18. data/lib/concerns_on_rails/models/auditable.rb +20 -4
  19. data/lib/concerns_on_rails/models/counter_cacheable.rb +106 -29
  20. data/lib/concerns_on_rails/models/duplicable.rb +101 -2
  21. data/lib/concerns_on_rails/models/encryptable.rb +81 -3
  22. data/lib/concerns_on_rails/models/expirable.rb +59 -22
  23. data/lib/concerns_on_rails/models/hashable.rb +35 -5
  24. data/lib/concerns_on_rails/models/lockable.rb +7 -4
  25. data/lib/concerns_on_rails/models/monetizable.rb +15 -18
  26. data/lib/concerns_on_rails/models/normalizable.rb +17 -5
  27. data/lib/concerns_on_rails/models/publishable.rb +36 -12
  28. data/lib/concerns_on_rails/models/sanitizable.rb +17 -4
  29. data/lib/concerns_on_rails/models/schedulable.rb +55 -13
  30. data/lib/concerns_on_rails/models/searchable.rb +8 -1
  31. data/lib/concerns_on_rails/models/sequenceable.rb +36 -3
  32. data/lib/concerns_on_rails/models/soft_deletable.rb +31 -24
  33. data/lib/concerns_on_rails/models/stateable.rb +17 -17
  34. data/lib/concerns_on_rails/models/storable.rb +18 -0
  35. data/lib/concerns_on_rails/models/taggable.rb +36 -3
  36. data/lib/concerns_on_rails/models/tokenizable.rb +7 -3
  37. data/lib/concerns_on_rails/support/address_data.rb +1 -2
  38. data/lib/concerns_on_rails/support/affix.rb +120 -1
  39. data/lib/concerns_on_rails/support/hooked_write.rb +149 -0
  40. data/lib/concerns_on_rails/support/html_sanitizers.rb +66 -0
  41. data/lib/concerns_on_rails/support/masker.rb +9 -4
  42. data/lib/concerns_on_rails/support/money.rb +127 -2
  43. data/lib/concerns_on_rails/support/numeric_operand.rb +186 -0
  44. data/lib/concerns_on_rails/support/scalar_param.rb +23 -0
  45. data/lib/concerns_on_rails/support/sequence_calculator.rb +42 -3
  46. data/lib/concerns_on_rails/support/unique_retry.rb +12 -2
  47. data/lib/concerns_on_rails/version.rb +1 -1
  48. data/lib/concerns_on_rails.rb +2 -0
  49. metadata +10 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f292a3f1cf7d1fae3b1e47be9daa416ae0b6c8b69ce0d4fd2efd583496f2d9aa
4
- data.tar.gz: 661ffce3f3fa4fb305957b239fbb560752ea22f369da3268d03575efb35701da
3
+ metadata.gz: 01db17beb47c485890bfd3c755086624432941547fdd77ecc52536164a2a3404
4
+ data.tar.gz: 8aeca6a8b1e4d901a8d1b128a75ff27f25f5f85138034819b75ed66cfb16cc4e
5
5
  SHA512:
6
- metadata.gz: a49931859e637327e55313e7473236a4ceca6a5f61c01f7f701a7d7acba317c8fe6a83ec05ec2dafa5d7e61da45d8d2623a26f15a876d9e7d12036cd9c40c451
7
- data.tar.gz: 625395f36729c2d789b303cfeec6537dfa173364b70c0a6c971707616a0fb3161d5eef083417aadcc9586a7b99c89d9a7617355e08284cdb76d210a62a3df82f
6
+ metadata.gz: 7615ea7a35f68bffb71e55975a9ad3097d683287dd1cddc81d4d0397343fba1d38289a8589db573ebe3a5f310ed5e2ddf43f621082f6bc16f56587402594df0e
7
+ data.tar.gz: '093ac4ae4b2de63d42722de0f75b23ce9423cc8528fa6c1768e849a515079d501b5df095e64c54227cd967d355ce302c63d55fe4467b7903083770ff38115adc'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,169 @@
1
1
  <!-- CHANGELOG.md -->
2
2
 
3
+ ## 1.30.0 (2026-09-27)
4
+
5
+ The fixes from the 2026-09-23 whole-gem audit of 1.29.0: 37 bugs (8 HIGH) across six PRs
6
+ (#109–#114). It is a minor release, not a patch, because a few fixes add an option or a method,
7
+ and several change observable behaviour. Read **Changed** before upgrading. No new
8
+ concerns and no migrations. Every fix ships with a regression spec that fails on 1.29.0.
9
+ 2146 examples, 0 failures (2121 on Rails 6.0), green on PostgreSQL and MySQL.
10
+
11
+ ### Changed
12
+ - **Throttleable: counters that use the default name reset once on upgrade.** The default
13
+ rule name changes from `"rule<n>"` to `"<DeclaringController>#rule<n>"`, so the cache key
14
+ changes, and so does the `rule` field of `rate_limited.concerns_on_rails`. Rules with an
15
+ explicit `name:` are not affected. (#109)
16
+ - **WebhookVerifiable: rule lookup is by specificity.** A rule that names the action beats any
17
+ catch-all, whichever class declared either. The most-derived class wins, then declaration
18
+ order. Such actions no longer inherit a parent catch-all's `replay:`/`tolerance:`/scheme, so
19
+ declare those on the subclass rule if you still want them. A catch-all declared before
20
+ specific rules no longer shadows them. `commit_webhook_replay_claim` is now private. (#109)
21
+ - **Cacheable**: POST/PUT/PATCH/DELETE responses, and statuses other than
22
+ 200/203/204/206/304, no longer receive the rule's positive `Cache-Control`. `no_store` is
23
+ unchanged. `Vary` merging is now case-insensitive, leaves `Vary: *` alone, and keeps Rails'
24
+ own `Vary: Accept`. (#109)
25
+ - **Idempotentable: an unreachable store returns 503 `idempotency_store_unavailable`**
26
+ (with `Retry-After`) instead of a permanent 409 `idempotency_conflict`. The action still
27
+ does not run. Unreachable means both claims failed and both reads returned nil. (#109)
28
+ - **Filterable: numeric operands are exact, never truncated.** On an integer column,
29
+ `?stock=5.5` now matches nothing (it used to match 5), and `?stock=1e1` matches 10 (it used
30
+ to match 1). `?stock_lt=<huge>` returns every non-NULL row. One garbage member fails a whole
31
+ `in`/`not_in` list closed, and garbage fails `not` closed too. A JSON `true` or boolean
32
+ against a numeric column fails closed. `type: :integer` on a float/decimal column no longer
33
+ truncates. A numeric `type:` on a `with:` lambda pre-casts strictly, and fails closed
34
+ without calling the lambda. Operands over 100 characters are refused. (#113)
35
+ - **Sequenceable (STI)**: declared on an STI **base** class, all subclasses now share one
36
+ table-wide counter. They used to each start at 1 and collide on a shared unique index.
37
+ **Upgrade note:** a subclass that *inherited* a base declaration and numbered per type now
38
+ jumps once to the table-wide MAX + 1. Declare `scope: :type` to keep per-type numbering, and
39
+ index `(type, <column>)`. `next_<field>` under `scope: :type` previews the receiving class's
40
+ sequence. (#111)
41
+ - **Hooked verbs** (Publishable, SoftDeletable, Expirable, Activatable, Anonymizable,
42
+ Stateable): a hook's `ActiveRecord::Rollback` now vetoes the write for real. The verb
43
+ returns `false`, and the `*_all` verbs raise `RecordNotSaved`. The exception is
44
+ `anonymize_all!`, which skips the vetoed record and doesn't count it. After any aborted
45
+ write, the written attributes return to their pre-call in-memory values. (#114)
46
+ - **Publishable**: with a boolean column, `Post.published.new` no longer pre-sets the flag.
47
+ The scope is now `("published" = TRUE)`, or `<> FALSE` on Rails 6.0. (#114)
48
+ - **Expirable**: `expire!`/`expire_all` with nil or a blank time now mean "now" (they used to
49
+ mean "never expires"). An unparseable time raises `ArgumentError`. (#114)
50
+ - **Expirable / Activatable / Schedulable**: `prefix:`/`suffix:` now also define affixed
51
+ predicates (`term_active?`, `window_expired?`, …). The macro raises if one would shadow a
52
+ different column's query method. (#114)
53
+ - **Auditable**: new entries store `"at"` with microseconds. Second-precision entries still
54
+ parse. (#111)
55
+ - **Hashable**: a `:custom` alphabet with duplicate characters is de-duplicated, with a
56
+ deprecation warning (it will raise in 2.0). Fewer than 2 distinct characters raises. (#111)
57
+
58
+ ### Added
59
+ - **Idempotentable**: `on_store_unavailable:` (`:reject` default, or `:proceed` to run the
60
+ action without deduplication). (#109)
61
+ - **Anonymizable**: `slug:` (`:auto` default, `true`, `false`). It rewrites a friendly_id slug
62
+ built from an anonymized column to a random slug, and deletes its history rows. (#110)
63
+ - **Taggable**: `Model.normalize_tags!` rewrites rows written around the callbacks (e.g.
64
+ `"ruby, rails"`) to the form `tagged_with` matches. It returns a count. (#112)
65
+ - **Support::UniqueRetry**: `savepoint:` runs each attempt in its own savepoint, so a retry
66
+ works inside a caller's transaction on PostgreSQL. (#110, #111)
67
+
68
+ ### Fixed
69
+ - **Encryptable**: `Model.needs_reencryption` / `reencrypt_all!` now cover the whole table.
70
+ Rows hidden by a `default_scope` used to be skipped, and became undecryptable once the old
71
+ key was dropped. On a relation, both cover exactly that relation. On Rails 6.0–7.0,
72
+ `<field>_ciphertext` returns the stored ciphertext after a save, and `reencrypt!` works
73
+ right after a save. (#110)
74
+ - **Anonymizable**: erasure is never blocked by ciphertext that won't decrypt, and one bad
75
+ row no longer rolls back `anonymize_all!`. A slug built from erased PII is replaced in the
76
+ same UPDATE. (#110)
77
+ - **Storable**: `where_<key>` on an `encryptable` column raises instead of silently matching
78
+ nothing (or every row). (#110)
79
+ - **CounterCacheable**: a destroy decrements only when its DELETE removed a row, and reads
80
+ persisted values. `belongs_to primary_key:` is honoured. A child destroyed by its parent's
81
+ `dependent: :destroy` no longer decrements that parent, which also fixes
82
+ `StaleObjectError`. (#111)
83
+ - **Duplicable**: counter-cache columns are zeroed on the copy, so a deep copy counts its
84
+ children once. `duplicate!` re-reads the counters. (#111)
85
+ - **Sequenceable**: `assign_<field>!` restores the number when the save fails, so a retry
86
+ draws a fresh one. (#111)
87
+ - **Hashable / Tokenizable**: uniqueness prechecks see sibling STI rows, and
88
+ `regenerate_<field>!` retries work inside a caller's transaction. (#111)
89
+ - **Lockable / Addressable**: models load again when the schema is unreachable. (#111)
90
+ - **Aliasable**: `accepts_nested_attributes_for :alias` now updates and destroys existing
91
+ children. It used to save only new ones. (#112)
92
+ - **Sanitizable**: `with: :strip, on: :write` stores plain text (`"Tom & Jerry"`, not
93
+ `"Tom &amp; Jerry"`). It stays idempotent and linear-time, and `sanitize_all!` repairs
94
+ double-escaped rows. (#112)
95
+ - **Addressable**: `if:`/`unless:` from later calls and subclasses are honoured. (#112)
96
+ - **Monetizable**: the writer reads the concern's own formatted output (`"$1,234.50"`).
97
+ Garbage, NaN, Infinity and oversized input cast to nil instead of raising. (#112)
98
+ - **Normalizable**: whitespace presets treat Unicode whitespace as whitespace. (#112)
99
+ - **Maskable**: `:email`/`:phone` fail closed. A malformed value gets the full mask instead of
100
+ being returned raw. (#112)
101
+ - **Taggable**: `taggable_by` raises on an empty `delimiter:`. (#112)
102
+ - **CursorPaginatable**: `max_per_page: 0` no longer lets a huge `per_page` overflow `LIMIT`
103
+ (a 500). Both paginators share `Support::ScalarParam.per_page`, with a 1,000,000 ceiling. A
104
+ cursor whose values can't be bound is a 400 `invalid_cursor`. (#113)
105
+ - **Filterable**: `1e99999999` on an unconstrained numeric column no longer costs ~1.3 s and
106
+ 476 MB per request. `contains`/`starts_with` on non-string columns match nothing, instead of
107
+ a PostgreSQL 500. (#113)
108
+ - **Localizable / Timezoneable**: `rescue_from` handlers render under the resolved
109
+ locale/zone. `Accept-Language` ties keep header order, and `q` is case-insensitive. (#113)
110
+ - **Stateable**: the in-memory state and `<state>_at` are restored after a vetoed transition
111
+ or `RecordInvalid`. (#114)
112
+ - **Rails 6.0**: a hooked write aborted inside a caller's transaction no longer makes a
113
+ record created earlier in it INSERT twice. (#114)
114
+
115
+ ### Internal
116
+ - New `Support::HookedWrite` (the one before-hook → write → after-hook path) and
117
+ `Support::NumericOperand` (Filterable's exact numeric reader). (#113, #114)
118
+ - Development dependencies: rubocop 1.91, simplecov 1.2, permittable 0.7 in the lockfile. (#106)
119
+ - CLAUDE.md describes the new behaviour. (#115)
120
+
121
+ ## 1.29.0 (2026-09-21)
122
+
123
+ CI now runs **every Rails line the gemspec admits** — 6.0, 6.1, 7.0, 7.1, 7.2, 8.0 and 8.1 —
124
+ where it previously ran 7.0 through 8.0. Extending the matrix downwards found 201 failures
125
+ on the two new old lines and 0 on the new top line; all of them are fixed, and all seven
126
+ legs are green and blocking.
127
+
128
+ ### Changed
129
+ - **Rails 5.x is no longer declared.** The `actionpack` / `activerecord` / `activesupport`
130
+ floor moves from `>= 5.0` to `>= 6.0`. This drops nothing that ever worked: with
131
+ `required_ruby_version = ">= 3.2.0"`, the `>= 5.0` half was unreachable by construction —
132
+ Ruby 3's keyword-argument separation breaks Active Record 5.2's own `create_table`
133
+ (`schema_statements.rb:290`), so no bundle could satisfy both. Verified rather than
134
+ assumed: on Ruby 3.2 the suite fails 1,302 examples on Rails 5.2, every one of them
135
+ inside the framework.
136
+
137
+ ### Fixed
138
+ - **Controllers::Includable**: `?fields[table]=...` raised `LocalJumpError` ("no block
139
+ given") on Rails 6.0. `requested_fields` called the block-less `params[:fields].each_pair`
140
+ and chained `with_object`, but `ActionController::Parameters#each_pair` only began
141
+ returning an Enumerator in 6.1. It now iterates with a block, which behaves the same on
142
+ every line.
143
+ - **Models::Searchable**: the generated `search` scope raised
144
+ `ArgumentError: wrong number of arguments (given 2, expected 1)` for every call on Rails
145
+ 6.0. Rails 6.0 forwards a scope's arguments through `define_method(name) { |*args| }`
146
+ with no `ruby2_keywords`, so under Ruby 3 the `ranked:` keyword reaches the body as a
147
+ trailing positional Hash. The scope now takes its options positionally and splats them
148
+ back out — same call signature, and an unknown key still raises.
149
+
150
+ ### Documented
151
+ - Two features need a line newer than the 6.0 floor, and both now say so in the README's
152
+ compatibility table and in their own docs: `Controllers::Sortable`'s `nulls:` ordering
153
+ (Rails 6.1+, already raised at macro time) and `Models::Aliasable`'s **query side**
154
+ (Rails 6.1+ — resolving a `where`-hash key to an association, which is what makes Rails
155
+ alias the join, landed in 6.1; Aliasable's read/write side works on 6.0).
156
+
157
+ ### Internal
158
+ - `gemfiles/rails_6.0.gemfile`, `rails_6.1.gemfile` and `rails_8.1.gemfile`; every gemfile
159
+ below 7.1 now pins `concurrent-ruby < 1.3.5`, which dropped its own `require "logger"`
160
+ that Active Support only started doing for itself in 7.1.
161
+ - The spec harness clears Active Record's schema cache and (on <= 6.1)
162
+ `ActiveSupport::Dependencies::Reference` between examples, gives anonymous spec models a
163
+ `model_name`, and gains a `min_rails:` metadata tag for examples whose feature is version
164
+ gated. Without the first two, specs that reuse table and model names across files saw the
165
+ PREVIOUS file's columns and classes on 6.0/6.1.
166
+
3
167
  ## 1.28.9 (2026-09-19)
4
168
 
5
169
  CI now runs the matrix the gemspec actually claims: Rails 7.0–8.0 across Ruby 3.2/3.3/3.4,