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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +78 -6
- data/lib/concerns_on_rails/configuration.rb +27 -0
- data/lib/concerns_on_rails/controllers/idempotentable.rb +7 -4
- data/lib/concerns_on_rails/controllers/permittable.rb +25 -0
- data/lib/concerns_on_rails/controllers/throttleable.rb +7 -3
- data/lib/concerns_on_rails/legacy_aliases.rb +25 -29
- data/lib/concerns_on_rails/models/activatable.rb +1 -1
- data/lib/concerns_on_rails/models/addressable.rb +1 -1
- data/lib/concerns_on_rails/models/auditable.rb +1 -1
- data/lib/concerns_on_rails/models/counter_cacheable.rb +1 -1
- data/lib/concerns_on_rails/models/encryptable.rb +2 -2
- data/lib/concerns_on_rails/models/expirable.rb +1 -1
- data/lib/concerns_on_rails/models/hashable.rb +2 -1
- data/lib/concerns_on_rails/models/lockable.rb +2 -1
- data/lib/concerns_on_rails/models/monetizable.rb +1 -1
- data/lib/concerns_on_rails/models/publishable.rb +1 -1
- data/lib/concerns_on_rails/models/schedulable.rb +1 -1
- data/lib/concerns_on_rails/models/sequenceable.rb +3 -3
- data/lib/concerns_on_rails/models/sluggable.rb +12 -2
- data/lib/concerns_on_rails/models/soft_deletable.rb +1 -1
- data/lib/concerns_on_rails/models/sortable.rb +11 -2
- data/lib/concerns_on_rails/models/stateable.rb +1 -1
- data/lib/concerns_on_rails/models/storable.rb +1 -1
- data/lib/concerns_on_rails/models/taggable.rb +1 -1
- data/lib/concerns_on_rails/models/tokenizable.rb +1 -1
- data/lib/concerns_on_rails/support/column_guard.rb +23 -4
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +96 -67
- metadata +67 -5
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,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
|
[](https://rubyonrails.org)
|
|
13
13
|
[](#-license)
|
|
14
14
|
|
|
15
|
-
🧩 **
|
|
15
|
+
🧩 **26 model concerns** · 🎮 **16 controller concerns** · 🪶 **lean deps** · ✅ **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.
|
|
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
|
|
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.
|
|
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
|
|
37
|
-
#
|
|
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
|
|
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`
|
|
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
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
data/lib/concerns_on_rails.rb
CHANGED
|
@@ -3,9 +3,87 @@ require "active_support/deprecation"
|
|
|
3
3
|
require "concerns_on_rails/version"
|
|
4
4
|
|
|
5
5
|
module ConcernsOnRails
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
#
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-08-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
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
|