concerns_on_rails 1.9.0 → 1.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d01db047d91e470c1c2bb03f6fbe95be1eea7592199d9a548b6ac5dbacc84452
4
- data.tar.gz: f47e6149addbee52badc7ae4d56fbc66c636adc75d5a32f7742a57d18eda1cd8
3
+ metadata.gz: 743380a3be200eda289e97edef227f8b900550ba08af52c4a06b995fc5c8297a
4
+ data.tar.gz: 7e8e8cb4bfd923819ec96178d75e68378829fb0f80b5a2e3cf3cd84e150fcc32
5
5
  SHA512:
6
- metadata.gz: 1a856b89dd65fc126612d33aa276cfe49949484594a6d9c1cca2af01e91474883b1eb170e3f36647a125375ed645914cff6a1564cac3f955e2d7066a7c3c0e37
7
- data.tar.gz: 2056b850ffddb607084f672018498b4f366a0ab7c0f97bf22e806355031eae146c256f2f62c4bc3ef5e769ee75a3bd05d2bfc56864a5994ea9b204a1a9b730ab
6
+ metadata.gz: 48e77b0bb95ee7419207289be7e1f74166f43aa4e31f4af6a6269ac2a84ccb90ac38e81a2a11a44cdafb7bd678d48153070c41d502b37f0a6baeabfd114f73f0
7
+ data.tar.gz: bb03d4bcfb6b26b7963f70cdaee8e591293fc8f96e34aa636aab84df6ce20647ae9df01e3d52554c8b6a9cb52cfce68c2da2c0f967ede2a3ec7960be737f4bd2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  <!-- CHANGELOG.md -->
2
2
 
3
+ ## 1.10.0 (2026-06-03)
4
+
5
+ ### Added
6
+ - **Models::Addressable**: Declarative postal-address normalization + format validation via a single `addressable_by` macro. Maps the canonical parts (`line1` / `line2` / `city` / `state` / `postal_code` / `country`) onto real columns — any subset works, missing columns are skipped, and required parts are schema-checked. Normalizes in `before_validation` (strip + squish, postal-code upcasing with canonical CA spacing, 2-letter country/state codes upcased) and validates required-part presence, ISO 3166-1 alpha-2 country codes, per-country postal formats (US/CA/GB/AU/DE/FR + a permissive fallback), and opt-in US/CA state codes. Offline and dependency-free; layer real deliverability checks via the opt-in `verify_with:` callable. Adds helpers `full_address`, `address_lines`, `address_present?`, `address_complete?`, and `address_attributes`.
7
+
8
+ ### Internal
9
+ - Added `ConcernsOnRails::Support::AddressData` — ISO 3166-1 alpha-2 country codes, per-country postal-format patterns, US state / CA province sets, and the case-insensitive lookups (`valid_country?`, `postal_format_for`, `valid_state?`, `normalize_postal`) backing the Addressable concern.
10
+
3
11
  ## 1.9.0 (2026-05-25)
4
12
 
5
13
  ### Added
data/README.md CHANGED
@@ -37,6 +37,7 @@ Article.published.without_deleted.find("hello-world")
37
37
  - [Activatable](#-activatable) — boolean active/inactive toggle
38
38
  - [Tokenizable](#-tokenizable) — security tokens with timing-safe lookup
39
39
  - [Stateable](#-stateable) — lightweight string-backed state machine
40
+ - [Addressable](#-addressable) — postal address normalization + format validation
40
41
  - **Controller concerns**
41
42
  - [Paginatable](#-paginatable) — offset pagination with headers
42
43
  - [Filterable](#-filterable) — declarative URL-param filters
@@ -53,7 +54,7 @@ Article.published.without_deleted.find("hello-world")
53
54
 
54
55
  ## ✨ Why this gem?
55
56
 
56
- - **Twelve model concerns + six controller concerns**, all production-ready
57
+ - **Thirteen model concerns + six controller concerns**, all production-ready
57
58
  - **One include, one macro** — no boilerplate, no glue code
58
59
  - **Lean dependencies** — only `acts_as_list` (Sortable) and `friendly_id` (Sluggable); controller concerns have zero extra deps
59
60
  - **Schema-validated configuration** — every macro checks that the configured column exists and raises `ArgumentError` early
@@ -630,6 +631,77 @@ stateable_by :state, states: %i[open closed], prefix: true
630
631
 
631
632
  ---
632
633
 
634
+ ## 🏠 Addressable
635
+
636
+ Normalize and format-validate a postal address spread across several columns — one macro for whitespace cleanup, postal-code and ISO country-code checks, required-part presence, and a `full_address` helper. No external geocoding service required.
637
+
638
+ ```ruby
639
+ class Location < ApplicationRecord
640
+ include ConcernsOnRails::Addressable
641
+
642
+ addressable_by # standard columns: line1, line2, city, state, postal_code, country
643
+ end
644
+
645
+ loc = Location.create(line1: " 1 Infinite Loop ", city: "Cupertino",
646
+ state: "ca", postal_code: "95014", country: "us")
647
+ loc.line1 # => "1 Infinite Loop" (stripped + squished)
648
+ loc.state # => "CA" (2-letter code upcased)
649
+ loc.country # => "US"
650
+ loc.full_address # => "1 Infinite Loop, Cupertino, CA, 95014, US"
651
+ ```
652
+
653
+ Map onto your own column names and tune behavior:
654
+
655
+ ```ruby
656
+ class Place < ApplicationRecord
657
+ include ConcernsOnRails::Addressable
658
+
659
+ addressable_by line1: :street, postal_code: :zip, country: :country_code,
660
+ required: %i[line1 city postal_code country],
661
+ default_country: "GB", # country used when the record has none
662
+ validate_state: true, # opt-in US/CA state-code check
663
+ verify_with: ->(rec) { Usps.verify(rec) } # opt-in external verifier
664
+ end
665
+ ```
666
+
667
+ **Options**
668
+
669
+ | Option | Default | Purpose |
670
+ |-------------------|--------------------------------------|---------------------------------------------------------------------|
671
+ | `line1:` … `country:` | same-named columns | Map each canonical part to a real column. Missing columns are skipped. |
672
+ | `required:` | `%i[line1 city postal_code country]` | Parts (by canonical name) that must be present. Each must map to an existing column. |
673
+ | `default_country:`| `"US"` | Country used to pick the postal-code format when the record has no recognized country. |
674
+ | `validate_state:` | `false` | When `true`, validates the state against US / CA code sets. |
675
+ | `verify_with:` | `nil` | A callable for real-world verification (see below). |
676
+
677
+ **What it normalizes** (in `before_validation`)
678
+ - Text parts: `strip` + `squish`.
679
+ - `postal_code`: squish + upcase, with canonical spacing for CA (`A1A1A1` → `A1A 1A1`).
680
+ - `country` / `state`: upcased when they look like a 2-letter code (full names left alone).
681
+
682
+ **What it validates**
683
+ - Presence of every `required:` part.
684
+ - `country`: a 2-letter value must be a real ISO 3166-1 alpha-2 code.
685
+ - `postal_code`: matched against a per-country pattern (US, CA, GB, AU, DE, FR) with a permissive fallback for everything else.
686
+ - `state`: only when `validate_state: true` and the country is US/CA.
687
+
688
+ **External verification (`verify_with:`)** — runs **only after** structural validation passes, so you never spend an API call on an obviously-broken address. The callable receives the record and may either add to `record.errors` itself, or return:
689
+
690
+ | Return value | Effect |
691
+ |-------------------|-------------------------------------------------|
692
+ | `true` / `nil` | success |
693
+ | `false` | adds a generic `:base` error |
694
+ | `String` | added as a `:base` error |
695
+ | `Array` | each element added as a `:base` error |
696
+
697
+ **Notes**
698
+ - Scope is **format/structure only** — it checks shape, not real-world deliverability. Plug a USPS/Google/Smarty client into `verify_with:` for that.
699
+ - Error messages are plain English strings — no host-app i18n setup required.
700
+ - Partial schemas just work: a model without a `line2` (or any other) column simply omits that part.
701
+ - Pairs with [Normalizable](#-normalizable) when you also have non-address fields to clean up.
702
+
703
+ ---
704
+
633
705
  # 🎮 Controller Concerns
634
706
 
635
707
  Pure ActionController + ActiveRecord — **zero extra runtime dependencies** (no Kaminari, Pundit, or Ransack).
@@ -14,4 +14,5 @@ module ConcernsOnRails
14
14
  Activatable = Models::Activatable
15
15
  Tokenizable = Models::Tokenizable
16
16
  Stateable = Models::Stateable
17
+ Addressable = Models::Addressable
17
18
  end
@@ -0,0 +1,212 @@
1
+ require "active_support/concern"
2
+
3
+ module ConcernsOnRails
4
+ module Models
5
+ # Declarative normalization + format validation for a postal address spread
6
+ # across several columns. One macro wires up whitespace cleanup, postal-code
7
+ # and ISO country-code checks, required-part presence, and a `full_address`
8
+ # helper — no external geocoding service required.
9
+ #
10
+ # class Location < ApplicationRecord
11
+ # include ConcernsOnRails::Addressable
12
+ #
13
+ # addressable_by # standard line1/line2/city/state/postal_code/country columns
14
+ # # addressable_by line1: :street, postal_code: :zip, country: :country_code,
15
+ # # required: %i[line1 city postal_code], default_country: "GB",
16
+ # # validate_state: true, verify_with: ->(rec) { Usps.verify(rec) }
17
+ # end
18
+ #
19
+ # Scope is *format/structure* only — it checks shape, not real-world
20
+ # deliverability. Layer a real verifier on via `verify_with:`. Relates to
21
+ # the per-field Normalizable concern.
22
+ module Addressable
23
+ extend ActiveSupport::Concern
24
+
25
+ # Canonical address part => default column name.
26
+ DEFAULT_FIELDS = {
27
+ line1: :line1, line2: :line2, city: :city,
28
+ state: :state, postal_code: :postal_code, country: :country
29
+ }.freeze
30
+
31
+ # Parts required by default (each must map to an existing column).
32
+ DEFAULT_REQUIRED = %i[line1 city postal_code country].freeze
33
+
34
+ included do
35
+ class_attribute :addressable_fields, instance_accessor: false, default: {}
36
+ class_attribute :addressable_required, instance_accessor: false, default: [].freeze
37
+ class_attribute :addressable_default_country, instance_accessor: false, default: "US"
38
+ class_attribute :addressable_validate_state, instance_accessor: false, default: false
39
+ class_attribute :addressable_verifier, instance_accessor: false, default: nil
40
+
41
+ before_validation :normalize_address
42
+ validate :validate_address
43
+ end
44
+
45
+ # Defined as a real module (not `class_methods do`) so the public macro and
46
+ # its private helpers share one `private` and aren't constrained by
47
+ # Metrics/BlockLength. ActiveSupport::Concern auto-extends `ClassMethods`.
48
+ module ClassMethods
49
+ include ConcernsOnRails::Support::ColumnGuard
50
+
51
+ # Configure the address. Column overrides are passed as `part: :column`
52
+ # keyword pairs; everything else tunes behavior. See the module docs.
53
+ def addressable_by(required: DEFAULT_REQUIRED, default_country: "US",
54
+ validate_state: false, verify_with: nil, **mapping)
55
+ self.addressable_fields = resolve_addressable_fields(mapping)
56
+ self.addressable_required = Array(required).map(&:to_sym)
57
+ self.addressable_default_country = default_country.to_s.upcase
58
+ self.addressable_validate_state = validate_state
59
+ self.addressable_verifier = verify_with
60
+ ensure_required_columns!
61
+ end
62
+
63
+ private
64
+
65
+ def resolve_addressable_fields(mapping)
66
+ unknown = mapping.keys.map(&:to_sym) - DEFAULT_FIELDS.keys
67
+ raise ArgumentError, "ConcernsOnRails::Models::Addressable: unknown address part(s): #{unknown.join(', ')}" if unknown.any?
68
+
69
+ overrides = mapping.to_h { |part, column| [part.to_sym, column.to_sym] }
70
+ ensure_columns!("ConcernsOnRails::Models::Addressable", overrides.values)
71
+ DEFAULT_FIELDS.merge(overrides).select { |_part, column| column_names.include?(column.to_s) }
72
+ end
73
+
74
+ def ensure_required_columns!
75
+ missing = addressable_required.reject { |part| addressable_fields.key?(part) }
76
+ return if missing.empty?
77
+
78
+ raise ArgumentError,
79
+ "ConcernsOnRails::Models::Addressable: required address part(s) #{missing.join(', ')} " \
80
+ "have no matching column (table: #{table_name})"
81
+ end
82
+ end
83
+
84
+ # --- Normalization (before_validation) ------------------------------------
85
+
86
+ def normalize_address
87
+ country = resolved_country
88
+ self.class.addressable_fields.each do |part, column|
89
+ value = self[column]
90
+ next unless value.is_a?(String)
91
+
92
+ self[column] = normalize_part(part, country, value)
93
+ end
94
+ end
95
+
96
+ # --- Validation -----------------------------------------------------------
97
+
98
+ def validate_address
99
+ validate_required_parts
100
+ validate_country_code
101
+ validate_postal_code
102
+ validate_state_code if self.class.addressable_validate_state
103
+ run_address_verifier if self.class.addressable_verifier && errors.empty?
104
+ end
105
+
106
+ # --- Public helpers -------------------------------------------------------
107
+
108
+ # The present parts joined into a single line, in canonical order.
109
+ def full_address(separator: ", ")
110
+ address_lines.join(separator)
111
+ end
112
+
113
+ # The present parts as an ordered array (handy for multi-line rendering).
114
+ def address_lines
115
+ ordered_parts.filter_map { |part| self[self.class.addressable_fields[part]].presence }
116
+ end
117
+
118
+ # True when any configured part has a value.
119
+ def address_present?
120
+ ordered_parts.any? { |part| self[self.class.addressable_fields[part]].present? }
121
+ end
122
+
123
+ # True when every required part has a value (presence only, no format check).
124
+ def address_complete?
125
+ self.class.addressable_required.all? do |part|
126
+ (column = self.class.addressable_fields[part]) && self[column].present?
127
+ end
128
+ end
129
+
130
+ # `{ part => value }` for the present parts (handy for serializers / verifiers).
131
+ def address_attributes
132
+ self.class.addressable_fields.each_with_object({}) do |(part, column), acc|
133
+ value = self[column]
134
+ acc[part] = value if value.present?
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def ordered_parts
141
+ DEFAULT_FIELDS.keys.select { |part| self.class.addressable_fields.key?(part) }
142
+ end
143
+
144
+ # The 2-letter country code driving postal/state checks: the record's own
145
+ # country when it's a recognized code, otherwise the configured default.
146
+ def resolved_country
147
+ column = self.class.addressable_fields[:country]
148
+ value = column && self[column]
149
+ return self.class.addressable_default_country unless value.is_a?(String)
150
+
151
+ code = value.strip.upcase
152
+ ConcernsOnRails::Support::AddressData.valid_country?(code) ? code : self.class.addressable_default_country
153
+ end
154
+
155
+ def normalize_part(part, country, value)
156
+ squished = value.strip.squish
157
+ case part
158
+ when :postal_code then ConcernsOnRails::Support::AddressData.normalize_postal(country, value)
159
+ when :country, :state then squished.length == 2 ? squished.upcase : squished
160
+ else squished
161
+ end
162
+ end
163
+
164
+ def validate_required_parts
165
+ fields = self.class.addressable_fields
166
+ self.class.addressable_required.each do |part|
167
+ column = fields[part]
168
+ errors.add(column, "can't be blank") if column && self[column].blank?
169
+ end
170
+ end
171
+
172
+ def validate_country_code
173
+ column = self.class.addressable_fields[:country]
174
+ value = column && self[column]
175
+ return unless value.is_a?(String) && value.length == 2
176
+ return if ConcernsOnRails::Support::AddressData.valid_country?(value)
177
+
178
+ errors.add(column, "is not a valid ISO 3166-1 country code")
179
+ end
180
+
181
+ def validate_postal_code
182
+ column = self.class.addressable_fields[:postal_code]
183
+ value = column && self[column]
184
+ return if value.blank?
185
+
186
+ format = ConcernsOnRails::Support::AddressData.postal_format_for(resolved_country)
187
+ errors.add(column, "is not a valid postal code") unless value.to_s.match?(format)
188
+ end
189
+
190
+ def validate_state_code
191
+ column = self.class.addressable_fields[:state]
192
+ value = column && self[column]
193
+ return if value.blank?
194
+ return if ConcernsOnRails::Support::AddressData.valid_state?(resolved_country, value)
195
+
196
+ errors.add(column, "is not a valid state/province")
197
+ end
198
+
199
+ def run_address_verifier
200
+ apply_verifier_result(self.class.addressable_verifier.call(self))
201
+ end
202
+
203
+ def apply_verifier_result(result)
204
+ case result
205
+ when false then errors.add(:base, "address could not be verified")
206
+ when String then errors.add(:base, result)
207
+ when Array then result.each { |message| errors.add(:base, message) }
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,102 @@
1
+ module ConcernsOnRails
2
+ module Support
3
+ # Reference data + lookups for Models::Addressable. Kept here (mirroring
4
+ # ColumnGuard / RandomValue) so the concern itself stays lean and so the
5
+ # large constant tables live as plain literals rather than RuboCop-flagged
6
+ # blocks. All lookups are case-insensitive and string-safe.
7
+ #
8
+ # Scope is *format/structure* only — this validates shape (a well-formed
9
+ # postal code, a real ISO country code), never real-world deliverability.
10
+ module AddressData
11
+ module_function
12
+
13
+ # ISO 3166-1 alpha-2 country codes.
14
+ ISO_COUNTRY_CODES = Set.new(%w[
15
+ AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ
16
+ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ
17
+ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ
18
+ DE DJ DK DM DO DZ
19
+ EC EE EG EH ER ES ET
20
+ FI FJ FK FM FO FR
21
+ GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY
22
+ HK HM HN HR HT HU
23
+ ID IE IL IM IN IO IQ IR IS IT
24
+ JE JM JO JP
25
+ KE KG KH KI KM KN KP KR KW KY KZ
26
+ LA LB LC LI LK LR LS LT LU LV LY
27
+ MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ
28
+ NA NC NE NF NG NI NL NO NP NR NU NZ
29
+ OM
30
+ PA PE PF PG PH PK PL PM PN PR PS PT PW PY
31
+ QA
32
+ RE RO RS RU RW
33
+ SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ
34
+ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ
35
+ UA UG UM US UY UZ
36
+ VA VC VE VG VI VN VU
37
+ WF WS
38
+ YE YT
39
+ ZA ZM ZW
40
+ ]).freeze
41
+
42
+ # Per-country postal-code patterns (matched against the *normalized*,
43
+ # upcased value). `:default` is a permissive fallback for everything else.
44
+ POSTAL_FORMATS = {
45
+ "US" => /\A\d{5}(-\d{4})?\z/,
46
+ "CA" => /\A[A-Z]\d[A-Z] ?\d[A-Z]\d\z/,
47
+ "GB" => /\A[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}\z/,
48
+ "AU" => /\A\d{4}\z/,
49
+ "DE" => /\A\d{5}\z/,
50
+ "FR" => /\A\d{5}\z/,
51
+ default: /\A[A-Z0-9][A-Z0-9 -]{1,8}[A-Z0-9]\z/
52
+ }.freeze
53
+
54
+ # USPS state / territory abbreviations.
55
+ US_STATES = Set.new(%w[
56
+ AL AK AZ AR CA CO CT DE FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS
57
+ MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV
58
+ WI WY DC AS GU MP PR VI
59
+ ]).freeze
60
+
61
+ # Canadian province / territory codes.
62
+ CA_PROVINCES = Set.new(%w[AB BC MB NB NL NS NT NU ON PE QC SK YT]).freeze
63
+
64
+ # True when `code` is a known ISO 3166-1 alpha-2 country code.
65
+ def valid_country?(code)
66
+ return false unless code.is_a?(String)
67
+
68
+ ISO_COUNTRY_CODES.include?(code.upcase)
69
+ end
70
+
71
+ # Regexp to validate a postal code for the given country (falls back to
72
+ # the permissive `:default` pattern for unmapped countries).
73
+ def postal_format_for(country)
74
+ POSTAL_FORMATS[country.to_s.upcase] || POSTAL_FORMATS[:default]
75
+ end
76
+
77
+ # Validate a state/region against US / CA sets. Returns true for any other
78
+ # country (we only know those two), so callers needn't special-case.
79
+ def valid_state?(country, code)
80
+ return true unless code.is_a?(String)
81
+
82
+ case country.to_s.upcase
83
+ when "US" then US_STATES.include?(code.upcase)
84
+ when "CA" then CA_PROVINCES.include?(code.upcase)
85
+ else true
86
+ end
87
+ end
88
+
89
+ # Squish + upcase a postal code, adding canonical spacing for CA
90
+ # (`A1A1A1` -> `A1A 1A1`). Non-strings pass through unchanged.
91
+ def normalize_postal(country, value)
92
+ return value unless value.is_a?(String)
93
+
94
+ normalized = value.strip.squish.upcase
95
+ return normalized unless country.to_s.upcase == "CA"
96
+
97
+ compact = normalized.delete(" ")
98
+ compact.match?(/\A[A-Z]\d[A-Z]\d[A-Z]\d\z/) ? "#{compact[0, 3]} #{compact[3, 3]}" : normalized
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,3 @@
1
1
  module ConcernsOnRails
2
- VERSION = "1.9.0".freeze
2
+ VERSION = "1.10.0".freeze
3
3
  end
@@ -10,6 +10,7 @@ end
10
10
  # Shared internal helpers (must load before the concerns that use them)
11
11
  require "concerns_on_rails/support/column_guard"
12
12
  require "concerns_on_rails/support/random_value"
13
+ require "concerns_on_rails/support/address_data"
13
14
 
14
15
  # Model concerns
15
16
  require "concerns_on_rails/models/sluggable"
@@ -24,6 +25,7 @@ require "concerns_on_rails/models/searchable"
24
25
  require "concerns_on_rails/models/activatable"
25
26
  require "concerns_on_rails/models/tokenizable"
26
27
  require "concerns_on_rails/models/stateable"
28
+ require "concerns_on_rails/models/addressable"
27
29
 
28
30
  # Controller concerns
29
31
  require "concerns_on_rails/controllers/paginatable"
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.9.0
4
+ version: 1.10.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-05-25 00:00:00.000000000 Z
11
+ date: 2026-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -78,6 +78,7 @@ files:
78
78
  - lib/concerns_on_rails/controllers/sortable.rb
79
79
  - lib/concerns_on_rails/legacy_aliases.rb
80
80
  - lib/concerns_on_rails/models/activatable.rb
81
+ - lib/concerns_on_rails/models/addressable.rb
81
82
  - lib/concerns_on_rails/models/expirable.rb
82
83
  - lib/concerns_on_rails/models/hashable.rb
83
84
  - lib/concerns_on_rails/models/normalizable.rb
@@ -89,6 +90,7 @@ files:
89
90
  - lib/concerns_on_rails/models/sortable.rb
90
91
  - lib/concerns_on_rails/models/stateable.rb
91
92
  - lib/concerns_on_rails/models/tokenizable.rb
93
+ - lib/concerns_on_rails/support/address_data.rb
92
94
  - lib/concerns_on_rails/support/column_guard.rb
93
95
  - lib/concerns_on_rails/support/random_value.rb
94
96
  - lib/concerns_on_rails/version.rb