concerns_on_rails 1.4.2 → 1.5.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 +5 -0
- data/README.md +53 -0
- data/lib/concerns_on_rails/expirable.rb +85 -0
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d359c073e004cdacffbfeaf9e588abb581bf6794a631211fc3eca309f9522e31
|
|
4
|
+
data.tar.gz: c686a9ef9f5fa323a7d59ba4786e58c6d0a78265e65e01526036350b1ec8bc26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85af55992f8dad780dcbe588df3b8df21f042d46337eb331b85638040a1c016be004d17387e894fca8f787d8c1995b0d89f694fd9af9315cb4743ff556799d94
|
|
7
|
+
data.tar.gz: cd515ebcec0029801f5483872d94d7e833bbba64b0cd00da2247744e78c8789178ce950b4f0a52023612e19f8e3b2e40e6198800291a5e42655198e585692288
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.5.0 (2026-05-16)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Expirable: Single-timestamp expiry for tokens, API keys, sessions, and similar records. Adds `expirable_by` macro, `.active` / `.expired` / `.expiring_within(duration)` scopes, predicates (`active?`, `expired?`), mutators (`expire!`, `extend_expiry!`), and `time_until_expiry`. `nil` expiry means "never expires"; the expiry boundary is exclusive.
|
|
7
|
+
|
|
3
8
|
## 1.4.2 (2026-05-16)
|
|
4
9
|
|
|
5
10
|
### Added
|
data/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A simple collection of reusable Rails concerns to keep your models clean and DRY
|
|
|
12
12
|
- ❌ `SoftDeletable`: Soft delete records using a configurable timestamp field (e.g., `deleted_at`) with automatic scoping
|
|
13
13
|
- 🔐 `Hashable`: Auto-generate a random hex/UUID/integer/custom-alphabet value on create, with a `regenerate_<field>!` helper
|
|
14
14
|
- 🗓️ `Schedulable`: Manage time-windowed records via `starts_at` / `ends_at` with `.current`, `.upcoming`, `.expired`, and `.active_at(time)` scopes
|
|
15
|
+
- ⏳ `Expirable`: Single-timestamp expiry with `.active` / `.expired` / `.expiring_within(duration)` scopes and `expire!` / `extend_expiry!` / `time_until_expiry` helpers
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -300,6 +301,58 @@ promo.reschedule!(starts_at: 1.day.from_now,
|
|
|
300
301
|
|
|
301
302
|
---
|
|
302
303
|
|
|
304
|
+
### 7. ⏳ Expirable
|
|
305
|
+
|
|
306
|
+
For records with a single expiry timestamp — auth tokens, API keys, sessions, password-reset links, invitations.
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
class ApiToken < ApplicationRecord
|
|
310
|
+
include ConcernsOnRails::Expirable
|
|
311
|
+
|
|
312
|
+
# Default field: :expires_at
|
|
313
|
+
expirable_by
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
token = ApiToken.create!(expires_at: 1.hour.from_now)
|
|
317
|
+
token.active? # => true
|
|
318
|
+
token.expired? # => false
|
|
319
|
+
token.time_until_expiry # => ActiveSupport::Duration (~1.hour)
|
|
320
|
+
|
|
321
|
+
ApiToken.active # nil expiry OR future expiry
|
|
322
|
+
ApiToken.expired # past expiry
|
|
323
|
+
ApiToken.expiring_within(1.day) # future expiry within the next 1.day
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Mutators
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
token.expire! # sets expires_at to now
|
|
330
|
+
token.expire!(2.hours.from_now) # sets to an explicit time
|
|
331
|
+
token.extend_expiry!(by: 1.day) # pushes expiry forward
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`extend_expiry!` is "smart" about the base:
|
|
335
|
+
- If `expires_at` is nil or already in the past, the new value is `now + by`.
|
|
336
|
+
- If `expires_at` is still in the future, `by` is added to the existing value.
|
|
337
|
+
|
|
338
|
+
#### Custom field name
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
class License < ApplicationRecord
|
|
342
|
+
include ConcernsOnRails::Expirable
|
|
343
|
+
|
|
344
|
+
expirable_by :valid_until
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### Notes
|
|
349
|
+
- `nil` `expires_at` means **never expires** (the record stays `active?`).
|
|
350
|
+
- The expiry boundary is **exclusive**: at exactly `expires_at`, the record is `expired?`.
|
|
351
|
+
- No `default_scope` is added; chain `.active` explicitly to filter.
|
|
352
|
+
- `Expirable` overlaps with `Schedulable`'s open-ended pattern (`schedulable_by starts_at: nil, ends_at: :expires_at`). Use `Expirable` when the API ergonomics around expiry — `active?`, `expire!`, `extend_expiry!`, `expiring_within` — fit your domain better; use `Schedulable` when you also need a start time.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
303
356
|
## 🛠️ Development
|
|
304
357
|
|
|
305
358
|
To build the gem:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
|
|
3
|
+
module ConcernsOnRails
|
|
4
|
+
module Expirable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
DEFAULT_FIELD = :expires_at
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
class_attribute :expirable_field, instance_accessor: false, default: DEFAULT_FIELD
|
|
11
|
+
|
|
12
|
+
scope :active, lambda {
|
|
13
|
+
column = arel_table[expirable_field]
|
|
14
|
+
where(column.eq(nil).or(column.gt(Time.zone.now)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
scope :expired, lambda {
|
|
18
|
+
where(arel_table[expirable_field].lteq(Time.zone.now))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
scope :expiring_within, lambda { |duration|
|
|
22
|
+
column = arel_table[expirable_field]
|
|
23
|
+
now = Time.zone.now
|
|
24
|
+
where(column.gt(now)).where(column.lteq(now + duration))
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class_methods do
|
|
29
|
+
# Configure the expiry column.
|
|
30
|
+
# Example:
|
|
31
|
+
# expirable_by # uses :expires_at
|
|
32
|
+
# expirable_by :valid_until
|
|
33
|
+
def expirable_by(field = DEFAULT_FIELD)
|
|
34
|
+
self.expirable_field = field.to_sym
|
|
35
|
+
|
|
36
|
+
return if column_names.include?(expirable_field.to_s)
|
|
37
|
+
|
|
38
|
+
raise ArgumentError, "ConcernsOnRails::Expirable: expirable_field '#{expirable_field}' does not exist in the database"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def active?
|
|
43
|
+
!expired?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# nil means never expires; equal-to-now is treated as expired (exclusive boundary).
|
|
47
|
+
def expired?
|
|
48
|
+
value = self[self.class.expirable_field]
|
|
49
|
+
return false if value.nil?
|
|
50
|
+
|
|
51
|
+
value <= Time.zone.now
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def expire!(time = Time.zone.now)
|
|
55
|
+
update(self.class.expirable_field => time)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Push expiry forward by `by:`. If the record has no expiry yet, or has
|
|
59
|
+
# already expired, the new expiry is `now + by`. Otherwise it's added to
|
|
60
|
+
# the existing expiry.
|
|
61
|
+
def extend_expiry!(by:)
|
|
62
|
+
update(self.class.expirable_field => expiry_extension_base + by)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns an ActiveSupport::Duration of how long until expiry, or nil
|
|
66
|
+
# when there's no expiry set, or 0.seconds when already expired.
|
|
67
|
+
def time_until_expiry
|
|
68
|
+
value = self[self.class.expirable_field]
|
|
69
|
+
return nil if value.nil?
|
|
70
|
+
|
|
71
|
+
now = Time.zone.now
|
|
72
|
+
return 0.seconds if value <= now
|
|
73
|
+
|
|
74
|
+
(value - now).seconds
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def expiry_extension_base
|
|
80
|
+
value = self[self.class.expirable_field]
|
|
81
|
+
now = Time.zone.now
|
|
82
|
+
value.nil? || value <= now ? now : value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/concerns_on_rails.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Nguyen
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- CODE_OF_CONDUCT.md
|
|
71
71
|
- README.md
|
|
72
72
|
- lib/concerns_on_rails.rb
|
|
73
|
+
- lib/concerns_on_rails/expirable.rb
|
|
73
74
|
- lib/concerns_on_rails/hashable.rb
|
|
74
75
|
- lib/concerns_on_rails/publishable.rb
|
|
75
76
|
- lib/concerns_on_rails/schedulable.rb
|