concerns_on_rails 1.23.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +78 -6
  4. data/lib/concerns_on_rails/configuration.rb +27 -0
  5. data/lib/concerns_on_rails/controllers/idempotentable.rb +7 -4
  6. data/lib/concerns_on_rails/controllers/permittable.rb +25 -0
  7. data/lib/concerns_on_rails/controllers/throttleable.rb +7 -3
  8. data/lib/concerns_on_rails/legacy_aliases.rb +25 -29
  9. data/lib/concerns_on_rails/models/activatable.rb +1 -1
  10. data/lib/concerns_on_rails/models/addressable.rb +1 -1
  11. data/lib/concerns_on_rails/models/auditable.rb +1 -1
  12. data/lib/concerns_on_rails/models/counter_cacheable.rb +1 -1
  13. data/lib/concerns_on_rails/models/encryptable.rb +2 -2
  14. data/lib/concerns_on_rails/models/expirable.rb +1 -1
  15. data/lib/concerns_on_rails/models/hashable.rb +2 -1
  16. data/lib/concerns_on_rails/models/lockable.rb +2 -1
  17. data/lib/concerns_on_rails/models/monetizable.rb +1 -1
  18. data/lib/concerns_on_rails/models/publishable.rb +1 -1
  19. data/lib/concerns_on_rails/models/schedulable.rb +1 -1
  20. data/lib/concerns_on_rails/models/sequenceable.rb +3 -3
  21. data/lib/concerns_on_rails/models/sluggable.rb +12 -2
  22. data/lib/concerns_on_rails/models/soft_deletable.rb +1 -1
  23. data/lib/concerns_on_rails/models/sortable.rb +11 -2
  24. data/lib/concerns_on_rails/models/stateable.rb +1 -1
  25. data/lib/concerns_on_rails/models/storable.rb +1 -1
  26. data/lib/concerns_on_rails/models/taggable.rb +1 -1
  27. data/lib/concerns_on_rails/models/tokenizable.rb +1 -1
  28. data/lib/concerns_on_rails/support/column_guard.rb +23 -4
  29. data/lib/concerns_on_rails/version.rb +1 -1
  30. data/lib/concerns_on_rails.rb +96 -67
  31. metadata +67 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba3e5dc1bc6af51ec2bd4e29946973c5fc68736d4c1994fdbd472874f965c4bc
4
- data.tar.gz: 16b2372635abe33484efabcae5407d1debe5a0627df9df0496a04e930c46ec65
3
+ metadata.gz: 423bbd556d23280595c6a456d16a18255ada3898f88169831005526795f936fc
4
+ data.tar.gz: 96a50269d776642a46534f50864730ea54fc2f82d458ab3e0ef11424f742fc29
5
5
  SHA512:
6
- metadata.gz: 8a159b0026bca1ebc72dbc11c45e875caaa19ad2ec0615ee658abd5b3e8ab53e0bb309d816a5371ddba6bad5c609b82a76228e66d678d3346d624021634eddf5
7
- data.tar.gz: da89fb098f9e17fd9ae49662efdb027efb64958007bf790ee895c0faf39a1c9f2cb76d2e95a4b0b3b075bcb404fe926c36d970f78ca152bdd287be81e7a3a991
6
+ metadata.gz: 6c0b871a10e781fdd6d5fb9464eb8e245783adb2b3f20bb7067758934655ae8c82d0eb082d121e198e7a7a1040db60b467600bb8b8307a9484d034c1ebceecab
7
+ data.tar.gz: e550786d60b36e5d44239d59a283776f4debba254a43eacb9b2141112a94a0af8d2b53feeebc4ee3e6528f0973117abfa89a1e04ae4033e7f7058e2e788e5d5a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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
+
16
+ ## 1.24.0 (2026-08-15)
17
+
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.
19
+
20
+ ### Changed
21
+ - **Dependencies**: the gem now depends on `actionpack` / `activerecord` / `activesupport` instead of the full `rails` meta-gem (API-only hosts stop pulling Action Cable / Mailbox / Text), and the `acts_as_list` constraint is `>= 0.7.5, < 2` — the old `~> 0.7.5` pin was an unresolvable Bundler conflict for any app already on acts_as_list 1.x. Suite verified against acts_as_list 1.2.6.
22
+ - **Lazy loading**: `require "concerns_on_rails"` no longer eagerly loads all 40 concern/support files — everything is autoloaded on first constant reference, and `friendly_id` / `acts_as_list` load only when Sluggable / Sortable is actually used (a missing gem raises `ConcernsOnRails::MissingDependency`, a `LoadError` subclass whose message names the Gemfile line to add). The pre-1.6 top-level aliases (`ConcernsOnRails::Sluggable`, …) keep working, now resolved lazily via `const_missing`.
23
+ - **Support::ColumnGuard** (all model concerns): the missing-column `ArgumentError` now teaches the fix — it appends a ready-to-paste `bin/rails generate migration ...` command carrying the concern's expected column type (e.g. `AddDeletedAtToArticles deleted_at:datetime`; token/slug columns suggest `string:uniq`, Encryptable blind-index columns `string:index`).
24
+
25
+ ### Added
26
+ - **`ConcernsOnRails.setup`**: gem-wide configuration entry point (`lib/concerns_on_rails/configuration.rb`). First setting: `config.cache_store` — a store or a Proc (e.g. `-> { Rails.cache }`) used as the fallback store by `Controllers::Throttleable` and `Controllers::Idempotentable`, so one initializer line replaces per-controller `self.throttleable_store = ...` / `self.idempotency_store = ...` wiring. A class-level store still wins; with neither configured the concerns raise exactly as before.
27
+
3
28
  ## 1.23.0 (2026-07-26)
4
29
 
5
30
  Two new model concerns — the roadmap picks after 1.22's fixes round: the right-to-erasure capability that completes the sensitive-data suite, and the concern-aware deep copy that makes "duplicate this invoice" safe next to unique slugs, tokens, and sequence numbers. 1075 examples, 0 failures.
data/README.md CHANGED
@@ -12,7 +12,7 @@ One `include`, one declarative macro — done.
12
12
  [![Rails](https://img.shields.io/badge/rails-5.0--8.x-CC0000?logo=rubyonrails&logoColor=white)](https://rubyonrails.org)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-3fb950.svg)](#-license)
14
14
 
15
- 🧩 **23 model concerns** &nbsp;·&nbsp; 🎮 **16 controller concerns** &nbsp;·&nbsp; 🪶 **lean deps** &nbsp;·&nbsp; ✅ **schema-validated**
15
+ 🧩 **26 model concerns** &nbsp;·&nbsp; 🎮 **16 controller concerns** &nbsp;·&nbsp; 🪶 **lean deps** &nbsp;·&nbsp; ✅ **schema-validated**
16
16
 
17
17
  </div>
18
18
 
@@ -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
 
@@ -96,7 +97,7 @@ Article.published.without_deleted.find("hello-world")
96
97
 
97
98
  - **Twenty-six model concerns + sixteen controller concerns**, all production-ready
98
99
  - **One include, one macro** — no boilerplate, no glue code
99
- - **Lean dependencies** — only `acts_as_list` (Sortable) and `friendly_id` (Sluggable); controller concerns have zero extra deps
100
+ - **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
100
101
  - **Schema-validated configuration** — every macro checks that the configured column exists and raises `ArgumentError` early
101
102
  - **Composable** — concerns are independent; mix and match per model
102
103
 
@@ -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.23"
111
+ gem "concerns_on_rails", "~> 1.25"
111
112
  ```
112
113
 
113
114
  Or pull the latest from GitHub:
@@ -122,6 +123,27 @@ Then run:
122
123
  bundle install
123
124
  ```
124
125
 
126
+ ### Optional: one-initializer configuration
127
+
128
+ Everything works with zero configuration. The store-backed controller concerns
129
+ (`Throttleable`, `Idempotentable`) need a cache store — set it once, gem-wide,
130
+ instead of per controller class:
131
+
132
+ ```ruby
133
+ # config/initializers/concerns_on_rails.rb
134
+ ConcernsOnRails.setup do |config|
135
+ config.cache_store = -> { Rails.cache } # fallback for Throttleable / Idempotentable
136
+ end
137
+
138
+ # Encryptable's key lives in its own config (see the Encryptable section):
139
+ ConcernsOnRails.configure_encryption do |c|
140
+ c.key = -> { Rails.application.credentials.dig(:encryption, :key) }
141
+ end
142
+ ```
143
+
144
+ A per-class `self.throttleable_store = ...` / `self.idempotency_store = ...`
145
+ still wins over the gem-wide fallback.
146
+
125
147
  ---
126
148
 
127
149
  ## 🧪 Compatibility
@@ -1599,7 +1621,7 @@ Fixed-window counter: the key embeds a floored time bucket (`epoch / period`) so
1599
1621
 
1600
1622
  **Notes**
1601
1623
  - The store MUST support **atomic increment-with-expiry** (`Rails.cache` with `#increment`, or Redis) — a non-atomic store under-counts under concurrency.
1602
- - There is **no in-process default store** on purpose: the first throttled request raises `ArgumentError` until you set `throttleable_store`, so you never silently rate-limit per-process.
1624
+ - There is **no in-process default store** on purpose: the first throttled request raises `ArgumentError` until you set `throttleable_store` (or the gem-wide fallback `ConcernsOnRails.setup { |c| c.cache_store = -> { Rails.cache } }`), so you never silently rate-limit per-process.
1603
1625
  - When `Respondable` is included, the 429 body delegates to `render_error` (`code: "rate_limited"`).
1604
1626
  - Backports the essentials of Rails 7.2's `rate_limit` (with standardized headers) to Rails 5.0+. For richer rules (fail2ban, allow/deny lists, exponential backoff) reach for [`rack-attack`](https://github.com/rack/rack-attack).
1605
1627
 
@@ -1648,7 +1670,7 @@ Per-key lifecycle: claim atomically (`write unless_exist`, TTL `lock_ttl:`) →
1648
1670
 
1649
1671
  **Notes**
1650
1672
  - Cache keys are scoped per `controller#action` and the client key is SHA256-hashed, so the same key on different endpoints never collides.
1651
- - There is **no in-process default store** on purpose: the first keyed request raises `ArgumentError` until you set `idempotency_store`.
1673
+ - There is **no in-process default store** on purpose: the first keyed request raises `ArgumentError` until you set `idempotency_store` (or the gem-wide fallback `ConcernsOnRails.setup { |c| c.cache_store = -> { Rails.cache } }`).
1652
1674
  - When `Respondable` is included, the 400/409/422 bodies delegate to `render_error`.
1653
1675
  - Declare halting filters (authentication, `Throttleable`) **before** including this concern — a 401/403 rendered by an inner filter would be cached and replayed for the full TTL. Responses rendered by `rescue_from` handlers are never cached.
1654
1676
  - Keys must be ≤255 chars with no control characters (the raw key is echoed in `X-Idempotency-Key`); set `lock_ttl:` above the slowest declared action's worst case.
@@ -1764,6 +1786,56 @@ end
1764
1786
 
1765
1787
  ---
1766
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
+
1767
1839
  ## 🗂️ Module paths & namespacing
1768
1840
 
1769
1841
  Every concern is available under two paths:
@@ -1813,7 +1885,7 @@ Both forms reference the same module, so you can freely mix them.
1813
1885
  bundle install # install dev dependencies
1814
1886
  bundle exec rspec # run the test suite
1815
1887
  gem build concerns_on_rails.gemspec # build the gem
1816
- gem install ./concerns_on_rails-1.23.0.gem # install locally
1888
+ gem install ./concerns_on_rails-1.25.0.gem # install locally
1817
1889
  ```
1818
1890
 
1819
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,27 @@
1
+ module ConcernsOnRails
2
+ # Gem-wide configuration, set once from an initializer:
3
+ #
4
+ # ConcernsOnRails.setup do |config|
5
+ # config.cache_store = -> { Rails.cache }
6
+ # end
7
+ #
8
+ # `cache_store` is the fallback store consulted by Controllers::Throttleable
9
+ # and Controllers::Idempotentable when the controller class hasn't set its
10
+ # own (`self.throttleable_store = ...` / `self.idempotency_store = ...`
11
+ # still win). A store object or a zero-arg callable — prefer a Proc so
12
+ # `Rails.cache` is read lazily, after the framework has booted. The store
13
+ # contract is unchanged: atomic #increment for throttling, #read /
14
+ # #write(expires_in:, unless_exist:) / #delete for idempotency. There is
15
+ # still no in-process default on purpose — a non-atomic store silently
16
+ # under-counts, so the host must opt in explicitly (just once, here).
17
+ class Configuration
18
+ attr_accessor :cache_store
19
+
20
+ # The fallback store with any callable resolved (per lookup, so a Proc
21
+ # reading Rails.cache follows a swapped-out cache in tests). nil when the
22
+ # host never configured one.
23
+ def resolved_cache_store
24
+ cache_store.respond_to?(:call) ? cache_store.call : cache_store
25
+ end
26
+ end
27
+ end
@@ -33,8 +33,10 @@ module ConcernsOnRails
33
33
  # The claim is taken atomically via `write(..., unless_exist: true)`
34
34
  # (memcached `add` / Redis `SET NX` through Rails.cache); a store without
35
35
  # that atomicity is best-effort under concurrency. There is no in-process
36
- # default store on purpose — configure one explicitly or the first keyed
37
- # request raises ArgumentError. Note that responses rendered by
36
+ # default store on purpose — configure one explicitly (per controller as
37
+ # above, or once for the whole app via `ConcernsOnRails.setup { |c|
38
+ # c.cache_store = -> { Rails.cache } }`) or the first keyed request raises
39
+ # ArgumentError. Note that responses rendered by
38
40
  # `rescue_from` handlers bypass the around filter's success path and are
39
41
  # never cached.
40
42
  #
@@ -246,12 +248,13 @@ module ConcernsOnRails
246
248
  end
247
249
 
248
250
  def idempotency_store!
249
- store = self.class.idempotency_store
251
+ store = self.class.idempotency_store || ConcernsOnRails.config.resolved_cache_store
250
252
  return store if store
251
253
 
252
254
  raise ArgumentError,
253
255
  "ConcernsOnRails::Controllers::Idempotentable: no store configured. " \
254
- "Set `self.idempotency_store = Rails.cache` " \
256
+ "Set `self.idempotency_store = Rails.cache` on the controller, or the gem-wide " \
257
+ "fallback: ConcernsOnRails.setup { |c| c.cache_store = -> { Rails.cache } } " \
255
258
  "(must support #read, #write(expires_in:, unless_exist:) and #delete)."
256
259
  end
257
260
 
@@ -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
@@ -23,7 +23,9 @@ module ConcernsOnRails
23
23
  # exact. The store MUST support atomic increment-with-expiry (`Rails.cache`
24
24
  # with `#increment`, or Redis); a non-atomic store under-counts under
25
25
  # concurrency. There is no in-process default store on purpose — configure
26
- # one explicitly or the first throttled request raises ArgumentError.
26
+ # one explicitly (per controller as above, or once for the whole app via
27
+ # `ConcernsOnRails.setup { |c| c.cache_store = -> { Rails.cache } }`) or
28
+ # the first throttled request raises ArgumentError.
27
29
  module Throttleable
28
30
  extend ActiveSupport::Concern
29
31
 
@@ -161,12 +163,14 @@ module ConcernsOnRails
161
163
  end
162
164
 
163
165
  def throttle_store!
164
- store = self.class.throttleable_store
166
+ store = self.class.throttleable_store || ConcernsOnRails.config.resolved_cache_store
165
167
  return store if store
166
168
 
167
169
  raise ArgumentError,
168
170
  "ConcernsOnRails::Controllers::Throttleable: no store configured. " \
169
- "Set `self.throttleable_store = Rails.cache` (must support atomic #increment)."
171
+ "Set `self.throttleable_store = Rails.cache` on the controller, or the gem-wide " \
172
+ "fallback: ConcernsOnRails.setup { |c| c.cache_store = -> { Rails.cache } } " \
173
+ "(must support atomic #increment)."
170
174
  end
171
175
 
172
176
  def throttle_action_name
@@ -1,31 +1,27 @@
1
1
  module ConcernsOnRails
2
- # Backwards-compatibility aliases for pre-1.6 module paths.
3
- # Existing apps doing `include ConcernsOnRails::Sluggable` continue to work;
4
- # new code is encouraged to use the namespaced form: `ConcernsOnRails::Models::Sluggable`.
5
- Sluggable = Models::Sluggable
6
- Sortable = Models::Sortable
7
- Publishable = Models::Publishable
8
- SoftDeletable = Models::SoftDeletable
9
- Hashable = Models::Hashable
10
- Schedulable = Models::Schedulable
11
- Expirable = Models::Expirable
12
- Normalizable = Models::Normalizable
13
- Searchable = Models::Searchable
14
- Activatable = Models::Activatable
15
- Tokenizable = Models::Tokenizable
16
- Stateable = Models::Stateable
17
- Addressable = Models::Addressable
18
- Sequenceable = Models::Sequenceable
19
- Taggable = Models::Taggable
20
- Sanitizable = Models::Sanitizable
21
- Maskable = Models::Maskable
22
- Monetizable = Models::Monetizable
23
- Auditable = Models::Auditable
24
- Lockable = Models::Lockable
25
- Aliasable = Models::Aliasable
26
- Storable = Models::Storable
27
- CounterCacheable = Models::CounterCacheable
28
- Encryptable = Models::Encryptable
29
- Anonymizable = Models::Anonymizable
30
- Duplicable = Models::Duplicable
2
+ # Backwards-compatibility aliases for pre-1.6 module paths, resolved lazily:
3
+ # eager `Sluggable = Models::Sluggable` assignments would force-load every
4
+ # concern file at boot and defeat the loader's autoload laziness. The first
5
+ # reference to `ConcernsOnRails::<Name>` loads the real module and pins the
6
+ # alias constant, so `include ConcernsOnRails::Sluggable` works unchanged.
7
+ # New code is encouraged to use the namespaced form:
8
+ # `ConcernsOnRails::Models::Sluggable`.
9
+ #
10
+ # Note `Sortable` resolves to Models::Sortable (as it always has) — the
11
+ # controller concern is only reachable as Controllers::Sortable.
12
+ LEGACY_MODEL_ALIASES = %i[
13
+ Sluggable Sortable Publishable SoftDeletable Hashable Schedulable
14
+ Expirable Normalizable Searchable Activatable Tokenizable Stateable
15
+ Addressable Sequenceable Taggable Sanitizable Maskable Monetizable
16
+ Auditable Lockable Aliasable Storable CounterCacheable Encryptable
17
+ Anonymizable Duplicable
18
+ ].freeze
19
+
20
+ def self.const_missing(name)
21
+ return super unless LEGACY_MODEL_ALIASES.include?(name)
22
+
23
+ # Idempotent under a concurrent first reference: both threads pin the
24
+ # same module object, and once pinned const_missing never fires again.
25
+ const_set(name, Models.const_get(name))
26
+ end
31
27
  end
@@ -33,7 +33,7 @@ module ConcernsOnRails
33
33
 
34
34
  def activatable_by(field = DEFAULT_FIELD, prefix: nil, suffix: nil)
35
35
  self.activatable_field = field.to_sym
36
- ensure_columns!("ConcernsOnRails::Models::Activatable", activatable_field)
36
+ ensure_columns!("ConcernsOnRails::Models::Activatable", activatable_field, types: :boolean)
37
37
 
38
38
  # Affix the scope names so two concerns that each define `.active`
39
39
  # (e.g. SoftDeletable / Expirable) can coexist on one model.
@@ -86,7 +86,7 @@ module ConcernsOnRails
86
86
  raise ArgumentError, "#{LABEL}: unknown address part(s): #{unknown.join(', ')}" if unknown.any?
87
87
 
88
88
  overrides = mapping.to_h { |part, column| [part.to_sym, column.to_sym] }
89
- ensure_columns!(LABEL, overrides.values)
89
+ ensure_columns!(LABEL, overrides.values, types: :string)
90
90
  DEFAULT_FIELDS.merge(overrides).select { |_part, column| column_names.include?(column.to_s) }
91
91
  end
92
92
 
@@ -74,7 +74,7 @@ module ConcernsOnRails
74
74
  self.auditable_actor = actor
75
75
  self.auditable_max_entries = max_entries
76
76
  self.auditable_max_value_length = max_value_length
77
- ensure_columns!(LABEL, into, *fields)
77
+ ensure_columns!(LABEL, into, *fields, types: { into => :text })
78
78
  auditable_guard_encryptable!(fields)
79
79
 
80
80
  before_save :auditable_capture_changes
@@ -139,7 +139,7 @@ module ConcernsOnRails
139
139
  end
140
140
  return unless klass
141
141
 
142
- ensure_columns_on!(LABEL, klass, count_column)
142
+ ensure_columns_on!(LABEL, klass, count_column, types: :integer)
143
143
  end
144
144
 
145
145
  def counter_cacheable_recount_rule(rule)
@@ -232,13 +232,13 @@ module ConcernsOnRails
232
232
  def encryptable(*fields, type: :string, key: nil, blind_index: nil)
233
233
  type = type.to_sym
234
234
  encryptable_validate!(fields, type, blind_index)
235
- ensure_columns!(LABEL, *fields)
235
+ ensure_columns!(LABEL, *fields, types: :text)
236
236
 
237
237
  fields.each do |field|
238
238
  field = field.to_sym
239
239
  encryptable_guard_auditable!(field)
240
240
  bi = encryptable_normalize_blind_index(field, blind_index)
241
- ensure_columns!(LABEL, bi[:column]) if bi
241
+ ensure_columns!(LABEL, bi[:column], types: "string:index") if bi
242
242
  self.encryptable_rules = encryptable_rules.merge(field => { type: type, key: key, blind_index: bi })
243
243
  attribute field, EncryptedType.new(type: type, key: key)
244
244
  encryptable_define_helpers(field)
@@ -21,7 +21,7 @@ module ConcernsOnRails
21
21
  # expirable_by :valid_until
22
22
  def expirable_by(field = DEFAULT_FIELD, prefix: nil, suffix: nil)
23
23
  self.expirable_field = field.to_sym
24
- ensure_columns!("ConcernsOnRails::Models::Expirable", expirable_field)
24
+ ensure_columns!("ConcernsOnRails::Models::Expirable", expirable_field, types: :datetime)
25
25
  define_expirable_scopes(prefix, suffix)
26
26
  end
27
27
 
@@ -37,7 +37,8 @@ module ConcernsOnRails
37
37
  self.hashable_alphabet = alphabet
38
38
  self.hashable_unique = unique
39
39
 
40
- ensure_columns!("ConcernsOnRails::Models::Hashable", hashable_field)
40
+ ensure_columns!("ConcernsOnRails::Models::Hashable", hashable_field,
41
+ types: hashable_unique ? "string:uniq" : :string)
41
42
  validate_hashable_options!
42
43
  before_create :assign_hashable_value
43
44
 
@@ -75,7 +75,8 @@ module ConcernsOnRails
75
75
  self.lockable_locked_at_field = locked_at
76
76
  self.lockable_max_attempts = max_attempts
77
77
  self.lockable_unlock_in = unlock_in
78
- ensure_columns!(LABEL, attempts, locked_at)
78
+ ensure_columns!(LABEL, attempts, locked_at,
79
+ types: { attempts => :integer, locked_at => :datetime })
79
80
  validate_lockable_attempts_column!(attempts)
80
81
  define_lockable_scopes(prefix, suffix)
81
82
  end
@@ -47,7 +47,7 @@ module ConcernsOnRails
47
47
  raise ArgumentError, "ConcernsOnRails::Models::Monetizable: :subunit_to_unit must be a positive integer"
48
48
  end
49
49
 
50
- ensure_columns!("ConcernsOnRails::Models::Monetizable", fields)
50
+ ensure_columns!("ConcernsOnRails::Models::Monetizable", fields, types: :integer)
51
51
  config = { unit: unit, precision: precision, delimiter: delimiter, separator: separator, subunit_to_unit: subunit_to_unit }
52
52
  fields.each { |cents_field| define_money_accessors(cents_field.to_sym, as, config) }
53
53
  end
@@ -53,7 +53,7 @@ module ConcernsOnRails
53
53
  def publishable_by(field = nil, default_scope: false)
54
54
  self.publishable_field = field || :published_at
55
55
  @publishable_boolean_column = nil
56
- ensure_columns!("ConcernsOnRails::Models::Publishable", publishable_field)
56
+ ensure_columns!("ConcernsOnRails::Models::Publishable", publishable_field, types: :datetime)
57
57
  enable_published_default_scope if default_scope
58
58
  end
59
59
 
@@ -56,7 +56,7 @@ module ConcernsOnRails
56
56
  end
57
57
 
58
58
  ensure_columns!("ConcernsOnRails::Models::Schedulable",
59
- schedulable_starts_at_field, schedulable_ends_at_field)
59
+ schedulable_starts_at_field, schedulable_ends_at_field, types: :datetime)
60
60
  end
61
61
  end
62
62
 
@@ -62,10 +62,10 @@ module ConcernsOnRails
62
62
  reset = reset.to_sym
63
63
  scope_cols = Array(scope).map(&:to_sym)
64
64
 
65
- ensure_columns!(NAME, field)
66
- ensure_columns!(NAME, into) if into
65
+ ensure_columns!(NAME, field, types: :integer)
66
+ ensure_columns!(NAME, into, types: :string) if into
67
67
  ensure_columns!(NAME, *scope_cols) unless scope_cols.empty?
68
- ensure_columns!(NAME, :created_at) unless reset == :never
68
+ ensure_columns!(NAME, :created_at, types: :datetime) unless reset == :never
69
69
  validate_sequenceable_options!(reset, template)
70
70
 
71
71
  self.sequenceable_config = sequenceable_config.merge(
@@ -1,6 +1,15 @@
1
1
  require "active_support/concern"
2
2
  require "concerns_on_rails/support/column_guard"
3
- require "friendly_id"
3
+
4
+ # Loaded here — with the concern, on first use — rather than at gem boot, so
5
+ # apps that never include Sluggable never load friendly_id.
6
+ begin
7
+ require "friendly_id"
8
+ rescue LoadError
9
+ raise ConcernsOnRails::MissingDependency,
10
+ "ConcernsOnRails::Models::Sluggable requires the friendly_id gem. " \
11
+ 'Add `gem "friendly_id", "~> 5.4"` to your Gemfile to use it.'
12
+ end
4
13
 
5
14
  module ConcernsOnRails
6
15
  module Models
@@ -59,7 +68,8 @@ module ConcernsOnRails
59
68
  # first save with an opaque friendly_id error instead of this
60
69
  # concern's clear ArgumentError.
61
70
  ensure_columns!("ConcernsOnRails::Models::Sluggable",
62
- [sluggable_field, friendly_id_config.slug_column, scope].compact)
71
+ [sluggable_field, friendly_id_config.slug_column, scope].compact,
72
+ types: { friendly_id_config.slug_column.to_sym => "string:uniq" })
63
73
  return unless history || scope || reserved_words || finders
64
74
 
65
75
  reconfigure_friendly_id(history: history, scope: scope,
@@ -50,7 +50,7 @@ module ConcernsOnRails
50
50
  self.soft_delete_field = field || :deleted_at
51
51
  self.soft_delete_touch = touch
52
52
  self.soft_delete_default_scope = default_scope
53
- ensure_columns!("ConcernsOnRails::Models::SoftDeletable", soft_delete_field)
53
+ ensure_columns!("ConcernsOnRails::Models::SoftDeletable", soft_delete_field, types: :datetime)
54
54
  end
55
55
 
56
56
  # Soft-delete every matching record. Returns the Integer count of
@@ -1,6 +1,15 @@
1
1
  require "active_support/concern"
2
2
  require "concerns_on_rails/support/column_guard"
3
- require "acts_as_list"
3
+
4
+ # Loaded here — with the concern, on first use — rather than at gem boot, so
5
+ # apps that never include Sortable never load acts_as_list.
6
+ begin
7
+ require "acts_as_list"
8
+ rescue LoadError
9
+ raise ConcernsOnRails::MissingDependency,
10
+ "ConcernsOnRails::Models::Sortable requires the acts_as_list gem. " \
11
+ 'Add `gem "acts_as_list"` to your Gemfile to use it.'
12
+ end
4
13
 
5
14
  module ConcernsOnRails
6
15
  module Models
@@ -62,7 +71,7 @@ module ConcernsOnRails
62
71
  self.sortable_field = field
63
72
  self.sortable_direction = direction
64
73
 
65
- ensure_columns!("ConcernsOnRails::Models::Sortable", sortable_field)
74
+ ensure_columns!("ConcernsOnRails::Models::Sortable", sortable_field, types: :integer)
66
75
 
67
76
  return unless use_acts_as_list
68
77
 
@@ -90,7 +90,7 @@ module ConcernsOnRails
90
90
  self.stateable_transitions = options[:transitions] || {}
91
91
  self.stateable_prefix = stateable_affix(options[:prefix])
92
92
  self.stateable_suffix = stateable_affix(options[:suffix])
93
- ensure_columns!(LABEL, stateable_field)
93
+ ensure_columns!(LABEL, stateable_field, types: :string)
94
94
  end
95
95
 
96
96
  # `true` => use the field name; a string/symbol => use it literally; else none.
@@ -104,7 +104,7 @@ module ConcernsOnRails
104
104
  # named `prefix`/`suffix`. See the module docs.
105
105
  def storable_by(column, keys = {}, prefix: nil, suffix: nil, **kw_keys)
106
106
  column = column.to_sym
107
- ensure_columns!(LABEL, column)
107
+ ensure_columns!(LABEL, column, types: :text)
108
108
 
109
109
  prepared = storable_merge_key_specs(keys, kw_keys).map do |key, raw_spec|
110
110
  key = key.to_sym
@@ -53,7 +53,7 @@ module ConcernsOnRails
53
53
  self.taggable_field = field.to_sym
54
54
  self.taggable_delimiter = delimiter.to_s
55
55
  self.taggable_downcase = downcase
56
- ensure_columns!(LABEL, taggable_field)
56
+ ensure_columns!(LABEL, taggable_field, types: :string)
57
57
 
58
58
  before_validation :taggable_normalize!
59
59
  end
@@ -56,7 +56,7 @@ module ConcernsOnRails
56
56
  type = type.to_sym
57
57
  length = length.to_i
58
58
 
59
- ensure_columns!("ConcernsOnRails::Models::Tokenizable", field)
59
+ ensure_columns!("ConcernsOnRails::Models::Tokenizable", field, types: "string:uniq")
60
60
  validate_tokenizable_options!(type, length)
61
61
 
62
62
  # Build a fresh hash so subclasses don't mutate the parent's config.
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/string/inflections"
2
+
1
3
  module ConcernsOnRails
2
4
  module Support
3
5
  # Shared schema-validation helper mixed into a concern's ClassMethods.
@@ -27,24 +29,41 @@ module ConcernsOnRails
27
29
  # The phrase "does not exist" is preserved so existing specs that match
28
30
  # /does not exist/ keep passing.
29
31
  module ColumnGuard
30
- def ensure_columns!(concern, *fields)
31
- ensure_columns_on!(concern, self, *fields)
32
+ # `types:` teaches the error message: a Symbol/String applies to every
33
+ # listed field, a Hash maps field => type. When present, the raised
34
+ # ArgumentError appends a ready-to-paste migration command. Generator
35
+ # column-modifier syntax is welcome ("string:uniq" — tokens/slugs want a
36
+ # unique index anyway).
37
+ def ensure_columns!(concern, *fields, types: nil)
38
+ ensure_columns_on!(concern, self, *fields, types: types)
32
39
  end
33
40
 
34
41
  # Same contract, validated against another class (e.g. CounterCacheable
35
42
  # checks the counter column on the *parent* model).
36
- def ensure_columns_on!(concern, klass, *fields)
43
+ def ensure_columns_on!(concern, klass, *fields, types: nil)
37
44
  return false unless schema_reachable?(klass)
38
45
 
39
46
  fields.flatten.compact.each do |field|
40
47
  next if klass.column_names.include?(field.to_s)
41
48
 
42
49
  raise ArgumentError,
43
- "#{concern}: '#{field}' does not exist in the database (table: #{klass.table_name})"
50
+ "#{concern}: '#{field}' does not exist in the database (table: #{klass.table_name})." \
51
+ "#{column_migration_hint(klass, field, types)}"
44
52
  end
45
53
  true
46
54
  end
47
55
 
56
+ # " Add it with: bin/rails generate migration AddDeletedAtToArticles
57
+ # deleted_at:datetime" — every missing-column failure becomes a
58
+ # copy-paste fix. Without a known type the column name goes out bare
59
+ # (the generator defaults to string).
60
+ def column_migration_hint(klass, field, types)
61
+ type = types.is_a?(Hash) ? types[field.to_sym] : types
62
+ column = [field, type].compact.join(":")
63
+ " Add it with: bin/rails generate migration " \
64
+ "Add#{field.to_s.camelize}To#{klass.table_name.to_s.camelize} #{column}"
65
+ end
66
+
48
67
  # True when the class's table can actually be inspected. Connection
49
68
  # errors (ConnectionNotEstablished, NoDatabaseError, adapter errors)
50
69
  # all inherit from ActiveRecord::ActiveRecordError.
@@ -1,3 +1,3 @@
1
1
  module ConcernsOnRails
2
- VERSION = "1.23.0".freeze
2
+ VERSION = "1.25.0".freeze
3
3
  end
@@ -3,9 +3,87 @@ require "active_support/deprecation"
3
3
  require "concerns_on_rails/version"
4
4
 
5
5
  module ConcernsOnRails
6
- module Models; end
7
- module Controllers; end
8
- module Support; end
6
+ # Raised (as a LoadError subclass, so a bare `rescue LoadError` still works)
7
+ # when a concern needs a third-party gem that isn't available. friendly_id
8
+ # and acts_as_list load lazily with the concern that uses them (Sluggable /
9
+ # Sortable), so hosts that never touch those concerns never load them.
10
+ class MissingDependency < LoadError; end
11
+
12
+ # Everything below is autoloaded on first constant reference instead of
13
+ # eagerly required: an app that uses two concerns loads two files (plus
14
+ # their support helpers), not all forty — and friendly_id / acts_as_list
15
+ # stay unloaded unless Sluggable / Sortable is actually included.
16
+ # Each concern file requires the Support helpers it uses, so a direct
17
+ # `require "concerns_on_rails/models/sluggable"` keeps working too.
18
+
19
+ module Models
20
+ autoload :Sluggable, "concerns_on_rails/models/sluggable"
21
+ autoload :Sortable, "concerns_on_rails/models/sortable"
22
+ autoload :Publishable, "concerns_on_rails/models/publishable"
23
+ autoload :SoftDeletable, "concerns_on_rails/models/soft_deletable"
24
+ autoload :Hashable, "concerns_on_rails/models/hashable"
25
+ autoload :Schedulable, "concerns_on_rails/models/schedulable"
26
+ autoload :Expirable, "concerns_on_rails/models/expirable"
27
+ autoload :Normalizable, "concerns_on_rails/models/normalizable"
28
+ autoload :Searchable, "concerns_on_rails/models/searchable"
29
+ autoload :Activatable, "concerns_on_rails/models/activatable"
30
+ autoload :Tokenizable, "concerns_on_rails/models/tokenizable"
31
+ autoload :Stateable, "concerns_on_rails/models/stateable"
32
+ autoload :Addressable, "concerns_on_rails/models/addressable"
33
+ autoload :Sequenceable, "concerns_on_rails/models/sequenceable"
34
+ autoload :Taggable, "concerns_on_rails/models/taggable"
35
+ autoload :Sanitizable, "concerns_on_rails/models/sanitizable"
36
+ autoload :Maskable, "concerns_on_rails/models/maskable"
37
+ autoload :Monetizable, "concerns_on_rails/models/monetizable"
38
+ autoload :Auditable, "concerns_on_rails/models/auditable"
39
+ autoload :Lockable, "concerns_on_rails/models/lockable"
40
+ autoload :Aliasable, "concerns_on_rails/models/aliasable"
41
+ autoload :Storable, "concerns_on_rails/models/storable"
42
+ autoload :CounterCacheable, "concerns_on_rails/models/counter_cacheable"
43
+ autoload :Encryptable, "concerns_on_rails/models/encryptable"
44
+ autoload :Anonymizable, "concerns_on_rails/models/anonymizable"
45
+ autoload :Duplicable, "concerns_on_rails/models/duplicable"
46
+ end
47
+
48
+ module Controllers
49
+ autoload :Paginatable, "concerns_on_rails/controllers/paginatable"
50
+ autoload :Filterable, "concerns_on_rails/controllers/filterable"
51
+ autoload :Sortable, "concerns_on_rails/controllers/sortable"
52
+ autoload :Respondable, "concerns_on_rails/controllers/respondable"
53
+ autoload :ErrorHandleable, "concerns_on_rails/controllers/error_handleable"
54
+ autoload :Includable, "concerns_on_rails/controllers/includable"
55
+ autoload :SecureHeadable, "concerns_on_rails/controllers/secure_headable"
56
+ autoload :Localizable, "concerns_on_rails/controllers/localizable"
57
+ autoload :Authorizable, "concerns_on_rails/controllers/authorizable"
58
+ autoload :Throttleable, "concerns_on_rails/controllers/throttleable"
59
+ autoload :Timezoneable, "concerns_on_rails/controllers/timezoneable"
60
+ autoload :Idempotentable, "concerns_on_rails/controllers/idempotentable"
61
+ autoload :WebhookVerifiable, "concerns_on_rails/controllers/webhook_verifiable"
62
+ autoload :CursorPaginatable, "concerns_on_rails/controllers/cursor_paginatable"
63
+ autoload :Deprecatable, "concerns_on_rails/controllers/deprecatable"
64
+ autoload :Cacheable, "concerns_on_rails/controllers/cacheable"
65
+ autoload :Permittable, "concerns_on_rails/controllers/permittable"
66
+ end
67
+
68
+ module Support
69
+ autoload :ColumnGuard, "concerns_on_rails/support/column_guard"
70
+ autoload :ScalarParam, "concerns_on_rails/support/scalar_param"
71
+ autoload :UniqueRetry, "concerns_on_rails/support/unique_retry"
72
+ autoload :ErrorEnvelope, "concerns_on_rails/support/error_envelope"
73
+ autoload :FilterParameterRegistry, "concerns_on_rails/support/filter_parameter_registry"
74
+ autoload :RandomValue, "concerns_on_rails/support/random_value"
75
+ autoload :AddressData, "concerns_on_rails/support/address_data"
76
+ autoload :SequenceCalculator, "concerns_on_rails/support/sequence_calculator"
77
+ autoload :HtmlSanitizers, "concerns_on_rails/support/html_sanitizers"
78
+ autoload :Masker, "concerns_on_rails/support/masker"
79
+ autoload :Money, "concerns_on_rails/support/money"
80
+ autoload :Encryptor, "concerns_on_rails/support/encryptor"
81
+ end
82
+
83
+ # Encryption config + error types (Support::Encryptor requires it itself)
84
+ autoload :Encryption, "concerns_on_rails/encryption"
85
+ # Gem-wide configuration object behind ConcernsOnRails.setup
86
+ autoload :Configuration, "concerns_on_rails/configuration"
9
87
 
10
88
  # Guards the lazy singletons below: the first encrypted attribute read (or
11
89
  # deprecation warning) can happen on any request thread, and an unsynchronized
@@ -26,6 +104,20 @@ module ConcernsOnRails
26
104
  end
27
105
  end
28
106
 
107
+ # Gem-wide configuration (see Configuration), set from an initializer:
108
+ #
109
+ # ConcernsOnRails.setup do |config|
110
+ # config.cache_store = -> { Rails.cache }
111
+ # end
112
+ def self.config
113
+ @config || @config_mutex.synchronize { @config ||= Configuration.new }
114
+ end
115
+
116
+ def self.setup
117
+ yield config if block_given?
118
+ config
119
+ end
120
+
29
121
  # Gem-wide encryption configuration backing Models::Encryptable. Memoized like
30
122
  # `deprecator`; the host app supplies the key (see ConcernsOnRails::Encryption):
31
123
  #
@@ -54,70 +146,7 @@ module ConcernsOnRails
54
146
  end
55
147
  end
56
148
 
57
- # Encryption config + error types (loaded before the support codec that uses them)
58
- require "concerns_on_rails/encryption"
59
-
60
- # Shared internal helpers (must load before the concerns that use them)
61
- require "concerns_on_rails/support/column_guard"
62
- require "concerns_on_rails/support/scalar_param"
63
- require "concerns_on_rails/support/unique_retry"
64
- require "concerns_on_rails/support/error_envelope"
65
- require "concerns_on_rails/support/filter_parameter_registry"
66
- require "concerns_on_rails/support/random_value"
67
- require "concerns_on_rails/support/address_data"
68
- require "concerns_on_rails/support/sequence_calculator"
69
- require "concerns_on_rails/support/html_sanitizers"
70
- require "concerns_on_rails/support/masker"
71
- require "concerns_on_rails/support/money"
72
- require "concerns_on_rails/support/encryptor"
73
-
74
- # Model concerns
75
- require "concerns_on_rails/models/sluggable"
76
- require "concerns_on_rails/models/sortable"
77
- require "concerns_on_rails/models/publishable"
78
- require "concerns_on_rails/models/soft_deletable"
79
- require "concerns_on_rails/models/hashable"
80
- require "concerns_on_rails/models/schedulable"
81
- require "concerns_on_rails/models/expirable"
82
- require "concerns_on_rails/models/normalizable"
83
- require "concerns_on_rails/models/searchable"
84
- require "concerns_on_rails/models/activatable"
85
- require "concerns_on_rails/models/tokenizable"
86
- require "concerns_on_rails/models/stateable"
87
- require "concerns_on_rails/models/addressable"
88
- require "concerns_on_rails/models/sequenceable"
89
- require "concerns_on_rails/models/taggable"
90
- require "concerns_on_rails/models/sanitizable"
91
- require "concerns_on_rails/models/maskable"
92
- require "concerns_on_rails/models/monetizable"
93
- require "concerns_on_rails/models/auditable"
94
- require "concerns_on_rails/models/lockable"
95
- require "concerns_on_rails/models/aliasable"
96
- require "concerns_on_rails/models/storable"
97
- require "concerns_on_rails/models/counter_cacheable"
98
- require "concerns_on_rails/models/encryptable"
99
- require "concerns_on_rails/models/anonymizable"
100
- require "concerns_on_rails/models/duplicable"
101
-
102
- # Controller concerns
103
- require "concerns_on_rails/controllers/paginatable"
104
- require "concerns_on_rails/controllers/filterable"
105
- require "concerns_on_rails/controllers/sortable"
106
- require "concerns_on_rails/controllers/respondable"
107
- require "concerns_on_rails/controllers/error_handleable"
108
- require "concerns_on_rails/controllers/includable"
109
- require "concerns_on_rails/controllers/secure_headable"
110
- require "concerns_on_rails/controllers/localizable"
111
- require "concerns_on_rails/controllers/authorizable"
112
- require "concerns_on_rails/controllers/throttleable"
113
- require "concerns_on_rails/controllers/timezoneable"
114
- require "concerns_on_rails/controllers/idempotentable"
115
- require "concerns_on_rails/controllers/webhook_verifiable"
116
- require "concerns_on_rails/controllers/cursor_paginatable"
117
- require "concerns_on_rails/controllers/deprecatable"
118
- require "concerns_on_rails/controllers/cacheable"
119
-
120
- # Backwards compatibility (top-level aliases for pre-1.6 module paths)
149
+ # Backwards compatibility (lazy top-level aliases for pre-1.6 module paths)
121
150
  require "concerns_on_rails/legacy_aliases"
122
151
 
123
152
  # Boot-time integration (filter_parameters registration), Rails apps only
metadata CHANGED
@@ -1,17 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concerns_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.23.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-07-27 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
- name: rails
14
+ name: actionpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '5.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9'
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
15
55
  requirement: !ruby/object:Gem::Requirement
16
56
  requirements:
17
57
  - - ">="
@@ -34,16 +74,22 @@ dependencies:
34
74
  name: acts_as_list
35
75
  requirement: !ruby/object:Gem::Requirement
36
76
  requirements:
37
- - - "~>"
77
+ - - ">="
38
78
  - !ruby/object:Gem::Version
39
79
  version: 0.7.5
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '2'
40
83
  type: :runtime
41
84
  prerelease: false
42
85
  version_requirements: !ruby/object:Gem::Requirement
43
86
  requirements:
44
- - - "~>"
87
+ - - ">="
45
88
  - !ruby/object:Gem::Version
46
89
  version: 0.7.5
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '2'
47
93
  - !ruby/object:Gem::Dependency
48
94
  name: friendly_id
49
95
  requirement: !ruby/object:Gem::Requirement
@@ -58,6 +104,20 @@ dependencies:
58
104
  - - "~>"
59
105
  - !ruby/object:Gem::Version
60
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'
61
121
  description: A collection of plug-and-play ActiveSupport concerns for Rails models
62
122
  and Rails controllers
63
123
  email:
@@ -71,6 +131,7 @@ files:
71
131
  - LICENSE.txt
72
132
  - README.md
73
133
  - lib/concerns_on_rails.rb
134
+ - lib/concerns_on_rails/configuration.rb
74
135
  - lib/concerns_on_rails/controllers/authorizable.rb
75
136
  - lib/concerns_on_rails/controllers/cacheable.rb
76
137
  - lib/concerns_on_rails/controllers/cursor_paginatable.rb
@@ -81,6 +142,7 @@ files:
81
142
  - lib/concerns_on_rails/controllers/includable.rb
82
143
  - lib/concerns_on_rails/controllers/localizable.rb
83
144
  - lib/concerns_on_rails/controllers/paginatable.rb
145
+ - lib/concerns_on_rails/controllers/permittable.rb
84
146
  - lib/concerns_on_rails/controllers/respondable.rb
85
147
  - lib/concerns_on_rails/controllers/secure_headable.rb
86
148
  - lib/concerns_on_rails/controllers/sortable.rb