concerns_on_rails 1.24.0 → 1.25.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 +13 -0
- data/README.md +53 -2
- data/lib/concerns_on_rails/controllers/permittable.rb +25 -0
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 423bbd556d23280595c6a456d16a18255ada3898f88169831005526795f936fc
|
|
4
|
+
data.tar.gz: 96a50269d776642a46534f50864730ea54fc2f82d458ab3e0ef11424f742fc29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c0b871a10e781fdd6d5fb9464eb8e245783adb2b3f20bb7067758934655ae8c82d0eb082d121e198e7a7a1040db60b467600bb8b8307a9484d034c1ebceecab
|
|
7
|
+
data.tar.gz: e550786d60b36e5d44239d59a283776f4debba254a43eacb9b2141112a94a0af8d2b53feeebc4ee3e6528f0973117abfa89a1e04ae4033e7f7058e2e788e5d5a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.25.0 (2026-08-16)
|
|
4
|
+
|
|
5
|
+
One new controller concern — Permittable, typed/validated params contracts with a boot-time schema-drift guard — developed here and shipped as the standalone [`permittable` gem](https://rubygems.org/gems/permittable) (new runtime dependency; `ConcernsOnRails::Controllers::Permittable` is an alias). 1160 examples, 0 failures.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Controllers::Permittable**: declarative, typed params contracts ("strong parameters with types, validation, and drift detection"). `permit_params *actions, root:, model:, unknown:, enforce:` declares a per-action contract (repeatable; no actions = catch-all; last matching rule wins; inherited copy-on-write) whose block DSL (`required`/`optional`/`array`, nested blocks) types every field (`:string :integer :float :decimal :boolean :date :datetime`) and validates it (`in:`, `format:`, `length:`, `normalize:` presets/Proc, `default:` — itself contract-checked at class load — and custom `validate:` with symbol violation codes). `permitted_params` returns the cast/validated/defaulted hash (lazy; `enforce: true` moves the check to a before_action); violations raise `InvalidParameters`, auto-rescued into the shared ErrorEnvelope as 422 (400 for a missing `root:` key) with machine-readable `details: [{ param: "user.age", code: "inclusion" }]`, and instrument `invalid_parameters.concerns_on_rails`. Coercion is deliberately STRICT (no ActiveModel::Type leniency — `"abc"` is never `0`, `?age[]=1` type confusion is a violation, not a 500); `nil`/`""` are ABSENT (absent optionals are omitted, so partial updates never nil-out columns). The headline: `model:` enables the **schema-drift guard** — every non-`virtual:` scalar field is checked against the model's columns via Support::ColumnGuard at controller class load, so a column dropped by a migration fails the deploy (with the migration-hint error + `virtual: true` escape hatch), not the request. `unknown: :error/:log` polices undeclared keys at every nesting level (Rails' routing keys exempt at top level); `sensitive: true` registers fields with the gem-wide filter_parameters registry (the Encryptable pipe). Output reshaping replaces params-mutating before_actions safely: per-field `transform:` (a callable applied AFTER cast + validation — `->(v) { v.split(",") }`; defaults and absent fields untouched, partially-invalid arrays never transformed) and a once-per-contract `finalize do |p| ... end` that runs only when every field validated, executes on a bare runner (controller state unreachable — contracts stay pure), must return the final Hash, and gets `violate!(param, code)` — record one violation and halt — as the cross-field validation seam. The request's `params` is never mutated.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Permittable extracted into the standalone [`permittable` gem](https://github.com/VSN2015/permittable)** (new runtime dependency, `~> 0.1`, published on [rubygems.org](https://rubygems.org/gems/permittable) — this version resolves against permittable 0.1.2). `ConcernsOnRails::Controllers::Permittable` is now an alias for `::Permittable` — the include path, macro, helpers, and error classes are unchanged, and the concerns_on_rails spec suite runs against the alias as a compatibility suite. The gem's `sensitive:` registrations are routed into ConcernsOnRails' shared filter_parameters registry, so Permittable params and Encryptable attributes share one filter. The instrumentation event is now the gem's `invalid_parameters.permittable` (was `invalid_parameters.concerns_on_rails`; Permittable never shipped in a release, so nothing published breaks).
|
|
12
|
+
|
|
13
|
+
### Internal
|
|
14
|
+
- `spec/support/integration_harness.rb`: `dispatch` accepts `params:` (form-encoded request body) so specs can exercise real `ActionController::Parameters` bodies.
|
|
15
|
+
|
|
3
16
|
## 1.24.0 (2026-08-15)
|
|
4
17
|
|
|
5
18
|
A developer-experience wave from the 2026-08-15 usability review: leaner install, lazy loading, teaching errors, and one-initializer store configuration. No behavior changes for configured concerns. 1084 examples, 0 failures.
|
data/README.md
CHANGED
|
@@ -89,6 +89,7 @@ Article.published.without_deleted.find("hello-world")
|
|
|
89
89
|
| [🪝 WebhookVerifiable](#-webhookverifiable) | HMAC verification for inbound webhooks |
|
|
90
90
|
| [🌅 Deprecatable](#-deprecatable) | RFC `Deprecation` / `Sunset` headers + 410 |
|
|
91
91
|
| [🗄️ Cacheable](#-cacheable) | HTTP conditional GET (ETag / 304) + `Cache-Control` |
|
|
92
|
+
| [🛂 Permittable](#-permittable) | Typed, validated params contracts + schema-drift guard |
|
|
92
93
|
|
|
93
94
|
---
|
|
94
95
|
|
|
@@ -107,7 +108,7 @@ Article.published.without_deleted.find("hello-world")
|
|
|
107
108
|
Add to your application's `Gemfile`:
|
|
108
109
|
|
|
109
110
|
```ruby
|
|
110
|
-
gem "concerns_on_rails", "~> 1.
|
|
111
|
+
gem "concerns_on_rails", "~> 1.25"
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
Or pull the latest from GitHub:
|
|
@@ -1785,6 +1786,56 @@ end
|
|
|
1785
1786
|
|
|
1786
1787
|
---
|
|
1787
1788
|
|
|
1789
|
+
## 🛂 Permittable
|
|
1790
|
+
|
|
1791
|
+
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 is introspectable — and can be checked against a model's schema at boot.
|
|
1792
|
+
|
|
1793
|
+
```ruby
|
|
1794
|
+
class UsersController < ApplicationController
|
|
1795
|
+
include ConcernsOnRails::Controllers::Permittable
|
|
1796
|
+
|
|
1797
|
+
permit_params :create, :update, root: :user, model: User do
|
|
1798
|
+
required :name, :string, length: 1..80, normalize: :squish
|
|
1799
|
+
required :email, :string, format: URI::MailTo::EMAIL_REGEXP, normalize: :email
|
|
1800
|
+
optional :age, :integer, in: 18..120
|
|
1801
|
+
optional :ssn, :string, sensitive: true # auto-redacted from logs
|
|
1802
|
+
optional :plan, :string, in: %w[free pro], default: "free"
|
|
1803
|
+
array :tag_names, of: :string, length: 0..10
|
|
1804
|
+
optional :address do
|
|
1805
|
+
required :city, :string
|
|
1806
|
+
optional :zip, :string, format: /\A\d{5}\z/
|
|
1807
|
+
end
|
|
1808
|
+
end
|
|
1809
|
+
|
|
1810
|
+
def create
|
|
1811
|
+
user = User.create!(permitted_params) # cast, validated, defaulted
|
|
1812
|
+
end
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
# POST { user: { name: "Jo", email: "JO@x.com ", age: "30" } }
|
|
1816
|
+
# ⇒ permitted_params == { "name" => "Jo", "email" => "jo@x.com", "age" => 30, "plan" => "free" }
|
|
1817
|
+
# POST { user: { age: "12" } }
|
|
1818
|
+
# ⇒ 422 { error: { code: "invalid_parameters",
|
|
1819
|
+
# details: [{ param: "user.name", code: "missing" },
|
|
1820
|
+
# { param: "user.age", code: "inclusion" }] } }
|
|
1821
|
+
```
|
|
1822
|
+
|
|
1823
|
+
**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** through the same `ColumnGuard` all model concerns use. 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`). In CI, one `Rails.application.eager_load!` spec exercises every contract in the app.
|
|
1824
|
+
|
|
1825
|
+
**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' `controller`/`action`/`format` keys are exempt at the top level), `enforce:` (`false` = validate lazily on first `permitted_params` call; `true` = validate in a `before_action` so the action body never runs on bad input).
|
|
1826
|
+
|
|
1827
|
+
**Field DSL**: `required`/`optional` `name, type` (`:string` default, `:integer`, `:float`, `:decimal`, `:boolean`, `:date`, `:datetime`) with `in:` (Range/Array), `format:`/`length:`/`normalize:` (`:squish`/`:strip`/`:downcase`/`:upcase`/`:email` or a Proc; string fields only), `default:` (validated against the field's own contract at load time), `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). Nested hashes via a block; `array :name, of: :type` (or a block for arrays of hashes) with `length:` as element count and per-index violation paths (`items[1]`).
|
|
1828
|
+
|
|
1829
|
+
**Output reshaping** — the safe replacement for params-mutating before_actions, operating on the validated copy only (the request's `params` is never touched). `transform:` (scalar/array fields) 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 and `default:` values are untouched; 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)`, records a violation and halts the block immediately — doubling as the cross-field validation seam ("ends_at after starts_at").
|
|
1830
|
+
|
|
1831
|
+
**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, 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.
|
|
1832
|
+
|
|
1833
|
+
**Failure surface**: violations raise `Permittable::InvalidParameters` (carries `details` + `status`), auto-`rescue_from`d into the shared error envelope (`Respondable#render_error` when included, the identical inline JSON otherwise) and instrumented as `invalid_parameters.permittable` for dashboards. 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.
|
|
1834
|
+
|
|
1835
|
+
> Permittable ships as the standalone [`permittable` gem](https://github.com/VSN2015/permittable) (a runtime dependency of concerns_on_rails); `ConcernsOnRails::Controllers::Permittable` is an alias for `::Permittable`, with `sensitive:` registrations pooled into this gem's shared filter_parameters registry.
|
|
1836
|
+
|
|
1837
|
+
---
|
|
1838
|
+
|
|
1788
1839
|
## 🗂️ Module paths & namespacing
|
|
1789
1840
|
|
|
1790
1841
|
Every concern is available under two paths:
|
|
@@ -1834,7 +1885,7 @@ Both forms reference the same module, so you can freely mix them.
|
|
|
1834
1885
|
bundle install # install dev dependencies
|
|
1835
1886
|
bundle exec rspec # run the test suite
|
|
1836
1887
|
gem build concerns_on_rails.gemspec # build the gem
|
|
1837
|
-
gem install ./concerns_on_rails-1.
|
|
1888
|
+
gem install ./concerns_on_rails-1.25.0.gem # install locally
|
|
1838
1889
|
```
|
|
1839
1890
|
|
|
1840
1891
|
The test suite uses an in-memory SQLite database and a lightweight `FakeController` harness for controller-concern specs — no Rails routes or boot required.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "permittable"
|
|
2
|
+
|
|
3
|
+
module ConcernsOnRails
|
|
4
|
+
module Controllers
|
|
5
|
+
# Permittable lives in the standalone `permittable` gem (a runtime
|
|
6
|
+
# dependency — it was developed here and extracted). This alias keeps the
|
|
7
|
+
# concerns_on_rails include path, and everything documented for it,
|
|
8
|
+
# working unchanged:
|
|
9
|
+
#
|
|
10
|
+
# include ConcernsOnRails::Controllers::Permittable
|
|
11
|
+
#
|
|
12
|
+
# is the same module as `include Permittable`. Full docs: the permittable
|
|
13
|
+
# gem README / docs/concerns/permittable.md. Note the instrumentation
|
|
14
|
+
# event is the gem's: "invalid_parameters.permittable".
|
|
15
|
+
Permittable = ::Permittable
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# One registry for the whole process: route the gem's `sensitive:` field
|
|
20
|
+
# registrations into the registry ConcernsOnRails::Railtie already appends to
|
|
21
|
+
# config.filter_parameters, so Permittable params and Encryptable attributes
|
|
22
|
+
# share one filter. (Contracts declared through ::Permittable before this
|
|
23
|
+
# bridge loads registered on the gem's default registry — that one stays
|
|
24
|
+
# appended by Permittable::Railtie, so nothing is un-filtered.)
|
|
25
|
+
Permittable.filter_parameter_registry = ConcernsOnRails.filter_parameter_registry
|
data/lib/concerns_on_rails.rb
CHANGED
|
@@ -62,6 +62,7 @@ module ConcernsOnRails
|
|
|
62
62
|
autoload :CursorPaginatable, "concerns_on_rails/controllers/cursor_paginatable"
|
|
63
63
|
autoload :Deprecatable, "concerns_on_rails/controllers/deprecatable"
|
|
64
64
|
autoload :Cacheable, "concerns_on_rails/controllers/cacheable"
|
|
65
|
+
autoload :Permittable, "concerns_on_rails/controllers/permittable"
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
module Support
|
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.25.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-08-
|
|
11
|
+
date: 2026-08-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|
|
@@ -104,6 +104,20 @@ dependencies:
|
|
|
104
104
|
- - "~>"
|
|
105
105
|
- !ruby/object:Gem::Version
|
|
106
106
|
version: '5.4'
|
|
107
|
+
- !ruby/object:Gem::Dependency
|
|
108
|
+
name: permittable
|
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - "~>"
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0.1'
|
|
114
|
+
type: :runtime
|
|
115
|
+
prerelease: false
|
|
116
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - "~>"
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0.1'
|
|
107
121
|
description: A collection of plug-and-play ActiveSupport concerns for Rails models
|
|
108
122
|
and Rails controllers
|
|
109
123
|
email:
|
|
@@ -128,6 +142,7 @@ files:
|
|
|
128
142
|
- lib/concerns_on_rails/controllers/includable.rb
|
|
129
143
|
- lib/concerns_on_rails/controllers/localizable.rb
|
|
130
144
|
- lib/concerns_on_rails/controllers/paginatable.rb
|
|
145
|
+
- lib/concerns_on_rails/controllers/permittable.rb
|
|
131
146
|
- lib/concerns_on_rails/controllers/respondable.rb
|
|
132
147
|
- lib/concerns_on_rails/controllers/secure_headable.rb
|
|
133
148
|
- lib/concerns_on_rails/controllers/sortable.rb
|