concerns_on_rails 1.3.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d4b68b3625ea62546fa003654e7bd0618a56b09b615b406dcaed12f16560338
4
- data.tar.gz: 4d048584cdf09f1eb9859a4cca0ccd64182b331bfa8a6ffb897ff61ab1c4636a
3
+ metadata.gz: 6e784268a8fb00ef3e77b613eca3ddc44da0ca95e0f9f1784ce019356f2f5905
4
+ data.tar.gz: fdc824e479b2607b097762debf8940c622044f47a108a5482729e9b74c1233c0
5
5
  SHA512:
6
- metadata.gz: 8797f42f52e32309614fa95bf2a52b0a17e6adff54f07bbdcc9f9f27863a92fdfe0dfec1a14d7ad9b34285e25abc6a12cf37194a4da4532874a29d0fc7a13b01
7
- data.tar.gz: e60133f68c2b4778cdea6b34fdcddd279eee5076ef060c009734c9e42eda3d379d65c9adfa847dd30dcb47e06e4a39c88313644fa248e407a5d60e242e8e3756
6
+ metadata.gz: ae9b3876ae0eea6a02f51710d33074aba898bc1ab22609a33cfb27a8eb1747c5d4c12ef01fc1de180026658adf0f4d1d788842abc5f83322616d80dac35ac238
7
+ data.tar.gz: '09cb150b92adbcd6ab21b670b122d42ad6f71bcbb4616e5bfa89ac2cdc385fc8e42cbac98f8b11d3c2d9bba462c32a0fa2d8e9bd46c5b6e3e0f8407e41242e97'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  <!-- CHANGELOG.md -->
2
2
 
3
+ ## 1.4.2 (2026-05-16)
4
+
5
+ ### Added
6
+ - Schedulable: Manage time-windowed records via `starts_at` / `ends_at` columns. Adds `schedulable_by` macro, scopes (`.current`, `.upcoming`, `.expired`, `.active_at(time)`), predicates (`current?`, `upcoming?`, `expired?`, `active_at?`), and mutators (`start!`, `finish!`, `reschedule!`). Supports custom column names and open-ended schedules (`starts_at: nil`).
7
+
8
+ ### Internal
9
+ - Refactored `active_at?` into two private predicate helpers (`schedulable_started_by?` / `schedulable_not_ended_at?`) to satisfy `Metrics/CyclomaticComplexity`.
10
+
11
+ ### Notes
12
+ - The `v1.4.0` and `v1.4.1` tags were created but never released to RubyGems (CI failed on `Gemfile.lock` regeneration and a RuboCop complexity check respectively). `1.4.2` is the first usable release of the Schedulable concern.
13
+
3
14
  ## 1.3.0 (2026-05-16)
4
15
 
5
16
  ### Added
data/README.md CHANGED
@@ -11,6 +11,7 @@ A simple collection of reusable Rails concerns to keep your models clean and DRY
11
11
  - 📤 `Publishable`: Easily manage published/unpublished records using a simple `published_at` field
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
+ - 🗓️ `Schedulable`: Manage time-windowed records via `starts_at` / `ends_at` with `.current`, `.upcoming`, `.expired`, and `.active_at(time)` scopes
14
15
 
15
16
  ---
16
17
 
@@ -238,6 +239,67 @@ hashable_by :code, type: :custom, length: 8,
238
239
 
239
240
  ---
240
241
 
242
+ ### 6. 🗓️ Schedulable
243
+
244
+ Manage records with a time window using `starts_at` / `ends_at` columns.
245
+
246
+ ```ruby
247
+ class Promotion < ApplicationRecord
248
+ include ConcernsOnRails::Schedulable
249
+
250
+ # Defaults: starts_at: :starts_at, ends_at: :ends_at
251
+ schedulable_by
252
+ end
253
+
254
+ promo = Promotion.create!(starts_at: 1.hour.ago, ends_at: 1.day.from_now)
255
+ promo.current? # => true
256
+ promo.upcoming? # => false
257
+ promo.expired? # => false
258
+
259
+ Promotion.current # currently active
260
+ Promotion.upcoming # starts_at in the future
261
+ Promotion.expired # ends_at in the past
262
+ Promotion.active_at(Time.zone.now) # active at any specific time
263
+ ```
264
+
265
+ #### Custom column names
266
+
267
+ ```ruby
268
+ class Event < ApplicationRecord
269
+ include ConcernsOnRails::Schedulable
270
+
271
+ schedulable_by starts_at: :starts_on, ends_at: :ends_on
272
+ end
273
+ ```
274
+
275
+ #### Open-ended start (only an expiry)
276
+
277
+ ```ruby
278
+ class Coupon < ApplicationRecord
279
+ include ConcernsOnRails::Schedulable
280
+
281
+ schedulable_by starts_at: nil, ends_at: :expires_at
282
+ end
283
+ ```
284
+
285
+ #### Mutators
286
+
287
+ ```ruby
288
+ promo.start! # sets starts_at to now
289
+ promo.finish! # sets ends_at to now
290
+ promo.reschedule!(starts_at: 1.day.from_now,
291
+ ends_at: 2.days.from_now) # sets both
292
+ ```
293
+
294
+ #### Notes
295
+ - Boundary semantics are **inclusive start, exclusive end**: a record is active at exactly `starts_at`, but not at exactly `ends_at`.
296
+ - A `nil` `ends_at` means "no end" — the record stays active forever once started.
297
+ - A `nil` `starts_at` means "not yet started" — the record is not active (unless `starts_at` is unconfigured).
298
+ - No `default_scope` is added; chain `.current` (or any other scope) explicitly to filter.
299
+ - `schedulable_by` validates that the configured columns exist and raises `ArgumentError` otherwise.
300
+
301
+ ---
302
+
241
303
  ## 🛠️ Development
242
304
 
243
305
  To build the gem:
@@ -0,0 +1,128 @@
1
+ require "active_support/concern"
2
+
3
+ module ConcernsOnRails
4
+ module Schedulable
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_STARTS_AT_FIELD = :starts_at
8
+ DEFAULT_ENDS_AT_FIELD = :ends_at
9
+
10
+ included do
11
+ class_attribute :schedulable_starts_at_field, instance_accessor: false, default: DEFAULT_STARTS_AT_FIELD
12
+ class_attribute :schedulable_ends_at_field, instance_accessor: false, default: DEFAULT_ENDS_AT_FIELD
13
+
14
+ scope :active_at, lambda { |time|
15
+ starts_field = schedulable_starts_at_field
16
+ ends_field = schedulable_ends_at_field
17
+ relation = all
18
+ relation = relation.where(arel_table[starts_field].lteq(time)) if starts_field
19
+ relation = relation.where(arel_table[ends_field].eq(nil).or(arel_table[ends_field].gt(time))) if ends_field
20
+ relation
21
+ }
22
+
23
+ scope :current, -> { active_at(Time.zone.now) }
24
+
25
+ scope :upcoming, lambda {
26
+ field = schedulable_starts_at_field
27
+ next none unless field
28
+
29
+ where(arel_table[field].gt(Time.zone.now))
30
+ }
31
+
32
+ scope :expired, lambda {
33
+ field = schedulable_ends_at_field
34
+ next none unless field
35
+
36
+ where(arel_table[field].lteq(Time.zone.now))
37
+ }
38
+ end
39
+
40
+ class_methods do
41
+ # Configure the start/end timestamp columns.
42
+ # Example:
43
+ # schedulable_by # uses :starts_at and :ends_at
44
+ # schedulable_by starts_at: :starts_on, ends_at: :ends_on
45
+ # schedulable_by starts_at: nil, ends_at: :expires_at # open-ended start
46
+ def schedulable_by(starts_at: DEFAULT_STARTS_AT_FIELD, ends_at: DEFAULT_ENDS_AT_FIELD)
47
+ self.schedulable_starts_at_field = starts_at&.to_sym
48
+ self.schedulable_ends_at_field = ends_at&.to_sym
49
+
50
+ if schedulable_starts_at_field.nil? && schedulable_ends_at_field.nil?
51
+ raise ArgumentError, "ConcernsOnRails::Schedulable: at least one of starts_at: or ends_at: must be configured"
52
+ end
53
+
54
+ [schedulable_starts_at_field, schedulable_ends_at_field].compact.each do |field|
55
+ next if column_names.include?(field.to_s)
56
+
57
+ raise ArgumentError, "ConcernsOnRails::Schedulable: field '#{field}' does not exist in the database"
58
+ end
59
+ end
60
+ end
61
+
62
+ # Is the record active at the given time? Inclusive start, exclusive end.
63
+ def active_at?(time)
64
+ schedulable_started_by?(time) && schedulable_not_ended_at?(time)
65
+ end
66
+
67
+ def current?
68
+ active_at?(Time.zone.now)
69
+ end
70
+
71
+ def upcoming?
72
+ field = self.class.schedulable_starts_at_field
73
+ value = field && self[field]
74
+ return false unless value
75
+
76
+ value > Time.zone.now
77
+ end
78
+
79
+ def expired?
80
+ field = self.class.schedulable_ends_at_field
81
+ value = field && self[field]
82
+ return false unless value
83
+
84
+ value <= Time.zone.now
85
+ end
86
+
87
+ def start!(time = Time.zone.now)
88
+ field = self.class.schedulable_starts_at_field
89
+ raise "ConcernsOnRails::Schedulable: starts_at field not configured" unless field
90
+
91
+ update(field => time)
92
+ end
93
+
94
+ def finish!(time = Time.zone.now)
95
+ field = self.class.schedulable_ends_at_field
96
+ raise "ConcernsOnRails::Schedulable: ends_at field not configured" unless field
97
+
98
+ update(field => time)
99
+ end
100
+
101
+ def reschedule!(starts_at:, ends_at:)
102
+ attrs = {}
103
+ starts_field = self.class.schedulable_starts_at_field
104
+ ends_field = self.class.schedulable_ends_at_field
105
+ attrs[starts_field] = starts_at if starts_field
106
+ attrs[ends_field] = ends_at if ends_field
107
+ update(attrs)
108
+ end
109
+
110
+ private
111
+
112
+ def schedulable_started_by?(time)
113
+ field = self.class.schedulable_starts_at_field
114
+ return true unless field
115
+
116
+ value = self[field]
117
+ !value.nil? && value <= time
118
+ end
119
+
120
+ def schedulable_not_ended_at?(time)
121
+ field = self.class.schedulable_ends_at_field
122
+ return true unless field
123
+
124
+ value = self[field]
125
+ value.nil? || value > time
126
+ end
127
+ end
128
+ end
@@ -1,3 +1,3 @@
1
1
  module ConcernsOnRails
2
- VERSION = "1.3.0".freeze
2
+ VERSION = "1.4.2".freeze
3
3
  end
@@ -5,6 +5,7 @@ require "concerns_on_rails/publishable"
5
5
  require "concerns_on_rails/sluggable"
6
6
  require "concerns_on_rails/soft_deletable"
7
7
  require "concerns_on_rails/hashable"
8
+ require "concerns_on_rails/schedulable"
8
9
 
9
10
  module ConcernsOnRails
10
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concerns_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.2
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-05-15 00:00:00.000000000 Z
11
+ date: 2026-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -72,6 +72,7 @@ files:
72
72
  - lib/concerns_on_rails.rb
73
73
  - lib/concerns_on_rails/hashable.rb
74
74
  - lib/concerns_on_rails/publishable.rb
75
+ - lib/concerns_on_rails/schedulable.rb
75
76
  - lib/concerns_on_rails/sluggable.rb
76
77
  - lib/concerns_on_rails/soft_deletable.rb
77
78
  - lib/concerns_on_rails/sortable.rb
@@ -91,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
91
92
  requirements:
92
93
  - - ">="
93
94
  - !ruby/object:Gem::Version
94
- version: 2.7.0
95
+ version: 3.2.0
95
96
  required_rubygems_version: !ruby/object:Gem::Requirement
96
97
  requirements:
97
98
  - - ">="