concerns_on_rails 1.10.0 โ 1.11.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/README.md +69 -4
- data/lib/concerns_on_rails/legacy_aliases.rb +1 -0
- data/lib/concerns_on_rails/models/sequenceable.rb +135 -0
- data/lib/concerns_on_rails/support/sequence_calculator.rb +74 -0
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a34c6e186b582806890012bb0f8bf049c1fd0f56f74007af7669cdb5ae713eb2
|
|
4
|
+
data.tar.gz: 298f957409be23ecfd9bbd0de46161ac56af7a01a6a300d29cf3bff9ca2b94d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a96e094791d487cd9a534e4263de4bd902af9b533a8607ceb20906d4b38827c82ee0b370cb885d8a5800aef8a03a9b84cc718dc00074da1c0b1f35ade4fc2b3
|
|
7
|
+
data.tar.gz: e803d31da5694c6ad1a0ea28d928f9d552e75ded84420aa94cb816538e4cbe9b4a228f2b9dfa9428a8091b369444909d750523d2c5a0e9edae1f2df6fec95a78
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> ๐ป๐ณ **Hoร ng Sa and Trฦฐแปng Sa belong to Viแปt Nam.**
|
|
4
4
|
|
|
5
|
-
A plug-and-play collection of reusable ActiveSupport concerns for Rails **models** and **controllers** โ slugs, soft delete, scheduled publish, expiry, pagination, filtering, JSON envelopes, and more. One `include`, one declarative macro, done.
|
|
5
|
+
A plug-and-play collection of reusable ActiveSupport concerns for Rails **models** and **controllers** โ slugs, soft delete, scheduled publish, expiry, sequential reference numbers, pagination, filtering, JSON envelopes, and more. One `include`, one declarative macro, done.
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
class Article < ApplicationRecord
|
|
@@ -36,6 +36,7 @@ Article.published.without_deleted.find("hello-world")
|
|
|
36
36
|
- [Searchable](#-searchable) โ LIKE/ILIKE search across configured columns
|
|
37
37
|
- [Activatable](#-activatable) โ boolean active/inactive toggle
|
|
38
38
|
- [Tokenizable](#-tokenizable) โ security tokens with timing-safe lookup
|
|
39
|
+
- [Sequenceable](#-sequenceable) โ ordered, human-friendly reference numbers
|
|
39
40
|
- [Stateable](#-stateable) โ lightweight string-backed state machine
|
|
40
41
|
- [Addressable](#-addressable) โ postal address normalization + format validation
|
|
41
42
|
- **Controller concerns**
|
|
@@ -54,7 +55,7 @@ Article.published.without_deleted.find("hello-world")
|
|
|
54
55
|
|
|
55
56
|
## โจ Why this gem?
|
|
56
57
|
|
|
57
|
-
- **
|
|
58
|
+
- **Fourteen model concerns + six controller concerns**, all production-ready
|
|
58
59
|
- **One include, one macro** โ no boilerplate, no glue code
|
|
59
60
|
- **Lean dependencies** โ only `acts_as_list` (Sortable) and `friendly_id` (Sluggable); controller concerns have zero extra deps
|
|
60
61
|
- **Schema-validated configuration** โ every macro checks that the configured column exists and raises `ArgumentError` early
|
|
@@ -67,7 +68,7 @@ Article.published.without_deleted.find("hello-world")
|
|
|
67
68
|
Add to your application's `Gemfile`:
|
|
68
69
|
|
|
69
70
|
```ruby
|
|
70
|
-
gem "concerns_on_rails", "~> 1.
|
|
71
|
+
gem "concerns_on_rails", "~> 1.11"
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
Or pull the latest from GitHub:
|
|
@@ -578,6 +579,70 @@ User.authenticate_by_api_token(token) # timing-safe; returns user or nil
|
|
|
578
579
|
|
|
579
580
|
---
|
|
580
581
|
|
|
582
|
+
## ๐งพ Sequenceable
|
|
583
|
+
|
|
584
|
+
Ordered, human-friendly reference numbers โ invoice numbers, order numbers, ticket IDs, support cases. Unlike the *random* identifiers from [Hashable](#-hashable) / [Tokenizable](#-tokenizable), `Sequenceable` produces *sequential* ones backed by an integer column that is the source of truth.
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
class Invoice < ApplicationRecord
|
|
588
|
+
include ConcernsOnRails::Sequenceable
|
|
589
|
+
|
|
590
|
+
sequenceable_by :sequence, # integer column โ the source of truth
|
|
591
|
+
into: :number, # optional string column for the formatted value
|
|
592
|
+
prefix: "INV-",
|
|
593
|
+
padding: 5,
|
|
594
|
+
scope: :account_id, # one independent counter per account
|
|
595
|
+
reset: :year # restart numbering each calendar year
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
invoice = Invoice.create!(account_id: 1)
|
|
599
|
+
invoice.sequence # => 1, 2, 3 ... (per account, per year)
|
|
600
|
+
invoice.number # => "INV-2026-00001"
|
|
601
|
+
invoice.formatted_sequence # => "INV-2026-00001"
|
|
602
|
+
|
|
603
|
+
Invoice.next_sequence(account_id: 1) # => 4 (peek the next value, without creating)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Options**
|
|
607
|
+
|
|
608
|
+
| Option | Default | Purpose |
|
|
609
|
+
|----------------------|-------------|------------------------------------------------------------------------------------------|
|
|
610
|
+
| `field` (positional) | `:sequence` | Integer column holding the sequence โ the source of truth. |
|
|
611
|
+
| `into:` | `nil` | String column to persist the formatted reference into (immutable display value). |
|
|
612
|
+
| `prefix:` | `""` | Prepended to the formatted value. |
|
|
613
|
+
| `padding:` | `0` | Zero-pad width of the numeric portion (`0` = no padding). |
|
|
614
|
+
| `separator:` | `"-"` | Joins prefix / period token / number in the default format. |
|
|
615
|
+
| `start_at:` | `1` | First value when the scope/period has no rows yet. |
|
|
616
|
+
| `scope:` | `nil` | Column (or array of columns) the counter is scoped to โ e.g. one sequence per `account_id`. |
|
|
617
|
+
| `reset:` | `:never` | `:never` / `:year` / `:month` / `:day` โ restart numbering each period (needs `created_at`). |
|
|
618
|
+
| `template:` | `nil` | `->(seq, record) { ... }` full custom formatter; overrides `prefix` / `padding` / period. |
|
|
619
|
+
|
|
620
|
+
**Default format**
|
|
621
|
+
|
|
622
|
+
| `reset:` | Example | Shape |
|
|
623
|
+
|-----------|-----------------------|----------------------------------|
|
|
624
|
+
| `:never` | `INV-00001` | `prefix + padded` |
|
|
625
|
+
| `:year` | `INV-2026-00001` | `prefix + YYYY + sep + padded` |
|
|
626
|
+
| `:month` | `INV-202606-00001` | `prefix + YYYYMM + sep + padded` |
|
|
627
|
+
| `:day` | `INV-20260604-00001` | `prefix + YYYYMMDD + sep + padded` |
|
|
628
|
+
|
|
629
|
+
**Generated API**
|
|
630
|
+
|
|
631
|
+
| Method | What it does |
|
|
632
|
+
|-----------------------------------|---------------------------------------------------------------------------------------|
|
|
633
|
+
| `formatted_<field>` | The formatted string โ the persisted `into:` value when set, otherwise computed. |
|
|
634
|
+
| `Model.next_<field>(scope_attrs)` | Peek the next integer for a scope without creating a record. |
|
|
635
|
+
|
|
636
|
+
**Notes**
|
|
637
|
+
- The next value is `MAX(<field>) + 1` within the scope (and period), so numbering is dense and ordered โ not random.
|
|
638
|
+
- Caller-supplied values are respected: `Invoice.create!(sequence: 100)` is not overwritten (and its `into:` string is still formatted from `100`).
|
|
639
|
+
- Generation reads `MAX` then inserts, so two concurrent inserts can race. It's **best-effort** โ add a **scoped unique index** on `<field>` (and on `into:`) for a real guarantee, the same way you would for any `MAX`-based numbering.
|
|
640
|
+
- `reset:` requires a `created_at` column; the period is taken from each row's creation time.
|
|
641
|
+
- For fixed-width display (`00042`), make the `into:` column a **string** โ integer columns drop leading zeros.
|
|
642
|
+
- Distinct from `Hashable` / `Tokenizable`, which generate *random* values; reach for those when the identifier must be unguessable.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
581
646
|
## ๐ Stateable
|
|
582
647
|
|
|
583
648
|
Lightweight string-backed state machine โ the 80% of AASM without the dependency.
|
|
@@ -956,7 +1021,7 @@ Both forms reference the same module, so you can freely mix them.
|
|
|
956
1021
|
bundle install # install dev dependencies
|
|
957
1022
|
bundle exec rspec # run the test suite
|
|
958
1023
|
gem build concerns_on_rails.gemspec # build the gem
|
|
959
|
-
gem install ./concerns_on_rails-1.
|
|
1024
|
+
gem install ./concerns_on_rails-1.11.0.gem # install locally
|
|
960
1025
|
```
|
|
961
1026
|
|
|
962
1027
|
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,135 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
|
|
3
|
+
module ConcernsOnRails
|
|
4
|
+
module Models
|
|
5
|
+
# Generates ordered, human-friendly sequential reference numbers โ invoice
|
|
6
|
+
# numbers, order numbers, ticket numbers, support cases. Unlike Hashable /
|
|
7
|
+
# Tokenizable (which produce *random* identifiers), Sequenceable produces
|
|
8
|
+
# *ordered* ones backed by an integer column that is the source of truth.
|
|
9
|
+
#
|
|
10
|
+
# class Invoice < ApplicationRecord
|
|
11
|
+
# include ConcernsOnRails::Sequenceable
|
|
12
|
+
#
|
|
13
|
+
# sequenceable_by :sequence, # integer column โ source of truth
|
|
14
|
+
# into: :number, # optional string column for the formatted value
|
|
15
|
+
# prefix: "INV-",
|
|
16
|
+
# padding: 5,
|
|
17
|
+
# scope: :account_id, # one counter per account
|
|
18
|
+
# reset: :year # restart numbering each calendar year
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# invoice = Invoice.create!(account_id: 1)
|
|
22
|
+
# invoice.sequence # => 1, 2, 3 ... (per account, per year)
|
|
23
|
+
# invoice.number # => "INV-2026-00001"
|
|
24
|
+
# invoice.formatted_sequence # => "INV-2026-00001"
|
|
25
|
+
# Invoice.next_sequence(account_id: 1) # peek the next value without creating
|
|
26
|
+
#
|
|
27
|
+
# The integer is computed as MAX(field) within the scope (+ period) + 1, so
|
|
28
|
+
# numbering is dense and ordered. Generation is best-effort under concurrency
|
|
29
|
+
# โ pair the column(s) with a scoped unique DB index for a real guarantee.
|
|
30
|
+
module Sequenceable
|
|
31
|
+
extend ActiveSupport::Concern
|
|
32
|
+
|
|
33
|
+
RESET_PERIODS = %i[never year month day].freeze
|
|
34
|
+
MAX_GENERATION_ATTEMPTS = 10
|
|
35
|
+
NAME = "ConcernsOnRails::Models::Sequenceable".freeze
|
|
36
|
+
|
|
37
|
+
included do
|
|
38
|
+
class_attribute :sequenceable_config, instance_accessor: false, default: {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class_methods do
|
|
42
|
+
include ConcernsOnRails::Support::ColumnGuard
|
|
43
|
+
include ConcernsOnRails::Support::SequenceCalculator
|
|
44
|
+
|
|
45
|
+
# Configure a sequenceable field.
|
|
46
|
+
#
|
|
47
|
+
# Options:
|
|
48
|
+
# into: string column to persist the formatted value into (default nil)
|
|
49
|
+
# prefix: string prepended to the formatted value (default "")
|
|
50
|
+
# padding: zero-pad width of the numeric portion (default 0 = no padding)
|
|
51
|
+
# separator: joins prefix / period token / number in the default format (default "-")
|
|
52
|
+
# start_at: first value per scope/period when no rows exist yet (default 1)
|
|
53
|
+
# scope: column or array of columns the counter is scoped to (default nil)
|
|
54
|
+
# reset: :never (default) | :year | :month | :day โ restart per period (needs created_at)
|
|
55
|
+
# template: ->(seq, record) { ... } full custom formatter; overrides prefix/padding/period
|
|
56
|
+
def sequenceable_by(field = :sequence, into: nil, prefix: "", padding: 0,
|
|
57
|
+
separator: "-", start_at: 1, scope: nil, reset: :never, template: nil)
|
|
58
|
+
field = field.to_sym
|
|
59
|
+
into = into&.to_sym
|
|
60
|
+
reset = reset.to_sym
|
|
61
|
+
scope_cols = Array(scope).map(&:to_sym)
|
|
62
|
+
|
|
63
|
+
ensure_columns!(NAME, field)
|
|
64
|
+
ensure_columns!(NAME, into) if into
|
|
65
|
+
ensure_columns!(NAME, *scope_cols) unless scope_cols.empty?
|
|
66
|
+
ensure_columns!(NAME, :created_at) unless reset == :never
|
|
67
|
+
validate_sequenceable_options!(reset, template)
|
|
68
|
+
|
|
69
|
+
self.sequenceable_config = sequenceable_config.merge(
|
|
70
|
+
field => { into: into, prefix: prefix.to_s, padding: padding.to_i,
|
|
71
|
+
separator: separator.to_s, start_at: start_at.to_i,
|
|
72
|
+
scope: scope_cols, reset: reset, template: template }
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
before_create -> { assign_sequenceable_value(field) }
|
|
76
|
+
define_sequenceable_methods(field)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class_methods do
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def define_sequenceable_methods(field)
|
|
84
|
+
define_method("formatted_#{field}") do
|
|
85
|
+
cfg = self.class.sequenceable_config.fetch(field)
|
|
86
|
+
return self[cfg[:into]] if cfg[:into] && self[cfg[:into]].present?
|
|
87
|
+
return nil if self[field].blank?
|
|
88
|
+
|
|
89
|
+
self.class.send(:format_sequence, field, self[field], self)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
define_singleton_method("next_#{field}") do |scope_attrs = {}|
|
|
93
|
+
sequence_base_value(field, nil, scope_attrs)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_sequenceable_options!(reset, template)
|
|
98
|
+
unless RESET_PERIODS.include?(reset)
|
|
99
|
+
raise ArgumentError, "#{NAME}: unknown reset '#{reset}'. Valid values: #{RESET_PERIODS.join(', ')}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
return if template.nil? || template.respond_to?(:call)
|
|
103
|
+
|
|
104
|
+
raise ArgumentError, "#{NAME}: template must be callable (respond to #call)"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Assigns the sequence (and, when configured, the formatted string) only when
|
|
109
|
+
# the integer column is blank, so callers can pass an explicit value. The
|
|
110
|
+
# increment-until-free loop is a best-effort guard against pre-taken values;
|
|
111
|
+
# a scoped unique index is the real concurrency guarantee.
|
|
112
|
+
def assign_sequenceable_value(field)
|
|
113
|
+
cfg = self.class.sequenceable_config.fetch(field)
|
|
114
|
+
|
|
115
|
+
if self[field].blank?
|
|
116
|
+
candidate = self.class.send(:sequence_base_value, field, self, {})
|
|
117
|
+
attempts = 0
|
|
118
|
+
while self.class.send(:sequence_value_taken?, field, candidate, self, {})
|
|
119
|
+
attempts += 1
|
|
120
|
+
if attempts >= MAX_GENERATION_ATTEMPTS
|
|
121
|
+
raise "#{NAME}: could not find a free value for '#{field}' after " \
|
|
122
|
+
"#{MAX_GENERATION_ATTEMPTS} attempts โ add a scoped unique index"
|
|
123
|
+
end
|
|
124
|
+
candidate += 1
|
|
125
|
+
end
|
|
126
|
+
self[field] = candidate
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return unless cfg[:into] && self[cfg[:into]].blank?
|
|
130
|
+
|
|
131
|
+
self[cfg[:into]] = self.class.send(:format_sequence, field, self[field], self)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module ConcernsOnRails
|
|
2
|
+
module Support
|
|
3
|
+
# Internal helpers for Models::Sequenceable: computing the next value within a
|
|
4
|
+
# scope (+ period) and formatting it. Mixed into the model's class methods, so
|
|
5
|
+
# `self` is the model class and `unscoped` / `sequenceable_config` resolve
|
|
6
|
+
# against it. Kept here to keep the concern itself focused on configuration.
|
|
7
|
+
module SequenceCalculator
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Next integer that would be assigned for the given scope: MAX within the
|
|
11
|
+
# scope (+ period) + 1, or start_at when the scope/period is still empty.
|
|
12
|
+
def sequence_base_value(field, record, scope_attrs)
|
|
13
|
+
cfg = sequenceable_config.fetch(field)
|
|
14
|
+
max = sequence_relation(field, record, scope_attrs).maximum(field)
|
|
15
|
+
max ? max + 1 : cfg[:start_at]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sequence_value_taken?(field, candidate, record, scope_attrs)
|
|
19
|
+
sequence_relation(field, record, scope_attrs).exists?(field => candidate)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Relation of existing rows that share this record's scope (and period, when
|
|
23
|
+
# reset is enabled). Reads from `unscoped` so a model's default_scope never
|
|
24
|
+
# hides rows the counter must account for.
|
|
25
|
+
def sequence_relation(field, record, scope_attrs)
|
|
26
|
+
cfg = sequenceable_config.fetch(field)
|
|
27
|
+
rel = unscoped
|
|
28
|
+
|
|
29
|
+
cfg[:scope].each do |col|
|
|
30
|
+
value = record ? record[col] : (scope_attrs[col] || scope_attrs[col.to_s])
|
|
31
|
+
rel = rel.where(col => value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return rel if cfg[:reset] == :never
|
|
35
|
+
|
|
36
|
+
rel.where(created_at: period_range(cfg[:reset], base_time(record)))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_sequence(field, seq, record)
|
|
40
|
+
cfg = sequenceable_config.fetch(field)
|
|
41
|
+
return cfg[:template].call(seq, record) if cfg[:template]
|
|
42
|
+
|
|
43
|
+
padded = cfg[:padding].positive? ? seq.to_s.rjust(cfg[:padding], "0") : seq.to_s
|
|
44
|
+
return "#{cfg[:prefix]}#{padded}" if cfg[:reset] == :never
|
|
45
|
+
|
|
46
|
+
token = period_token(cfg[:reset], base_time(record))
|
|
47
|
+
"#{cfg[:prefix]}#{token}#{cfg[:separator]}#{padded}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def period_range(reset, time)
|
|
51
|
+
case reset
|
|
52
|
+
when :year then time.beginning_of_year..time.end_of_year
|
|
53
|
+
when :month then time.beginning_of_month..time.end_of_month
|
|
54
|
+
when :day then time.beginning_of_day..time.end_of_day
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def period_token(reset, time)
|
|
59
|
+
case reset
|
|
60
|
+
when :year then time.year.to_s
|
|
61
|
+
when :month then time.strftime("%Y%m")
|
|
62
|
+
when :day then time.strftime("%Y%m%d")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# created_at is the natural anchor for the period, but it may not be set yet
|
|
67
|
+
# during before_create โ fall back to the current time, which is what the
|
|
68
|
+
# timestamp will resolve to anyway.
|
|
69
|
+
def base_time(record)
|
|
70
|
+
record&.created_at || Time.current
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/concerns_on_rails.rb
CHANGED
|
@@ -11,6 +11,7 @@ end
|
|
|
11
11
|
require "concerns_on_rails/support/column_guard"
|
|
12
12
|
require "concerns_on_rails/support/random_value"
|
|
13
13
|
require "concerns_on_rails/support/address_data"
|
|
14
|
+
require "concerns_on_rails/support/sequence_calculator"
|
|
14
15
|
|
|
15
16
|
# Model concerns
|
|
16
17
|
require "concerns_on_rails/models/sluggable"
|
|
@@ -26,6 +27,7 @@ require "concerns_on_rails/models/activatable"
|
|
|
26
27
|
require "concerns_on_rails/models/tokenizable"
|
|
27
28
|
require "concerns_on_rails/models/stateable"
|
|
28
29
|
require "concerns_on_rails/models/addressable"
|
|
30
|
+
require "concerns_on_rails/models/sequenceable"
|
|
29
31
|
|
|
30
32
|
# Controller concerns
|
|
31
33
|
require "concerns_on_rails/controllers/paginatable"
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Nguyen
|
|
@@ -85,6 +85,7 @@ files:
|
|
|
85
85
|
- lib/concerns_on_rails/models/publishable.rb
|
|
86
86
|
- lib/concerns_on_rails/models/schedulable.rb
|
|
87
87
|
- lib/concerns_on_rails/models/searchable.rb
|
|
88
|
+
- lib/concerns_on_rails/models/sequenceable.rb
|
|
88
89
|
- lib/concerns_on_rails/models/sluggable.rb
|
|
89
90
|
- lib/concerns_on_rails/models/soft_deletable.rb
|
|
90
91
|
- lib/concerns_on_rails/models/sortable.rb
|
|
@@ -93,6 +94,7 @@ files:
|
|
|
93
94
|
- lib/concerns_on_rails/support/address_data.rb
|
|
94
95
|
- lib/concerns_on_rails/support/column_guard.rb
|
|
95
96
|
- lib/concerns_on_rails/support/random_value.rb
|
|
97
|
+
- lib/concerns_on_rails/support/sequence_calculator.rb
|
|
96
98
|
- lib/concerns_on_rails/version.rb
|
|
97
99
|
homepage: https://github.com/VSN2015/concerns_on_rails
|
|
98
100
|
licenses:
|