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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5361fdba6417a0a46213e0e8e41f8ced13d00f76cb17f102404d1d794a98a39
4
- data.tar.gz: 0fab62907022084c9b54d1cca52f8b4eeefbecced0a4bc905e4b3a8a55f8f582
3
+ metadata.gz: 423bbd556d23280595c6a456d16a18255ada3898f88169831005526795f936fc
4
+ data.tar.gz: 96a50269d776642a46534f50864730ea54fc2f82d458ab3e0ef11424f742fc29
5
5
  SHA512:
6
- metadata.gz: 5654215000d983a63260507d6741090add3b7861e200e81ebd180ad7d7de61891c0646c49b666d4b01b179b51ae88d1da73386ffb35bc1cbb24328d8f3238c99
7
- data.tar.gz: 436b80fa129a4a3922a0a6b7ba4945f90205f1071a4d190875fa26e87458bb39ba483b0f682bff0c64cd3fb09fda79ec15cec776f22b3d085af03d14fe5a2275
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.24"
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.24.0.gem # install locally
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
@@ -1,3 +1,3 @@
1
1
  module ConcernsOnRails
2
- VERSION = "1.24.0".freeze
2
+ VERSION = "1.25.0".freeze
3
3
  end
@@ -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.24.0
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-15 00:00:00.000000000 Z
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