concerns_on_rails 1.2.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: 603d571510be78d62d3d71f071b703dbf70bf505e6463e1766e4a99c4c039012
4
- data.tar.gz: eefa6a75c74a0ff0db01e129cc85c73af7ae3b16fb38c2ceab757ff5fc1e2656
3
+ metadata.gz: 6e784268a8fb00ef3e77b613eca3ddc44da0ca95e0f9f1784ce019356f2f5905
4
+ data.tar.gz: fdc824e479b2607b097762debf8940c622044f47a108a5482729e9b74c1233c0
5
5
  SHA512:
6
- metadata.gz: 01b37a0a698df95131be947ebc0874c970dc1f4cadf591aca2de84b1bc4da8075967cc5d469a45713165d07c763e55c7296b34a621248bffa82e7af127c5b5b0
7
- data.tar.gz: e0978b66b4208ef653563602e9b153f10f56867cd2ad821ae8323f12fad8eff9c917127f64ae567a0ffa62945b562c5244aed72e743419e47a22a1de8f121ad1
6
+ metadata.gz: ae9b3876ae0eea6a02f51710d33074aba898bc1ab22609a33cfb27a8eb1747c5d4c12ef01fc1de180026658adf0f4d1d788842abc5f83322616d80dac35ac238
7
+ data.tar.gz: '09cb150b92adbcd6ab21b670b122d42ad6f71bcbb4616e5bfa89ac2cdc385fc8e42cbac98f8b11d3c2d9bba462c32a0fa2d8e9bd46c5b6e3e0f8407e41242e97'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
14
+ ## 1.3.0 (2026-05-16)
15
+
16
+ ### Added
17
+ - Hashable: Auto-generate a random value on create (`:hex`, `:uuid`, `:integer`, or `:custom` alphabet). Adds `hashable_by` macro and a dynamic `regenerate_<field>!` instance method.
18
+
3
19
  ## 1.1.0 (2025-04-17)
4
20
 
5
21
  ### Added
data/README.md CHANGED
@@ -10,6 +10,8 @@ A simple collection of reusable Rails concerns to keep your models clean and DRY
10
10
  - 🔢 `Sortable`: Sort records based on a field using `acts_as_list`, with flexible sorting field and direction
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
+ - 🔐 `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
13
15
 
14
16
  ---
15
17
 
@@ -194,6 +196,110 @@ end
194
196
 
195
197
  ---
196
198
 
199
+ ### 5. Hashable
200
+
201
+ Auto-generate a random value (hex, UUID, fixed-digit integer, or custom-alphabet string) on create.
202
+
203
+ ```ruby
204
+ class Order < ApplicationRecord
205
+ include ConcernsOnRails::Hashable
206
+
207
+ # Defaults: type: :hex, length: 16 (32-char hex string)
208
+ hashable_by :token
209
+ end
210
+
211
+ order = Order.create!
212
+ order.token # => "a3f7c9b1e2d40859e2f1c9b73d40a857"
213
+ order.regenerate_token! # rolls a new random value and persists it
214
+ ```
215
+
216
+ #### Types
217
+
218
+ ```ruby
219
+ hashable_by :token, type: :hex, length: 16
220
+ hashable_by :external_id, type: :uuid
221
+ hashable_by :code, type: :integer, length: 6
222
+ hashable_by :code, type: :custom, length: 8,
223
+ alphabet: "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
224
+ ```
225
+
226
+ | Type | `length` means | Example output |
227
+ |------------|-------------------------|--------------------------|
228
+ | `:hex` | byte count (output is `length * 2` hex chars) | `"a3f7c9b1e2d40859"` |
229
+ | `:uuid` | ignored | `"550e8400-e29b-41d4-a716-446655440000"` |
230
+ | `:integer` | digit count | `483921` |
231
+ | `:custom` | output length, samples from `alphabet:` | `"K7M3PQ9A"` |
232
+
233
+ #### Notes
234
+ - Auto-assigns on `before_create` only when the field is blank, so callers can still pass an explicit value.
235
+ - A `regenerate_<field>!` instance method is defined dynamically to match the configured column.
236
+ - No uniqueness retry is built in. For collision-prone configurations (e.g. short integer codes), add a unique index and rescue at the application level.
237
+ - For fixed-width numeric codes (e.g. `000042`), use a string column — integer columns drop leading zeros.
238
+ - If your model has `validates :<field>, presence: true`, switch to a `before_validation` callback in your model since the concern uses `before_create`.
239
+
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
+
197
303
  ## 🛠️ Development
198
304
 
199
305
  To build the gem:
@@ -0,0 +1,77 @@
1
+ require "active_support/concern"
2
+ require "securerandom"
3
+
4
+ module ConcernsOnRails
5
+ module Hashable
6
+ extend ActiveSupport::Concern
7
+
8
+ VALID_TYPES = %i[hex uuid integer custom].freeze
9
+
10
+ included do
11
+ class_attribute :hashable_field, instance_accessor: false
12
+ class_attribute :hashable_type, instance_accessor: false, default: :hex
13
+ class_attribute :hashable_length, instance_accessor: false, default: 16
14
+ class_attribute :hashable_alphabet, instance_accessor: false, default: nil
15
+ end
16
+
17
+ class_methods do
18
+ # Define hashable field and generation options.
19
+ # Example:
20
+ # hashable_by :token
21
+ # hashable_by :token, type: :hex, length: 16
22
+ # hashable_by :external_id, type: :uuid
23
+ # hashable_by :code, type: :integer, length: 6
24
+ # hashable_by :code, type: :custom, length: 8, alphabet: "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
25
+ def hashable_by(field, type: :hex, length: 16, alphabet: nil)
26
+ self.hashable_field = field.to_sym
27
+ self.hashable_type = type.to_sym
28
+ self.hashable_length = length.to_i
29
+ self.hashable_alphabet = alphabet
30
+
31
+ validate_hashable_options!
32
+ before_create :assign_hashable_value
33
+
34
+ define_method("regenerate_#{hashable_field}!") do
35
+ update!(self.class.hashable_field => self.class.generate_hashable_value)
36
+ end
37
+ end
38
+ end
39
+
40
+ class_methods do
41
+ # Generate a new random value using the configured type/length/alphabet.
42
+ def generate_hashable_value
43
+ case hashable_type
44
+ when :hex then SecureRandom.hex(hashable_length)
45
+ when :uuid then SecureRandom.uuid
46
+ when :integer then SecureRandom.random_number(10**hashable_length).to_s.rjust(hashable_length, "0").to_i
47
+ when :custom then Array.new(hashable_length) { hashable_alphabet[SecureRandom.random_number(hashable_alphabet.size)] }.join
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def validate_hashable_options!
54
+ unless column_names.include?(hashable_field.to_s)
55
+ raise ArgumentError, "ConcernsOnRails::Hashable: hashable_field '#{hashable_field}' does not exist in the database"
56
+ end
57
+
58
+ unless VALID_TYPES.include?(hashable_type)
59
+ raise ArgumentError, "ConcernsOnRails::Hashable: unknown type '#{hashable_type}'. Valid types: #{VALID_TYPES.join(', ')}"
60
+ end
61
+
62
+ return unless hashable_type == :custom && (!hashable_alphabet.is_a?(String) || hashable_alphabet.empty?)
63
+
64
+ raise ArgumentError, "ConcernsOnRails::Hashable: type :custom requires a non-empty alphabet: String"
65
+ end
66
+ end
67
+
68
+ # Assigns the generated value only when the field is blank,
69
+ # so callers can still pass an explicit value at create time.
70
+ def assign_hashable_value
71
+ field = self.class.hashable_field
72
+ return if self[field].present?
73
+
74
+ self[field] = self.class.generate_hashable_value
75
+ end
76
+ end
77
+ end
@@ -8,7 +8,7 @@ module ConcernsOnRails
8
8
  class_attribute :publishable_field, instance_accessor: false, default: :published_at
9
9
 
10
10
  scope :published, -> { where(arel_table[publishable_field].lteq(Time.zone.now)) }
11
- scope :unpublished, -> {
11
+ scope :unpublished, lambda {
12
12
  where(arel_table[publishable_field].eq(nil).or(arel_table[publishable_field].gt(Time.zone.now)))
13
13
  }
14
14
  end
@@ -17,9 +17,9 @@ module ConcernsOnRails
17
17
  def publishable_by(field = nil)
18
18
  self.publishable_field = field || :published_at
19
19
 
20
- unless column_names.include?(publishable_field.to_s)
21
- raise ArgumentError, "ConcernsOnRails::Publishable: publishable_field '#{publishable_field}' does not exist in the database"
22
- end
20
+ return if column_names.include?(publishable_field.to_s)
21
+
22
+ raise ArgumentError, "ConcernsOnRails::Publishable: publishable_field '#{publishable_field}' does not exist in the database"
23
23
  end
24
24
  end
25
25
 
@@ -44,6 +44,7 @@ module ConcernsOnRails
44
44
  def published?
45
45
  value = self[self.class.publishable_field]
46
46
  return false unless value.present?
47
+
47
48
  value.respond_to?(:<=) ? value <= Time.zone.now : true
48
49
  end
49
50
 
@@ -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
@@ -12,6 +12,7 @@ module ConcernsOnRails
12
12
  self.sluggable_field ||= :name
13
13
 
14
14
  extend FriendlyId
15
+
15
16
  # we need use a lambda to access the instance variable
16
17
  # instead of friendly_id :slug_source, use: :slugged
17
18
  friendly_id :slug_source, use: :slugged
@@ -37,11 +38,12 @@ module ConcernsOnRails
37
38
  end
38
39
 
39
40
  private
41
+
40
42
  # Validate sluggable_field exists in database
41
43
  def validate_sluggable_field!
42
- unless column_names.include?(sluggable_field.to_s)
43
- raise ArgumentError, "ConcernsOnRails::Sluggable: sluggable_field '#{sluggable_field}' does not exist in the database"
44
- end
44
+ return if column_names.include?(sluggable_field.to_s)
45
+
46
+ raise ArgumentError, "ConcernsOnRails::Sluggable: sluggable_field '#{sluggable_field}' does not exist in the database"
45
47
  end
46
48
  end
47
49
 
@@ -55,4 +57,4 @@ module ConcernsOnRails
55
57
  respond_to?(field) ? send(field) : to_s
56
58
  end
57
59
  end
58
- end
60
+ end
@@ -8,14 +8,13 @@ module ConcernsOnRails
8
8
  # declare class attributes and set default values
9
9
  class_attribute :soft_delete_field, instance_accessor: false, default: :deleted_at
10
10
  class_attribute :soft_delete_touch, instance_accessor: false, default: true
11
-
11
+
12
12
  # scopes
13
13
  scope :active, -> { unscope(where: soft_delete_field).where(soft_delete_field => nil) }
14
14
  scope :without_deleted, -> { unscope(where: soft_delete_field).where(soft_delete_field => nil) }
15
15
  scope :soft_deleted, -> { unscope(where: soft_delete_field).where.not(soft_delete_field => nil) }
16
16
  # Optionally, uncomment to hide deleted by default:
17
17
  default_scope { without_deleted }
18
-
19
18
  end
20
19
 
21
20
  class_methods do
@@ -26,9 +25,9 @@ module ConcernsOnRails
26
25
  self.soft_delete_field = field || :deleted_at
27
26
  self.soft_delete_touch = touch
28
27
 
29
- unless column_names.include?(soft_delete_field.to_s)
30
- raise ArgumentError, "ConcernsOnRails::SoftDeletable: soft_delete_field '#{soft_delete_field}' does not exist in the database"
31
- end
28
+ return if column_names.include?(soft_delete_field.to_s)
29
+
30
+ raise ArgumentError, "ConcernsOnRails::SoftDeletable: soft_delete_field '#{soft_delete_field}' does not exist in the database"
32
31
  end
33
32
 
34
33
  # Override destroy_all to perform soft delete on all records
@@ -50,24 +49,26 @@ module ConcernsOnRails
50
49
 
51
50
  def soft_delete!
52
51
  return true if deleted?
52
+
53
53
  before_soft_delete
54
54
  result = if self.class.soft_delete_touch
55
- update(self.class.soft_delete_field => Time.zone.now)
56
- else
57
- update_column(self.class.soft_delete_field, Time.zone.now)
58
- end
55
+ update(self.class.soft_delete_field => Time.zone.now)
56
+ else
57
+ update_column(self.class.soft_delete_field, Time.zone.now)
58
+ end
59
59
  after_soft_delete if result
60
60
  result
61
61
  end
62
62
 
63
63
  def restore!
64
64
  return true unless deleted?
65
+
65
66
  before_restore
66
67
  result = if self.class.soft_delete_touch
67
- update(self.class.soft_delete_field => nil)
68
- else
69
- update_column(self.class.soft_delete_field, nil)
70
- end
68
+ update(self.class.soft_delete_field => nil)
69
+ else
70
+ update_column(self.class.soft_delete_field, nil)
71
+ end
71
72
  after_restore if result
72
73
  result
73
74
  end
@@ -77,15 +78,15 @@ module ConcernsOnRails
77
78
  self.class.unscoped.where(self.class.primary_key => id).delete_all
78
79
  freeze
79
80
  end
80
-
81
+
81
82
  def deleted?
82
83
  self[self.class.soft_delete_field].present?
83
84
  end
84
85
 
85
86
  # alias methods
86
87
  # define here to avoid issue: undefined method `deleted?' for module `ConcernsOnRails::SoftDeletable'
87
- alias_method :is_soft_deleted?, :deleted?
88
- alias_method :soft_deleted?, :deleted?
88
+ alias is_soft_deleted? deleted?
89
+ alias soft_deleted? deleted?
89
90
 
90
91
  def is_really_deleted?
91
92
  !self.class.unscoped.exists?(id)
@@ -26,6 +26,7 @@ module ConcernsOnRails
26
26
  unless column_names.include?(sortable_field.to_s)
27
27
  raise ArgumentError, "#{name}: '#{sortable_field}' column not found. Call `sortable_by :your_column` to configure the sort field."
28
28
  end
29
+
29
30
  order(sortable_field => sortable_direction)
30
31
  end
31
32
  end
@@ -40,7 +41,9 @@ module ConcernsOnRails
40
41
  # sortable_by position: :desc
41
42
  #
42
43
  # sortable_by :position, use_acts_as_list: false
43
- def sortable_by(field_config, use_acts_as_list: true)
44
+ def sortable_by(field_config = nil, use_acts_as_list: true, **field_options)
45
+ field_config = field_options if field_config.nil? && field_options.any?
46
+
44
47
  # parse field_config
45
48
  field, direction = parse_sortable_config(field_config)
46
49
 
@@ -57,6 +60,7 @@ module ConcernsOnRails
57
60
  end
58
61
 
59
62
  private
63
+
60
64
  def parse_sortable_config(config)
61
65
  if config.is_a?(Hash)
62
66
  # extract key and value
@@ -71,9 +75,9 @@ module ConcernsOnRails
71
75
 
72
76
  # Validate sortable_field exists in database
73
77
  def validate_sortable_field!
74
- unless column_names.include?(sortable_field.to_s)
75
- raise ArgumentError, "ConcernsOnRails::Sortable: sortable_field '#{sortable_field}' does not exist in the database"
76
- end
78
+ return if column_names.include?(sortable_field.to_s)
79
+
80
+ raise ArgumentError, "ConcernsOnRails::Sortable: sortable_field '#{sortable_field}' does not exist in the database"
77
81
  end
78
82
  end
79
83
  end
@@ -1,3 +1,3 @@
1
1
  module ConcernsOnRails
2
- VERSION = "1.2.0"
2
+ VERSION = "1.4.2".freeze
3
3
  end
@@ -4,6 +4,8 @@ require "concerns_on_rails/sortable"
4
4
  require "concerns_on_rails/publishable"
5
5
  require "concerns_on_rails/sluggable"
6
6
  require "concerns_on_rails/soft_deletable"
7
+ require "concerns_on_rails/hashable"
8
+ require "concerns_on_rails/schedulable"
7
9
 
8
10
  module ConcernsOnRails
9
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.2.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-10 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
@@ -70,7 +70,9 @@ files:
70
70
  - CODE_OF_CONDUCT.md
71
71
  - README.md
72
72
  - lib/concerns_on_rails.rb
73
+ - lib/concerns_on_rails/hashable.rb
73
74
  - lib/concerns_on_rails/publishable.rb
75
+ - lib/concerns_on_rails/schedulable.rb
74
76
  - lib/concerns_on_rails/sluggable.rb
75
77
  - lib/concerns_on_rails/soft_deletable.rb
76
78
  - lib/concerns_on_rails/sortable.rb
@@ -90,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
92
  requirements:
91
93
  - - ">="
92
94
  - !ruby/object:Gem::Version
93
- version: 2.7.0
95
+ version: 3.2.0
94
96
  required_rubygems_version: !ruby/object:Gem::Requirement
95
97
  requirements:
96
98
  - - ">="