concerns_on_rails 1.31.0 → 1.32.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +24 -13
- data/lib/concerns_on_rails/version.rb +1 -1
- metadata +12 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5638a56a20ce9b346dd013448af31656fe6925eaefbf8bd87ee8906c17825b4
|
|
4
|
+
data.tar.gz: b3feeb0feaab7f14bf5d03ea46a9e7a1a69f0d087173cf2fb0baa1efb2c86daa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25623c6d25c7fafc8e8c4dfd5f16350df059a5f0bb2999c0d457b986cfcff1c55365f8e072ab5dd01cb9ae48e6226ff335a546f02c1521ca0c3934898e92c296
|
|
7
|
+
data.tar.gz: ad5dd6aa2c310c9d711fd9513e01488e79f164df54aa2accdbca8ab9a46f3c2fe296c0733c6b0c404c0df09147c28160f7c5177e0938b6637eb3037d71f1e363
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.32.0 (2026-10-07)
|
|
4
|
+
|
|
5
|
+
A dependency release (#127): the `permittable` floor rises to `>= 0.8`, and the Permittable docs
|
|
6
|
+
are rewritten against permittable 0.10. It is a minor release because hosts still on permittable
|
|
7
|
+
0.1–0.7 must upgrade it (the same rule as the Rails floor raise in 1.29.0). No code change, no
|
|
8
|
+
new concerns and no migrations. 2723 examples, 0 failures, green on PostgreSQL, MySQL and every
|
|
9
|
+
Rails line from 6.0 to 8.1.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **`permittable` dependency floor raised from `~> 0.1` to `>= 0.8, < 1`.** The Permittable
|
|
13
|
+
documentation (README section and docs page) was rewritten against permittable 0.10 — monitor
|
|
14
|
+
mode, `:json` fields, `nullable:`, `message:`/I18n copy, `format:` presets, field groups,
|
|
15
|
+
RFC 9457 output, the type-checking drift guard, the `permittable:generate`/`:audit`/`:openapi`
|
|
16
|
+
tasks, RSpec matchers and standalone contracts — and the old pin let a host resolve a 0.x that
|
|
17
|
+
had none of it. 0.8 is the floor because it is the newest line that still supports Rails 6.0
|
|
18
|
+
(0.9+ requires ActiveSupport >= 6.1), so the 6.0 CI line keeps resolving; the docs carry a
|
|
19
|
+
heads-up listing what a 0.8 host lacks and how Respondable's `:problem_details` and the gem's
|
|
20
|
+
own `Permittable.error_format = :problem` interact. No code change; the gem README stays the
|
|
21
|
+
canonical Permittable reference.
|
|
22
|
+
|
|
3
23
|
## 1.31.0 (2026-10-05)
|
|
4
24
|
|
|
5
25
|
The fixes from two audits, each fix put through adversarial review rounds until a reviewer
|
data/README.md
CHANGED
|
@@ -101,7 +101,7 @@ Every concern below links to its section in this README **and** to its standalon
|
|
|
101
101
|
| [🪝 WebhookVerifiable](#-webhookverifiable) | HMAC verification for inbound webhooks | [📖](https://vsn2015.github.io/concerns_on_rails/#/c/webhook-verifiable) |
|
|
102
102
|
| [🌅 Deprecatable](#-deprecatable) | RFC `Deprecation` / `Sunset` headers + 410 | [📖](https://vsn2015.github.io/concerns_on_rails/#/c/deprecatable) |
|
|
103
103
|
| [🗄️ Cacheable](#-cacheable) | HTTP conditional GET (ETag / 304) + `Cache-Control` | [📖](https://vsn2015.github.io/concerns_on_rails/#/c/cacheable) |
|
|
104
|
-
| [🛂 Permittable](#-permittable) | Typed, validated params contracts
|
|
104
|
+
| [🛂 Permittable](#-permittable) | Typed, validated params contracts, schema-drift guard, OpenAPI export | [📖](https://vsn2015.github.io/concerns_on_rails/#/c/permittable) |
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
@@ -148,7 +148,7 @@ across all 43 concerns — press <kbd>/</kbd> and type.
|
|
|
148
148
|
- **Lean dependencies** — only `acts_as_list` (Sortable) and `friendly_id` (Sluggable), and both load **lazily**: an app that never includes those concerns never loads them. Depends on `activerecord`/`actionpack`/`activesupport`, not the full `rails` meta-gem; controller concerns have zero extra deps
|
|
149
149
|
- **Schema-validated configuration** — every macro checks that the configured columns exist and raises `ArgumentError` early — listing *every* missing column at once, with one ready-to-paste `rails generate migration` command that adds them all
|
|
150
150
|
- **Composable** — concerns are independent; mix and match per model
|
|
151
|
-
- **Tested like an app, not a snippet** — **2,
|
|
151
|
+
- **Tested like an app, not a snippet** — **2,723 RSpec examples** run against a real database on every CI build
|
|
152
152
|
- **Documented twice** — everything in this README also lives as a per-concern page on the [docs site](https://vsn2015.github.io/concerns_on_rails), searchable and deep-linkable
|
|
153
153
|
|
|
154
154
|
---
|
|
@@ -158,7 +158,7 @@ across all 43 concerns — press <kbd>/</kbd> and type.
|
|
|
158
158
|
Add to your application's `Gemfile`:
|
|
159
159
|
|
|
160
160
|
```ruby
|
|
161
|
-
gem "concerns_on_rails", "~> 1.
|
|
161
|
+
gem "concerns_on_rails", "~> 1.32"
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
Or pull the latest from GitHub:
|
|
@@ -2583,7 +2583,12 @@ etag_with { current_user&.role } # => Cache-Control: private, max-ag
|
|
|
2583
2583
|
|
|
2584
2584
|
## 🛂 Permittable
|
|
2585
2585
|
|
|
2586
|
-
Declarative, **typed params contracts** — what strong parameters would be if it also knew types, bounds, defaults, and *why* a request was bad. `params.permit` (and Rails 8's `params.expect`) only answer "which keys may pass"; a Permittable contract additionally **casts** each field, **validates** it, applies **defaults**, and turns every failure into a machine-readable 422. Because the contract is class-level data (not code inside the action), it
|
|
2586
|
+
Declarative, **typed params contracts** — what strong parameters would be if it also knew types, bounds, defaults, and *why* a request was bad. `params.permit` (and Rails 8's `params.expect`) only answer "which keys may pass"; a Permittable contract additionally **casts** each field, **validates** it, applies **defaults**, optionally **reshapes** the output, and turns every failure into a machine-readable 422. Because the contract is class-level data (not code inside the action), it has readers beyond the validator: a boot-time **schema-drift guard**, an **OpenAPI 3.1** exporter, **RSpec matchers**, a route-set **coverage audit**, a **draft generator**, and a standalone `Contract` for any Hash.
|
|
2587
|
+
|
|
2588
|
+
> **Heads up — two things to know before relying on this section**
|
|
2589
|
+
>
|
|
2590
|
+
> 1. **Your lockfile, not this page, decides which features you have.** The pin is `>= 0.8, < 1`, so everything up to 0.8 (monitor mode, `:json`, `nullable:`, `message:`, field groups, matchers, generator, OpenAPI) is guaranteed, and an app that locked 0.8 keeps it until `bundle update permittable`. permittable 0.9+ requires ActiveSupport >= 6.1, so on Rails 6.0 Bundler stops at permittable **0.8**. That version lacks `format:` presets, `Permittable.check_column_types` and the enum rule, `Permittable.error_format = :problem`, `permittable:audit`, `params.expect` scanning in the generator, and the 0.10 canonical-numeric tightening (`"1_8"` and `" 99 "` still cast there). Everything else here applies. To make the newer surface a hard requirement, pin `gem "permittable", ">= 0.10"` in the app's Gemfile.
|
|
2591
|
+
> 2. **Two RFC 9457 switches exist — use one.** With [Respondable](#-respondable) included, Permittable's 422s render through `render_error`, so `respondable_by error_format: :problem_details` turns them into problem documents together with every other concern. The gem's own `Permittable.error_format = :problem` also renders RFC 9457 but deliberately **bypasses** `render_error`, so a controller with both set follows the gem's setting and ignores Respondable's envelope customisations.
|
|
2587
2592
|
|
|
2588
2593
|
```ruby
|
|
2589
2594
|
class UsersController < ApplicationController
|
|
@@ -2591,10 +2596,12 @@ class UsersController < ApplicationController
|
|
|
2591
2596
|
|
|
2592
2597
|
permit_params :create, :update, root: :user, model: User do
|
|
2593
2598
|
required :name, :string, length: 1..80, normalize: :squish
|
|
2594
|
-
required :email, :string, format:
|
|
2599
|
+
required :email, :string, format: :email, normalize: :email
|
|
2595
2600
|
optional :age, :integer, in: 18..120
|
|
2596
2601
|
optional :ssn, :string, sensitive: true # auto-redacted from logs
|
|
2597
2602
|
optional :plan, :string, in: %w[free pro], default: "free"
|
|
2603
|
+
optional :nickname, :string, nullable: true # explicit null clears the column
|
|
2604
|
+
optional :metadata, :json, max_depth: 3, length: 0..32
|
|
2598
2605
|
array :tag_names, of: :string, length: 0..10
|
|
2599
2606
|
optional :address do
|
|
2600
2607
|
required :city, :string
|
|
@@ -2615,19 +2622,23 @@ end
|
|
|
2615
2622
|
# { param: "user.age", code: "inclusion" }] } }
|
|
2616
2623
|
```
|
|
2617
2624
|
|
|
2618
|
-
**The schema-drift guard** is the headline: with `model:` (a class, or `true` to infer from the controller name), every non-`virtual:` scalar field is checked against the model's columns **at controller class load
|
|
2625
|
+
**The schema-drift guard** is the headline: with `model:` (a class, or `true` to infer from the controller name), every non-`virtual:` scalar field is checked against the model's columns **at controller class load**. A column dropped by a migration fails the deploy (production eager-loads controllers) with a teaching error — the missing-column message plus a ready-to-paste migration command plus the `virtual: true` escape hatch. Nested/array fields are implicitly virtual; the check skips gracefully when the schema is unreachable (`db:create`, `assets:precompile`). Opt into the type half with `Permittable.check_column_types = true` (families: text / numeric incl. boolean / temporal; a Rails `enum` must be declared `:string, in: Model.statuses.keys`). In CI, one `Rails.application.eager_load!` spec exercises every contract in the app.
|
|
2626
|
+
|
|
2627
|
+
**Options** (`permit_params *actions, …`, repeatable; no actions = catch-all; **last matching rule wins**, subclasses inherit copy-on-write): `root:` (key to unwrap, `require(:user)`-style; missing root → **400**; default `false` = top-level params), `model:` (schema-drift guard), `unknown:` (`:ignore` default / `:log` / `:error` — undeclared keys at every nesting level; Rails' router/form/CSRF keys, the route's path params and ParamsWrapper's copy are exempt at the top level), `enforce:` (`false` = validate lazily on first `permitted_params` call, memoized per action; `true` = validate in a `before_action`), `mode:` (`:monitor` reports instead of rejecting — see below), `desc:` (OpenAPI operation description).
|
|
2628
|
+
|
|
2629
|
+
**Field DSL**: `required`/`optional` `name, type` (`:string` default, `:integer`, `:float`, `:decimal`, `:boolean`, `:date`, `:datetime`, and `:json` — a free-form hash for `jsonb` columns, bounded by `length:` keys and `max_depth:`) with `in:` (Range, or a list snapshotted and cast at load — `in: Post.statuses` works), `format:` (Regexp or a preset: `:email`/`:uuid`/`:url`/`:slug`/`:hostname`), `length:`/`normalize:` (`:squish`/`:strip`/`:downcase`/`:upcase`/`:email` or a Proc; string fields only), `default:` (validated against the field's own contract at load time), `nullable:` (an explicit `null`/`""` yields `nil` instead of counting as absent — the one way to clear a column; wins over `default:`), `message:` (per-field copy, String or code → String; app-wide copy via I18n `permittable.errors.<code>`), `validate:` (Proc — falsy fails as `"invalid"`, a returned Symbol becomes the violation code), `transform:` (see below), `virtual:`, `sensitive:` (registers with the gem-wide filter_parameters registry, same pipe as Encryptable; cascades into nested blocks, `sensitive: false` opts a sub-field out), `desc:`/`example:` (docs). Nested hashes via a block; `array :name, of: :type` (or a block for arrays of hashes) with `length:` as element count — short-circuiting, and there is no default cap — and per-index violation paths (`items[1]`). Reusable **field groups**: `Permittable.fields { … }` + `use Group` (`optional: true` turns a create contract into a PATCH contract; `only:`/`except:`).
|
|
2619
2630
|
|
|
2620
|
-
**
|
|
2631
|
+
**Output reshaping** — the safe replacement for params-mutating before_actions, operating on the validated copy only (the request's `params` is never touched). `transform:` applies a callable **after** cast + validation to reshape that field's output — `transform: ->(v) { v.split(",") }` turns a validated delimited String into an Array (absent fields stay absent, a `default:` on a transformed field is handed out as authored, a partially-invalid array is never transformed). `finalize do |p| … end` (once per contract, top level only) runs after every field validated cleanly, receives the result hash, and returns the final shape — zip parallel fields into value objects, drop scaffolding keys. It executes on a bare runner, **not** the controller, so contracts stay pure; its one extra verb, `violate!(param, code, message: nil)`, records a violation and halts the block immediately — doubling as the cross-field validation seam ("ends_at after starts_at").
|
|
2621
2632
|
|
|
2622
|
-
**
|
|
2633
|
+
**Coercion is strict** — deliberately not `ActiveModel::Type` ("abc".to_i == 0 and `Boolean.cast("abc") == true` silently corrupt untrusted input): a value the type can't faithfully represent is an `invalid_type` violation, numeric strings must be canonical (`"1_8"` and `" 99 "` are refused), dates must be complete (never filled in from today), booleans accept only `true/false/"true"/"false"/"1"/"0"/1/0`, and array/hash type-confusion (`?age[]=1`) is rejected instead of 500ing. `nil` and `""` are both **absent**: absent optional fields are *omitted* from the result (partial updates never nil-out columns), absent required fields violate, `default:` fills absence.
|
|
2623
2634
|
|
|
2624
|
-
**
|
|
2635
|
+
**Adopting on a live API**: `bin/rails permittable:generate` drafts a contract per controller from the model's columns and the `params.permit`/`params.expect` calls already in the source (in monitor mode, every guess a visible TODO); `mode: :monitor` / `Permittable.mode = :monitor` runs the pipeline but **reports instead of rejecting** (notification with `mode: :monitor`, raw params passed through, `permittable_violations` for the action to branch on); `bin/rails permittable:audit` lists every routed action with its effective mode and flags write actions that accept a body with no contract (`audit[strict]` exits 1 — a CI gate); then flip to enforce controller by controller.
|
|
2625
2636
|
|
|
2626
|
-
**
|
|
2637
|
+
**Beyond the controller**: `require "permittable/rspec"` gives `permit_param(:age).for_action(:create).as(:integer).within(18..120)` (the declaration) and `accept_params(payload).returning(hash)` / `reject_params(payload).with_violation("user.age", :inclusion)` (the behaviour) without dispatching a request; `Permittable::Contract.define(root: :user) { … }` runs the same DSL on any Hash (`call` → a `Result`, `call!` raises); `bin/rails permittable:openapi[path]` exports an **OpenAPI 3.1** document from the same frozen data the server enforces — anything JSON Schema can't express stays visible as an `x-permittable-*` extension instead of being mistranslated, and the output is deterministic so it can be committed.
|
|
2627
2638
|
|
|
2628
|
-
**Failure surface**: violations raise `Permittable::InvalidParameters` (carries `details` + `status`), auto-`rescue_from`d into the shared error envelope
|
|
2639
|
+
**Failure surface**: violations raise `Permittable::InvalidParameters` (carries `details` + `status`; a missing/malformed `root:` is **400**, field violations **422**), auto-`rescue_from`d into the shared error envelope — `Respondable#render_error` when included (so `respondable_by error_format: :problem_details` makes them RFC 9457 along with the rest of the app), the identical inline JSON otherwise; the gem's own `Permittable.error_format = :problem` renders RFC 9457 too but bypasses `render_error`, so pick one switch. Instrumented as `invalid_parameters.permittable` (`controller`, `action`, `mode`, `details`), exactly once per action per request. Introspect contracts via `permittable_contracts` / `permit_rule_for(action)`. Naming note: legacy InheritedResources controllers also define `permitted_params` — don't mix the two on one controller.
|
|
2629
2640
|
|
|
2630
|
-
> Permittable ships as the standalone [`permittable` gem](https://github.com/VSN2015/permittable) (a runtime dependency
|
|
2641
|
+
> Permittable ships as the standalone [`permittable` gem](https://github.com/VSN2015/permittable) (a runtime dependency, `>= 0.8, < 1`; this section describes 0.10); `ConcernsOnRails::Controllers::Permittable` is an alias for `::Permittable`, with `sensitive:` registrations pooled into this gem's shared filter_parameters registry. Full reference: [the gem README](https://github.com/VSN2015/permittable#readme) · [docs page](https://vsn2015.github.io/concerns_on_rails/#/c/permittable).
|
|
2631
2642
|
|
|
2632
2643
|
---
|
|
2633
2644
|
|
|
@@ -2699,7 +2710,7 @@ Point your agent at `llms.txt` for an overview, or paste a single concern's `.md
|
|
|
2699
2710
|
bundle install # install dev dependencies
|
|
2700
2711
|
bundle exec rspec # run the test suite (2,723 examples)
|
|
2701
2712
|
gem build concerns_on_rails.gemspec # build the gem
|
|
2702
|
-
gem install ./concerns_on_rails-1.
|
|
2713
|
+
gem install ./concerns_on_rails-1.32.0.gem # install locally
|
|
2703
2714
|
|
|
2704
2715
|
# Preview the docs site locally (GitHub Pages serves docs/ as-is):
|
|
2705
2716
|
cd docs && python3 -m http.server 8000 # → http://localhost:8000
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: concerns_on_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.32.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Nguyen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|
|
@@ -108,16 +108,22 @@ dependencies:
|
|
|
108
108
|
name: permittable
|
|
109
109
|
requirement: !ruby/object:Gem::Requirement
|
|
110
110
|
requirements:
|
|
111
|
-
- - "
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0.8'
|
|
114
|
+
- - "<"
|
|
112
115
|
- !ruby/object:Gem::Version
|
|
113
|
-
version: '
|
|
116
|
+
version: '1'
|
|
114
117
|
type: :runtime
|
|
115
118
|
prerelease: false
|
|
116
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
117
120
|
requirements:
|
|
118
|
-
- - "
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.8'
|
|
124
|
+
- - "<"
|
|
119
125
|
- !ruby/object:Gem::Version
|
|
120
|
-
version: '
|
|
126
|
+
version: '1'
|
|
121
127
|
description: A collection of plug-and-play ActiveSupport concerns for Rails models
|
|
122
128
|
and Rails controllers
|
|
123
129
|
email:
|